Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8cbf156
`size` of huge list
ggreif Feb 27, 2025
a6cbcb0
increase the test size
ggreif Feb 27, 2025
f819986
use accumulator in `size`
ggreif Feb 27, 2025
38281ef
rewrite `repeat` as a loop
ggreif Feb 27, 2025
4dd0aad
test get with huge
ggreif Feb 27, 2025
fb68f5e
overshoot huge list
ggreif Feb 27, 2025
bae2079
be more clear
ggreif Feb 27, 2025
59168da
reverse of huge
ggreif Feb 27, 2025
37af31c
test huge `forEach`
ggreif Feb 27, 2025
a6716fa
huge `all` and `any`
ggreif Feb 27, 2025
6c27294
fix
ggreif Feb 27, 2025
15786df
tweaks
ggreif Feb 27, 2025
9f28342
format
ggreif Feb 27, 2025
d3fd126
Merge branch 'main' into pure/List-tailrec
ggreif Feb 27, 2025
11ce261
tweak
ggreif Feb 27, 2025
3237f87
go deeper in `tabulate` test
ggreif Feb 28, 2025
a4da095
try `tabulate` as a `while` loop
ggreif Feb 28, 2025
be63a5b
we have to count up
ggreif Feb 28, 2025
6ccfd0e
suite for `fromIter`
ggreif Feb 28, 2025
f99627c
use `Iter.forEach` in `fromIter`
ggreif Feb 28, 2025
c899097
test huge `concat` too
ggreif Feb 28, 2025
7f8beaf
use `revAppend` helper in `concat`
ggreif Feb 28, 2025
7872bd4
fix warning
ggreif Feb 28, 2025
22044e1
simplify non-TR
ggreif Feb 28, 2025
a4d6285
add doc for `join`
ggreif Feb 28, 2025
803fd49
s/equalFunc/equalItem/g
ggreif Feb 28, 2025
3407fa9
simplify `compare`
ggreif Feb 28, 2025
4ba2415
large test for `mapResult`
ggreif Feb 28, 2025
1d707c7
make `mapResult` tail-recursive
ggreif Feb 28, 2025
03b0f17
fix large test
ggreif Feb 28, 2025
242325a
clean up
ggreif Feb 28, 2025
d353e38
try interpreter
ggreif Feb 28, 2025
1791ac0
fix (revert) `toText` and add test
ggreif Feb 28, 2025
b39f18f
back to interpreter
ggreif Feb 28, 2025
6791f59
Merge branch 'main' into pure/List-tailrec
ggreif Feb 28, 2025
b975271
resolve the `merge` TODO
ggreif Feb 28, 2025
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
149 changes: 89 additions & 60 deletions src/pure/List.mo
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +61 to +66
Copy link
Contributor

@crusso crusso Feb 28, 2025

Choose a reason for hiding this comment

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

Suggested change
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);
public func size<T>(list : List<T>) : Nat = {
var size = 0;
var cur = list;
loop {
switch tmp {
case (?(_, next)) {
size += 1;
cur:= next
};
case null { return size }
}
}
}

Is probably cheaper (no call to a helper).

Copy link
Contributor Author

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 var allocates in this example (it shouldn't).

Copy link
Contributor Author

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.

Copy link
Contributor

@crusso crusso Feb 28, 2025

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 ...

Copy link
Contributor Author

@ggreif ggreif Feb 28, 2025

Choose a reason for hiding this comment

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

I actually test reverse for stack overflow and I haven't seen any 😖
With depth 100_000!

Copy link
Contributor Author

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?

Copy link
Contributor Author

@ggreif ggreif Feb 28, 2025

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!

Copy link
Contributor

@crusso crusso Feb 28, 2025

Choose a reason for hiding this comment

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

I actually test reverse for stack overflow and I haven't seen any 😖 With depth 100_000!

If you test in the interpreter you won't see because the interpreter is in CPS... On wasm, you should see

Copy link
Contributor Author

@ggreif ggreif Feb 28, 2025

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.

Copy link
Contributor

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.


/// Check whether the list contains a given value. Uses the provided equality function to compare values.
///
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
///
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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))))))),
/// ```
///
Expand All @@ -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;
Expand All @@ -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
};

Expand All @@ -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
Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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...

Copy link
Contributor

Choose a reason for hiding this comment

The 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.
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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 # "]"
Expand Down
Loading
Loading