Scala - sortBy and sortWith
The `sortBy` method transforms each element into a comparable value and sorts based on that extracted value. This approach works seamlessly with any type that has an implicit `Ordering` instance.
Key Insights
sortByextracts a comparable value from each element for sorting, whilesortWithuses a custom comparison function for fine-grained control over ordering logicsortByis more concise for simple cases and leverages implicitOrderinginstances, whereassortWithprovides explicit boolean comparisons for complex multi-field sorting- Both methods are stable sorts that preserve relative ordering of equal elements, with O(n log n) time complexity using TimSort under the hood
Understanding sortBy: Extraction-Based Sorting
The sortBy method transforms each element into a comparable value and sorts based on that extracted value. This approach works seamlessly with any type that has an implicit Ordering instance.
case class Employee(name: String, salary: Int, department: String)
val employees = List(
Employee("Alice", 75000, "Engineering"),
Employee("Bob", 65000, "Sales"),
Employee("Charlie", 85000, "Engineering"),
Employee("Diana", 70000, "Marketing")
)
// Sort by salary (ascending)
val bySalary = employees.sortBy(_.salary)
// Result: Bob (65000), Diana (70000), Alice (75000), Charlie (85000)
// Sort by name (alphabetically)
val byName = employees.sortBy(_.name)
// Result: Alice, Bob, Charlie, Diana
// Sort by salary descending
val bySalaryDesc = employees.sortBy(_.salary)(Ordering[Int].reverse)
// Result: Charlie (85000), Alice (75000), Diana (70000), Bob (65000)
The power of sortBy lies in its simplicity. The method signature is sortBy[B](f: A => B)(implicit ord: Ordering[B]), meaning you provide a function that extracts a sortable value, and Scala handles the comparison automatically.
Sorting with Multiple Criteria Using sortBy
For multi-field sorting, sortBy accepts tuples. Scala compares tuples lexicographically—first by the first element, then by the second if the first elements are equal, and so on.
// Sort by department, then by salary within each department
val byDeptAndSalary = employees.sortBy(e => (e.department, e.salary))
// Sort by department ascending, salary descending
val byDeptAscSalaryDesc = employees.sortBy(e =>
(e.department, -e.salary)
)
// More complex: department ascending, salary descending
val complexSort = employees.sortBy(e =>
(e.department, e.salary)
)(Ordering.Tuple2(Ordering[String], Ordering[Int].reverse))
The tuple approach works well for up to 3-4 fields. Beyond that, readability suffers and sortWith becomes preferable.
Working with Option Types in sortBy
Handling Option values requires careful consideration of how None should be ordered relative to Some values.
case class Product(name: String, price: Option[Double])
val products = List(
Product("Laptop", Some(999.99)),
Product("Mouse", Some(25.50)),
Product("TBD Item", None),
Product("Keyboard", Some(75.00))
)
// Default Option ordering: None < Some
val sorted1 = products.sortBy(_.price)
// Result: TBD Item (None), Mouse (25.50), Keyboard (75.00), Laptop (999.99)
// Put None values last
val sorted2 = products.sortBy(_.price)(
Ordering.Option(Ordering[Double]).reverse.reverse
)
// More explicit: use getOrElse for control
val sorted3 = products.sortBy(_.price.getOrElse(Double.MaxValue))
// Result: Mouse, Keyboard, Laptop, TBD Item
Understanding sortWith: Comparison-Based Sorting
The sortWith method takes a comparison function (A, A) => Boolean that returns true if the first element should come before the second. This provides complete control over comparison logic.
// Sort employees by salary descending
val salaryDesc = employees.sortWith(_.salary > _.salary)
// Sort by name length, then alphabetically for same length
val byNameLength = employees.sortWith { (e1, e2) =>
if (e1.name.length != e2.name.length)
e1.name.length < e2.name.length
else
e1.name < e2.name
}
// Result: Bob (3), Alice (5), Diana (5), Charlie (7)
The comparison function must be consistent and transitive. If f(a, b) returns true and f(b, c) returns true, then f(a, c) should also return true.
Complex Multi-Field Sorting with sortWith
When sorting logic becomes intricate, sortWith offers better readability than nested tuple orderings.
case class Transaction(
date: java.time.LocalDate,
amount: Double,
category: String,
priority: Int
)
val transactions = List(
Transaction(java.time.LocalDate.of(2024, 1, 15), 100.0, "Food", 1),
Transaction(java.time.LocalDate.of(2024, 1, 15), 200.0, "Food", 2),
Transaction(java.time.LocalDate.of(2024, 1, 10), 150.0, "Transport", 1)
)
// Sort by: date desc, priority asc, amount desc
val complexSort = transactions.sortWith { (t1, t2) =>
if (t1.date != t2.date)
t1.date.isAfter(t2.date)
else if (t1.priority != t2.priority)
t1.priority < t2.priority
else
t1.amount > t2.amount
}
This approach scales better than tuple-based sorting and makes the sorting criteria explicit and maintainable.
Performance Considerations and Gotchas
Both methods use TimSort, which performs well on partially sorted data with O(n log n) worst-case complexity. However, there are important performance characteristics to understand.
// sortBy creates intermediate objects for extraction
val large = (1 to 1000000).map(i => Employee(s"Name$i", i, s"Dept${i % 10}"))
// This creates 1M tuples in memory
val sorted1 = large.sortBy(e => (e.department, e.salary))
// sortWith avoids intermediate allocations
val sorted2 = large.sortWith { (e1, e2) =>
if (e1.department != e2.department)
e1.department < e2.department
else
e1.salary < e2.salary
}
For large collections, sortWith can be more memory-efficient since it doesn’t create intermediate values. However, the difference is often negligible unless dealing with millions of elements.
Custom Ordering Instances
For frequently used sorting patterns, define custom Ordering instances rather than repeating logic.
object EmployeeOrderings {
implicit val byDepartmentThenSalary: Ordering[Employee] =
Ordering.by(e => (e.department, e.salary))
val bySalaryDesc: Ordering[Employee] =
Ordering.by[Employee, Int](_.salary).reverse
val byNameCaseInsensitive: Ordering[Employee] =
Ordering.by(_.name.toLowerCase)
}
// Usage
import EmployeeOrderings._
val sorted1 = employees.sorted // uses implicit ordering
val sorted2 = employees.sorted(bySalaryDesc)
val sorted3 = employees.sorted(byNameCaseInsensitive)
This approach promotes reusability and keeps sorting logic centralized.
Choosing Between sortBy and sortWith
Use sortBy when:
- Sorting by a single field or simple tuple of fields
- The extraction function is straightforward
- You want concise, readable code
Use sortWith when:
- Comparison logic involves complex conditions
- You need maximum performance on large collections
- The sorting criteria don’t map cleanly to extractable values
- You’re implementing custom comparison algorithms
// Prefer sortBy for simple cases
val simple = employees.sortBy(_.salary)
// Prefer sortWith for complex logic
val complex = employees.sortWith { (e1, e2) =>
val salaryDiff = math.abs(e1.salary - e2.salary)
if (salaryDiff < 5000) // similar salaries
e1.name < e2.name
else
e1.salary > e2.salary
}
Both methods are stable sorts, preserving the relative order of elements that compare as equal. Understanding when to use each method leads to cleaner, more maintainable Scala code.