diff --git a/Changelog.md b/Changelog.md index ce60ea523..e5341298e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,10 @@ * Add `entriesFrom` and `reverseEntriesFrom` to `Map`, `valuesFrom` and `reverseValuesFrom` to `Set` and `Text.toText` (#272). * Update code examples in doc comments (#224, #282, #303, #315). +## 0.5.0 + +* Add `concat` of slices function. + ## 0.4.0 * Add `isReplicated : () -> Bool` to `InternetComputer` (#213). diff --git a/README.md b/README.md index 2f384fb49..2a38eb44a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ It's also possible to use both versions in parallel: ```toml base = "0.14.4" -new-base = "0.4.0" +new-base = "0.5.0" ``` Since this is a preview release for community feedback, expect breaking changes. diff --git a/bench/List/ConcatSlices.bench.mo b/bench/List/ConcatSlices.bench.mo new file mode 100644 index 000000000..1a39b9084 --- /dev/null +++ b/bench/List/ConcatSlices.bench.mo @@ -0,0 +1,72 @@ +import Bench "mo:bench"; + +import List "../../src/List"; +import Nat "../../src/Nat"; +import Runtime "../../src/Runtime"; +import Iter "../../src/Iter"; + +module { + public func init() : Bench.Bench { + let bench = Bench.Bench(); + + bench.name("Create list from two slices"); + bench.description("Create two lists of size given in the column, create a new list by concatenating middle halves of each list. Each row is a different concatenation method."); + + bench.rows([ + "List.concatSlices", + "List.fromIter . Iter.concat . List.range", + "List.addCount_ . List.values_from_", + "List.addCount . List.valuesFrom" + ]); + bench.cols([ + "1000", + "100000", + "1000000" + ]); + + bench.runner( + func(row, col) { + let ?size = Nat.fromText(col) else Runtime.trap("Invalid size"); + let start = size / 4; + let end = size - start : Nat; + let count = end - start : Nat; + let list1 = List.repeat(1, size); + let list2 = List.repeat(2, size); + let result : List.List = switch row { + case "List.concatSlices" { + List.concatSlices([(list1, start, end), (list2, start, end)]) + }; + case "List.fromIter . Iter.concat . List.range" { + List.fromIter(Iter.concat(List.range(list1, start, end), List.range(list2, start, end))) + }; + case "List.addCount_ . List.values_from_" { + let list = List.empty(); + List.addCount_(list, List.values_from_(start, list1), count); + List.addCount_(list, List.values_from_(start, list2), count); + list + }; + case "List.addCount . List.valuesFrom" { + let list = List.empty(); + List.addCount(list, List.valuesFrom(list1, start), count); + List.addCount(list, List.valuesFrom(list2, start), count); + list + }; + case _ Runtime.unreachable() + }; + assert List.size(result) == count * 2; + // Uncomment to check that the result is correct + // var i = 0; + // while (i < List.size(result)) { + // if (i < count) { + // assert List.get(result, i) == 1 + // } else { + // assert List.get(result, i) == 2 + // }; + // i += 1 + // } + } + ); + + bench + } +} diff --git a/bench/List/Iteration.bench.mo b/bench/List/Iteration.bench.mo new file mode 100644 index 000000000..4d3b08c2c --- /dev/null +++ b/bench/List/Iteration.bench.mo @@ -0,0 +1,72 @@ +import Bench "mo:bench"; + +import List "../../src/List"; +import Nat "../../src/Nat"; +import Runtime "../../src/Runtime"; + +module { + public func init() : Bench.Bench { + let bench = Bench.Bench(); + + bench.name("List iteration"); + bench.description(""); + + bench.rows([ + "while get", + "List.values_ while unsafe_next", + "while put", + "List.values_ while next_set" + ]); + bench.cols([ + "1000", + "100000", + "1000000" + ]); + + func onValue(value : Nat) { + ignore value + }; + + bench.runner( + func(row, col) { + let ?size = Nat.fromText(col) else Runtime.trap("Invalid size"); + let list = List.repeat(0, size); + switch row { + case "while get" { + var i = 0; + while (i < size) { + onValue(List.get(list, i)); + i += 1 + } + }; + case "List.values_ while unsafe_next" { + let unsafeIter = List.values_(list); + var i = 0; + while (i < size) { + onValue(unsafeIter.unsafe_next()); + i += 1 + } + }; + case "while put" { + var i = 0; + while (i < size) { + List.put(list, i, i * 2); + i += 1 + } + }; + case "List.values_ while next_set" { + let unsafeIter = List.values_(list); + var i = 0; + while (i < size) { + unsafeIter.next_set(i * 2); + i += 1 + } + }; + case _ Runtime.unreachable() + } + } + ); + + bench + } +} diff --git a/mops.toml b/mops.toml index 957b4741d..c52b5f43d 100644 --- a/mops.toml +++ b/mops.toml @@ -1,6 +1,6 @@ [package] name = "new-base" -version = "0.4.0" +version = "0.5.0" description = "The new Motoko base library (preview)" repository = "https://github.com/dfinity/new-motoko-base" keywords = [ diff --git a/src/List.mo b/src/List.mo index 3eae0136e..518c959a5 100644 --- a/src/List.mo +++ b/src/List.mo @@ -22,6 +22,7 @@ import Order "Order"; import Option "Option"; import VarArray "VarArray"; import Types "Types"; +import Runtime "Runtime"; module { /// `List` provides a mutable list of elements of type `T`. @@ -62,32 +63,21 @@ module { /// Space: `O(1)` public func singleton(element : T) : List = repeat(element, 1); - /// Creates a new List with `size` copies of the initial value. - /// - /// Example: - /// ```motoko include=import - /// let list = List.repeat(2, 4); - /// assert List.toArray(list) == [2, 2, 2, 2]; - /// ``` - /// - /// Runtime: `O(size)` - /// - /// Space: `O(size)` - public func repeat(initValue : T, size : Nat) : List { + private func repeatInternal(initValue : ?T, size : Nat) : List { let (blockIndex, elementIndex) = locate(size); let blocks = new_index_block_length(Nat32.fromNat(if (elementIndex == 0) { blockIndex - 1 } else blockIndex)); let data_blocks = VarArray.repeat<[var ?T]>([var], blocks); var i = 1; while (i < blockIndex) { - data_blocks[i] := VarArray.repeat(?initValue, data_block_size(i)); + data_blocks[i] := VarArray.repeat(initValue, data_block_size(i)); i += 1 }; if (elementIndex != 0 and blockIndex < blocks) { let block = VarArray.repeat(null, data_block_size(i)); var j = 0; while (j < elementIndex) { - block[j] := ?initValue; + block[j] := initValue; j += 1 }; data_blocks[i] := block @@ -100,6 +90,19 @@ module { } }; + /// Creates a new List with `size` copies of the initial value. + /// + /// Example: + /// ```motoko include=import + /// let list = List.repeat(2, 4); + /// assert List.toArray(list) == [2, 2, 2, 2]; + /// ``` + /// + /// Runtime: `O(size)` + /// + /// Space: `O(size)` + public func repeat(initValue : T, size : Nat) : List = repeatInternal(?initValue, size); + /// Converts a mutable `List` to a purely functional `PureList`. /// /// Example: @@ -134,17 +137,101 @@ module { list }; - /// Add to list `count` copies of the initial value. - /// - /// ```motoko include=import - /// let list = List.repeat(2, 4); // [2, 2, 2, 2] - /// List.addRepeat(list, 2, 1); // [2, 2, 2, 2, 1, 1] - /// ``` - /// - /// The maximum number of elements in a `List` is 2^32. - /// - /// Runtime: `O(count)` - public func addRepeat(list : List, initValue : T, count : Nat) { + private func addRepeatInternal(list : List, initValue : ?T, count : Nat) { + let (blockIndex, elementIndex) = locate(size(list) + count); + let blocks = new_index_block_length(Nat32.fromNat(if (elementIndex == 0) { blockIndex - 1 } else blockIndex)); + + let old_blocks = list.blocks.size(); + if (old_blocks < blocks) { + let old_data_blocks = list.blocks; + list.blocks := VarArray.repeat<[var ?T]>([var], blocks); + var i = 0; + while (i < old_blocks) { + list.blocks[i] := old_data_blocks[i]; + i += 1 + } + }; + + var cnt = count; + while (cnt > 0) { + let db_size = data_block_size(list.blockIndex); + if (list.elementIndex == 0 and db_size <= cnt) { + list.blocks[list.blockIndex] := VarArray.repeat(initValue, db_size); + cnt -= db_size; + list.blockIndex += 1 + } else { + if (list.blocks[list.blockIndex].size() == 0) { + list.blocks[list.blockIndex] := VarArray.repeat(null, db_size) + }; + let from = list.elementIndex; + let to = Nat.min(list.elementIndex + cnt, db_size); + + let block = list.blocks[list.blockIndex]; + var i = from; + while (i < to) { + block[i] := initValue; + i += 1 + }; + + list.elementIndex := to; + if (list.elementIndex == db_size) { + list.elementIndex := 0; + list.blockIndex += 1 + }; + cnt -= to - from + } + } + }; + + // TODO: refactor, extract common code from addRepeatInternal + public func addCount(list : List, iter : Iter.Iter, count : Nat) { + let (blockIndex, elementIndex) = locate(size(list) + count); + let blocks = new_index_block_length(Nat32.fromNat(if (elementIndex == 0) { blockIndex - 1 } else blockIndex)); + + let old_blocks = list.blocks.size(); + if (old_blocks < blocks) { + let old_data_blocks = list.blocks; + list.blocks := VarArray.repeat<[var ?T]>([var], blocks); + var i = 0; + while (i < old_blocks) { + list.blocks[i] := old_data_blocks[i]; + i += 1 + } + }; + + var cnt = count; + while (cnt > 0) { + let db_size = data_block_size(list.blockIndex); + if (list.elementIndex == 0 and db_size <= cnt) { + list.blocks[list.blockIndex] := VarArray.tabulate(db_size, func _ = iter.next()); + cnt -= db_size; + list.blockIndex += 1 + } else { + if (list.blocks[list.blockIndex].size() == 0) { + list.blocks[list.blockIndex] := VarArray.repeat(null, db_size) + }; + let from = list.elementIndex; + let to = Nat.min(list.elementIndex + cnt, db_size); + + let block = list.blocks[list.blockIndex]; + var i = from; + while (i < to) { + block[i] := iter.next(); + i += 1 + }; + + list.elementIndex := to; + if (list.elementIndex == db_size) { + list.elementIndex := 0; + list.blockIndex += 1 + }; + cnt -= to - from + } + } + }; + + // TODO: refactor, extract common code from addRepeatInternal + public func addCount_(list : List, iter : UnsafeIter, count : Nat) { let (blockIndex, elementIndex) = locate(size(list) + count); let blocks = new_index_block_length(Nat32.fromNat(if (elementIndex == 0) { blockIndex - 1 } else blockIndex)); @@ -163,7 +250,7 @@ module { while (cnt > 0) { let db_size = data_block_size(list.blockIndex); if (list.elementIndex == 0 and db_size <= cnt) { - list.blocks[list.blockIndex] := VarArray.repeat(?initValue, db_size); + list.blocks[list.blockIndex] := VarArray.tabulate(db_size, func _ = ?iter.unsafe_next()); cnt -= db_size; list.blockIndex += 1 } else { @@ -176,7 +263,7 @@ module { let block = list.blocks[list.blockIndex]; var i = from; while (i < to) { - block[i] := ?initValue; + block[i] := ?iter.unsafe_next_i(i); i += 1 }; @@ -190,6 +277,18 @@ module { } }; + /// Add to list `count` copies of the initial value. + /// + /// ```motoko include=import + /// let list = List.repeat(2, 4); // [2, 2, 2, 2] + /// List.addRepeat(list, 2, 1); // [2, 2, 2, 2, 1, 1] + /// ``` + /// + /// The maximum number of elements in a `List` is 2^32. + /// + /// Runtime: `O(count)` + public func addRepeat(list : List, initValue : T, count : Nat) = addRepeatInternal(list, ?initValue, count); + /// Resets the list to size 0, de-referencing all elements. /// /// Example: @@ -812,6 +911,8 @@ module { /// Runtime: `O(1)` public func values(list : List) : Iter.Iter = values_(list); + public func valuesFrom(list : List, start : Nat) : Iter.Iter = values_from_(start, list); + /// Returns an Iterator (`Iter`) over the items (value-index pairs) in the list. /// Each item is a tuple of `(value, index)`. The iterator provides a single method /// `next()` which returns elements in order, or `null` when out of elements. @@ -1036,16 +1137,26 @@ module { /// Runtime: `O(size)` public func toArray(list : List) : [T] = Array.tabulate(size(list), values_(list).unsafe_next_i); - private func values_(list : List) : { + public type UnsafeIter = { next : () -> ?T; unsafe_next : () -> T; - unsafe_next_i : Nat -> T - } = object { + unsafe_next_i : Nat -> T; + next_set : T -> () + }; + + public func values_(list : List) : UnsafeIter = values_from_(0, list); + + public func values_from_(start : Nat, list : List) : UnsafeIter = object { let blocks = list.blocks.size(); var blockIndex = 0; var elementIndex = 0; - var db_size = 0; - var db : [var ?T] = [var]; + if (start != 0) { + let (block, element) = if (start == 0) (0, 0) else locate(start - 1); + blockIndex := block; + elementIndex := element + 1 + }; + var db : [var ?T] = list.blocks[blockIndex]; + var db_size = db.size(); public func next() : ?T { if (elementIndex == db_size) { @@ -1108,6 +1219,19 @@ module { }; case (_) Prim.trap(INTERNAL_ERROR) } + }; + + public func next_set(value : T) { + if (elementIndex == db_size) { + blockIndex += 1; + if (blockIndex >= blocks) Prim.trap(INTERNAL_ERROR); + db := list.blocks[blockIndex]; + db_size := db.size(); + if (db_size == 0) Prim.trap(INTERNAL_ERROR); + elementIndex := 0 + }; + db[elementIndex] := ?value; + elementIndex += 1 } }; @@ -1805,5 +1929,75 @@ module { /// Space: `O(1)` public func isEmpty(list : List) : Bool { list.blockIndex == 1 and list.elementIndex == 0 + }; + + /// Concatenates the provided slices into a new list. + /// Each slice is a tuple of a list, a starting index (inclusive), and an ending index (exclusive). + /// + /// Example: + /// ```motoko include=import + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// + /// let list1 = List.fromArray([1,2,3]); + /// let list2 = List.fromArray([4,5,6]); + /// let result = List.concatSlices([(list1, 0, 2), (list2, 1, 3)]); + /// assert Iter.toArray(List.values(result)) == [1,2,5,6]; + /// ``` + /// + /// Runtime: `O(sum_size)` where `sum_size` is the sum of the sizes of all slices. + /// + /// Space: `O(sum_size)` + public func concatSlices(slices : [(List, fromInclusive : Nat, toExclusive : Nat)]) : List { + var length = 0; + for (slice in slices.vals()) { + let (list, start, end) = slice; + let sz = size(list); + let ok = start <= end and end <= sz; + if (not ok) { + Runtime.trap("Invalid slice in concat") + }; + length += end - start + }; + + var result = repeatInternal(null, length); + var resultIter = values_(result); + for (slice in slices.vals()) { + let (list, start, end) = slice; + let values = values_from_(start, list); + var i = start; + while (i < end) { + let copiedValue = values.unsafe_next(); + resultIter.next_set(copiedValue); + i += 1 + } + }; + + result + }; + + /// Concatenates the provided lists into a new list. + /// + /// Example: + /// ```motoko include=import + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// + /// let list1 = List.fromArray([1, 2, 3]); + /// let list2 = List.fromArray([4, 5, 6]); + /// let result = List.concat([list1, list2]); + /// assert Iter.toArray(List.values(result)) == [1, 2, 3, 4, 5, 6]; + /// ``` + /// + /// Runtime: `O(sum_size)` where `sum_size` is the sum of the sizes of all lists. + /// + /// Space: `O(sum_size)` + public func concat(lists : [List]) : List { + concatSlices(Array.tabulate<(List, Nat, Nat)>(lists.size(), func(i) = (lists[i], 0, size(lists[i])))) + }; + + public func range(list : List, start : Nat, end : Nat) : Iter.Iter { + let values = values_from_(start, list); + Iter.take(values, end - start : Nat) } } diff --git a/test/List.test.mo b/test/List.test.mo index 599253199..316e46662 100644 --- a/test/List.test.mo +++ b/test/List.test.mo @@ -1324,4 +1324,154 @@ Test.suite( } ) } +); + +run( + suite( + "concat slices", + [ + test( + "concat with valid slices", + do { + let list1 = List.fromArray([1, 2, 3]); + let list2 = List.fromArray([4, 5, 6]); + let slice1 = (list1, 0, 2); // [1, 2] + let slice2 = (list2, 1, 3); // [5, 6] + let result = List.concatSlices([slice1, slice2]); + List.toArray(result) + }, + M.equals(T.array(T.natTestable, [1, 2, 5, 6])) + ), + test( + "concat with empty slices", + do { + let list1 = List.fromArray([1, 2, 3]); + let slice1 = (list1, 1, 1); // [] + let result = List.concatSlices([slice1]); + List.toArray(result) + }, + M.equals(T.array(T.natTestable, [] : [Nat])) + ), + test( + "concat with overlapping slices", + do { + let list1 = List.fromArray([1, 2, 3, 4]); + let slice1 = (list1, 0, 2); // [1, 2] + let slice2 = (list1, 1, 4); // [2, 3, 4] + let result = List.concatSlices([slice1, slice2]); + List.toArray(result) + }, + M.equals(T.array(T.natTestable, [1, 2, 2, 3, 4])) + ) + ] + ) +); + +run( + suite( + "concat slices (complicated cases)", + [ + test( + "concat with many slices from different lists", + do { + let l1 = List.fromArray([10, 11, 12, 13]); + let l2 = List.fromArray([20, 21]); + let l3 = List.fromArray([30, 31, 32]); + let slices = [ + (l1, 1, 3), // [11, 12] + (l2, 0, 2), // [20, 21] + (l3, 1, 3) // [31, 32] + ]; + let result = List.concatSlices(slices); + List.toArray(result) + }, + M.equals(T.array(T.natTestable, [11, 12, 20, 21, 31, 32])) + ), + test( + "concat with all slices empty", + do { + let l1 = List.fromArray([1, 2]); + let l2 = List.fromArray([3, 4]); + let slices = [ + (l1, 0, 0), // [] + (l2, 1, 1) // [] + ]; + let result = List.concatSlices(slices); + List.toArray(result) + }, + M.equals(T.array(T.natTestable, [] : [Nat])) + ), + test( + "concat with single element slices", + do { + let l1 = List.fromArray([1, 2, 3]); + let l2 = List.fromArray([4, 5, 6]); + let slices = [ + (l1, 0, 1), // [1] + (l1, 1, 2), // [2] + (l2, 2, 3) // [6] + ]; + let result = List.concatSlices(slices); + List.toArray(result) + }, + M.equals(T.array(T.natTestable, [1, 2, 6])) + ), + test( + "concat with slices covering full and partial lists", + do { + let l1 = List.fromArray([1, 2, 3]); + let l2 = List.fromArray([4, 5, 6, 7]); + let slices = [ + (l1, 0, 3), // [1,2,3] + (l2, 1, 3) // [5,6] + ]; + let result = List.concatSlices(slices); + List.toArray(result) + }, + M.equals(T.array(T.natTestable, [1, 2, 3, 5, 6])) + ), + test( + "concat with repeated slices from the same list", + do { + let l = List.fromArray([9, 8, 7, 6]); + let slices = [ + (l, 0, 2), // [9,8] + (l, 2, 4), // [7,6] + (l, 1, 3) // [8,7] + ]; + let result = List.concatSlices(slices); + List.toArray(result) + }, + M.equals(T.array(T.natTestable, [9, 8, 7, 6, 8, 7])) + ), + test( + "concat with a large number of small slices", + do { + let l = List.fromArray(Array.tabulate(20, func(i) = i)); + let slices = Array.tabulate<(List.List, Nat, Nat)>(20, func(i) = (l, i, i + 1)); + let result = List.concatSlices(slices); + List.toArray(result) + }, + M.equals(T.array(T.natTestable, Array.tabulate(20, func(i) = i))) + ) + ] + ) +); + +run( + suite( + "concat", + [ + test( + "concat two lists", + do { + let list1 = List.fromArray([1, 2, 3]); + let list2 = List.fromArray([4, 5, 6]); + let result = List.concat([list1, list2]); + List.toArray(result) + }, + M.equals(T.array(T.natTestable, [1, 2, 3, 4, 5, 6])) + ) + ] + ) ) diff --git a/validation/api/api.lock.json b/validation/api/api.lock.json index 6110762ac..aa25fec0c 100644 --- a/validation/api/api.lock.json +++ b/validation/api/api.lock.json @@ -510,6 +510,8 @@ "public func clear(list : List)", "public func clone(list : List) : List", "public func compare(list1 : List, list2 : List, compare : (T, T) -> Order.Order) : Order.Order", + "public func concat(lists : [List]) : List", + "public func concatSlices(slices : [(List, fromInclusive : Nat, toExclusive : Nat)]) : List", "public func contains(list : List, equal : (T, T) -> Bool, element : T) : Bool", "public func empty() : List", "public func entries(list : List) : Iter.Iter<(T, Nat)>",