-
Notifications
You must be signed in to change notification settings - Fork 9
chore: make pure/List mostly tail-recursive
#189
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
Changes from all commits
8cbf156
a6cbcb0
f819986
38281ef
4dd0aad
fb68f5e
bae2079
59168da
37af31c
a6716fa
6c27294
15786df
9f28342
d3fd126
11ce261
3237f87
a4da095
be63a5b
6ccfd0e
f99627c
c899097
7f8beaf
7872bd4
22044e1
a4d6285
803fd49
3407fa9
4ba2415
1d707c7
03b0f17
242325a
d353e38
1791ac0
b39f18f
6791f59
b975271
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,10 +58,12 @@ module { | |
| /// Runtime: O(size) | ||
| /// | ||
| /// Space: O(1) | ||
| public func size<T>(list : List<T>) : Nat = switch list { | ||
| case null 0; | ||
| case (?(_, t)) 1 + size t | ||
| }; | ||
| public func size<T>(list : List<T>) : Nat = ( | ||
| func go(n : Nat, list : List<T>) : Nat = switch list { | ||
| case (?(_, t)) go(n + 1, t); | ||
| case null n | ||
| } | ||
| )(0, list); | ||
|
|
||
| /// Check whether the list contains a given value. Uses the provided equality function to compare values. | ||
| /// | ||
|
|
@@ -96,8 +98,8 @@ module { | |
| /// | ||
| /// Space: O(1) | ||
| public func get<T>(list : List<T>, n : Nat) : ?T = switch list { | ||
| case null null; | ||
| case (?(h, t)) if (n == 0) ?h else get(t, n - 1 : Nat) | ||
| case (?(h, t)) if (n == 0) ?h else get(t, n - 1 : Nat); | ||
| case null null | ||
| }; | ||
|
|
||
| /// Add `item` to the head of `list`, and return the new list. | ||
|
|
@@ -153,13 +155,12 @@ module { | |
| /// Runtime: O(size) | ||
| /// | ||
| /// Space: O(size) | ||
| public func reverse<T>(list : List<T>) : List<T> { | ||
| public func reverse<T>(list : List<T>) : List<T> = ( | ||
| func go(acc : List<T>, list : List<T>) : List<T> = switch list { | ||
| case null acc; | ||
| case (?(h, t)) go(?(h, acc), t) | ||
| }; | ||
| go(null, list) | ||
| }; | ||
| case (?(h, t)) go(?(h, acc), t); | ||
| case null acc | ||
| } | ||
| )(null, list); | ||
|
|
||
| /// Call the given function for its side effect, with each list element in turn. | ||
| /// | ||
|
|
@@ -176,8 +177,8 @@ module { | |
| /// | ||
| /// *Runtime and space assumes that `f` runs in O(1) time and space. | ||
| public func forEach<T>(list : List<T>, f : T -> ()) = switch list { | ||
| case null (); | ||
| case (?(h, t)) { f h; forEach(t, f) } | ||
| case (?(h, t)) { f h; forEach(t, f) }; | ||
| case null () | ||
| }; | ||
|
|
||
| /// Call the given function `f` on each list element and collect the results | ||
|
|
@@ -254,16 +255,16 @@ module { | |
| /// Space: O(size) | ||
| /// | ||
| /// *Runtime and space assumes that `f` runs in O(1) time and space. | ||
| public func mapResult<T, R, E>(list : List<T>, f : T -> Result.Result<R, E>) : Result.Result<List<R>, E> = switch list { | ||
| case null #ok null; | ||
| case (?(h, t)) { | ||
| switch (f h, mapResult(t, f)) { | ||
| case (#ok r, #ok l) #ok(?(r, l)); | ||
| case (#err e, _) #err e; | ||
| case (_, #err e) #err e | ||
|
|
||
| public func mapResult<T, R, E>(list : List<T>, f : T -> Result.Result<R, E>) : Result.Result<List<R>, E> = ( | ||
| func rev(acc : List<R>, list : List<T>, f : T -> Result.Result<R, E>) : Result.Result<List<R>, E> = switch list { | ||
| case null #ok acc; | ||
| case (?(h, t)) switch (f h) { | ||
| case (#ok fh) rev(?(fh, acc), t, f); | ||
| case (#err e) #err e | ||
| } | ||
| } | ||
| }; | ||
| )(null, list, f) |> Result.mapOk(_, func(l : List<R>) : List<R> = reverse l); | ||
|
|
||
| /// Create two new lists from the results of a given function (`f`). | ||
| /// The first list only includes the elements for which the given | ||
|
|
@@ -302,11 +303,26 @@ module { | |
| /// Runtime: O(size(l)) | ||
| /// | ||
| /// Space: O(size(l)) | ||
| public func concat<T>(list1 : List<T>, list2 : List<T>) : List<T> = switch list1 { | ||
| case null list2; | ||
| case (?(h, t)) ?(h, concat(t, list2)) | ||
| }; | ||
| public func concat<T>(list1 : List<T>, list2 : List<T>) : List<T> = ( | ||
| func revAppend<T>(l : List<T>, m : List<T>) : List<T> = switch l { | ||
| case (?(h, t)) revAppend(t, ?(h, m)); | ||
| case null m | ||
| } | ||
| )(reverse list1, list2); | ||
|
|
||
| /// Flatten, or repatedly concatenate, an iterator of lists as a list. | ||
| /// | ||
| /// Example: | ||
| /// ```motoko include=initialize | ||
| /// List.join<Nat>( | ||
| /// [ ?(0, ?(1, ?(2, null))), | ||
| /// ?(3, ?(4, ?(5, null))) ] |> Iter.fromArray _) | ||
| /// ); // => ?(0, ?(1, ?(2, ?(3, ?(4, ?(5, null)))))) | ||
| /// ``` | ||
| /// | ||
| /// Runtime: O(size*size) | ||
| /// | ||
| /// Space: O(size*size) | ||
| public func join<T>(list : Iter.Iter<List<T>>) : List<T> { | ||
| let ?l = list.next() else return null; | ||
| let ls = join list; | ||
|
|
@@ -363,8 +379,8 @@ module { | |
| /// | ||
| /// Space: O(1) | ||
| public func drop<T>(list : List<T>, n : Nat) : List<T> = if (n == 0) list else switch list { | ||
| case null null; | ||
| case (?(_h, t)) drop(t, n - 1 : Nat) | ||
| case (?(_, t)) drop(t, n - 1 : Nat); | ||
| case null null | ||
| }; | ||
|
|
||
| /// Collapses the elements in `list` into a single value by starting with `base` | ||
|
|
@@ -485,15 +501,15 @@ module { | |
|
|
||
| /// Merge two ordered lists into a single ordered list. | ||
| /// This function requires both list to be ordered as specified | ||
| /// by the given relation `lessThanOrEqual`. | ||
| /// by the given relation `compare`. | ||
| /// | ||
| /// Example: | ||
| /// ```motoko include=initialize | ||
| /// | ||
| /// List.merge<Nat>( | ||
| /// ?(1, ?(2, ?(4, null))), | ||
| /// ?(2, ?(4, ?(6, null))), | ||
| /// func (n1, n2) = n1 <= n2 | ||
| /// Nat.compare | ||
| /// ); // => ?(1, ?(2, ?(2, ?(4, ?(4, ?(6, null))))))), | ||
| /// ``` | ||
| /// | ||
|
|
@@ -503,14 +519,14 @@ module { | |
| /// | ||
| /// *Runtime and space assumes that `lessThanOrEqual` runs in O(1) time and space. | ||
| // TODO: replace by merge taking a compare : (T, T) -> Order.Order function? | ||
| public func merge<T>(list1 : List<T>, list2 : List<T>, lessThanOrEqual : (T, T) -> Bool) : List<T> = switch (list1, list2) { | ||
| public func merge<T>(list1 : List<T>, list2 : List<T>, compare : (T, T) -> Order.Order) : List<T> = switch (list1, list2) { | ||
| case (?(h1, t1), ?(h2, t2)) { | ||
| if (lessThanOrEqual(h1, h2)) { | ||
| ?(h1, merge(t1, list2, lessThanOrEqual)) | ||
| } else if (lessThanOrEqual(h2, h1)) { | ||
| ?(h2, merge(list1, t2, lessThanOrEqual)) | ||
| if (compare(h1, h2) != #greater) { | ||
| ?(h1, merge(t1, list2, compare)) | ||
| } else if (compare(h2, h1) != #greater) { | ||
| ?(h2, merge(list1, t2, compare)) | ||
| } else { | ||
| ?(h1, ?(h2, merge(list1, list2, lessThanOrEqual))) | ||
| ?(h1, ?(h2, merge(list1, list2, compare))) | ||
| } | ||
| }; | ||
| case (null, _) list2; | ||
|
|
@@ -529,10 +545,10 @@ module { | |
| /// | ||
| /// Space: O(1) | ||
| /// | ||
| /// *Runtime and space assumes that `equalFunc` runs in O(1) time and space. | ||
| public func equal<T>(list1 : List<T>, list2 : List<T>, equalFunc : (T, T) -> Bool) : Bool = switch (list1, list2) { | ||
| /// *Runtime and space assumes that `equalItem` runs in O(1) time and space. | ||
| public func equal<T>(list1 : List<T>, list2 : List<T>, equalItem : (T, T) -> Bool) : Bool = switch (list1, list2) { | ||
| case (null, null) true; | ||
| case (?(h1, t1), ?(h2, t2)) equalFunc(h1, h2) and equal(t1, t2, equalFunc); | ||
| case (?(h1, t1), ?(h2, t2)) equalItem(h1, h2) and equal(t1, t2, equalItem); | ||
| case _ false | ||
| }; | ||
|
|
||
|
|
@@ -554,18 +570,14 @@ module { | |
| /// Space: O(1) | ||
| /// | ||
| /// *Runtime and space assumes that argument `compare` runs in O(1) time and space. | ||
| public func compare<T>(list1 : List<T>, list2 : List<T>, compare : (T, T) -> Order.Order) : Order.Order { | ||
| type Order = Order.Order; | ||
| func go(list1 : List<T>, list2 : List<T>, comp : (T, T) -> Order) : Order = switch (list1, list2) { | ||
| case (?(h1, t1), ?(h2, t2)) switch (comp(h1, h2)) { | ||
| case (#equal) go(t1, t2, comp); | ||
| case o o | ||
| }; | ||
| case (null, null) #equal; | ||
| case (null, _) #less; | ||
| case _ #greater | ||
| public func compare<T>(list1 : List<T>, list2 : List<T>, compareItem : (T, T) -> Order.Order) : Order.Order = switch (list1, list2) { | ||
| case (?(h1, t1), ?(h2, t2)) switch (compareItem(h1, h2)) { | ||
| case (#equal) compare(t1, t2, compareItem); | ||
| case o o | ||
| }; | ||
| go(list1, list2, compare) // FIXME: only needed because of above shadowing | ||
| case (null, null) #equal; | ||
| case (null, _) #less; | ||
| case _ #greater | ||
| }; | ||
|
|
||
| /// Generate a list based on a length and a function that maps from | ||
|
|
@@ -585,8 +597,13 @@ module { | |
| /// | ||
| /// *Runtime and space assumes that `f` runs in O(1) time and space. | ||
| public func tabulate<T>(n : Nat, f : Nat -> T) : List<T> { | ||
| func go(at : Nat, n : Nat) : List<T> = if (n == 0) null else ?(f at, go(at + 1, n - 1)); | ||
| go(0, n) | ||
| var i = 0; | ||
| var l : List<T> = null; | ||
| while (i < n) { | ||
| l := ?(f i, l); | ||
| i += 1 | ||
| }; | ||
| reverse l | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a shame. A clever optimisation would spot the linear heap allocation discipline arising from the recursive algorithm (did somebody say abstract interpretation?) and pre-allocate the list nodes. Then fill them in like an array... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's called tail-modulo-cons optimization but we don't have it. |
||
| }; | ||
|
|
||
| /// Create a list with exactly one element. | ||
|
|
@@ -616,7 +633,15 @@ module { | |
| /// Runtime: O(n) | ||
| /// | ||
| /// Space: O(n) | ||
| public func repeat<T>(item : T, n : Nat) : List<T> = if (n == 0) null else ?(item, repeat(item, n - 1 : Nat)); | ||
| public func repeat<T>(item : T, n : Nat) : List<T> { | ||
| var res : List<T> = null; | ||
| var i : Int = n; | ||
| while (i != 0) { | ||
| i -= 1; | ||
| res := ?(item, res) | ||
| }; | ||
| res | ||
| }; | ||
|
|
||
| /// Create a list of pairs from a pair of lists. | ||
| /// | ||
|
|
@@ -797,23 +822,27 @@ module { | |
| /// Runtime: O(size) | ||
| /// | ||
| /// Space: O(size) | ||
| public func fromIter<T>(iter : Iter.Iter<T>) : List<T> = switch (iter.next()) { | ||
| case null null; | ||
| case (?item) ?(item, fromIter iter) | ||
| public func fromIter<T>(iter : Iter.Iter<T>) : List<T> { | ||
| var result : List<T> = null; | ||
| Iter.forEach<T>( | ||
| iter, | ||
| func x = result := ?(x, result) | ||
| ); | ||
| reverse result | ||
| }; | ||
|
|
||
| public func toText<T>(list : List<T>, f : T -> Text) : Text { | ||
| var text = "["; | ||
| var first = false; | ||
| var first = true; | ||
| forEach( | ||
| list, | ||
| func(item : T) { | ||
| if first { | ||
| text #= ", " | ||
| first := false | ||
| } else { | ||
| first := true | ||
| text #= ", " | ||
| }; | ||
| text #= f(item) | ||
| text #= f item | ||
| } | ||
| ); | ||
| text # "]" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is probably cheaper (no call to a helper).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I was hoping that the compiler could create the same (or better) code by TCO. I.e. not sure if
varallocates in this example (it shouldn't).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did the same transformation in
repeat, though.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reverse needs it too. TCO won't inline the auxiliary function so ...
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually test
reversefor stack overflow and I haven't seen any 😖With depth 100_000!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But for TCO one only have to turn the body of the helper into a loop, no need to inline it into the wrapper. Or am I mistaken?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course the interpreter won't do TCO, and if we want constant stack there we'll have to manually TCO anyway... Or maybe due to CPS the interpreter will do the right thing. Questions, questions!
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you test in the interpreter you won't see because the interpreter is in CPS... On wasm, you should see
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I test on Wasm, and don't see it :-) I'll look at IR/Wasm soon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, you reverse is tail recursive, but it allocates and calls a helper that is probably slower than just using a loop and a temp, without the additional function call.