Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make 'list::sort' quick by using natural mergesort #711

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/stdlib/list/sortBy.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Cons(-1, Cons(1, Cons(3, Cons(5, Nil()))))
Cons(5, Cons(3, Cons(1, Cons(-1, Nil()))))
Cons((-1, 1), Cons((0, 0), Cons((1, 0), Cons((0, 1), Nil()))))
Nil()
14 changes: 14 additions & 0 deletions examples/stdlib/list/sortBy.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module examples/pos/list/sortBy

import list

def main() = {
// synchronized with doctest in `sortBy`
println([1, 3, -1, 5].sortBy { (a, b) => a <= b })
println([1, 3, -1, 5].sortBy { (a, b) => a >= b })

val sorted: List[(Int, Int)] = [(1, 0), (0, 1), (-1, 1), (0, 0)]
.sortBy { (a, b) => a.first + a.second <= b.first + b.second }
println(show(sorted.map { case (a, b) => "(" ++ show(a) ++ ", " ++ show(b) ++ ")" }))
println(Nil[Int]().sortBy { (a, b) => a <= b })
}
110 changes: 94 additions & 16 deletions libraries/common/list.effekt
Original file line number Diff line number Diff line change
Expand Up @@ -649,35 +649,113 @@ def partition[A](l: List[A]) { pred: A => Bool }: (List[A], List[A]) = {
(lefts.reverse, rights.reverse)
}

/// Sort a list using a given comparison function.
/// Utilities for sorting, see 'sortBy' for more details.
namespace sort {
/// Splits the given list into monotonic segments (so a list of lists).
///
/// Internally used in the mergesort 'sortBy' to prepare the to-be-merged partitions.
def sequences[A](list: List[A]) { compare: (A, A) => Bool }: List[List[A]] = list match {
case Cons(a, Cons(b, rest)) =>
if (compare(a, b)) {
ascending(b, rest) { diffRest => Cons(a, diffRest) } {compare}
} else {
descending(b, [a], rest) {compare}
}
case _ => [list]
}

/// When in an ascending sequence, try to add `current` to `run` (if possible)
def ascending[A](current: A, rest: List[A]) { runDiff: List[A] => List[A] } { compare: (A, A) => Bool }: List[List[A]] = rest match {
case Cons(next, tail) and compare(current, next) =>
ascending(next, tail) { diffRest => runDiff(Cons(current, diffRest)) } {compare}
case _ => Cons(runDiff([current]), sequences(rest) {compare})
}

/// When in an descending sequence, try to add `current` to `run` (if possible)
def descending[A](current: A, run: List[A], rest: List[A]) { compare: (A, A) => Bool }: List[List[A]] = rest match {
case Cons(next, tail) and not(compare(current, next)) =>
descending(next, Cons(current, run), tail) {compare}
case _ => Cons(Cons(current, run), sequences(rest) {compare})
}

def mergeAll[A](runs: List[List[A]]) { compare: (A, A) => Bool }: List[A] = runs match {
case Cons(single, Nil()) => single
case _ => {
// recursively merge in pairs until there's only a single list
val newRuns = mergePairs(runs) {compare}
mergeAll(newRuns) {compare}
}
}

def mergePairs[A](runs: List[List[A]]) { compare: (A, A) => Bool }: List[List[A]] = runs match {
case Cons(a, Cons(b, rest)) =>
Cons(merge(a, b) {compare}, mergePairs(rest) {compare})
case _ => runs
}
jiribenes marked this conversation as resolved.
Show resolved Hide resolved

def merge[A](l1: List[A], l2: List[A]) { compare: (A, A) => Bool }: List[A] =
(l1, l2) match {
case (Nil(), _) => l2
case (_, Nil()) => l1
case (Cons(h1, t1), Cons(h2, t2)) =>
if (compare(h1, h2)) {
Cons(h1, merge(t1, l2) {compare})
} else {
Cons(h2, merge(l1, t2) {compare})
}
}
}

/// Sort a list given a comparison operator (like less-or-equal!)
/// The sorting algorithm is stable and should act reasonably well on partially sorted data.
///
/// Examples:
/// ```
/// > [1, 3, -1, 5].sortBy { (a, b) => a <= b }
/// [-1, 1, 3, 5]
///
/// > [1, 3, -1, 5].sortBy { (a, b) => a >= b }
/// [5, 3, 1, -1]
///
/// > [(1, 0), (0, 1), (-1, 1), (0, 0)].sortBy { (a, b) => a.first + a.second <= b.first + b.second }
/// [(1, -1), (0, 0), (1, 0), (0, 1)]
///
/// > Nil[Int]().sortBy { (a, b) => a <= b }
/// []
/// ```
///
/// Note: this implementation is not stacksafe!
/// (works for ~5M random elements just fine, but OOMs on ~10M random elements)
///
/// O(N log N)
def sortBy[A](l: List[A]) { compare: (A, A) => Bool }: List[A] =
l match {
case Nil() => Nil()
case Cons(pivot, rest) =>
val (lt, gt) = rest.partition { el => compare(el, pivot) };
val leftSorted = sortBy(lt) { (a, b) => compare(a, b) }
val rightSorted = sortBy(gt) { (a, b) => compare(a, b) }
leftSorted.append(Cons(pivot, rightSorted))
}
/// O(N log N) worstcase
def sortBy[A](list: List[A]) { compare: (A, A) => Bool }: List[A] = {
val monotonicRuns = sort::sequences(list) {compare}
sort::mergeAll(monotonicRuns) {compare}
}

/// Sort a list of integers in an ascending order.
/// See 'sortBy' for more details.
///
/// O(N log N) worstcase
def sort(l: List[Int]): List[Int] = l.sortBy { (a, b) => a <= b }

/// Sort a list of doubles in an ascending order.
/// See 'sortBy' for more details.
///
/// O(N log N) worstcase
def sort(l: List[Double]): List[Double] = l.sortBy { (a, b) => a <= b }

def sort(l: List[Int]): List[Int] = l.sortBy { (a, b) => a < b }
def sort(l: List[Double]): List[Double] = l.sortBy { (a, b) => a < b }

/// Check if a list is sorted according to the given comparison function.
/// Check if a list is sorted according to the given comparison function (less-or-equal).
///
/// O(N)
def isSortedBy[A](list: List[A]) { compare: (A, A) => Bool }: Bool = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Consider changing the names of comparison functions from compare to something like lteq or lessThanOrEqual even, just to express properly what we expect of them...

def go(list: List[A]): Bool = {
list match {
case Nil() => true
case Cons(x, Nil()) => true
case Cons(x, Cons(y, rest)) =>
val next = Cons(y, rest) // Future work: Replace this by an @-pattern!
compare(x, y) && go(next)
case _ => true
}
}
go(list)
Expand Down
Loading