From d97833b2b2e26e49f562b2237b7e170857ad54bd Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Tue, 3 Sep 2024 16:06:13 +0400 Subject: [PATCH 01/20] Extract persistent RBTree from mutable wrapper into a separate module No changes in logic so far, just simple refactoring --- src/PersistentOrderedMap.mo | 383 ++++++++++++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 src/PersistentOrderedMap.mo diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo new file mode 100644 index 00000000..885ab541 --- /dev/null +++ b/src/PersistentOrderedMap.mo @@ -0,0 +1,383 @@ +/// Key-value map implemented as a red-black tree (RBTree) with nodes storing key-value pairs. +/// +/// A red-black tree is a balanced binary search tree ordered by the keys. +/// +/// The tree data structure internally colors each of its nodes either red or black, +/// and uses this information to balance the tree during the modifying operations. +/// +/// Performance: +/// * Runtime: `O(log(n))` worst case cost per insertion, removal, and retrieval operation. +/// * Space: `O(n)` for storing the entire tree. +/// `n` denotes the number of key-value entries (i.e. nodes) stored in the tree. +/// +/// Note: +/// * Map operations, such as retrieval, insertion, and removal create `O(log(n))` temporary objects that become garbage. +/// +/// Credits: +/// +/// The core of this implementation is derived from: +/// +/// * Ken Friis Larsen's [RedBlackMap.sml](https://github.com/kfl/mosml/blob/master/src/mosmllib/Redblackmap.sml), which itself is based on: +/// * Stefan Kahrs, "Red-black trees with types", Journal of Functional Programming, 11(4): 425-432 (2001), [version 1 in web appendix](http://www.cs.ukc.ac.uk/people/staff/smk/redblack/rb.html). + + +import Debug "Debug"; +import I "Iter"; +import List "List"; +import Nat "Nat"; +import O "Order"; + +// TODO: a faster, more compact and less indirect representation would be: +// type Map = { +// #red : (Map, K, V, Map); +// #black : (Map, K, V, Map); +// #leaf +//}; +// (this inlines the colors into the variant, flattens a tuple, and removes a (now) redundant optin, for considerable heap savings.) +// It would also make sense to maintain the size in a separate root for 0(1) access. + +module { + + /// Node color: Either red (`#R`) or black (`#B`). + public type Color = { #R; #B }; + + /// Red-black tree of nodes with key-value entries, ordered by the keys. + /// The keys have the generic type `K` and the values the generic type `V`. + /// Leaves are considered implicitly black. + public type Map = { + #node : (Color, Map, (K, ?V), Map); + #leaf + }; + + type IterRep = List.List<{ #tr : Map; #xy : (X, ?Y) }>; + + /// Get an iterator for the entries of the `tree`, in ascending (`#fwd`) or descending (`#bwd`) order as specified by `direction`. + /// The iterator takes a snapshot view of the tree and is not affected by concurrent modifications. + /// + /// Example: + /// ```motoko + /// // Write new examples + /// ``` + /// + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the tree. + /// + /// Note: Full tree iteration creates `O(n)` temporary objects that will be collected as garbage. + public func iter(tree : Map, direction : { #fwd; #bwd }) : I.Iter<(X, Y)> { + object { + var trees : IterRep = ?(#tr(tree), null); + public func next() : ?(X, Y) { + switch (direction, trees) { + case (_, null) { null }; + case (_, ?(#tr(#leaf), ts)) { + trees := ts; + next() + }; + case (_, ?(#xy(xy), ts)) { + trees := ts; + switch (xy.1) { + case null { next() }; + case (?y) { ?(xy.0, y) } + } + }; // TODO: Let's try to float-out case on direction + case (#fwd, ?(#tr(#node(_, l, xy, r)), ts)) { + trees := ?(#tr(l), ?(#xy(xy), ?(#tr(r), ts))); + next() + }; + case (#bwd, ?(#tr(#node(_, l, xy, r)), ts)) { + trees := ?(#tr(r), ?(#xy(xy), ?(#tr(l), ts))); + next() + } + } + } + } + }; + + /// Remove the value associated with a given key. + public func removeRec(x : X, compare : (X, X) -> O.Order, t : Map) : (?Y, Map) { + let (t1, r) = remove(t, compare, x); + (r, t1); + }; + + public func getRec(x : X, compare : (X, X) -> O.Order, t : Map) : ?Y { + switch t { + case (#leaf) { null }; + case (#node(_c, l, xy, r)) { + switch (compare(x, xy.0)) { + case (#less) { getRec(x, compare, l) }; + case (#equal) { xy.1 }; + case (#greater) { getRec(x, compare, r) } + } + } + } + }; + + /// Determine the size of the tree as the number of key-value entries. + /// + /// Example: + /// ```motoko + /// // Write new examples + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the tree. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. + public func size(t : Map) : Nat { + switch t { + case (#leaf) { 0 }; + case (#node(_, l, xy, r)) { + size(l) + size(r) + (switch (xy.1) { case null 0; case _ 1 }) + } + } + }; + + func redden(t : Map) : Map { + switch t { + case (#node (#B, l, xy, r)) { + (#node (#R, l, xy, r)) + }; + case _ { + Debug.trap "RBTree.red" + } + } + }; + + func lbalance(left : Map, xy : (X,?Y), right : Map) : Map { + switch (left, right) { + case (#node(#R, #node(#R, l1, xy1, r1), xy2, r2), r) { + #node( + #R, + #node(#B, l1, xy1, r1), + xy2, + #node(#B, r2, xy, r)) + }; + case (#node(#R, l1, xy1, #node(#R, l2, xy2, r2)), r) { + #node( + #R, + #node(#B, l1, xy1, l2), + xy2, + #node(#B, r2, xy, r)) + }; + case _ { + #node(#B, left, xy, right) + } + } + }; + + func rbalance(left : Map, xy : (X,?Y), right : Map) : Map { + switch (left, right) { + case (l, #node(#R, l1, xy1, #node(#R, l2, xy2, r2))) { + #node( + #R, + #node(#B, l, xy, l1), + xy1, + #node(#B, l2, xy2, r2)) + }; + case (l, #node(#R, #node(#R, l1, xy1, r1), xy2, r2)) { + #node( + #R, + #node(#B, l, xy, l1), + xy1, + #node(#B, r1, xy2, r2)) + }; + case _ { + #node(#B, left, xy, right) + }; + } + }; + + public func insert( + tree : Map, + compare : (X, X) -> O.Order, + x : X, + y : Y + ) + : (Map, ?Y) { + var y0 : ?Y = null; + func ins(tree : Map) : Map { + switch tree { + case (#leaf) { + #node(#R, #leaf, (x,?y), #leaf) + }; + case (#node(#B, left, xy, right)) { + switch (compare (x, xy.0)) { + case (#less) { + lbalance(ins left, xy, right) + }; + case (#greater) { + rbalance(left, xy, ins right) + }; + case (#equal) { + y0 := xy.1; + #node(#B, left, (x,?y), right) + } + } + }; + case (#node(#R, left, xy, right)) { + switch (compare (x, xy.0)) { + case (#less) { + #node(#R, ins left, xy, right) + }; + case (#greater) { + #node(#R, left, xy, ins right) + }; + case (#equal) { + y0 := xy.1; + #node(#R, left, (x,?y), right) + } + } + } + }; + }; + switch (ins tree) { + case (#node(#R, left, xy, right)) { + (#node(#B, left, xy, right), y0); + }; + case other { (other, y0) }; + }; + }; + + + func balLeft(left : Map, xy : (X,?Y), right : Map) : Map { + switch (left, right) { + case (#node(#R, l1, xy1, r1), r) { + #node( + #R, + #node(#B, l1, xy1, r1), + xy, + r) + }; + case (_, #node(#B, l2, xy2, r2)) { + rbalance(left, xy, #node(#R, l2, xy2, r2)) + }; + case (_, #node(#R, #node(#B, l2, xy2, r2), xy3, r3)) { + #node(#R, + #node(#B, left, xy, l2), + xy2, + rbalance(r2, xy3, redden r3)) + }; + case _ { Debug.trap "balLeft" }; + } + }; + + func balRight(left : Map, xy : (X,?Y), right : Map) : Map { + switch (left, right) { + case (l, #node(#R, l1, xy1, r1)) { + #node(#R, + l, + xy, + #node(#B, l1, xy1, r1)) + }; + case (#node(#B, l1, xy1, r1), r) { + lbalance(#node(#R, l1, xy1, r1), xy, r); + }; + case (#node(#R, l1, xy1, #node(#B, l2, xy2, r2)), r3) { + #node(#R, + lbalance(redden l1, xy1, l2), + xy2, + #node(#B, r2, xy, r3)) + }; + case _ { Debug.trap "balRight" }; + } + }; + + func append(left : Map, right: Map) : Map { + switch (left, right) { + case (#leaf, _) { right }; + case (_, #leaf) { left }; + case (#node (#R, l1, xy1, r1), + #node (#R, l2, xy2, r2)) { + switch (append (r1, l2)) { + case (#node (#R, l3, xy3, r3)) { + #node( + #R, + #node(#R, l1, xy1, l3), + xy3, + #node(#R, r3, xy2, r2)) + }; + case r1l2 { + #node(#R, l1, xy1, #node(#R, r1l2, xy2, r2)) + } + } + }; + case (t1, #node(#R, l2, xy2, r2)) { + #node(#R, append(t1, l2), xy2, r2) + }; + case (#node(#R, l1, xy1, r1), t2) { + #node(#R, l1, xy1, append(r1, t2)) + }; + case (#node(#B, l1, xy1, r1), #node (#B, l2, xy2, r2)) { + switch (append (r1, l2)) { + case (#node (#R, l3, xy3, r3)) { + #node(#R, + #node(#B, l1, xy1, l3), + xy3, + #node(#B, r3, xy2, r2)) + }; + case r1l2 { + balLeft ( + l1, + xy1, + #node(#B, r1l2, xy2, r2) + ) + } + } + } + } + }; + + func remove(tree : Map, compare : (X, X) -> O.Order, x : X) : (Map, ?Y) { + var y0 : ?Y = null; + func delNode(left : Map, xy : (X, ?Y), right : Map) : Map { + switch (compare (x, xy.0)) { + case (#less) { + let newLeft = del left; + switch left { + case (#node(#B, _, _, _)) { + balLeft(newLeft, xy, right) + }; + case _ { + #node(#R, newLeft, xy, right) + } + } + }; + case (#greater) { + let newRight = del right; + switch right { + case (#node(#B, _, _, _)) { + balRight(left, xy, newRight) + }; + case _ { + #node(#R, left, xy, newRight) + } + } + }; + case (#equal) { + y0 := xy.1; + append(left, right) + }; + } + }; + func del(tree : Map) : Map { + switch tree { + case (#leaf) { + tree + }; + case (#node(_, left, xy, right)) { + delNode(left, xy, right) + } + }; + }; + switch (del(tree)) { + case (#node(#R, left, xy, right)) { + (#node(#B, left, xy, right), y0); + }; + case other { (other, y0) }; + }; + } + +} From 14b886fb73a8f8e0fcc333c501881332a61e4946 Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Wed, 11 Sep 2024 17:38:46 +0400 Subject: [PATCH 02/20] [DMS-66] Extend API for persistent ordered map Add `MapOps` class with the following signature: public class MapOps(compare : (K,K) -> O.Order) { public func put(rbMap : Map, key : K, value : V) : Map public func fromIter(i : I.Iter<(K,V)>) : Map public func replace(rbMap : Map, key : K, value : V) : (Map, ?V) public func mapFilter(f : (K, V1) -> ?V2, rbMap : Map) : Map public func get(key : K, rbMap : Map) : ?V public func delete(rbMap : Map, key : K) : Map public func remove< V>(rbMap : Map, key : K) : (Map, ?V) }; The other functionality provided as standalone functions, as they don't require comparator: public type Direction = { #fwd; #bwd }; public func iter(rbMap : Map, direction : Direction) : I.Iter<(K, V)> public func entries(m : Map) : I.Iter<(K, V)> public func keys(m : Map, direction : Direction) : I.Iter public func vals(m : Map, direction : Direction) : I.Iter public func map(f : (K, V1) -> V2, rbMap : Map) : Map public func size(t : Map) : Nat public func foldLeft( combine : (Key, Value, Accum) -> Accum, base : Accum, rbMap : Map ) : Accum And foldRight with the same signature as foldLeft The following functions are new for the API: - MapOps.put, MapOps.delete - MapOps.fromIter, entries, keys, vals - MapOps.mapFilter, map - foldLeft, foldRight --- src/PersistentOrderedMap.mo | 642 ++++++++++++++++++++++-------------- 1 file changed, 400 insertions(+), 242 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 885ab541..12c56492 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -45,14 +45,60 @@ module { /// The keys have the generic type `K` and the values the generic type `V`. /// Leaves are considered implicitly black. public type Map = { - #node : (Color, Map, (K, ?V), Map); + #node : (Color, Map, (K, V), Map); #leaf }; - type IterRep = List.List<{ #tr : Map; #xy : (X, ?Y) }>; + /// Opertaions on `Map`, that require a comparator. + /// + /// The object should be created once, then used for all the operations + /// with `Map` to maintain invariant that comparator did not changed. + public class MapOps(compare : (K,K) -> O.Order) { + + /// Returns a new map, containing all entries given by the iterator `i`. + /// If there are multiple entries with the same key the last one is taken. + public func fromIter(i : I.Iter<(K,V)>) : Map + = Internal.fromIter(i, compare); + + /// Insert the value `value` with key `key` into map `rbMap`. Overwrites any existing entry with key `key`. + /// Returns a modified map. + public func put(rbMap : Map, key : K, value : V) : Map + = Internal.put(rbMap, compare, key, value); + + /// Insert the value `value` with key `key` into `rbMap`. Returns modified map and + /// the previous value associated with key `key` or `null` if no such value exists. + public func replace(rbMap : Map, key : K, value : V) : (Map, ?V) + = Internal.replace(rbMap, compare, key, value); + + /// Creates a new map by applying `f` to each entry in `rbMap`. For each entry + /// `(k, v)` in the old map, if `f` evaluates to `null`, the entry is discarded. + /// Otherwise, the entry is transformed into a new entry `(k, v2)`, where + /// the new value `v2` is the result of applying `f` to `(k, v)`. + public func mapFilter(f : (K, V1) -> ?V2, rbMap : Map) : Map + = Internal.mapFilter(compare, f, rbMap); - /// Get an iterator for the entries of the `tree`, in ascending (`#fwd`) or descending (`#bwd`) order as specified by `direction`. - /// The iterator takes a snapshot view of the tree and is not affected by concurrent modifications. + /// Get the value associated with key `key` in the given `rbMap` if present and `null` otherwise. + public func get(key : K, rbMap : Map) : ?V + = Internal.get(key, compare, rbMap); + + /// Deletes the entry with the key `key` from the `rbMap`. Has no effect if `key` is not + /// present in the map. Returns modified map. + public func delete(rbMap : Map, key : K) : Map + = Internal.delete(rbMap, compare, key); + + /// Deletes the entry with the key `key`. Returns modified map and the + /// previous value associated with key `key` or `null` if no such value exists. + public func remove(rbMap : Map, key : K) : (Map, ?V) + = Internal.remove(rbMap, compare, key); + + }; + + type IterRep = List.List<{ #tr : Map; #xy : (K, V) }>; + + public type Direction = { #fwd; #bwd }; + + /// Get an iterator for the entries of the `rbMap`, in ascending (`#fwd`) or descending (`#bwd`) order as specified by `direction`. + /// The iterator takes a snapshot view of the map and is not affected by concurrent modifications. /// /// Example: /// ```motoko @@ -62,13 +108,13 @@ module { /// Cost of iteration over all elements: /// Runtime: `O(n)`. /// Space: `O(log(n))` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the tree. + /// where `n` denotes the number of key-value entries stored in the map. /// - /// Note: Full tree iteration creates `O(n)` temporary objects that will be collected as garbage. - public func iter(tree : Map, direction : { #fwd; #bwd }) : I.Iter<(X, Y)> { + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func iter(rbMap : Map, direction : Direction) : I.Iter<(K, V)> { object { - var trees : IterRep = ?(#tr(tree), null); - public func next() : ?(X, Y) { + var trees : IterRep = ?(#tr(rbMap), null); + public func next() : ?(K, V) { switch (direction, trees) { case (_, null) { null }; case (_, ?(#tr(#leaf), ts)) { @@ -77,11 +123,8 @@ module { }; case (_, ?(#xy(xy), ts)) { trees := ts; - switch (xy.1) { - case null { next() }; - case (?y) { ?(xy.0, y) } - } - }; // TODO: Let's try to float-out case on direction + ?xy + }; // TODO: Let's float-out case on direction case (#fwd, ?(#tr(#node(_, l, xy, r)), ts)) { trees := ?(#tr(l), ?(#xy(xy), ?(#tr(r), ts))); next() @@ -95,23 +138,36 @@ module { } }; - /// Remove the value associated with a given key. - public func removeRec(x : X, compare : (X, X) -> O.Order, t : Map) : (?Y, Map) { - let (t1, r) = remove(t, compare, x); - (r, t1); - }; + /// Returns an Iterator (`Iter`) over the key-value pairs in the map. + /// Iterator provides a single method `next()`, which returns + /// pairs in no specific order, or `null` when out of pairs to iterate over. + public func entries(m : Map) : I.Iter<(K, V)> = iter(m, #fwd); - public func getRec(x : X, compare : (X, X) -> O.Order, t : Map) : ?Y { - switch t { - case (#leaf) { null }; - case (#node(_c, l, xy, r)) { - switch (compare(x, xy.0)) { - case (#less) { getRec(x, compare, l) }; - case (#equal) { xy.1 }; - case (#greater) { getRec(x, compare, r) } - } + /// Returns an Iterator (`Iter`) over the keys of the map. + /// Iterator provides a single method `next()`, which returns + /// keys in no specific order, or `null` when out of keys to iterate over. + public func keys(m : Map, direction : Direction) : I.Iter + = I.map(iter(m, direction), func(kv : (K, V)) : K {kv.0}); + + /// Returns an Iterator (`Iter`) over the values of the map. + /// Iterator provides a single method `next()`, which returns + /// values in no specific order, or `null` when out of values to iterate over. + public func vals(m : Map, direction : Direction) : I.Iter + = I.map(iter(m, direction), func(kv : (K, V)) : V {kv.1}); + + /// Creates a new map by applying `f` to each entry in `rbMap`. Each entry + /// `(k, v)` in the old map is transformed into a new entry `(k, v2)`, where + /// the new value `v2` is created by applying `f` to `(k, v)`. + public func map(f : (K, V1) -> V2, rbMap : Map) : Map { + func mapRec(m : Map) : Map { + switch m { + case (#leaf) { #leaf }; + case (#node(c, l, xy, r)) { + #node(c, mapRec l, (xy.0, f xy), mapRec r) // TODO: try destination-passing style to avoid non tail-call recursion + }; } - } + }; + mapRec(rbMap) }; /// Determine the size of the tree as the number of key-value entries. @@ -126,258 +182,360 @@ module { /// where `n` denotes the number of key-value entries stored in the tree. /// /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. - public func size(t : Map) : Nat { + public func size(t : Map) : Nat { switch t { case (#leaf) { 0 }; - case (#node(_, l, xy, r)) { - size(l) + size(r) + (switch (xy.1) { case null 0; case _ 1 }) + case (#node(_, l, _, r)) { + size(l) + size(r) + 1 } } }; - func redden(t : Map) : Map { - switch t { - case (#node (#B, l, xy, r)) { - (#node (#R, l, xy, r)) - }; - case _ { - Debug.trap "RBTree.red" - } - } + /// Collapses the elements in `rbMap` into a single value by starting with `base` + /// and progessively combining keys and values into `base` with `combine`. Iteration runs + /// left to right. + public func foldLeft( + combine : (Key, Value, Accum) -> Accum, + base : Accum, + rbMap : Map + ) : Accum + { + var acc = base; + for(val in iter(rbMap, #fwd)){ + acc := combine(val.0, val.1, acc); + }; + acc }; - func lbalance(left : Map, xy : (X,?Y), right : Map) : Map { - switch (left, right) { - case (#node(#R, #node(#R, l1, xy1, r1), xy2, r2), r) { - #node( - #R, - #node(#B, l1, xy1, r1), - xy2, - #node(#B, r2, xy, r)) - }; - case (#node(#R, l1, xy1, #node(#R, l2, xy2, r2)), r) { - #node( - #R, - #node(#B, l1, xy1, l2), - xy2, - #node(#B, r2, xy, r)) - }; - case _ { - #node(#B, left, xy, right) - } - } + /// Collapses the elements in `rbMap` into a single value by starting with `base` + /// and progessively combining keys and values into `base` with `combine`. Iteration runs + /// right to left. + public func foldRight( + combine : (Key, Value, Accum) -> Accum, + base : Accum, + rbMap : Map + ) : Accum + { + var acc = base; + for(val in iter(rbMap, #bwd)){ + acc := combine(val.0, val.1, acc); + }; + acc }; - func rbalance(left : Map, xy : (X,?Y), right : Map) : Map { - switch (left, right) { - case (l, #node(#R, l1, xy1, #node(#R, l2, xy2, r2))) { - #node( - #R, - #node(#B, l, xy, l1), - xy1, - #node(#B, l2, xy2, r2)) - }; - case (l, #node(#R, #node(#R, l1, xy1, r1), xy2, r2)) { - #node( - #R, - #node(#B, l, xy, l1), - xy1, - #node(#B, r1, xy2, r2)) - }; - case _ { - #node(#B, left, xy, right) + + module Internal { + + public func fromIter(i : I.Iter<(K,V)>, compare : (K, K) -> O.Order) : Map + { + var map = #leaf : Map; + for(val in i) { + map := put(map, compare, val.0, val.1); }; - } - }; + map + }; - public func insert( - tree : Map, - compare : (X, X) -> O.Order, - x : X, - y : Y - ) - : (Map, ?Y) { - var y0 : ?Y = null; - func ins(tree : Map) : Map { - switch tree { - case (#leaf) { - #node(#R, #leaf, (x,?y), #leaf) - }; - case (#node(#B, left, xy, right)) { - switch (compare (x, xy.0)) { - case (#less) { - lbalance(ins left, xy, right) - }; - case (#greater) { - rbalance(left, xy, ins right) - }; - case (#equal) { - y0 := xy.1; - #node(#B, left, (x,?y), right) - } - } - }; - case (#node(#R, left, xy, right)) { - switch (compare (x, xy.0)) { - case (#less) { - #node(#R, ins left, xy, right) - }; - case (#greater) { - #node(#R, left, xy, ins right) - }; - case (#equal) { - y0 := xy.1; - #node(#R, left, (x,?y), right) - } + public func mapFilter(compare : (K, K) -> O.Order, f : (K, V1) -> ?V2, t : Map) : Map{ + var map = #leaf : Map; + for(kv in iter(t, #fwd)) + { + switch(f kv){ + case null {}; + case (?v1) { + // The keys still are monotonic, so we can + // merge trees using `append` and avoid compare here + map := put(map, compare, kv.0, v1); } } }; + map }; - switch (ins tree) { - case (#node(#R, left, xy, right)) { - (#node(#B, left, xy, right), y0); - }; - case other { (other, y0) }; + + public func get(x : K, compare : (K, K) -> O.Order, t : Map) : ?V { + switch t { + case (#leaf) { null }; + case (#node(_c, l, xy, r)) { + switch (compare(x, xy.0)) { + case (#less) { get(x, compare, l) }; + case (#equal) { ?xy.1 }; + case (#greater) { get(x, compare, r) } + } + } + } }; - }; + func redden(t : Map) : Map { + switch t { + case (#node (#B, l, xy, r)) { + (#node (#R, l, xy, r)) + }; + case _ { + Debug.trap "RBTree.red" + } + } + }; - func balLeft(left : Map, xy : (X,?Y), right : Map) : Map { - switch (left, right) { - case (#node(#R, l1, xy1, r1), r) { - #node( - #R, - #node(#B, l1, xy1, r1), - xy, - r) - }; - case (_, #node(#B, l2, xy2, r2)) { - rbalance(left, xy, #node(#R, l2, xy2, r2)) - }; - case (_, #node(#R, #node(#B, l2, xy2, r2), xy3, r3)) { - #node(#R, - #node(#B, left, xy, l2), - xy2, - rbalance(r2, xy3, redden r3)) - }; - case _ { Debug.trap "balLeft" }; - } - }; + func lbalance(left : Map, xy : (K,V), right : Map) : Map { + switch (left, right) { + case (#node(#R, #node(#R, l1, xy1, r1), xy2, r2), r) { + #node( + #R, + #node(#B, l1, xy1, r1), + xy2, + #node(#B, r2, xy, r)) + }; + case (#node(#R, l1, xy1, #node(#R, l2, xy2, r2)), r) { + #node( + #R, + #node(#B, l1, xy1, l2), + xy2, + #node(#B, r2, xy, r)) + }; + case _ { + #node(#B, left, xy, right) + } + } + }; - func balRight(left : Map, xy : (X,?Y), right : Map) : Map { - switch (left, right) { - case (l, #node(#R, l1, xy1, r1)) { - #node(#R, - l, - xy, - #node(#B, l1, xy1, r1)) - }; - case (#node(#B, l1, xy1, r1), r) { - lbalance(#node(#R, l1, xy1, r1), xy, r); - }; - case (#node(#R, l1, xy1, #node(#B, l2, xy2, r2)), r3) { - #node(#R, - lbalance(redden l1, xy1, l2), - xy2, - #node(#B, r2, xy, r3)) - }; - case _ { Debug.trap "balRight" }; - } - }; + func rbalance(left : Map, xy : (K,V), right : Map) : Map { + switch (left, right) { + case (l, #node(#R, l1, xy1, #node(#R, l2, xy2, r2))) { + #node( + #R, + #node(#B, l, xy, l1), + xy1, + #node(#B, l2, xy2, r2)) + }; + case (l, #node(#R, #node(#R, l1, xy1, r1), xy2, r2)) { + #node( + #R, + #node(#B, l, xy, l1), + xy1, + #node(#B, r1, xy2, r2)) + }; + case _ { + #node(#B, left, xy, right) + }; + } + }; - func append(left : Map, right: Map) : Map { - switch (left, right) { - case (#leaf, _) { right }; - case (_, #leaf) { left }; - case (#node (#R, l1, xy1, r1), - #node (#R, l2, xy2, r2)) { - switch (append (r1, l2)) { - case (#node (#R, l3, xy3, r3)) { - #node( - #R, - #node(#R, l1, xy1, l3), - xy3, - #node(#R, r3, xy2, r2)) + type ClashResolver = { old : A; new : A } -> A; + + func insertWith ( + m : Map, + compare : (K, K) -> O.Order, + key : K, + val : V, + onClash : ClashResolver + ) + : Map{ + func ins(tree : Map) : Map { + switch tree { + case (#leaf) { + #node(#R, #leaf, (key,val), #leaf) + }; + case (#node(#B, left, xy, right)) { + switch (compare (key, xy.0)) { + case (#less) { + lbalance(ins left, xy, right) + }; + case (#greater) { + rbalance(left, xy, ins right) + }; + case (#equal) { + let newVal = onClash({ new = val; old = xy.1 }); + #node(#B, left, (key,newVal), right) + } + } }; - case r1l2 { - #node(#R, l1, xy1, #node(#R, r1l2, xy2, r2)) + case (#node(#R, left, xy, right)) { + switch (compare (key, xy.0)) { + case (#less) { + #node(#R, ins left, xy, right) + }; + case (#greater) { + #node(#R, left, xy, ins right) + }; + case (#equal) { + let newVal = onClash { new = val; old = xy.1 }; + #node(#R, left, (key,newVal), right) + } + } } - } + }; }; - case (t1, #node(#R, l2, xy2, r2)) { - #node(#R, append(t1, l2), xy2, r2) + switch (ins m) { + case (#node(#R, left, xy, right)) { + #node(#B, left, xy, right); + }; + case other { other }; }; - case (#node(#R, l1, xy1, r1), t2) { - #node(#R, l1, xy1, append(r1, t2)) + }; + + public func replace( + m : Map, + compare : (K, K) -> O.Order, + key : K, + val : V + ) + : (Map, ?V) { + var oldVal : ?V = null; + func onClash( clash : { old : V; new : V } ) : V + { + oldVal := ?clash.old; + clash.new }; - case (#node(#B, l1, xy1, r1), #node (#B, l2, xy2, r2)) { - switch (append (r1, l2)) { - case (#node (#R, l3, xy3, r3)) { - #node(#R, - #node(#B, l1, xy1, l3), - xy3, - #node(#B, r3, xy2, r2)) - }; - case r1l2 { - balLeft ( - l1, - xy1, - #node(#B, r1l2, xy2, r2) - ) - } - } + let res = insertWith(m, compare, key, val, onClash); + (res, oldVal) + }; + + public func put ( + m : Map, + compare : (K, K) -> O.Order, + key : K, + val : V + ) : Map = replace(m, compare, key, val).0; + + + func balLeft(left : Map, xy : (K,V), right : Map) : Map { + switch (left, right) { + case (#node(#R, l1, xy1, r1), r) { + #node( + #R, + #node(#B, l1, xy1, r1), + xy, + r) + }; + case (_, #node(#B, l2, xy2, r2)) { + rbalance(left, xy, #node(#R, l2, xy2, r2)) + }; + case (_, #node(#R, #node(#B, l2, xy2, r2), xy3, r3)) { + #node(#R, + #node(#B, left, xy, l2), + xy2, + rbalance(r2, xy3, redden r3)) + }; + case _ { Debug.trap "balLeft" }; } - } - }; + }; + + func balRight(left : Map, xy : (K,V), right : Map) : Map { + switch (left, right) { + case (l, #node(#R, l1, xy1, r1)) { + #node(#R, + l, + xy, + #node(#B, l1, xy1, r1)) + }; + case (#node(#B, l1, xy1, r1), r) { + lbalance(#node(#R, l1, xy1, r1), xy, r); + }; + case (#node(#R, l1, xy1, #node(#B, l2, xy2, r2)), r3) { + #node(#R, + lbalance(redden l1, xy1, l2), + xy2, + #node(#B, r2, xy, r3)) + }; + case _ { Debug.trap "balRight" }; + } + }; - func remove(tree : Map, compare : (X, X) -> O.Order, x : X) : (Map, ?Y) { - var y0 : ?Y = null; - func delNode(left : Map, xy : (X, ?Y), right : Map) : Map { - switch (compare (x, xy.0)) { - case (#less) { - let newLeft = del left; - switch left { - case (#node(#B, _, _, _)) { - balLeft(newLeft, xy, right) + func append(left : Map, right: Map) : Map { + switch (left, right) { + case (#leaf, _) { right }; + case (_, #leaf) { left }; + case (#node (#R, l1, xy1, r1), + #node (#R, l2, xy2, r2)) { + switch (append (r1, l2)) { + case (#node (#R, l3, xy3, r3)) { + #node( + #R, + #node(#R, l1, xy1, l3), + xy3, + #node(#R, r3, xy2, r2)) }; - case _ { - #node(#R, newLeft, xy, right) + case r1l2 { + #node(#R, l1, xy1, #node(#R, r1l2, xy2, r2)) } } }; - case (#greater) { - let newRight = del right; - switch right { - case (#node(#B, _, _, _)) { - balRight(left, xy, newRight) + case (t1, #node(#R, l2, xy2, r2)) { + #node(#R, append(t1, l2), xy2, r2) + }; + case (#node(#R, l1, xy1, r1), t2) { + #node(#R, l1, xy1, append(r1, t2)) + }; + case (#node(#B, l1, xy1, r1), #node (#B, l2, xy2, r2)) { + switch (append (r1, l2)) { + case (#node (#R, l3, xy3, r3)) { + #node(#R, + #node(#B, l1, xy1, l3), + xy3, + #node(#B, r3, xy2, r2)) }; - case _ { - #node(#R, left, xy, newRight) + case r1l2 { + balLeft ( + l1, + xy1, + #node(#B, r1l2, xy2, r2) + ) } } - }; - case (#equal) { - y0 := xy.1; - append(left, right) - }; + } } }; - func del(tree : Map) : Map { - switch tree { - case (#leaf) { - tree - }; - case (#node(_, left, xy, right)) { - delNode(left, xy, right) + + public func delete(m : Map, compare : (K, K) -> O.Order, key : K) : Map + = remove(m, compare, key).0; + + public func remove(tree : Map, compare : (K, K) -> O.Order, x : K) : (Map, ?V) { + var y0 : ?V = null; + func delNode(left : Map, xy : (K, V), right : Map) : Map { + switch (compare (x, xy.0)) { + case (#less) { + let newLeft = del left; + switch left { + case (#node(#B, _, _, _)) { + balLeft(newLeft, xy, right) + }; + case _ { + #node(#R, newLeft, xy, right) + } + } + }; + case (#greater) { + let newRight = del right; + switch right { + case (#node(#B, _, _, _)) { + balRight(left, xy, newRight) + }; + case _ { + #node(#R, left, xy, newRight) + } + } + }; + case (#equal) { + y0 := ?xy.1; + append(left, right) + }; } }; - }; - switch (del(tree)) { - case (#node(#R, left, xy, right)) { - (#node(#B, left, xy, right), y0); + func del(tree : Map) : Map { + switch tree { + case (#leaf) { + tree + }; + case (#node(_, left, xy, right)) { + delNode(left, xy, right) + } + }; }; - case other { (other, y0) }; - }; + switch (del(tree)) { + case (#node(#R, left, xy, right)) { + (#node(#B, left, xy, right), y0); + }; + case other { (other, y0) }; + }; + } } - } From 564f95a249253421b86acd7d330bb1365503863c Mon Sep 17 00:00:00 2001 From: Pavel Golovin Date: Tue, 17 Sep 2024 18:29:19 +0200 Subject: [PATCH 03/20] [DMS-66] fix order of method arguments Problem: now order is not consistent within new module and with old modules as well. Solution: make the map argument always go first --- src/PersistentOrderedMap.mo | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 12c56492..3d852b09 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -74,12 +74,12 @@ module { /// `(k, v)` in the old map, if `f` evaluates to `null`, the entry is discarded. /// Otherwise, the entry is transformed into a new entry `(k, v2)`, where /// the new value `v2` is the result of applying `f` to `(k, v)`. - public func mapFilter(f : (K, V1) -> ?V2, rbMap : Map) : Map - = Internal.mapFilter(compare, f, rbMap); + public func mapFilter(rbMap : Map, f : (K, V1) -> ?V2) : Map + = Internal.mapFilter(rbMap, compare, f); /// Get the value associated with key `key` in the given `rbMap` if present and `null` otherwise. - public func get(key : K, rbMap : Map) : ?V - = Internal.get(key, compare, rbMap); + public func get(rbMap : Map, key : K) : ?V + = Internal.get(rbMap, compare, key); /// Deletes the entry with the key `key` from the `rbMap`. Has no effect if `key` is not /// present in the map. Returns modified map. @@ -158,7 +158,7 @@ module { /// Creates a new map by applying `f` to each entry in `rbMap`. Each entry /// `(k, v)` in the old map is transformed into a new entry `(k, v2)`, where /// the new value `v2` is created by applying `f` to `(k, v)`. - public func map(f : (K, V1) -> V2, rbMap : Map) : Map { + public func map(rbMap : Map, f : (K, V1) -> V2) : Map { func mapRec(m : Map) : Map { switch m { case (#leaf) { #leaf }; @@ -195,9 +195,9 @@ module { /// and progessively combining keys and values into `base` with `combine`. Iteration runs /// left to right. public func foldLeft( - combine : (Key, Value, Accum) -> Accum, + rbMap : Map, base : Accum, - rbMap : Map + combine : (Key, Value, Accum) -> Accum ) : Accum { var acc = base; @@ -211,9 +211,9 @@ module { /// and progessively combining keys and values into `base` with `combine`. Iteration runs /// right to left. public func foldRight( - combine : (Key, Value, Accum) -> Accum, + rbMap : Map, base : Accum, - rbMap : Map + combine : (Key, Value, Accum) -> Accum ) : Accum { var acc = base; @@ -235,7 +235,7 @@ module { map }; - public func mapFilter(compare : (K, K) -> O.Order, f : (K, V1) -> ?V2, t : Map) : Map{ + public func mapFilter(t : Map, compare : (K, K) -> O.Order, f : (K, V1) -> ?V2) : Map{ var map = #leaf : Map; for(kv in iter(t, #fwd)) { @@ -251,14 +251,14 @@ module { map }; - public func get(x : K, compare : (K, K) -> O.Order, t : Map) : ?V { + public func get(t : Map, compare : (K, K) -> O.Order, x : K) : ?V { switch t { case (#leaf) { null }; case (#node(_c, l, xy, r)) { switch (compare(x, xy.0)) { - case (#less) { get(x, compare, l) }; + case (#less) { get(l, compare, x) }; case (#equal) { ?xy.1 }; - case (#greater) { get(x, compare, r) } + case (#greater) { get(r, compare, x) } } } } From 174e877f614e2205e8601553af3fb51ef5e828fa Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Mon, 23 Sep 2024 15:16:50 +0400 Subject: [PATCH 04/20] [DMS-76] Persistent ordered map unit testing In addition to tests this patch removes `direction` argument from `keys` and `values` function to keep them simple and provides a new function `Map.empty` to create a map without knowing its internal representation. --- src/PersistentOrderedMap.mo | 11 +- test/PersistentOrderedMap.test.mo | 538 ++++++++++++++++++++++++++++++ 2 files changed, 545 insertions(+), 4 deletions(-) create mode 100644 test/PersistentOrderedMap.test.mo diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 3d852b09..73f507b8 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -138,6 +138,9 @@ module { } }; + /// Create a new empty map. + public func empty() : Map = #leaf; + /// Returns an Iterator (`Iter`) over the key-value pairs in the map. /// Iterator provides a single method `next()`, which returns /// pairs in no specific order, or `null` when out of pairs to iterate over. @@ -146,14 +149,14 @@ module { /// Returns an Iterator (`Iter`) over the keys of the map. /// Iterator provides a single method `next()`, which returns /// keys in no specific order, or `null` when out of keys to iterate over. - public func keys(m : Map, direction : Direction) : I.Iter - = I.map(iter(m, direction), func(kv : (K, V)) : K {kv.0}); + public func keys(m : Map) : I.Iter + = I.map(entries(m), func(kv : (K, V)) : K {kv.0}); /// Returns an Iterator (`Iter`) over the values of the map. /// Iterator provides a single method `next()`, which returns /// values in no specific order, or `null` when out of values to iterate over. - public func vals(m : Map, direction : Direction) : I.Iter - = I.map(iter(m, direction), func(kv : (K, V)) : V {kv.1}); + public func vals(m : Map) : I.Iter + = I.map(entries(m), func(kv : (K, V)) : V {kv.1}); /// Creates a new map by applying `f` to each entry in `rbMap`. Each entry /// `(k, v)` in the old map is transformed into a new entry `(k, v2)`, where diff --git a/test/PersistentOrderedMap.test.mo b/test/PersistentOrderedMap.test.mo new file mode 100644 index 00000000..58e7b41d --- /dev/null +++ b/test/PersistentOrderedMap.test.mo @@ -0,0 +1,538 @@ +// @testmode wasi + +import Map "../src/PersistentOrderedMap"; +import Nat "../src/Nat"; +import Iter "../src/Iter"; +import Debug "../src/Debug"; +import Array "../src/Array"; + +import Suite "mo:matchers/Suite"; +import T "mo:matchers/Testable"; +import M "mo:matchers/Matchers"; + +let { run; test; suite } = Suite; + +let entryTestable = T.tuple2Testable(T.natTestable, T.textTestable); + +class MapMatcher(expected : [(Nat, Text)]) : M.Matcher> { + public func describeMismatch(actual : Map.Map, _description : M.Description) { + Debug.print(debug_show (Iter.toArray(Map.entries(actual))) # " should be " # debug_show (expected)) + }; + + public func matches(actual : Map.Map) : Bool { + Iter.toArray(Map.entries(actual)) == expected + } +}; + +let natMapOps = Map.MapOps(Nat.compare); + +func checkMap(rbMap : Map.Map) { + ignore blackDepth(rbMap) +}; + +func blackDepth(node : Map.Map) : Nat { + switch node { + case (#leaf) 0; + case (#node(color, left, (key, _), right)) { + checkKey(left, func(x) { x < key }); + checkKey(right, func(x) { x > key }); + let leftBlacks = blackDepth(left); + let rightBlacks = blackDepth(right); + assert (leftBlacks == rightBlacks); + switch color { + case (#R) { + assert (not isRed(left)); + assert (not isRed(right)); + leftBlacks + }; + case (#B) { + leftBlacks + 1 + } + } + } + } +}; + + +func isRed(node : Map.Map) : Bool { + switch node { + case (#leaf) false; + case (#node(color, _, _, _)) color == #R + } +}; + +func checkKey(node : Map.Map, isValid : Nat -> Bool) { + switch node { + case (#leaf) {}; + case (#node(_, _, (key, _), _)) { + assert (isValid(key)) + } + } +}; + +func insert(rbTree : Map.Map, key : Nat) : Map.Map { + let updatedTree = natMapOps.put(rbTree, key, debug_show (key)); + checkMap(updatedTree); + updatedTree +}; + +func getAll(rbTree : Map.Map, keys : [Nat]) { + for (key in keys.vals()) { + let value = natMapOps.get(rbTree, key); + assert (value == ?debug_show (key)) + } +}; + +func clear(initialRbMap : Map.Map) : Map.Map { + var rbMap = initialRbMap; + for ((key, value) in Map.entries(initialRbMap)) { + // stable iteration + assert (value == debug_show (key)); + let (newMap, result) = natMapOps.remove(rbMap, key); + rbMap := newMap; + assert (result == ?debug_show (key)); + checkMap(rbMap) + }; + rbMap +}; + +func expectedEntries(keys : [Nat]) : [(Nat, Text)] { + Array.tabulate<(Nat, Text)>(keys.size(), func(index) { (keys[index], debug_show (keys[index])) }) +}; + +func concatenateKeys(key : Nat, value : Text, accum : Text) : Text { + accum # debug_show(key) +}; + +func concatenateValues(key : Nat, value : Text, accum : Text) : Text { + accum # value +}; + +func multiplyKeyAndConcat(key : Nat, value : Text) : Text { + debug_show(key * 2) # value +}; + +func ifKeyLessThan(threshold : Nat, f : (Nat, Text) -> Text) : (Nat, Text) -> ?Text + = func (key, value) { + if(key < threshold) + ?f(key, value) + else null + }; + +/* --------------------------------------- */ + +var buildTestMap = func() : Map.Map { + Map.empty() +}; + +run( + suite( + "empty", + [ + test( + "size", + Map.size(buildTestMap()), + M.equals(T.nat(0)) + ), + test( + "iterate forward", + Iter.toArray(Map.iter(buildTestMap(), #fwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, [])) + ), + test( + "iterate backward", + Iter.toArray(Map.iter(buildTestMap(), #bwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, [])) + ), + test( + "entries", + Iter.toArray(Map.entries(buildTestMap())), + M.equals(T.array<(Nat, Text)>(entryTestable, [])) + ), + test( + "keys", + Iter.toArray(Map.keys(buildTestMap())), + M.equals(T.array(T.natTestable, [])) + ), + test( + "vals", + Iter.toArray(Map.vals(buildTestMap())), + M.equals(T.array(T.textTestable, [])) + ), + test( + "empty from iter", + natMapOps.fromIter(Iter.fromArray([])), + MapMatcher([]) + ), + test( + "get absent", + natMapOps.get(buildTestMap(), 0), + M.equals(T.optional(T.textTestable, null : ?Text)) + ), + test( + "remove absent", + natMapOps.remove(buildTestMap(), 0).1, + M.equals(T.optional(T.textTestable, null : ?Text)) + ), + test( + "replace absent/no value", + natMapOps.replace(buildTestMap(), 0, "Test").1, + M.equals(T.optional(T.textTestable, null : ?Text)) + ), + test( + "replace absent/key appeared", + natMapOps.replace(buildTestMap(), 0, "Test").0, + MapMatcher([(0, "Test")]) + ), + test( + "empty right fold keys", + Map.foldRight(buildTestMap(), "", concatenateKeys), + M.equals(T.text("")) + ), + test( + "empty left fold keys", + Map.foldLeft(buildTestMap(), "", concatenateKeys), + M.equals(T.text("")) + ), + test( + "empty right fold values", + Map.foldRight(buildTestMap(), "", concatenateValues), + M.equals(T.text("")) + ), + test( + "empty left fold values", + Map.foldLeft(buildTestMap(), "", concatenateValues), + M.equals(T.text("")) + ), + test( + "traverse empty map", + Map.map(buildTestMap(), multiplyKeyAndConcat), + MapMatcher([]) + ), + test( + "empty map filter", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(0, multiplyKeyAndConcat)), + MapMatcher([]) + ), + ] + ) +); + +/* --------------------------------------- */ + +buildTestMap := func() : Map.Map { + insert(Map.empty(), 0); +}; + +var expected = expectedEntries([0]); + +run( + suite( + "single root", + [ + test( + "size", + Map.size(buildTestMap()), + M.equals(T.nat(1)) + ), + test( + "iterate forward", + Iter.toArray(Map.iter(buildTestMap(), #fwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, expected)) + ), + test( + "iterate backward", + Iter.toArray(Map.iter(buildTestMap(), #bwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, expected)) + ), + test( + "entries", + Iter.toArray(Map.entries(buildTestMap())), + M.equals(T.array<(Nat, Text)>(entryTestable, expected)) + ), + test( + "keys", + Iter.toArray(Map.keys(buildTestMap())), + M.equals(T.array(T.natTestable, [0])) + ), + test( + "vals", + Iter.toArray(Map.vals(buildTestMap())), + M.equals(T.array(T.textTestable, ["0"])) + ), + test( + "from iter", + natMapOps.fromIter(Iter.fromArray(expected)), + MapMatcher(expected) + ), + test( + "get", + natMapOps.get(buildTestMap(), 0), + M.equals(T.optional(T.textTestable, ?"0")) + ), + test( + "replace function result", + natMapOps.replace(buildTestMap(), 0, "TEST").1, + M.equals(T.optional(T.textTestable, ?"0")) + ), + test( + "replace map result", + do { + let rbMap = buildTestMap(); + natMapOps.replace(rbMap, 0, "TEST").0 + }, + MapMatcher([(0, "TEST")]) + ), + test( + "remove function result", + natMapOps.remove(buildTestMap(), 0).1, + M.equals(T.optional(T.textTestable, ?"0")) + ), + test( + "remove map result", + do { + var rbMap = buildTestMap(); + rbMap := natMapOps.remove(rbMap, 0).0; + checkMap(rbMap); + rbMap + }, + MapMatcher([]) + ), + test( + "right fold keys", + Map.foldRight(buildTestMap(), "", concatenateKeys), + M.equals(T.text("0")) + ), + test( + "left fold keys", + Map.foldLeft(buildTestMap(), "", concatenateKeys), + M.equals(T.text("0")) + ), + test( + "right fold values", + Map.foldRight(buildTestMap(), "", concatenateValues), + M.equals(T.text("0")) + ), + test( + "left fold values", + Map.foldLeft(buildTestMap(), "", concatenateValues), + M.equals(T.text("0")) + ), + test( + "traverse map", + Map.map(buildTestMap(), multiplyKeyAndConcat), + MapMatcher([(0, "00")]) + ), + test( + "map filter/filter all", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(0, multiplyKeyAndConcat)), + MapMatcher([]) + ), + test( + "map filter/no filer", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(1, multiplyKeyAndConcat)), + MapMatcher([(0, "00")]) + ), + ] + ) +); + +/* --------------------------------------- */ + +expected := expectedEntries([0, 1, 2]); + +func rebalanceTests(buildTestMap : () -> Map.Map) : [Suite.Suite] = + [ + test( + "size", + Map.size(buildTestMap()), + M.equals(T.nat(3)) + ), + test( + "map match", + buildTestMap(), + MapMatcher(expected) + ), + test( + "iterate forward", + Iter.toArray(Map.iter(buildTestMap(), #fwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, expected)) + ), + test( + "iterate backward", + Iter.toArray(Map.iter(buildTestMap(), #bwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, Array.reverse(expected))) + ), + test( + "entries", + Iter.toArray(Map.entries(buildTestMap())), + M.equals(T.array<(Nat, Text)>(entryTestable, expected)) + ), + test( + "keys", + Iter.toArray(Map.keys(buildTestMap())), + M.equals(T.array(T.natTestable, [0, 1, 2])) + ), + test( + "vals", + Iter.toArray(Map.vals(buildTestMap())), + M.equals(T.array(T.textTestable, ["0", "1", "2"])) + ), + test( + "from iter", + natMapOps.fromIter(Iter.fromArray(expected)), + MapMatcher(expected) + ), + test( + "get all", + do { + let rbMap = buildTestMap(); + getAll(rbMap, [0, 1, 2]); + rbMap + }, + MapMatcher(expected) + ), + test( + "clear", + clear(buildTestMap()), + MapMatcher([]) + ), + test( + "right fold keys", + Map.foldRight(buildTestMap(), "", concatenateKeys), + M.equals(T.text("210")) + ), + test( + "left fold keys", + Map.foldLeft(buildTestMap(), "", concatenateKeys), + M.equals(T.text("012")) + ), + test( + "right fold values", + Map.foldRight(buildTestMap(), "", concatenateValues), + M.equals(T.text("210")) + ), + test( + "left fold values", + Map.foldLeft(buildTestMap(), "", concatenateValues), + M.equals(T.text("012")) + ), + test( + "traverse map", + Map.map(buildTestMap(), multiplyKeyAndConcat), + MapMatcher([(0, "00"), (1, "21"), (2, "42")]) + ), + test( + "map filter/filter all", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(0, multiplyKeyAndConcat)), + MapMatcher([]) + ), + test( + "map filter/filter one", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(1, multiplyKeyAndConcat)), + MapMatcher([(0, "00")]) + ), + test( + "map filter/no filer", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(3, multiplyKeyAndConcat)), + MapMatcher([(0, "00"), (1, "21"), (2, "42")]) + ), + ]; + +buildTestMap := func() : Map.Map { + var rbMap = Map.empty() : Map.Map; + rbMap := insert(rbMap, 2); + rbMap := insert(rbMap, 1); + rbMap := insert(rbMap, 0); + rbMap +}; + +run(suite("rebalance left, left", rebalanceTests(buildTestMap))); + +/* --------------------------------------- */ + +buildTestMap := func() : Map.Map { + var rbMap = Map.empty() : Map.Map; + rbMap := insert(rbMap, 2); + rbMap := insert(rbMap, 0); + rbMap := insert(rbMap, 1); + rbMap +}; + +run(suite("rebalance left, right", rebalanceTests(buildTestMap))); + +/* --------------------------------------- */ + +buildTestMap := func() : Map.Map { + var rbMap = Map.empty() : Map.Map; + rbMap := insert(rbMap, 0); + rbMap := insert(rbMap, 2); + rbMap := insert(rbMap, 1); + rbMap +}; + +run(suite("rebalance right, left", rebalanceTests(buildTestMap))); + +/* --------------------------------------- */ + +buildTestMap := func() : Map.Map { + var rbMap = Map.empty() : Map.Map; + rbMap := insert(rbMap, 0); + rbMap := insert(rbMap, 1); + rbMap := insert(rbMap, 2); + rbMap +}; + +run(suite("rebalance right, right", rebalanceTests(buildTestMap))); + +/* --------------------------------------- */ + +run( + suite( + "repeated operations", + [ + test( + "repeated insert", + do { + var rbMap = buildTestMap(); + assert (natMapOps.get(rbMap, 1) == ?"1"); + rbMap := natMapOps.put(rbMap, 1, "TEST-1"); + natMapOps.get(rbMap, 1) + }, + M.equals(T.optional(T.textTestable, ?"TEST-1")) + ), + test( + "repeated replace", + do { + let rbMap0 = buildTestMap(); + let (rbMap1, firstResult) = natMapOps.replace(rbMap0, 1, "TEST-1"); + assert (firstResult == ?"1"); + let (rbMap2, secondResult) = natMapOps.replace(rbMap1, 1, "1"); + assert (secondResult == ?"TEST-1"); + rbMap2 + }, + MapMatcher(expected) + ), + test( + "repeated remove", + do { + var rbMap0 = buildTestMap(); + let (rbMap1, result) = natMapOps.remove(rbMap0, 1); + assert (result == ?"1"); + checkMap(rbMap1); + natMapOps.remove(rbMap1, 1).1 + }, + M.equals(T.optional(T.textTestable, null : ?Text)) + ), + test( + "repeated delete", + do { + var rbMap = buildTestMap(); + rbMap := natMapOps.delete(rbMap, 1); + natMapOps.delete(rbMap, 1) + }, + MapMatcher(expectedEntries([0, 2])) + ) + ] + ) +); From 92925abb10d27029c30e4bb2df07cd62376eb7a5 Mon Sep 17 00:00:00 2001 From: Pavel Golovin Date: Mon, 23 Sep 2024 20:53:13 +0200 Subject: [PATCH 05/20] [DMS-67] add property-based tests for PersistentOrderedMap --- test/PersistentOrderedMap.prop.test.mo | 240 +++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 test/PersistentOrderedMap.prop.test.mo diff --git a/test/PersistentOrderedMap.prop.test.mo b/test/PersistentOrderedMap.prop.test.mo new file mode 100644 index 00000000..b2b553cd --- /dev/null +++ b/test/PersistentOrderedMap.prop.test.mo @@ -0,0 +1,240 @@ +// @testmode wasi + +import Map "../src/PersistentOrderedMap"; +import Nat "../src/Nat"; +import Iter "../src/Iter"; +import Debug "../src/Debug"; +import Array "../src/Array"; +import Option "../src/Option"; + +import Suite "mo:matchers/Suite"; +import T "mo:matchers/Testable"; +import M "mo:matchers/Matchers"; + +import Random2 "mo:base/Random"; + +let { run; test; suite } = Suite; + +class MapMatcher(expected : Map.Map) : M.Matcher> { + public func describeMismatch(actual : Map.Map, _description : M.Description) { + Debug.print(debug_show (Iter.toArray(Map.entries(actual))) # " should be " # debug_show (Iter.toArray(Map.entries(expected)))) + }; + + public func matches(actual : Map.Map) : Bool { + Iter.toArray(Map.entries(actual)) == Iter.toArray(Map.entries(expected)) + } +}; + +object Random { + var number = 4711; + public func next() : Nat { + number := (15485863 * number + 5) % 15485867; + number + }; + + public func nextNat(range: (Nat, Nat)): Nat { + let n = next(); + let v = n % (range.1 - range.0 + 1) + range.0; + v + }; + + public func nextEntries(range: (Nat, Nat), size: Nat): [(Nat, Text)] { + Array.tabulate<(Nat, Text)>(size, func(_ix) { + let key = nextNat(range); (key, debug_show(key)) } ) + } +}; + +let natMap = Map.MapOps(Nat.compare); + +func mapGen(samples_number: Nat, size: Nat, range: (Nat, Nat)): Iter.Iter> { + object { + var n = 0; + public func next(): ?Map.Map { + n += 1; + if (n > samples_number) { + null + } else { + ?natMap.fromIter(Random.nextEntries(range, size).vals()) + } + } + } +}; + + +func run_all_props(range: (Nat, Nat), size: Nat, map_samples: Nat, query_samples: Nat) { + func prop(name: Text, f: Map.Map -> Bool): Suite.Suite { + var error_msg: Text = ""; + test(name, do { + var error = true; + label stop for(map in mapGen(map_samples, size, range)) { + if (not f(map)) { + error_msg := "Property \"" # name # "\" failed\n"; + error_msg #= "\n m: " # debug_show(Iter.toArray(Map.entries(map))); + break stop; + } + }; + error_msg + }, M.describedAs(error_msg, M.equals(T.text("")))) + }; + func prop_with_key(name: Text, f: (Map.Map, Nat) -> Bool): Suite.Suite { + var error_msg: Text = ""; + test(name, do { + label stop for(map in mapGen(map_samples, size, range)) { + for (_query_ix in Iter.range(0, query_samples-1)) { + let key = Random.nextNat(range); + if (not f(map, key)) { + error_msg #= "Property \"" # name # "\" failed"; + error_msg #= "\n m: " # debug_show(Iter.toArray(Map.entries(map))); + error_msg #= "\n k: " # debug_show(key); + break stop; + } + } + }; + error_msg + }, M.describedAs(error_msg, M.equals(T.text("")))) + }; + run( + suite("Property tests", + [ + suite("empty", [ + test("get(empty(), k) == null", label res : Bool { + for (_query_ix in Iter.range(0, query_samples-1)) { + let k = Random.nextNat(range); + if(natMap.get(Map.empty(), k) != null) + break res(false); + }; + true; + }, M.equals(T.bool(true))) + ]), + + suite("get & put", [ + prop_with_key("get(put(m, k, v), k) == ?v", func (m, k) { + natMap.get(natMap.put(m, k, "v"), k) == ?"v" + }), + prop_with_key("get(put(put(m, k, v1), k, v2), k) == ?v2", func (m, k) { + let (v1, v2) = ("V1", "V2"); + natMap.get(natMap.put(natMap.put(m, k, v1), k, v2), k) == v2 + }), + ]), + + suite("replace", [ + prop_with_key("replace(m, k, v).0 == put(m, k, v)", func (m, k) { + natMap.replace(m, k, "v").0 == natMap.put(m, k, "v") + }), + prop_with_key("replace(put(m, k, v1), k, v2).1 == ?v1", func (m, k) { + natMap.replace(natMap.put(m, k, "v1"), k, "v2").1 == ?"v1" + }), + prop_with_key("get(m, k) == null ==> replace(m, k, v).1 == null", func (m, k) { + if (natMap.get(m, k) == null) { + natMap.replace(m, k, "v").1 == null + } else { true } + }), + ]), + + suite("delete", [ + prop_with_key("get(m, k) == null ==> delete(m, k) == m", func (m, k) { + if (natMap.get(m, k) == null) { + MapMatcher(m).matches(natMap.delete(m, k)) + } else { true } + }), + prop_with_key("delete(put(m, k, v), k) == m", func (m, k) { + if (natMap.get(m, k) == null) { + MapMatcher(m).matches(natMap.delete(natMap.put(m, k, "v"), k)) + } else { true } + }), + prop_with_key("delete(delete(m, k), k)) == delete(m, k)", func (m, k) { + let m1 = natMap.delete(natMap.delete(m, k), k); + let m2 = natMap.delete(m, k); + MapMatcher(m2).matches(m1) + }) + ]), + + suite("remove", [ + prop_with_key("remove(m, k).0 == delete(m, k)", func (m, k) { + let m1 = natMap.remove(m, k).0; + let m2 = natMap.delete(m, k); + MapMatcher(m2).matches(m1) + }), + prop_with_key("remove(put(m, k, v), k).1 == ?v", func (m, k) { + natMap.remove(natMap.put(m, k, "v"), k).1 == ?"v" + }), + prop_with_key("remove(remove(m, k).0, k).1 == null", func (m, k) { + natMap.remove(natMap.remove(m, k).0, k).1 == null + }), + prop_with_key("put(remove(m, k).0, k, remove(m, k).1) == m", func (m, k) { + if (natMap.get(m, k) != null) { + MapMatcher(m).matches(natMap.put(natMap.remove(m, k).0, k, Option.get(natMap.remove(m, k).1, ""))) + } else { true } + }) + ]), + + suite("size", [ + prop_with_key("size(put(m, k, v)) == size(m) + int(get(m, k) == null)", func (m, k) { + Map.size(natMap.put(m, k, "v")) == Map.size(m) + (if (natMap.get(m, k) == null) {1} else {0}) + }), + prop_with_key("size(delete(m, k)) + int(get(m, k) != null) == size(m)", func (m, k) { + Map.size(natMap.delete(m, k)) + (if (natMap.get(m, k) != null) {1} else {0}) == Map.size(m) + }) + ]), + + suite("iter,keys,vals,entries", [ + prop("fromIter(iter(m, #fwd)) == m", func (m) { + MapMatcher(m).matches(natMap.fromIter(Map.iter(m, #fwd))) + }), + prop("fromIter(iter(m, #bwd)) == m", func (m) { + MapMatcher(m).matches(natMap.fromIter(Map.iter(m, #bwd))) + }), + prop("iter(m, #fwd) = zip(key(m), vals(m))", func (m) { + let k = Map.keys(m); + let v = Map.vals(m); + for (e in Map.iter(m, #fwd)) { + if (e.0 != k.next() or e.1 != v.next()) + return false; + }; + return true; + }), + prop("entries(m) == iter(m, #fwd)", func (m) { + let it = Map.iter(m, #fwd); + for (e in Map.entries(m)) { + if (it.next() != e) + return false; + }; + return true + }) + ]), + + suite("mapFilter", [ + prop_with_key("get(mapFilter(m, (!=k)), k) == null", func (m, k) { + natMap.get(natMap.mapFilter(m, + func (ki, vi) { if (ki != k) {?vi} else {null}}), k) == null + }), + prop_with_key("get(mapFilter(put(m, k, v), (==k)), k) == ?v", func (m, k) { + natMap.get(natMap.mapFilter(natMap.put(m, k, "v"), + func (ki, vi) { if (ki == k) {?vi} else {null}}), k) == ?"v" + }) + ]), + + suite("map", [ + prop("map(m, id) == m", func (m) { + MapMatcher(m).matches(Map.map(m, func (k, v) {v})) + }) + ]), + + suite("folds", [ + prop("foldLeft as iter(#fwd)", func (m) { + let it = Map.iter(m, #fwd); + Map.foldLeft(m, true, func (k, v, acc) {acc and it.next() == ?(k, v)}) + }), + prop("foldRight as iter(#bwd)", func(m) { + let it = Map.iter(m, #bwd); + Map.foldRight(m, true, func (k, v, acc) {acc and it.next() == ?(k, v)}) + }) + ]), + ])) +}; + +run_all_props((1, 3), 0, 1, 10); +run_all_props((1, 5), 5, 100, 100); +run_all_props((1, 10), 10, 100, 100); +run_all_props((1, 100), 20, 100, 100); +run_all_props((1, 1000), 100, 100, 100); \ No newline at end of file From af994d6b440b329830c5d9e3d77cbf4ca45630a7 Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Tue, 24 Sep 2024 18:33:23 +0400 Subject: [PATCH 06/20] [DMS-77] Add usage examples to the documentation --- src/PersistentOrderedMap.mo | 368 +++++++++++++++++++++++++++++++++++- 1 file changed, 358 insertions(+), 10 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 73f507b8..72ae82c2 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -1,4 +1,4 @@ -/// Key-value map implemented as a red-black tree (RBTree) with nodes storing key-value pairs. +/// Stable key-value map implemented as a red-black tree with nodes storing key-value pairs. /// /// A red-black tree is a balanced binary search tree ordered by the keys. /// @@ -53,20 +53,100 @@ module { /// /// The object should be created once, then used for all the operations /// with `Map` to maintain invariant that comparator did not changed. + /// + /// `MapOps` contains methods that require `compare` internally: + /// operations that may reshape a `Map` or should find something. public class MapOps(compare : (K,K) -> O.Order) { /// Returns a new map, containing all entries given by the iterator `i`. /// If there are multiple entries with the same key the last one is taken. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap)))); + /// + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// ``` + /// + /// Runtime: `O(n * log(n))`. + /// Space: `O(n)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the tree and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(n * log(n))` temporary objects that will be collected as garbage. public func fromIter(i : I.Iter<(K,V)>) : Map = Internal.fromIter(i, compare); /// Insert the value `value` with key `key` into map `rbMap`. Overwrites any existing entry with key `key`. /// Returns a modified map. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// var rbMap = Map.empty(); + /// + /// rbMap := mapOps.put(rbMap, 0, "Zero"); + /// rbMap := mapOps.put(rbMap, 2, "Two"); + /// rbMap := mapOps.put(rbMap, 1, "One"); + /// + /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap)))); + /// + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the tree and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. public func put(rbMap : Map, key : K, value : V) : Map = Internal.put(rbMap, compare, key, value); /// Insert the value `value` with key `key` into `rbMap`. Returns modified map and /// the previous value associated with key `key` or `null` if no such value exists. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap0 = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// let (rbMap1, old1) = mapOps.replace(rbMap0, 0, "Nil"); + /// + /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap1)))); + /// Debug.print(debug_show(old1)); + /// // [(0, "Nil"), (1, "One"), (2, "Two")] + /// // ?"Zero" + /// + /// let (rbMap2, old2) = mapOps.replace(rbMap0, 3, "Three"); + /// + /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap2)))); + /// Debug.print(debug_show(old2)); + /// // [(0, "Zero"), (1, "One"), (2, "Two"), (3, "Three")] + /// // null + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the tree and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. public func replace(rbMap : Map, key : K, value : V) : (Map, ?V) = Internal.replace(rbMap, compare, key, value); @@ -74,25 +154,146 @@ module { /// `(k, v)` in the old map, if `f` evaluates to `null`, the entry is discarded. /// Otherwise, the entry is transformed into a new entry `(k, v2)`, where /// the new value `v2` is the result of applying `f` to `(k, v)`. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func f(key : Nat, val : Text) : ?Text { + /// if(key == 0) {null} + /// else { ?("Twenty " # val)} + /// }; + /// + /// let newRbMap = mapOps.mapFilter(rbMap, f); + /// + /// Debug.print(debug_show(Iter.toArray(Map.entries(newRbMap)))); + /// + /// // [(1, "Twenty One"), (2, "Twenty Two")] + /// ``` + /// + /// Runtime: `O(n)`. + /// Space: `O(n)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the tree and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. public func mapFilter(rbMap : Map, f : (K, V1) -> ?V2) : Map = Internal.mapFilter(rbMap, compare, f); /// Get the value associated with key `key` in the given `rbMap` if present and `null` otherwise. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show mapOps.get(rbMap, 1)); + /// Debug.print(debug_show mapOps.get(rbMap, 42)); + /// + /// // ?"One" + /// // null + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the tree and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. public func get(rbMap : Map, key : K) : ?V = Internal.get(rbMap, compare, key); /// Deletes the entry with the key `key` from the `rbMap`. Has no effect if `key` is not /// present in the map. Returns modified map. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(Map.entries(mapOps.delete(rbMap, 1))))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(mapOps.delete(rbMap, 42))))); + /// + /// // [(0, "Zero"), (2, "Two")] + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the tree and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. public func delete(rbMap : Map, key : K) : Map = Internal.delete(rbMap, compare, key); /// Deletes the entry with the key `key`. Returns modified map and the /// previous value associated with key `key` or `null` if no such value exists. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap0 = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// let (rbMap1, old1) = mapOps.remove(rbMap0, 0); + /// + /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap1)))); + /// Debug.print(debug_show(old1)); + /// // [(1, "One"), (2, "Two")] + /// // ?"Zero" + /// + /// let (rbMap2, old2) = mapOps.remove(rbMap0, 42); + /// + /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap2)))); + /// Debug.print(debug_show(old2)); + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// // null + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the tree and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. public func remove(rbMap : Map, key : K) : (Map, ?V) = Internal.remove(rbMap, compare, key); }; + /// Create a new empty map. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// + /// let rbMap = Map.empty(); + /// + /// Debug.print(debug_show(Map.size(rbMap))); + /// + /// // 0 + /// ``` + /// + /// Cost of empty map creation + /// Runtime: `O(1)`. + /// Space: `O(1)` + public func empty() : Map = #leaf; + type IterRep = List.List<{ #tr : Map; #xy : (K, V) }>; public type Direction = { #fwd; #bwd }; @@ -102,7 +303,18 @@ module { /// /// Example: /// ```motoko - /// // Write new examples + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(Map.iter(rbMap, #fwd)))); + /// Debug.print(debug_show(Iter.toArray(Map.iter(rbMap, #bwd)))); + /// + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// // [(2, "Two"), (1, "One"), (0, "Zero")] /// ``` /// /// Cost of iteration over all elements: @@ -138,29 +350,110 @@ module { } }; - /// Create a new empty map. - public func empty() : Map = #leaf; - /// Returns an Iterator (`Iter`) over the key-value pairs in the map. /// Iterator provides a single method `next()`, which returns /// pairs in no specific order, or `null` when out of pairs to iterate over. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap)))); + /// + /// + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// ``` + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. public func entries(m : Map) : I.Iter<(K, V)> = iter(m, #fwd); /// Returns an Iterator (`Iter`) over the keys of the map. /// Iterator provides a single method `next()`, which returns /// keys in no specific order, or `null` when out of keys to iterate over. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(Map.keys(rbMap)))); + /// + /// // [0, 1, 2] + /// ``` + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. public func keys(m : Map) : I.Iter = I.map(entries(m), func(kv : (K, V)) : K {kv.0}); /// Returns an Iterator (`Iter`) over the values of the map. /// Iterator provides a single method `next()`, which returns /// values in no specific order, or `null` when out of values to iterate over. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(Map.vals(rbMap)))); + /// + /// // ["Zero", "One", "Two"] + /// ``` + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. public func vals(m : Map) : I.Iter = I.map(entries(m), func(kv : (K, V)) : V {kv.1}); /// Creates a new map by applying `f` to each entry in `rbMap`. Each entry /// `(k, v)` in the old map is transformed into a new entry `(k, v2)`, where /// the new value `v2` is created by applying `f` to `(k, v)`. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func f(key : Nat, _val : Text) : Nat = key * 2; + /// + /// let resMap = Map.map(rbMap, f); + /// + /// Debug.print(debug_show(Iter.toArray(Map.entries(resMap)))); + /// + /// // [(0, 0), (1, 2), (2, 4)] + /// ``` + /// + /// Cost of mapping all the elements: + /// Runtime: `O(n)`. + /// Space: `O(n)` retained memory + /// where `n` denotes the number of key-value entries stored in the map. public func map(rbMap : Map, f : (K, V1) -> V2) : Map { func mapRec(m : Map) : Map { switch m { @@ -177,14 +470,21 @@ module { /// /// Example: /// ```motoko - /// // Write new examples + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Map.size(rbMap))); + /// + /// // 3 /// ``` /// - /// Runtime: `O(log(n))`. - /// Space: `O(1)` retained memory plus garbage, see the note below. + /// Runtime: `O(n)`. + /// Space: `O(1)`. /// where `n` denotes the number of key-value entries stored in the tree. - /// - /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. public func size(t : Map) : Nat { switch t { case (#leaf) { 0 }; @@ -197,6 +497,30 @@ module { /// Collapses the elements in `rbMap` into a single value by starting with `base` /// and progessively combining keys and values into `base` with `combine`. Iteration runs /// left to right. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func folder(key : Nat, val : Text, accum : (Nat, Text)) : ((Nat, Text)) + /// = (key + accum.0, accum.1 # val); + /// + /// Debug.print(debug_show(Map.foldLeft(rbMap, (0, ""), folder))); + /// + /// // (3, "ZeroOneTwo") + /// ``` + /// + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: depends on `combine` function plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. public func foldLeft( rbMap : Map, base : Accum, @@ -213,6 +537,30 @@ module { /// Collapses the elements in `rbMap` into a single value by starting with `base` /// and progessively combining keys and values into `base` with `combine`. Iteration runs /// right to left. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat" + /// import Iter "mo:base/Iter" + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func folder(key : Nat, val : Text, accum : (Nat, Text)) : ((Nat, Text)) + /// = (key + accum.0, accum.1 # val); + /// + /// Debug.print(debug_show(Map.foldRight(rbMap, (0, ""), folder))); + /// + /// // (3, "TwoOneZero") + /// ``` + /// + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: depends on `combine` function plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. public func foldRight( rbMap : Map, base : Accum, From e04073426110e93790d0079a14ffc5ceb878d0f6 Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Wed, 2 Oct 2024 14:56:44 +0400 Subject: [PATCH 07/20] Specify keys order in iterators --- src/PersistentOrderedMap.mo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 72ae82c2..222b93ea 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -352,7 +352,7 @@ module { /// Returns an Iterator (`Iter`) over the key-value pairs in the map. /// Iterator provides a single method `next()`, which returns - /// pairs in no specific order, or `null` when out of pairs to iterate over. + /// pairs in ascending order by keys, or `null` when out of pairs to iterate over. /// /// Example: /// ```motoko @@ -378,7 +378,7 @@ module { /// Returns an Iterator (`Iter`) over the keys of the map. /// Iterator provides a single method `next()`, which returns - /// keys in no specific order, or `null` when out of keys to iterate over. + /// keys in ascending order, or `null` when out of keys to iterate over. /// /// Example: /// ```motoko From 070673fb0d2f4e4d60d4c8f7c6b72ea2d6b375f3 Mon Sep 17 00:00:00 2001 From: Pavel Golovin Date: Mon, 7 Oct 2024 21:02:16 +0200 Subject: [PATCH 08/20] PersistentOrderedMap: fix typos --- src/PersistentOrderedMap.mo | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 222b93ea..18506243 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -33,7 +33,7 @@ import O "Order"; // #black : (Map, K, V, Map); // #leaf //}; -// (this inlines the colors into the variant, flattens a tuple, and removes a (now) redundant optin, for considerable heap savings.) +// (this inlines the colors into the variant, flattens a tuple, and removes a (now) redundant option, for considerable heap savings.) // It would also make sense to maintain the size in a separate root for 0(1) access. module { @@ -49,10 +49,10 @@ module { #leaf }; - /// Opertaions on `Map`, that require a comparator. + /// Operations on `Map`, that require a comparator. /// /// The object should be created once, then used for all the operations - /// with `Map` to maintain invariant that comparator did not changed. + /// with `Map` to ensure that the same comparator is used for every operation. /// /// `MapOps` contains methods that require `compare` internally: /// operations that may reshape a `Map` or should find something. @@ -495,7 +495,7 @@ module { }; /// Collapses the elements in `rbMap` into a single value by starting with `base` - /// and progessively combining keys and values into `base` with `combine`. Iteration runs + /// and progressively combining keys and values into `base` with `combine`. Iteration runs /// left to right. /// /// Example: @@ -535,7 +535,7 @@ module { }; /// Collapses the elements in `rbMap` into a single value by starting with `base` - /// and progessively combining keys and values into `base` with `combine`. Iteration runs + /// and progressively combining keys and values into `base` with `combine`. Iteration runs /// right to left. /// /// Example: From ef1375909c93762405dd5a970aaaf1d743bda0af Mon Sep 17 00:00:00 2001 From: Pavel Golovin Date: Mon, 14 Oct 2024 18:12:48 +0200 Subject: [PATCH 09/20] PersistentOrderedMap: fix imports in examples --- src/PersistentOrderedMap.mo | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 18506243..5f299c2e 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -66,6 +66,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -92,6 +93,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// var rbMap = Map.empty(); @@ -122,6 +124,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap0 = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -160,6 +163,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -191,6 +195,7 @@ module { /// ```motoko /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -218,6 +223,7 @@ module { /// ```motoko /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -246,6 +252,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap0 = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -281,6 +288,7 @@ module { /// Example: /// ```motoko /// import Map "mo:base/PersistentOrderedMap"; + /// import Debug "mo:base/Debug"; /// /// let rbMap = Map.empty(); /// @@ -306,6 +314,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -359,6 +368,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -385,6 +395,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -411,6 +422,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -437,6 +449,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -473,6 +486,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -503,6 +517,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); @@ -543,6 +558,7 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Nat "mo:base/Nat" /// import Iter "mo:base/Iter" + /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); From d4b02eb6780cc295f81373faaf506d5f1fdc95da Mon Sep 17 00:00:00 2001 From: Pavel Golovin Date: Mon, 21 Oct 2024 17:21:53 +0200 Subject: [PATCH 10/20] PerisistentOrderedMap: fix docs * rename `rbMap` into `m` in signature for brevity & consistent language * rename `rbMap` into `map` in examples for brevity & encapsulation sake * rename `tree` into `map` in doc comments for the encapsulation sake --- src/PersistentOrderedMap.mo | 170 ++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 5f299c2e..b0eef6fd 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -69,23 +69,23 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let m = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap)))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(m)))); /// /// // [(0, "Zero"), (1, "One"), (2, "Two")] /// ``` /// /// Runtime: `O(n * log(n))`. /// Space: `O(n)` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the tree and + /// where `n` denotes the number of key-value entries stored in the map and /// assuming that the `compare` function implements an `O(1)` comparison. /// /// Note: Creates `O(n * log(n))` temporary objects that will be collected as garbage. public func fromIter(i : I.Iter<(K,V)>) : Map = Internal.fromIter(i, compare); - /// Insert the value `value` with key `key` into map `rbMap`. Overwrites any existing entry with key `key`. + /// Insert the value `value` with key `key` into the map `m`. Overwrites any existing entry with key `key`. /// Returns a modified map. /// /// Example: @@ -96,27 +96,27 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// var rbMap = Map.empty(); + /// var map = Map.empty(); /// - /// rbMap := mapOps.put(rbMap, 0, "Zero"); - /// rbMap := mapOps.put(rbMap, 2, "Two"); - /// rbMap := mapOps.put(rbMap, 1, "One"); + /// map := mapOps.put(map, 0, "Zero"); + /// map := mapOps.put(map, 2, "Two"); + /// map := mapOps.put(map, 1, "One"); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap)))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(map)))); /// /// // [(0, "Zero"), (1, "One"), (2, "Two")] /// ``` /// /// Runtime: `O(log(n))`. /// Space: `O(1)` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the tree and + /// where `n` denotes the number of key-value entries stored in the map and /// assuming that the `compare` function implements an `O(1)` comparison. /// /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. - public func put(rbMap : Map, key : K, value : V) : Map - = Internal.put(rbMap, compare, key, value); + public func put(m : Map, key : K, value : V) : Map + = Internal.put(m, compare, key, value); - /// Insert the value `value` with key `key` into `rbMap`. Returns modified map and + /// Insert the value `value` with key `key` into the map `m`. Returns modified map and /// the previous value associated with key `key` or `null` if no such value exists. /// /// Example: @@ -127,18 +127,18 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap0 = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map0 = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// let (rbMap1, old1) = mapOps.replace(rbMap0, 0, "Nil"); + /// let (map1, old1) = mapOps.replace(map0, 0, "Nil"); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap1)))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(map1)))); /// Debug.print(debug_show(old1)); /// // [(0, "Nil"), (1, "One"), (2, "Two")] /// // ?"Zero" /// - /// let (rbMap2, old2) = mapOps.replace(rbMap0, 3, "Three"); + /// let (map2, old2) = mapOps.replace(map0, 3, "Three"); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap2)))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(map2)))); /// Debug.print(debug_show(old2)); /// // [(0, "Zero"), (1, "One"), (2, "Two"), (3, "Three")] /// // null @@ -146,14 +146,14 @@ module { /// /// Runtime: `O(log(n))`. /// Space: `O(1)` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the tree and + /// where `n` denotes the number of key-value entries stored in the map and /// assuming that the `compare` function implements an `O(1)` comparison. /// /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. - public func replace(rbMap : Map, key : K, value : V) : (Map, ?V) - = Internal.replace(rbMap, compare, key, value); + public func replace(m : Map, key : K, value : V) : (Map, ?V) + = Internal.replace(m, compare, key, value); - /// Creates a new map by applying `f` to each entry in `rbMap`. For each entry + /// Creates a new map by applying `f` to each entry in the map `m`. For each entry /// `(k, v)` in the old map, if `f` evaluates to `null`, the entry is discarded. /// Otherwise, the entry is transformed into a new entry `(k, v2)`, where /// the new value `v2` is the result of applying `f` to `(k, v)`. @@ -166,30 +166,30 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// /// func f(key : Nat, val : Text) : ?Text { /// if(key == 0) {null} /// else { ?("Twenty " # val)} /// }; /// - /// let newRbMap = mapOps.mapFilter(rbMap, f); + /// let newMap = mapOps.mapFilter(map, f); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(newRbMap)))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(newMap)))); /// /// // [(1, "Twenty One"), (2, "Twenty Two")] /// ``` /// /// Runtime: `O(n)`. /// Space: `O(n)` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the tree and + /// where `n` denotes the number of key-value entries stored in the map and /// assuming that the `compare` function implements an `O(1)` comparison. /// /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. - public func mapFilter(rbMap : Map, f : (K, V1) -> ?V2) : Map - = Internal.mapFilter(rbMap, compare, f); + public func mapFilter(m : Map, f : (K, V1) -> ?V2) : Map + = Internal.mapFilter(m, compare, f); - /// Get the value associated with key `key` in the given `rbMap` if present and `null` otherwise. + /// Get the value associated with key `key` in the given map `m` if present and `null` otherwise. /// /// Example: /// ```motoko @@ -198,10 +198,10 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// Debug.print(debug_show mapOps.get(rbMap, 1)); - /// Debug.print(debug_show mapOps.get(rbMap, 42)); + /// Debug.print(debug_show mapOps.get(map, 1)); + /// Debug.print(debug_show mapOps.get(map, 42)); /// /// // ?"One" /// // null @@ -209,14 +209,14 @@ module { /// /// Runtime: `O(log(n))`. /// Space: `O(1)` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the tree and + /// where `n` denotes the number of key-value entries stored in the map and /// assuming that the `compare` function implements an `O(1)` comparison. /// /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. - public func get(rbMap : Map, key : K) : ?V - = Internal.get(rbMap, compare, key); + public func get(m : Map, key : K) : ?V + = Internal.get(m, compare, key); - /// Deletes the entry with the key `key` from the `rbMap`. Has no effect if `key` is not + /// Deletes the entry with the key `key` from the map `m`. Has no effect if `key` is not /// present in the map. Returns modified map. /// /// Example: @@ -226,10 +226,10 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(mapOps.delete(rbMap, 1))))); - /// Debug.print(debug_show(Iter.toArray(Map.entries(mapOps.delete(rbMap, 42))))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(mapOps.delete(map, 1))))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(mapOps.delete(map, 42))))); /// /// // [(0, "Zero"), (2, "Two")] /// // [(0, "Zero"), (1, "One"), (2, "Two")] @@ -237,12 +237,12 @@ module { /// /// Runtime: `O(log(n))`. /// Space: `O(1)` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the tree and + /// where `n` denotes the number of key-value entries stored in the map and /// assuming that the `compare` function implements an `O(1)` comparison. /// /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. - public func delete(rbMap : Map, key : K) : Map - = Internal.delete(rbMap, compare, key); + public func delete(m : Map, key : K) : Map + = Internal.delete(m, compare, key); /// Deletes the entry with the key `key`. Returns modified map and the /// previous value associated with key `key` or `null` if no such value exists. @@ -255,18 +255,18 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap0 = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map0 = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// let (rbMap1, old1) = mapOps.remove(rbMap0, 0); + /// let (map1, old1) = mapOps.remove(map0, 0); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap1)))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(map1)))); /// Debug.print(debug_show(old1)); /// // [(1, "One"), (2, "Two")] /// // ?"Zero" /// - /// let (rbMap2, old2) = mapOps.remove(rbMap0, 42); + /// let (map2, old2) = mapOps.remove(map0, 42); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap2)))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(map2)))); /// Debug.print(debug_show(old2)); /// // [(0, "Zero"), (1, "One"), (2, "Two")] /// // null @@ -274,12 +274,12 @@ module { /// /// Runtime: `O(log(n))`. /// Space: `O(1)` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the tree and + /// where `n` denotes the number of key-value entries stored in the map and /// assuming that the `compare` function implements an `O(1)` comparison. /// /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. - public func remove(rbMap : Map, key : K) : (Map, ?V) - = Internal.remove(rbMap, compare, key); + public func remove(m : Map, key : K) : (Map, ?V) + = Internal.remove(m, compare, key); }; @@ -290,9 +290,9 @@ module { /// import Map "mo:base/PersistentOrderedMap"; /// import Debug "mo:base/Debug"; /// - /// let rbMap = Map.empty(); + /// let map = Map.empty(); /// - /// Debug.print(debug_show(Map.size(rbMap))); + /// Debug.print(debug_show(Map.size(map))); /// /// // 0 /// ``` @@ -306,7 +306,7 @@ module { public type Direction = { #fwd; #bwd }; - /// Get an iterator for the entries of the `rbMap`, in ascending (`#fwd`) or descending (`#bwd`) order as specified by `direction`. + /// Get an iterator for the entries of the map `m`, in ascending (`#fwd`) or descending (`#bwd`) order as specified by `direction`. /// The iterator takes a snapshot view of the map and is not affected by concurrent modifications. /// /// Example: @@ -317,10 +317,10 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// Debug.print(debug_show(Iter.toArray(Map.iter(rbMap, #fwd)))); - /// Debug.print(debug_show(Iter.toArray(Map.iter(rbMap, #bwd)))); + /// Debug.print(debug_show(Iter.toArray(Map.iter(map, #fwd)))); + /// Debug.print(debug_show(Iter.toArray(Map.iter(map, #bwd)))); /// /// // [(0, "Zero"), (1, "One"), (2, "Two")] /// // [(2, "Two"), (1, "One"), (0, "Zero")] @@ -332,9 +332,9 @@ module { /// where `n` denotes the number of key-value entries stored in the map. /// /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. - public func iter(rbMap : Map, direction : Direction) : I.Iter<(K, V)> { + public func iter(m : Map, direction : Direction) : I.Iter<(K, V)> { object { - var trees : IterRep = ?(#tr(rbMap), null); + var trees : IterRep = ?(#tr(m), null); public func next() : ?(K, V) { switch (direction, trees) { case (_, null) { null }; @@ -371,9 +371,9 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(rbMap)))); + /// Debug.print(debug_show(Iter.toArray(Map.entries(map)))); /// /// /// // [(0, "Zero"), (1, "One"), (2, "Two")] @@ -398,9 +398,9 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// Debug.print(debug_show(Iter.toArray(Map.keys(rbMap)))); + /// Debug.print(debug_show(Iter.toArray(Map.keys(map)))); /// /// // [0, 1, 2] /// ``` @@ -425,9 +425,9 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// Debug.print(debug_show(Iter.toArray(Map.vals(rbMap)))); + /// Debug.print(debug_show(Iter.toArray(Map.vals(map)))); /// /// // ["Zero", "One", "Two"] /// ``` @@ -440,7 +440,7 @@ module { public func vals(m : Map) : I.Iter = I.map(entries(m), func(kv : (K, V)) : V {kv.1}); - /// Creates a new map by applying `f` to each entry in `rbMap`. Each entry + /// Creates a new map by applying `f` to each entry in the map `m`. Each entry /// `(k, v)` in the old map is transformed into a new entry `(k, v2)`, where /// the new value `v2` is created by applying `f` to `(k, v)`. /// @@ -452,11 +452,11 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let m = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// /// func f(key : Nat, _val : Text) : Nat = key * 2; /// - /// let resMap = Map.map(rbMap, f); + /// let resMap = Map.map(m, f); /// /// Debug.print(debug_show(Iter.toArray(Map.entries(resMap)))); /// @@ -467,7 +467,7 @@ module { /// Runtime: `O(n)`. /// Space: `O(n)` retained memory /// where `n` denotes the number of key-value entries stored in the map. - public func map(rbMap : Map, f : (K, V1) -> V2) : Map { + public func map(m : Map, f : (K, V1) -> V2) : Map { func mapRec(m : Map) : Map { switch m { case (#leaf) { #leaf }; @@ -476,10 +476,10 @@ module { }; } }; - mapRec(rbMap) + mapRec(m) }; - /// Determine the size of the tree as the number of key-value entries. + /// Determine the size of the map as the number of key-value entries. /// /// Example: /// ```motoko @@ -489,18 +489,18 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// Debug.print(debug_show(Map.size(rbMap))); + /// Debug.print(debug_show(Map.size(map))); /// /// // 3 /// ``` /// /// Runtime: `O(n)`. /// Space: `O(1)`. - /// where `n` denotes the number of key-value entries stored in the tree. - public func size(t : Map) : Nat { - switch t { + /// where `n` denotes the number of key-value entries stored in the map. + public func size(m : Map) : Nat { + switch m { case (#leaf) { 0 }; case (#node(_, l, _, r)) { size(l) + size(r) + 1 @@ -508,7 +508,7 @@ module { } }; - /// Collapses the elements in `rbMap` into a single value by starting with `base` + /// Collapses the elements in `map` into a single value by starting with `base` /// and progressively combining keys and values into `base` with `combine`. Iteration runs /// left to right. /// @@ -520,12 +520,12 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// /// func folder(key : Nat, val : Text, accum : (Nat, Text)) : ((Nat, Text)) /// = (key + accum.0, accum.1 # val); /// - /// Debug.print(debug_show(Map.foldLeft(rbMap, (0, ""), folder))); + /// Debug.print(debug_show(Map.foldLeft(map, (0, ""), folder))); /// /// // (3, "ZeroOneTwo") /// ``` @@ -537,19 +537,19 @@ module { /// /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. public func foldLeft( - rbMap : Map, + map : Map, base : Accum, combine : (Key, Value, Accum) -> Accum ) : Accum { var acc = base; - for(val in iter(rbMap, #fwd)){ + for(val in iter(map, #fwd)){ acc := combine(val.0, val.1, acc); }; acc }; - /// Collapses the elements in `rbMap` into a single value by starting with `base` + /// Collapses the elements in `map` into a single value by starting with `base` /// and progressively combining keys and values into `base` with `combine`. Iteration runs /// right to left. /// @@ -561,12 +561,12 @@ module { /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); - /// let rbMap = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// /// func folder(key : Nat, val : Text, accum : (Nat, Text)) : ((Nat, Text)) /// = (key + accum.0, accum.1 # val); /// - /// Debug.print(debug_show(Map.foldRight(rbMap, (0, ""), folder))); + /// Debug.print(debug_show(Map.foldRight(map, (0, ""), folder))); /// /// // (3, "TwoOneZero") /// ``` @@ -578,13 +578,13 @@ module { /// /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. public func foldRight( - rbMap : Map, + map : Map, base : Accum, combine : (Key, Value, Accum) -> Accum ) : Accum { var acc = base; - for(val in iter(rbMap, #bwd)){ + for(val in iter(map, #bwd)){ acc := combine(val.0, val.1, acc); }; acc From 26a33c1c9008009ae0ac61649cac96e68969d71f Mon Sep 17 00:00:00 2001 From: Pavel Golovin Date: Thu, 24 Oct 2024 18:56:35 +0200 Subject: [PATCH 11/20] PersistentOrderedMap: fix newline in prop tests --- test/PersistentOrderedMap.prop.test.mo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PersistentOrderedMap.prop.test.mo b/test/PersistentOrderedMap.prop.test.mo index b2b553cd..bc55f012 100644 --- a/test/PersistentOrderedMap.prop.test.mo +++ b/test/PersistentOrderedMap.prop.test.mo @@ -237,4 +237,4 @@ run_all_props((1, 3), 0, 1, 10); run_all_props((1, 5), 5, 100, 100); run_all_props((1, 10), 10, 100, 100); run_all_props((1, 100), 20, 100, 100); -run_all_props((1, 1000), 100, 100, 100); \ No newline at end of file +run_all_props((1, 1000), 100, 100, 100); From 35b48b1bcc41f6038b15ee3c9485f4ff400ba2db Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Fri, 11 Oct 2024 11:41:53 +0400 Subject: [PATCH 12/20] Reuse iteration code and parametrize it with map walking function --- src/PersistentOrderedMap.mo | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index b0eef6fd..1abd1304 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -333,30 +333,39 @@ module { /// /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. public func iter(m : Map, direction : Direction) : I.Iter<(K, V)> { - object { - var trees : IterRep = ?(#tr(m), null); - public func next() : ?(K, V) { - switch (direction, trees) { - case (_, null) { null }; - case (_, ?(#tr(#leaf), ts)) { + let turnLeftFirst : MapTraverser + = func (l, xy, r, ts) { ?(#tr(l), ?(#xy(xy), ?(#tr(r), ts))) }; + + let turnRightFirst : MapTraverser + = func (l, xy, r, ts) { ?(#tr(r), ?(#xy(xy), ?(#tr(l), ts))) }; + + switch direction { + case (#fwd) IterMap(m, turnLeftFirst); + case (#bwd) IterMap(m, turnRightFirst) + } + }; + + type MapTraverser = (Map, (K, V), Map, IterRep) -> IterRep; + + class IterMap(m : Map, mapTraverser : MapTraverser) { + var trees : IterRep = ?(#tr(m), null); + public func next() : ?(K, V) { + switch (trees) { + case (null) { null }; + case (?(#tr(#leaf), ts)) { trees := ts; next() }; - case (_, ?(#xy(xy), ts)) { + case (?(#xy(xy), ts)) { trees := ts; ?xy - }; // TODO: Let's float-out case on direction - case (#fwd, ?(#tr(#node(_, l, xy, r)), ts)) { - trees := ?(#tr(l), ?(#xy(xy), ?(#tr(r), ts))); - next() }; - case (#bwd, ?(#tr(#node(_, l, xy, r)), ts)) { - trees := ?(#tr(r), ?(#xy(xy), ?(#tr(l), ts))); + case (?(#tr(#node(_, l, xy, r)), ts)) { + trees := mapTraverser(l, xy, r, ts); next() } } } - } }; /// Returns an Iterator (`Iter`) over the key-value pairs in the map. From f665f1dcce857171030cba69df11a4d5bca8d493 Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Wed, 9 Oct 2024 12:53:18 +0400 Subject: [PATCH 13/20] Use direct recursion in folding functions --- src/PersistentOrderedMap.mo | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 1abd1304..8c44a0cd 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -551,11 +551,14 @@ module { combine : (Key, Value, Accum) -> Accum ) : Accum { - var acc = base; - for(val in iter(map, #fwd)){ - acc := combine(val.0, val.1, acc); - }; - acc + switch (map) { + case (#leaf) { base }; + case (#node(_, l, (k, v), r)) { + let left = foldLeft(l, base, combine); + let middle = combine(k, v, left); + foldLeft(r, middle, combine) + } + } }; /// Collapses the elements in `map` into a single value by starting with `base` @@ -592,11 +595,14 @@ module { combine : (Key, Value, Accum) -> Accum ) : Accum { - var acc = base; - for(val in iter(map, #bwd)){ - acc := combine(val.0, val.1, acc); - }; - acc + switch (map) { + case (#leaf) { base }; + case (#node(_, l, (k, v), r)) { + let right = foldRight(r, base, combine); + let middle = combine(k, v, right); + foldRight(l, middle, combine) + } + } }; From e15a3f268da1cf3554c25eb5be1d2b1bd85cce2f Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Wed, 9 Oct 2024 16:26:50 +0400 Subject: [PATCH 14/20] Implement mapFilter using foldLeft instad of iter --- src/PersistentOrderedMap.mo | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 8c44a0cd..6651e2cb 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -618,19 +618,15 @@ module { }; public func mapFilter(t : Map, compare : (K, K) -> O.Order, f : (K, V1) -> ?V2) : Map{ - var map = #leaf : Map; - for(kv in iter(t, #fwd)) - { - switch(f kv){ - case null {}; - case (?v1) { - // The keys still are monotonic, so we can - // merge trees using `append` and avoid compare here - map := put(map, compare, kv.0, v1); + func combine(key : K, value1 : V1, acc : Map) : Map { + switch (f(key, value1)){ + case null { acc }; + case (?value2) { + put(acc, compare, key, value2) } } }; - map + foldLeft(t, #leaf, combine) }; public func get(t : Map, compare : (K, K) -> O.Order, x : K) : ?V { From b29beea2c37169809057c52664819a9cf6ae967d Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Fri, 11 Oct 2024 12:58:08 +0400 Subject: [PATCH 15/20] Inline color field into map constructors --- src/PersistentOrderedMap.mo | 194 +++++++++++++++++------------- test/PersistentOrderedMap.test.mo | 41 ++++--- 2 files changed, 130 insertions(+), 105 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 6651e2cb..ec752ca9 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -38,14 +38,12 @@ import O "Order"; module { - /// Node color: Either red (`#R`) or black (`#B`). - public type Color = { #R; #B }; - /// Red-black tree of nodes with key-value entries, ordered by the keys. /// The keys have the generic type `K` and the values the generic type `V`. /// Leaves are considered implicitly black. public type Map = { - #node : (Color, Map, (K, V), Map); + #red : (Map, (K, V), Map); + #black : (Map, (K, V), Map); #leaf }; @@ -360,7 +358,11 @@ module { trees := ts; ?xy }; - case (?(#tr(#node(_, l, xy, r)), ts)) { + case (?(#tr(#red(l, xy, r)), ts)) { + trees := mapTraverser(l, xy, r, ts); + next() + }; + case (?(#tr(#black(l, xy, r)), ts)) { trees := mapTraverser(l, xy, r, ts); next() } @@ -480,8 +482,11 @@ module { func mapRec(m : Map) : Map { switch m { case (#leaf) { #leaf }; - case (#node(c, l, xy, r)) { - #node(c, mapRec l, (xy.0, f xy), mapRec r) // TODO: try destination-passing style to avoid non tail-call recursion + case (#red(l, xy, r)) { + #red(mapRec l, (xy.0, f xy), mapRec r) + }; + case (#black(l, xy, r)) { + #black(mapRec l, (xy.0, f xy), mapRec r) }; } }; @@ -511,7 +516,10 @@ module { public func size(m : Map) : Nat { switch m { case (#leaf) { 0 }; - case (#node(_, l, _, r)) { + case (#red(l, _, r)) { + size(l) + size(r) + 1 + }; + case (#black(l, _, r)) { size(l) + size(r) + 1 } } @@ -553,7 +561,12 @@ module { { switch (map) { case (#leaf) { base }; - case (#node(_, l, (k, v), r)) { + case (#red(l, (k, v), r)) { + let left = foldLeft(l, base, combine); + let middle = combine(k, v, left); + foldLeft(r, middle, combine) + }; + case (#black(l, (k, v), r)) { let left = foldLeft(l, base, combine); let middle = combine(k, v, left); foldLeft(r, middle, combine) @@ -597,7 +610,12 @@ module { { switch (map) { case (#leaf) { base }; - case (#node(_, l, (k, v), r)) { + case (#red(l, (k, v), r)) { + let right = foldRight(r, base, combine); + let middle = combine(k, v, right); + foldRight(l, middle, combine) + }; + case (#black(l, (k, v), r)) { let right = foldRight(r, base, combine); let middle = combine(k, v, right); foldRight(l, middle, combine) @@ -632,7 +650,14 @@ module { public func get(t : Map, compare : (K, K) -> O.Order, x : K) : ?V { switch t { case (#leaf) { null }; - case (#node(_c, l, xy, r)) { + case (#red(l, xy, r)) { + switch (compare(x, xy.0)) { + case (#less) { get(l, compare, x) }; + case (#equal) { ?xy.1 }; + case (#greater) { get(r, compare, x) } + } + }; + case (#black(l, xy, r)) { switch (compare(x, xy.0)) { case (#less) { get(l, compare, x) }; case (#equal) { ?xy.1 }; @@ -644,8 +669,8 @@ module { func redden(t : Map) : Map { switch t { - case (#node (#B, l, xy, r)) { - (#node (#R, l, xy, r)) + case (#black (l, xy, r)) { + (#red (l, xy, r)) }; case _ { Debug.trap "RBTree.red" @@ -655,44 +680,40 @@ module { func lbalance(left : Map, xy : (K,V), right : Map) : Map { switch (left, right) { - case (#node(#R, #node(#R, l1, xy1, r1), xy2, r2), r) { - #node( - #R, - #node(#B, l1, xy1, r1), + case (#red(#red(l1, xy1, r1), xy2, r2), r) { + #red( + #black(l1, xy1, r1), xy2, - #node(#B, r2, xy, r)) + #black(r2, xy, r)) }; - case (#node(#R, l1, xy1, #node(#R, l2, xy2, r2)), r) { - #node( - #R, - #node(#B, l1, xy1, l2), + case (#red(l1, xy1, #red(l2, xy2, r2)), r) { + #red( + #black(l1, xy1, l2), xy2, - #node(#B, r2, xy, r)) + #black(r2, xy, r)) }; case _ { - #node(#B, left, xy, right) + #black(left, xy, right) } } }; func rbalance(left : Map, xy : (K,V), right : Map) : Map { switch (left, right) { - case (l, #node(#R, l1, xy1, #node(#R, l2, xy2, r2))) { - #node( - #R, - #node(#B, l, xy, l1), + case (l, #red(l1, xy1, #red(l2, xy2, r2))) { + #red( + #black(l, xy, l1), xy1, - #node(#B, l2, xy2, r2)) + #black(l2, xy2, r2)) }; - case (l, #node(#R, #node(#R, l1, xy1, r1), xy2, r2)) { - #node( - #R, - #node(#B, l, xy, l1), + case (l, #red(#red(l1, xy1, r1), xy2, r2)) { + #red( + #black(l, xy, l1), xy1, - #node(#B, r1, xy2, r2)) + #black(r1, xy2, r2)) }; case _ { - #node(#B, left, xy, right) + #black(left, xy, right) }; } }; @@ -710,9 +731,9 @@ module { func ins(tree : Map) : Map { switch tree { case (#leaf) { - #node(#R, #leaf, (key,val), #leaf) + #red(#leaf, (key,val), #leaf) }; - case (#node(#B, left, xy, right)) { + case (#black(left, xy, right)) { switch (compare (key, xy.0)) { case (#less) { lbalance(ins left, xy, right) @@ -722,29 +743,29 @@ module { }; case (#equal) { let newVal = onClash({ new = val; old = xy.1 }); - #node(#B, left, (key,newVal), right) + #black(left, (key,newVal), right) } } }; - case (#node(#R, left, xy, right)) { + case (#red(left, xy, right)) { switch (compare (key, xy.0)) { case (#less) { - #node(#R, ins left, xy, right) + #red(ins left, xy, right) }; case (#greater) { - #node(#R, left, xy, ins right) + #red(left, xy, ins right) }; case (#equal) { let newVal = onClash { new = val; old = xy.1 }; - #node(#R, left, (key,newVal), right) + #red(left, (key,newVal), right) } } } }; }; switch (ins m) { - case (#node(#R, left, xy, right)) { - #node(#B, left, xy, right); + case (#red(left, xy, right)) { + #black(left, xy, right); }; case other { other }; }; @@ -777,19 +798,18 @@ module { func balLeft(left : Map, xy : (K,V), right : Map) : Map { switch (left, right) { - case (#node(#R, l1, xy1, r1), r) { - #node( - #R, - #node(#B, l1, xy1, r1), + case (#red(l1, xy1, r1), r) { + #red( + #black(l1, xy1, r1), xy, r) }; - case (_, #node(#B, l2, xy2, r2)) { - rbalance(left, xy, #node(#R, l2, xy2, r2)) + case (_, #black(l2, xy2, r2)) { + rbalance(left, xy, #red(l2, xy2, r2)) }; - case (_, #node(#R, #node(#B, l2, xy2, r2), xy3, r3)) { - #node(#R, - #node(#B, left, xy, l2), + case (_, #red(#black(l2, xy2, r2), xy3, r3)) { + #red( + #black(left, xy, l2), xy2, rbalance(r2, xy3, redden r3)) }; @@ -799,20 +819,20 @@ module { func balRight(left : Map, xy : (K,V), right : Map) : Map { switch (left, right) { - case (l, #node(#R, l1, xy1, r1)) { - #node(#R, + case (l, #red(l1, xy1, r1)) { + #red( l, xy, - #node(#B, l1, xy1, r1)) + #black(l1, xy1, r1)) }; - case (#node(#B, l1, xy1, r1), r) { - lbalance(#node(#R, l1, xy1, r1), xy, r); + case (#black(l1, xy1, r1), r) { + lbalance(#red(l1, xy1, r1), xy, r); }; - case (#node(#R, l1, xy1, #node(#B, l2, xy2, r2)), r3) { - #node(#R, + case (#red(l1, xy1, #black(l2, xy2, r2)), r3) { + #red( lbalance(redden l1, xy1, l2), xy2, - #node(#B, r2, xy, r3)) + #black(r2, xy, r3)) }; case _ { Debug.trap "balRight" }; } @@ -822,40 +842,39 @@ module { switch (left, right) { case (#leaf, _) { right }; case (_, #leaf) { left }; - case (#node (#R, l1, xy1, r1), - #node (#R, l2, xy2, r2)) { + case (#red (l1, xy1, r1), + #red (l2, xy2, r2)) { switch (append (r1, l2)) { - case (#node (#R, l3, xy3, r3)) { - #node( - #R, - #node(#R, l1, xy1, l3), + case (#red (l3, xy3, r3)) { + #red( + #red(l1, xy1, l3), xy3, - #node(#R, r3, xy2, r2)) + #red(r3, xy2, r2)) }; case r1l2 { - #node(#R, l1, xy1, #node(#R, r1l2, xy2, r2)) + #red(l1, xy1, #red(r1l2, xy2, r2)) } } }; - case (t1, #node(#R, l2, xy2, r2)) { - #node(#R, append(t1, l2), xy2, r2) + case (t1, #red(l2, xy2, r2)) { + #red(append(t1, l2), xy2, r2) }; - case (#node(#R, l1, xy1, r1), t2) { - #node(#R, l1, xy1, append(r1, t2)) + case (#red(l1, xy1, r1), t2) { + #red(l1, xy1, append(r1, t2)) }; - case (#node(#B, l1, xy1, r1), #node (#B, l2, xy2, r2)) { + case (#black(l1, xy1, r1), #black (l2, xy2, r2)) { switch (append (r1, l2)) { - case (#node (#R, l3, xy3, r3)) { - #node(#R, - #node(#B, l1, xy1, l3), + case (#red (l3, xy3, r3)) { + #red( + #black(l1, xy1, l3), xy3, - #node(#B, r3, xy2, r2)) + #black(r3, xy2, r2)) }; case r1l2 { balLeft ( l1, xy1, - #node(#B, r1l2, xy2, r2) + #black(r1l2, xy2, r2) ) } } @@ -873,22 +892,22 @@ module { case (#less) { let newLeft = del left; switch left { - case (#node(#B, _, _, _)) { + case (#black(_, _, _)) { balLeft(newLeft, xy, right) }; case _ { - #node(#R, newLeft, xy, right) + #red(newLeft, xy, right) } } }; case (#greater) { let newRight = del right; switch right { - case (#node(#B, _, _, _)) { + case (#black(_, _, _)) { balRight(left, xy, newRight) }; case _ { - #node(#R, left, xy, newRight) + #red(left, xy, newRight) } } }; @@ -903,14 +922,17 @@ module { case (#leaf) { tree }; - case (#node(_, left, xy, right)) { + case (#red(left, xy, right)) { + delNode(left, xy, right) + }; + case (#black(left, xy, right)) { delNode(left, xy, right) } }; }; switch (del(tree)) { - case (#node(#R, left, xy, right)) { - (#node(#B, left, xy, right), y0); + case (#red(left, xy, right)) { + (#black(left, xy, right), y0); }; case other { (other, y0) }; }; diff --git a/test/PersistentOrderedMap.test.mo b/test/PersistentOrderedMap.test.mo index 58e7b41d..0e6794c6 100644 --- a/test/PersistentOrderedMap.test.mo +++ b/test/PersistentOrderedMap.test.mo @@ -31,24 +31,24 @@ func checkMap(rbMap : Map.Map) { }; func blackDepth(node : Map.Map) : Nat { + func checkNode(left : Map.Map, key : Nat, right : Map.Map) : Nat { + checkKey(left, func(x) { x < key }); + checkKey(right, func(x) { x > key }); + let leftBlacks = blackDepth(left); + let rightBlacks = blackDepth(right); + assert (leftBlacks == rightBlacks); + leftBlacks + }; switch node { case (#leaf) 0; - case (#node(color, left, (key, _), right)) { - checkKey(left, func(x) { x < key }); - checkKey(right, func(x) { x > key }); - let leftBlacks = blackDepth(left); - let rightBlacks = blackDepth(right); - assert (leftBlacks == rightBlacks); - switch color { - case (#R) { - assert (not isRed(left)); - assert (not isRed(right)); - leftBlacks - }; - case (#B) { - leftBlacks + 1 - } - } + case (#red(left, (key, _), right)) { + let leftBlacks = checkNode(left, key, right); + assert (not isRed(left)); + assert (not isRed(right)); + leftBlacks + }; + case (#black(left, (key, _), right)) { + checkNode(left, key, right) + 1 } } }; @@ -56,15 +56,18 @@ func blackDepth(node : Map.Map) : Nat { func isRed(node : Map.Map) : Bool { switch node { - case (#leaf) false; - case (#node(color, _, _, _)) color == #R + case (#red(_, _, _)) true; + case _ false } }; func checkKey(node : Map.Map, isValid : Nat -> Bool) { switch node { case (#leaf) {}; - case (#node(_, _, (key, _), _)) { + case (#red( _, (key, _), _)) { + assert (isValid(key)) + }; + case (#black( _, (key, _), _)) { assert (isValid(key)) } } From 6dbaba6b840465da1fcc48f7b4cf64fc54cc0b81 Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Fri, 11 Oct 2024 16:26:01 +0400 Subject: [PATCH 16/20] Make internal module public --- src/PersistentOrderedMap.mo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index ec752ca9..23d85c3b 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -624,7 +624,7 @@ module { }; - module Internal { + public module Internal { public func fromIter(i : I.Iter<(K,V)>, compare : (K, K) -> O.Order) : Map { From 97ebc9d162c170cb75d6a84843642bd58bc95a21 Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Tue, 15 Oct 2024 16:50:06 +0400 Subject: [PATCH 17/20] Move leaf branch to the end of switch expression, as the most unlikely This PR didn't touch folds and map as benchmark showed us that it performs worse. --- src/PersistentOrderedMap.mo | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 23d85c3b..1e6ea507 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -515,13 +515,13 @@ module { /// where `n` denotes the number of key-value entries stored in the map. public func size(m : Map) : Nat { switch m { - case (#leaf) { 0 }; case (#red(l, _, r)) { size(l) + size(r) + 1 }; case (#black(l, _, r)) { size(l) + size(r) + 1 - } + }; + case (#leaf) { 0 } } }; @@ -649,7 +649,6 @@ module { public func get(t : Map, compare : (K, K) -> O.Order, x : K) : ?V { switch t { - case (#leaf) { null }; case (#red(l, xy, r)) { switch (compare(x, xy.0)) { case (#less) { get(l, compare, x) }; @@ -663,7 +662,8 @@ module { case (#equal) { ?xy.1 }; case (#greater) { get(r, compare, x) } } - } + }; + case (#leaf) { null } } }; @@ -730,9 +730,6 @@ module { : Map{ func ins(tree : Map) : Map { switch tree { - case (#leaf) { - #red(#leaf, (key,val), #leaf) - }; case (#black(left, xy, right)) { switch (compare (key, xy.0)) { case (#less) { @@ -760,6 +757,9 @@ module { #red(left, (key,newVal), right) } } + }; + case (#leaf) { + #red(#leaf, (key,val), #leaf) } }; }; @@ -919,14 +919,14 @@ module { }; func del(tree : Map) : Map { switch tree { - case (#leaf) { - tree - }; case (#red(left, xy, right)) { delNode(left, xy, right) }; case (#black(left, xy, right)) { delNode(left, xy, right) + }; + case (#leaf) { + tree } }; }; From acd2add0288195a78a14fa3658a0830c292ba68e Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Wed, 23 Oct 2024 12:18:43 +0400 Subject: [PATCH 18/20] Move all operations inside a MapOps class --- src/PersistentOrderedMap.mo | 705 +++++++++++++------------ test/PersistentOrderedMap.prop.test.mo | 66 +-- test/PersistentOrderedMap.test.mo | 88 +-- 3 files changed, 442 insertions(+), 417 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 1e6ea507..e93285a3 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -62,14 +62,14 @@ module { /// Example: /// ```motoko /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let m = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(m)))); + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(m)))); /// /// // [(0, "Zero"), (1, "One"), (2, "Two")] /// ``` @@ -89,8 +89,8 @@ module { /// Example: /// ```motoko /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); @@ -100,7 +100,7 @@ module { /// map := mapOps.put(map, 2, "Two"); /// map := mapOps.put(map, 1, "One"); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(map)))); + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map)))); /// /// // [(0, "Zero"), (1, "One"), (2, "Two")] /// ``` @@ -120,8 +120,8 @@ module { /// Example: /// ```motoko /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); @@ -129,14 +129,14 @@ module { /// /// let (map1, old1) = mapOps.replace(map0, 0, "Nil"); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(map1)))); + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map1)))); /// Debug.print(debug_show(old1)); /// // [(0, "Nil"), (1, "One"), (2, "Two")] /// // ?"Zero" /// /// let (map2, old2) = mapOps.replace(map0, 3, "Three"); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(map2)))); + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map2)))); /// Debug.print(debug_show(old2)); /// // [(0, "Zero"), (1, "One"), (2, "Two"), (3, "Three")] /// // null @@ -159,7 +159,7 @@ module { /// Example: /// ```motoko /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" + /// import Nat "mo:base/Nat"; /// import Iter "mo:base/Iter"; /// import Debug "mo:base/Debug"; /// @@ -173,7 +173,7 @@ module { /// /// let newMap = mapOps.mapFilter(map, f); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(newMap)))); + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(newMap)))); /// /// // [(1, "Twenty One"), (2, "Twenty Two")] /// ``` @@ -192,7 +192,7 @@ module { /// Example: /// ```motoko /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" + /// import Nat "mo:base/Nat"; /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); @@ -220,14 +220,14 @@ module { /// Example: /// ```motoko /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" + /// import Nat "mo:base/Nat"; /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(mapOps.delete(map, 1))))); - /// Debug.print(debug_show(Iter.toArray(Map.entries(mapOps.delete(map, 42))))); + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(mapOps.delete(map, 1))))); + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(mapOps.delete(map, 42))))); /// /// // [(0, "Zero"), (2, "Two")] /// // [(0, "Zero"), (1, "One"), (2, "Two")] @@ -248,8 +248,8 @@ module { /// Example: /// ```motoko /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; /// import Debug "mo:base/Debug"; /// /// let mapOps = Map.MapOps(Nat.compare); @@ -257,14 +257,14 @@ module { /// /// let (map1, old1) = mapOps.remove(map0, 0); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(map1)))); + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map1)))); /// Debug.print(debug_show(old1)); /// // [(1, "One"), (2, "Two")] /// // ?"Zero" /// /// let (map2, old2) = mapOps.remove(map0, 42); /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(map2)))); + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map2)))); /// Debug.print(debug_show(old2)); /// // [(0, "Zero"), (1, "One"), (2, "Two")] /// // null @@ -279,75 +279,296 @@ module { public func remove(m : Map, key : K) : (Map, ?V) = Internal.remove(m, compare, key); - }; + /// Create a new empty map. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// + /// let map = mapOps.empty(); + /// + /// Debug.print(debug_show(mapOps.size(map))); + /// + /// // 0 + /// ``` + /// + /// Cost of empty map creation + /// Runtime: `O(1)`. + /// Space: `O(1)` + public func empty() : Map = #leaf; - /// Create a new empty map. - /// - /// Example: - /// ```motoko - /// import Map "mo:base/PersistentOrderedMap"; - /// import Debug "mo:base/Debug"; - /// - /// let map = Map.empty(); - /// - /// Debug.print(debug_show(Map.size(map))); - /// - /// // 0 - /// ``` - /// - /// Cost of empty map creation - /// Runtime: `O(1)`. - /// Space: `O(1)` - public func empty() : Map = #leaf; + /// Get an iterator for the entries of the map `m`, in ascending (`#fwd`) or descending (`#bwd`) order as specified by `direction`. + /// The iterator takes a snapshot view of the map and is not affected by concurrent modifications. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.iter(map, #fwd)))); + /// Debug.print(debug_show(Iter.toArray(mapOps.iter(map, #bwd)))); + /// + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// // [(2, "Two"), (1, "One"), (0, "Zero")] + /// ``` + /// + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func iter(m : Map, direction : Direction) : I.Iter<(K, V)> + = Internal.iter(m, direction); - type IterRep = List.List<{ #tr : Map; #xy : (K, V) }>; + /// Returns an Iterator (`Iter`) over the key-value pairs in the map. + /// Iterator provides a single method `next()`, which returns + /// pairs in ascending order by keys, or `null` when out of pairs to iterate over. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map)))); + /// + /// + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// ``` + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func entries(m : Map) : I.Iter<(K, V)> = iter(m, #fwd); - public type Direction = { #fwd; #bwd }; + /// Returns an Iterator (`Iter`) over the keys of the map. + /// Iterator provides a single method `next()`, which returns + /// keys in ascending order, or `null` when out of keys to iterate over. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.keys(map)))); + /// + /// // [0, 1, 2] + /// ``` + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func keys(m : Map) : I.Iter + = I.map(entries(m), func(kv : (K, V)) : K {kv.0}); - /// Get an iterator for the entries of the map `m`, in ascending (`#fwd`) or descending (`#bwd`) order as specified by `direction`. - /// The iterator takes a snapshot view of the map and is not affected by concurrent modifications. - /// - /// Example: - /// ```motoko - /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" - /// import Debug "mo:base/Debug"; - /// - /// let mapOps = Map.MapOps(Nat.compare); - /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); - /// - /// Debug.print(debug_show(Iter.toArray(Map.iter(map, #fwd)))); - /// Debug.print(debug_show(Iter.toArray(Map.iter(map, #bwd)))); - /// - /// // [(0, "Zero"), (1, "One"), (2, "Two")] - /// // [(2, "Two"), (1, "One"), (0, "Zero")] - /// ``` - /// - /// Cost of iteration over all elements: - /// Runtime: `O(n)`. - /// Space: `O(log(n))` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the map. - /// - /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. - public func iter(m : Map, direction : Direction) : I.Iter<(K, V)> { - let turnLeftFirst : MapTraverser - = func (l, xy, r, ts) { ?(#tr(l), ?(#xy(xy), ?(#tr(r), ts))) }; - let turnRightFirst : MapTraverser - = func (l, xy, r, ts) { ?(#tr(r), ?(#xy(xy), ?(#tr(l), ts))) }; + /// Returns an Iterator (`Iter`) over the values of the map. + /// Iterator provides a single method `next()`, which returns + /// values in no specific order, or `null` when out of values to iterate over. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.vals(map)))); + /// + /// // ["Zero", "One", "Two"] + /// ``` + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func vals(m : Map) : I.Iter + = I.map(entries(m), func(kv : (K, V)) : V {kv.1}); - switch direction { - case (#fwd) IterMap(m, turnLeftFirst); - case (#bwd) IterMap(m, turnRightFirst) - } + /// Creates a new map by applying `f` to each entry in the map `m`. Each entry + /// `(k, v)` in the old map is transformed into a new entry `(k, v2)`, where + /// the new value `v2` is created by applying `f` to `(k, v)`. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func f(key : Nat, _val : Text) : Nat = key * 2; + /// + /// let resMap = mapOps.map(map, f); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(resMap)))); + /// + /// // [(0, 0), (1, 2), (2, 4)] + /// ``` + /// + /// Cost of mapping all the elements: + /// Runtime: `O(n)`. + /// Space: `O(n)` retained memory + /// where `n` denotes the number of key-value entries stored in the map. + public func map(m : Map, f : (K, V1) -> V2) : Map + = Internal.map(m, f); + + /// Determine the size of the map as the number of key-value entries. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(mapOps.size(map))); + /// + /// // 3 + /// ``` + /// + /// Runtime: `O(n)`. + /// Space: `O(1)`. + /// where `n` denotes the number of key-value entries stored in the tree. + public func size(m : Map) : Nat + = Internal.size(m); + + /// Collapses the elements in the `map` into a single value by starting with `base` + /// and progressively combining keys and values into `base` with `combine`. Iteration runs + /// left to right. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func folder(key : Nat, val : Text, accum : (Nat, Text)) : ((Nat, Text)) + /// = (key + accum.0, accum.1 # val); + /// + /// Debug.print(debug_show(mapOps.foldLeft(map, (0, ""), folder))); + /// + /// // (3, "ZeroOneTwo") + /// ``` + /// + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: depends on `combine` function plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func foldLeft( + map : Map, + base : Accum, + combine : (K, Value, Accum) -> Accum + ) : Accum + = Internal.foldLeft(map, base, combine); + + /// Collapses the elements in the `map` into a single value by starting with `base` + /// and progressively combining keys and values into `base` with `combine`. Iteration runs + /// right to left. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func folder(key : Nat, val : Text, accum : (Nat, Text)) : ((Nat, Text)) + /// = (key + accum.0, accum.1 # val); + /// + /// Debug.print(debug_show(mapOps.foldRight(map, (0, ""), folder))); + /// + /// // (3, "TwoOneZero") + /// ``` + /// + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: depends on `combine` function plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func foldRight( + map : Map, + base : Accum, + combine : (K, Value, Accum) -> Accum + ) : Accum + = Internal.foldRight(map, base, combine); }; - type MapTraverser = (Map, (K, V), Map, IterRep) -> IterRep; + public type Direction = { #fwd; #bwd }; + + module Internal { - class IterMap(m : Map, mapTraverser : MapTraverser) { - var trees : IterRep = ?(#tr(m), null); - public func next() : ?(K, V) { + public func fromIter(i : I.Iter<(K,V)>, compare : (K, K) -> O.Order) : Map + { + var map = #leaf : Map; + for(val in i) { + map := put(map, compare, val.0, val.1); + }; + map + }; + + type IterRep = List.List<{ #tr : Map; #xy : (K, V) }>; + + public func iter(map : Map, direction : Direction) : I.Iter<(K, V)> { + let turnLeftFirst : MapTraverser + = func (l, xy, r, ts) { ?(#tr(l), ?(#xy(xy), ?(#tr(r), ts))) }; + + let turnRightFirst : MapTraverser + = func (l, xy, r, ts) { ?(#tr(r), ?(#xy(xy), ?(#tr(l), ts))) }; + + switch direction { + case (#fwd) IterMap(map, turnLeftFirst); + case (#bwd) IterMap(map, turnRightFirst) + } + }; + + type MapTraverser = (Map, (K, V), Map, IterRep) -> IterRep; + + class IterMap(map : Map, mapTraverser : MapTraverser) { + var trees : IterRep = ?(#tr(map), null); + public func next() : ?(K, V) { switch (trees) { case (null) { null }; case (?(#tr(#leaf), ts)) { @@ -368,274 +589,66 @@ module { } } } - }; - - /// Returns an Iterator (`Iter`) over the key-value pairs in the map. - /// Iterator provides a single method `next()`, which returns - /// pairs in ascending order by keys, or `null` when out of pairs to iterate over. - /// - /// Example: - /// ```motoko - /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" - /// import Debug "mo:base/Debug"; - /// - /// let mapOps = Map.MapOps(Nat.compare); - /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); - /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(map)))); - /// - /// - /// // [(0, "Zero"), (1, "One"), (2, "Two")] - /// ``` - /// Cost of iteration over all elements: - /// Runtime: `O(n)`. - /// Space: `O(log(n))` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the map. - /// - /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. - public func entries(m : Map) : I.Iter<(K, V)> = iter(m, #fwd); - - /// Returns an Iterator (`Iter`) over the keys of the map. - /// Iterator provides a single method `next()`, which returns - /// keys in ascending order, or `null` when out of keys to iterate over. - /// - /// Example: - /// ```motoko - /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" - /// import Debug "mo:base/Debug"; - /// - /// let mapOps = Map.MapOps(Nat.compare); - /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); - /// - /// Debug.print(debug_show(Iter.toArray(Map.keys(map)))); - /// - /// // [0, 1, 2] - /// ``` - /// Cost of iteration over all elements: - /// Runtime: `O(n)`. - /// Space: `O(log(n))` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the map. - /// - /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. - public func keys(m : Map) : I.Iter - = I.map(entries(m), func(kv : (K, V)) : K {kv.0}); - - /// Returns an Iterator (`Iter`) over the values of the map. - /// Iterator provides a single method `next()`, which returns - /// values in no specific order, or `null` when out of values to iterate over. - /// - /// Example: - /// ```motoko - /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" - /// import Debug "mo:base/Debug"; - /// - /// let mapOps = Map.MapOps(Nat.compare); - /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); - /// - /// Debug.print(debug_show(Iter.toArray(Map.vals(map)))); - /// - /// // ["Zero", "One", "Two"] - /// ``` - /// Cost of iteration over all elements: - /// Runtime: `O(n)`. - /// Space: `O(log(n))` retained memory plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the map. - /// - /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. - public func vals(m : Map) : I.Iter - = I.map(entries(m), func(kv : (K, V)) : V {kv.1}); - - /// Creates a new map by applying `f` to each entry in the map `m`. Each entry - /// `(k, v)` in the old map is transformed into a new entry `(k, v2)`, where - /// the new value `v2` is created by applying `f` to `(k, v)`. - /// - /// Example: - /// ```motoko - /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" - /// import Debug "mo:base/Debug"; - /// - /// let mapOps = Map.MapOps(Nat.compare); - /// let m = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); - /// - /// func f(key : Nat, _val : Text) : Nat = key * 2; - /// - /// let resMap = Map.map(m, f); - /// - /// Debug.print(debug_show(Iter.toArray(Map.entries(resMap)))); - /// - /// // [(0, 0), (1, 2), (2, 4)] - /// ``` - /// - /// Cost of mapping all the elements: - /// Runtime: `O(n)`. - /// Space: `O(n)` retained memory - /// where `n` denotes the number of key-value entries stored in the map. - public func map(m : Map, f : (K, V1) -> V2) : Map { - func mapRec(m : Map) : Map { - switch m { - case (#leaf) { #leaf }; - case (#red(l, xy, r)) { - #red(mapRec l, (xy.0, f xy), mapRec r) - }; - case (#black(l, xy, r)) { - #black(mapRec l, (xy.0, f xy), mapRec r) - }; - } }; - mapRec(m) - }; - /// Determine the size of the map as the number of key-value entries. - /// - /// Example: - /// ```motoko - /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" - /// import Debug "mo:base/Debug"; - /// - /// let mapOps = Map.MapOps(Nat.compare); - /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); - /// - /// Debug.print(debug_show(Map.size(map))); - /// - /// // 3 - /// ``` - /// - /// Runtime: `O(n)`. - /// Space: `O(1)`. - /// where `n` denotes the number of key-value entries stored in the map. - public func size(m : Map) : Nat { - switch m { - case (#red(l, _, r)) { - size(l) + size(r) + 1 - }; - case (#black(l, _, r)) { - size(l) + size(r) + 1 - }; - case (#leaf) { 0 } - } - }; - - /// Collapses the elements in `map` into a single value by starting with `base` - /// and progressively combining keys and values into `base` with `combine`. Iteration runs - /// left to right. - /// - /// Example: - /// ```motoko - /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" - /// import Debug "mo:base/Debug"; - /// - /// let mapOps = Map.MapOps(Nat.compare); - /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); - /// - /// func folder(key : Nat, val : Text, accum : (Nat, Text)) : ((Nat, Text)) - /// = (key + accum.0, accum.1 # val); - /// - /// Debug.print(debug_show(Map.foldLeft(map, (0, ""), folder))); - /// - /// // (3, "ZeroOneTwo") - /// ``` - /// - /// Cost of iteration over all elements: - /// Runtime: `O(n)`. - /// Space: depends on `combine` function plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the map. - /// - /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. - public func foldLeft( - map : Map, - base : Accum, - combine : (Key, Value, Accum) -> Accum - ) : Accum - { - switch (map) { - case (#leaf) { base }; - case (#red(l, (k, v), r)) { - let left = foldLeft(l, base, combine); - let middle = combine(k, v, left); - foldLeft(r, middle, combine) + public func map(map : Map, f : (K, V1) -> V2) : Map { + func mapRec(m : Map) : Map { + switch m { + case (#leaf) { #leaf }; + case (#red(l, xy, r)) { + #red(mapRec l, (xy.0, f xy), mapRec r) + }; + case (#black(l, xy, r)) { + #black(mapRec l, (xy.0, f xy), mapRec r) + }; + } }; - case (#black(l, (k, v), r)) { - let left = foldLeft(l, base, combine); - let middle = combine(k, v, left); - foldLeft(r, middle, combine) - } - } - }; + mapRec(map) + }; - /// Collapses the elements in `map` into a single value by starting with `base` - /// and progressively combining keys and values into `base` with `combine`. Iteration runs - /// right to left. - /// - /// Example: - /// ```motoko - /// import Map "mo:base/PersistentOrderedMap"; - /// import Nat "mo:base/Nat" - /// import Iter "mo:base/Iter" - /// import Debug "mo:base/Debug"; - /// - /// let mapOps = Map.MapOps(Nat.compare); - /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); - /// - /// func folder(key : Nat, val : Text, accum : (Nat, Text)) : ((Nat, Text)) - /// = (key + accum.0, accum.1 # val); - /// - /// Debug.print(debug_show(Map.foldRight(map, (0, ""), folder))); - /// - /// // (3, "TwoOneZero") - /// ``` - /// - /// Cost of iteration over all elements: - /// Runtime: `O(n)`. - /// Space: depends on `combine` function plus garbage, see the note below. - /// where `n` denotes the number of key-value entries stored in the map. - /// - /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. - public func foldRight( - map : Map, - base : Accum, - combine : (Key, Value, Accum) -> Accum - ) : Accum - { - switch (map) { - case (#leaf) { base }; - case (#red(l, (k, v), r)) { - let right = foldRight(r, base, combine); - let middle = combine(k, v, right); - foldRight(l, middle, combine) - }; - case (#black(l, (k, v), r)) { - let right = foldRight(r, base, combine); - let middle = combine(k, v, right); - foldRight(l, middle, combine) + public func foldLeft( + map : Map, + base : Accum, + combine : (Key, Value, Accum) -> Accum + ) : Accum + { + switch (map) { + case (#leaf) { base }; + case (#red(l, (k, v), r)) { + let left = foldLeft(l, base, combine); + let middle = combine(k, v, left); + foldLeft(r, middle, combine) + }; + case (#black(l, (k, v), r)) { + let left = foldLeft(l, base, combine); + let middle = combine(k, v, left); + foldLeft(r, middle, combine) + } } - } - }; - - - public module Internal { + }; - public func fromIter(i : I.Iter<(K,V)>, compare : (K, K) -> O.Order) : Map + public func foldRight( + map : Map, + base : Accum, + combine : (Key, Value, Accum) -> Accum + ) : Accum { - var map = #leaf : Map; - for(val in i) { - map := put(map, compare, val.0, val.1); - }; - map + switch (map) { + case (#leaf) { base }; + case (#red(l, (k, v), r)) { + let right = foldRight(r, base, combine); + let middle = combine(k, v, right); + foldRight(l, middle, combine) + }; + case (#black(l, (k, v), r)) { + let right = foldRight(r, base, combine); + let middle = combine(k, v, right); + foldRight(l, middle, combine) + } + } }; - public func mapFilter(t : Map, compare : (K, K) -> O.Order, f : (K, V1) -> ?V2) : Map{ + public func mapFilter(map : Map, compare : (K, K) -> O.Order, f : (K, V1) -> ?V2) : Map{ func combine(key : K, value1 : V1, acc : Map) : Map { switch (f(key, value1)){ case null { acc }; @@ -644,7 +657,19 @@ module { } } }; - foldLeft(t, #leaf, combine) + foldLeft(map, #leaf, combine) + }; + + public func size(t : Map) : Nat { + switch t { + case (#red(l, _, r)) { + size(l) + size(r) + 1 + }; + case (#black(l, _, r)) { + size(l) + size(r) + 1 + }; + case (#leaf) { 0 } + } }; public func get(t : Map, compare : (K, K) -> O.Order, x : K) : ?V { diff --git a/test/PersistentOrderedMap.prop.test.mo b/test/PersistentOrderedMap.prop.test.mo index bc55f012..d0c12609 100644 --- a/test/PersistentOrderedMap.prop.test.mo +++ b/test/PersistentOrderedMap.prop.test.mo @@ -17,11 +17,11 @@ let { run; test; suite } = Suite; class MapMatcher(expected : Map.Map) : M.Matcher> { public func describeMismatch(actual : Map.Map, _description : M.Description) { - Debug.print(debug_show (Iter.toArray(Map.entries(actual))) # " should be " # debug_show (Iter.toArray(Map.entries(expected)))) + Debug.print(debug_show (Iter.toArray(natMap.entries(actual))) # " should be " # debug_show (Iter.toArray(natMap.entries(expected)))) }; public func matches(actual : Map.Map) : Bool { - Iter.toArray(Map.entries(actual)) == Iter.toArray(Map.entries(expected)) + Iter.toArray(natMap.entries(actual)) == Iter.toArray(natMap.entries(expected)) } }; @@ -39,7 +39,7 @@ object Random { }; public func nextEntries(range: (Nat, Nat), size: Nat): [(Nat, Text)] { - Array.tabulate<(Nat, Text)>(size, func(_ix) { + Array.tabulate<(Nat, Text)>(size, func(_ix) { let key = nextNat(range); (key, debug_show(key)) } ) } }; @@ -51,8 +51,8 @@ func mapGen(samples_number: Nat, size: Nat, range: (Nat, Nat)): Iter.Iter { n += 1; - if (n > samples_number) { - null + if (n > samples_number) { + null } else { ?natMap.fromIter(Random.nextEntries(range, size).vals()) } @@ -66,10 +66,10 @@ func run_all_props(range: (Nat, Nat), size: Nat, map_samples: Nat, query_samples var error_msg: Text = ""; test(name, do { var error = true; - label stop for(map in mapGen(map_samples, size, range)) { + label stop for(map in mapGen(map_samples, size, range)) { if (not f(map)) { error_msg := "Property \"" # name # "\" failed\n"; - error_msg #= "\n m: " # debug_show(Iter.toArray(Map.entries(map))); + error_msg #= "\n m: " # debug_show(Iter.toArray(natMap.entries(map))); break stop; } }; @@ -84,7 +84,7 @@ func run_all_props(range: (Nat, Nat), size: Nat, map_samples: Nat, query_samples let key = Random.nextNat(range); if (not f(map, key)) { error_msg #= "Property \"" # name # "\" failed"; - error_msg #= "\n m: " # debug_show(Iter.toArray(Map.entries(map))); + error_msg #= "\n m: " # debug_show(Iter.toArray(natMap.entries(map))); error_msg #= "\n k: " # debug_show(key); break stop; } @@ -95,12 +95,12 @@ func run_all_props(range: (Nat, Nat), size: Nat, map_samples: Nat, query_samples }; run( suite("Property tests", - [ + [ suite("empty", [ test("get(empty(), k) == null", label res : Bool { for (_query_ix in Iter.range(0, query_samples-1)) { let k = Random.nextNat(range); - if(natMap.get(Map.empty(), k) != null) + if(natMap.get(natMap.empty(), k) != null) break res(false); }; true; @@ -116,7 +116,7 @@ func run_all_props(range: (Nat, Nat), size: Nat, map_samples: Nat, query_samples natMap.get(natMap.put(natMap.put(m, k, v1), k, v2), k) == v2 }), ]), - + suite("replace", [ prop_with_key("replace(m, k, v).0 == put(m, k, v)", func (m, k) { natMap.replace(m, k, "v").0 == natMap.put(m, k, "v") @@ -156,7 +156,7 @@ func run_all_props(range: (Nat, Nat), size: Nat, map_samples: Nat, query_samples MapMatcher(m2).matches(m1) }), prop_with_key("remove(put(m, k, v), k).1 == ?v", func (m, k) { - natMap.remove(natMap.put(m, k, "v"), k).1 == ?"v" + natMap.remove(natMap.put(m, k, "v"), k).1 == ?"v" }), prop_with_key("remove(remove(m, k).0, k).1 == null", func (m, k) { natMap.remove(natMap.remove(m, k).0, k).1 == null @@ -170,67 +170,67 @@ func run_all_props(range: (Nat, Nat), size: Nat, map_samples: Nat, query_samples suite("size", [ prop_with_key("size(put(m, k, v)) == size(m) + int(get(m, k) == null)", func (m, k) { - Map.size(natMap.put(m, k, "v")) == Map.size(m) + (if (natMap.get(m, k) == null) {1} else {0}) + natMap.size(natMap.put(m, k, "v")) == natMap.size(m) + (if (natMap.get(m, k) == null) {1} else {0}) }), prop_with_key("size(delete(m, k)) + int(get(m, k) != null) == size(m)", func (m, k) { - Map.size(natMap.delete(m, k)) + (if (natMap.get(m, k) != null) {1} else {0}) == Map.size(m) + natMap.size(natMap.delete(m, k)) + (if (natMap.get(m, k) != null) {1} else {0}) == natMap.size(m) }) ]), - + suite("iter,keys,vals,entries", [ prop("fromIter(iter(m, #fwd)) == m", func (m) { - MapMatcher(m).matches(natMap.fromIter(Map.iter(m, #fwd))) + MapMatcher(m).matches(natMap.fromIter(natMap.iter(m, #fwd))) }), prop("fromIter(iter(m, #bwd)) == m", func (m) { - MapMatcher(m).matches(natMap.fromIter(Map.iter(m, #bwd))) + MapMatcher(m).matches(natMap.fromIter(natMap.iter(m, #bwd))) }), prop("iter(m, #fwd) = zip(key(m), vals(m))", func (m) { - let k = Map.keys(m); - let v = Map.vals(m); - for (e in Map.iter(m, #fwd)) { + let k = natMap.keys(m); + let v = natMap.vals(m); + for (e in natMap.iter(m, #fwd)) { if (e.0 != k.next() or e.1 != v.next()) return false; }; return true; }), prop("entries(m) == iter(m, #fwd)", func (m) { - let it = Map.iter(m, #fwd); - for (e in Map.entries(m)) { + let it = natMap.iter(m, #fwd); + for (e in natMap.entries(m)) { if (it.next() != e) return false; }; return true }) ]), - + suite("mapFilter", [ prop_with_key("get(mapFilter(m, (!=k)), k) == null", func (m, k) { - natMap.get(natMap.mapFilter(m, + natMap.get(natMap.mapFilter(m, func (ki, vi) { if (ki != k) {?vi} else {null}}), k) == null }), prop_with_key("get(mapFilter(put(m, k, v), (==k)), k) == ?v", func (m, k) { - natMap.get(natMap.mapFilter(natMap.put(m, k, "v"), + natMap.get(natMap.mapFilter(natMap.put(m, k, "v"), func (ki, vi) { if (ki == k) {?vi} else {null}}), k) == ?"v" }) ]), - + suite("map", [ prop("map(m, id) == m", func (m) { - MapMatcher(m).matches(Map.map(m, func (k, v) {v})) + MapMatcher(m).matches(natMap.map(m, func (k, v) {v})) }) ]), - + suite("folds", [ prop("foldLeft as iter(#fwd)", func (m) { - let it = Map.iter(m, #fwd); - Map.foldLeft(m, true, func (k, v, acc) {acc and it.next() == ?(k, v)}) + let it = natMap.iter(m, #fwd); + natMap.foldLeft(m, true, func (k, v, acc) {acc and it.next() == ?(k, v)}) }), prop("foldRight as iter(#bwd)", func(m) { - let it = Map.iter(m, #bwd); - Map.foldRight(m, true, func (k, v, acc) {acc and it.next() == ?(k, v)}) + let it = natMap.iter(m, #bwd); + natMap.foldRight(m, true, func (k, v, acc) {acc and it.next() == ?(k, v)}) }) ]), - ])) + ])) }; run_all_props((1, 3), 0, 1, 10); diff --git a/test/PersistentOrderedMap.test.mo b/test/PersistentOrderedMap.test.mo index 0e6794c6..4c78c618 100644 --- a/test/PersistentOrderedMap.test.mo +++ b/test/PersistentOrderedMap.test.mo @@ -14,18 +14,18 @@ let { run; test; suite } = Suite; let entryTestable = T.tuple2Testable(T.natTestable, T.textTestable); +let natMapOps = Map.MapOps(Nat.compare); + class MapMatcher(expected : [(Nat, Text)]) : M.Matcher> { public func describeMismatch(actual : Map.Map, _description : M.Description) { - Debug.print(debug_show (Iter.toArray(Map.entries(actual))) # " should be " # debug_show (expected)) + Debug.print(debug_show (Iter.toArray(natMapOps.entries(actual))) # " should be " # debug_show (expected)) }; public func matches(actual : Map.Map) : Bool { - Iter.toArray(Map.entries(actual)) == expected + Iter.toArray(natMapOps.entries(actual)) == expected } }; -let natMapOps = Map.MapOps(Nat.compare); - func checkMap(rbMap : Map.Map) { ignore blackDepth(rbMap) }; @@ -88,7 +88,7 @@ func getAll(rbTree : Map.Map, keys : [Nat]) { func clear(initialRbMap : Map.Map) : Map.Map { var rbMap = initialRbMap; - for ((key, value) in Map.entries(initialRbMap)) { + for ((key, value) in natMapOps.entries(initialRbMap)) { // stable iteration assert (value == debug_show (key)); let (newMap, result) = natMapOps.remove(rbMap, key); @@ -125,7 +125,7 @@ func ifKeyLessThan(threshold : Nat, f : (Nat, Text) -> Text) : (Nat, Text) -> ?T /* --------------------------------------- */ var buildTestMap = func() : Map.Map { - Map.empty() + natMapOps.empty() }; run( @@ -134,32 +134,32 @@ run( [ test( "size", - Map.size(buildTestMap()), + natMapOps.size(buildTestMap()), M.equals(T.nat(0)) ), test( "iterate forward", - Iter.toArray(Map.iter(buildTestMap(), #fwd)), + Iter.toArray(natMapOps.iter(buildTestMap(), #fwd)), M.equals(T.array<(Nat, Text)>(entryTestable, [])) ), test( "iterate backward", - Iter.toArray(Map.iter(buildTestMap(), #bwd)), + Iter.toArray(natMapOps.iter(buildTestMap(), #bwd)), M.equals(T.array<(Nat, Text)>(entryTestable, [])) ), test( "entries", - Iter.toArray(Map.entries(buildTestMap())), + Iter.toArray(natMapOps.entries(buildTestMap())), M.equals(T.array<(Nat, Text)>(entryTestable, [])) ), test( "keys", - Iter.toArray(Map.keys(buildTestMap())), + Iter.toArray(natMapOps.keys(buildTestMap())), M.equals(T.array(T.natTestable, [])) ), test( "vals", - Iter.toArray(Map.vals(buildTestMap())), + Iter.toArray(natMapOps.vals(buildTestMap())), M.equals(T.array(T.textTestable, [])) ), test( @@ -189,27 +189,27 @@ run( ), test( "empty right fold keys", - Map.foldRight(buildTestMap(), "", concatenateKeys), + natMapOps.foldRight(buildTestMap(), "", concatenateKeys), M.equals(T.text("")) ), test( "empty left fold keys", - Map.foldLeft(buildTestMap(), "", concatenateKeys), + natMapOps.foldLeft(buildTestMap(), "", concatenateKeys), M.equals(T.text("")) ), test( "empty right fold values", - Map.foldRight(buildTestMap(), "", concatenateValues), + natMapOps.foldRight(buildTestMap(), "", concatenateValues), M.equals(T.text("")) ), test( "empty left fold values", - Map.foldLeft(buildTestMap(), "", concatenateValues), + natMapOps.foldLeft(buildTestMap(), "", concatenateValues), M.equals(T.text("")) ), test( "traverse empty map", - Map.map(buildTestMap(), multiplyKeyAndConcat), + natMapOps.map(buildTestMap(), multiplyKeyAndConcat), MapMatcher([]) ), test( @@ -224,7 +224,7 @@ run( /* --------------------------------------- */ buildTestMap := func() : Map.Map { - insert(Map.empty(), 0); + insert(natMapOps.empty(), 0); }; var expected = expectedEntries([0]); @@ -235,32 +235,32 @@ run( [ test( "size", - Map.size(buildTestMap()), + natMapOps.size(buildTestMap()), M.equals(T.nat(1)) ), test( "iterate forward", - Iter.toArray(Map.iter(buildTestMap(), #fwd)), + Iter.toArray(natMapOps.iter(buildTestMap(), #fwd)), M.equals(T.array<(Nat, Text)>(entryTestable, expected)) ), test( "iterate backward", - Iter.toArray(Map.iter(buildTestMap(), #bwd)), + Iter.toArray(natMapOps.iter(buildTestMap(), #bwd)), M.equals(T.array<(Nat, Text)>(entryTestable, expected)) ), test( "entries", - Iter.toArray(Map.entries(buildTestMap())), + Iter.toArray(natMapOps.entries(buildTestMap())), M.equals(T.array<(Nat, Text)>(entryTestable, expected)) ), test( "keys", - Iter.toArray(Map.keys(buildTestMap())), + Iter.toArray(natMapOps.keys(buildTestMap())), M.equals(T.array(T.natTestable, [0])) ), test( "vals", - Iter.toArray(Map.vals(buildTestMap())), + Iter.toArray(natMapOps.vals(buildTestMap())), M.equals(T.array(T.textTestable, ["0"])) ), test( @@ -303,27 +303,27 @@ run( ), test( "right fold keys", - Map.foldRight(buildTestMap(), "", concatenateKeys), + natMapOps.foldRight(buildTestMap(), "", concatenateKeys), M.equals(T.text("0")) ), test( "left fold keys", - Map.foldLeft(buildTestMap(), "", concatenateKeys), + natMapOps.foldLeft(buildTestMap(), "", concatenateKeys), M.equals(T.text("0")) ), test( "right fold values", - Map.foldRight(buildTestMap(), "", concatenateValues), + natMapOps.foldRight(buildTestMap(), "", concatenateValues), M.equals(T.text("0")) ), test( "left fold values", - Map.foldLeft(buildTestMap(), "", concatenateValues), + natMapOps.foldLeft(buildTestMap(), "", concatenateValues), M.equals(T.text("0")) ), test( "traverse map", - Map.map(buildTestMap(), multiplyKeyAndConcat), + natMapOps.map(buildTestMap(), multiplyKeyAndConcat), MapMatcher([(0, "00")]) ), test( @@ -348,7 +348,7 @@ func rebalanceTests(buildTestMap : () -> Map.Map) : [Suite.Suite] = [ test( "size", - Map.size(buildTestMap()), + natMapOps.size(buildTestMap()), M.equals(T.nat(3)) ), test( @@ -358,27 +358,27 @@ func rebalanceTests(buildTestMap : () -> Map.Map) : [Suite.Suite] = ), test( "iterate forward", - Iter.toArray(Map.iter(buildTestMap(), #fwd)), + Iter.toArray(natMapOps.iter(buildTestMap(), #fwd)), M.equals(T.array<(Nat, Text)>(entryTestable, expected)) ), test( "iterate backward", - Iter.toArray(Map.iter(buildTestMap(), #bwd)), + Iter.toArray(natMapOps.iter(buildTestMap(), #bwd)), M.equals(T.array<(Nat, Text)>(entryTestable, Array.reverse(expected))) ), test( "entries", - Iter.toArray(Map.entries(buildTestMap())), + Iter.toArray(natMapOps.entries(buildTestMap())), M.equals(T.array<(Nat, Text)>(entryTestable, expected)) ), test( "keys", - Iter.toArray(Map.keys(buildTestMap())), + Iter.toArray(natMapOps.keys(buildTestMap())), M.equals(T.array(T.natTestable, [0, 1, 2])) ), test( "vals", - Iter.toArray(Map.vals(buildTestMap())), + Iter.toArray(natMapOps.vals(buildTestMap())), M.equals(T.array(T.textTestable, ["0", "1", "2"])) ), test( @@ -402,27 +402,27 @@ func rebalanceTests(buildTestMap : () -> Map.Map) : [Suite.Suite] = ), test( "right fold keys", - Map.foldRight(buildTestMap(), "", concatenateKeys), + natMapOps.foldRight(buildTestMap(), "", concatenateKeys), M.equals(T.text("210")) ), test( "left fold keys", - Map.foldLeft(buildTestMap(), "", concatenateKeys), + natMapOps.foldLeft(buildTestMap(), "", concatenateKeys), M.equals(T.text("012")) ), test( "right fold values", - Map.foldRight(buildTestMap(), "", concatenateValues), + natMapOps.foldRight(buildTestMap(), "", concatenateValues), M.equals(T.text("210")) ), test( "left fold values", - Map.foldLeft(buildTestMap(), "", concatenateValues), + natMapOps.foldLeft(buildTestMap(), "", concatenateValues), M.equals(T.text("012")) ), test( "traverse map", - Map.map(buildTestMap(), multiplyKeyAndConcat), + natMapOps.map(buildTestMap(), multiplyKeyAndConcat), MapMatcher([(0, "00"), (1, "21"), (2, "42")]) ), test( @@ -443,7 +443,7 @@ func rebalanceTests(buildTestMap : () -> Map.Map) : [Suite.Suite] = ]; buildTestMap := func() : Map.Map { - var rbMap = Map.empty() : Map.Map; + var rbMap = natMapOps.empty() : Map.Map; rbMap := insert(rbMap, 2); rbMap := insert(rbMap, 1); rbMap := insert(rbMap, 0); @@ -455,7 +455,7 @@ run(suite("rebalance left, left", rebalanceTests(buildTestMap))); /* --------------------------------------- */ buildTestMap := func() : Map.Map { - var rbMap = Map.empty() : Map.Map; + var rbMap = natMapOps.empty() : Map.Map; rbMap := insert(rbMap, 2); rbMap := insert(rbMap, 0); rbMap := insert(rbMap, 1); @@ -467,7 +467,7 @@ run(suite("rebalance left, right", rebalanceTests(buildTestMap))); /* --------------------------------------- */ buildTestMap := func() : Map.Map { - var rbMap = Map.empty() : Map.Map; + var rbMap = natMapOps.empty() : Map.Map; rbMap := insert(rbMap, 0); rbMap := insert(rbMap, 2); rbMap := insert(rbMap, 1); @@ -479,7 +479,7 @@ run(suite("rebalance right, left", rebalanceTests(buildTestMap))); /* --------------------------------------- */ buildTestMap := func() : Map.Map { - var rbMap = Map.empty() : Map.Map; + var rbMap = natMapOps.empty() : Map.Map; rbMap := insert(rbMap, 0); rbMap := insert(rbMap, 1); rbMap := insert(rbMap, 2); From d9e2cd1ea3543f4341affac9c912a9e6ed634408 Mon Sep 17 00:00:00 2001 From: Andrei Borzenkov Date: Wed, 23 Oct 2024 13:21:21 +0400 Subject: [PATCH 19/20] Eliminate tuple in map constructors --- src/PersistentOrderedMap.mo | 230 +++++++++++++++--------------- test/PersistentOrderedMap.test.mo | 10 +- 2 files changed, 120 insertions(+), 120 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index e93285a3..2dce47d7 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -42,8 +42,8 @@ module { /// The keys have the generic type `K` and the values the generic type `V`. /// Leaves are considered implicitly black. public type Map = { - #red : (Map, (K, V), Map); - #black : (Map, (K, V), Map); + #red : (Map, K, V, Map); + #black : (Map, K, V, Map); #leaf }; @@ -553,10 +553,10 @@ module { public func iter(map : Map, direction : Direction) : I.Iter<(K, V)> { let turnLeftFirst : MapTraverser - = func (l, xy, r, ts) { ?(#tr(l), ?(#xy(xy), ?(#tr(r), ts))) }; + = func (l, x, y, r, ts) { ?(#tr(l), ?(#xy(x, y), ?(#tr(r), ts))) }; let turnRightFirst : MapTraverser - = func (l, xy, r, ts) { ?(#tr(r), ?(#xy(xy), ?(#tr(l), ts))) }; + = func (l, x, y, r, ts) { ?(#tr(r), ?(#xy(x, y), ?(#tr(l), ts))) }; switch direction { case (#fwd) IterMap(map, turnLeftFirst); @@ -564,7 +564,7 @@ module { } }; - type MapTraverser = (Map, (K, V), Map, IterRep) -> IterRep; + type MapTraverser = (Map, K, V, Map, IterRep) -> IterRep; class IterMap(map : Map, mapTraverser : MapTraverser) { var trees : IterRep = ?(#tr(map), null); @@ -579,12 +579,12 @@ module { trees := ts; ?xy }; - case (?(#tr(#red(l, xy, r)), ts)) { - trees := mapTraverser(l, xy, r, ts); + case (?(#tr(#red(l, x, y, r)), ts)) { + trees := mapTraverser(l, x, y, r, ts); next() }; - case (?(#tr(#black(l, xy, r)), ts)) { - trees := mapTraverser(l, xy, r, ts); + case (?(#tr(#black(l, x, y, r)), ts)) { + trees := mapTraverser(l, x, y, r, ts); next() } } @@ -595,11 +595,11 @@ module { func mapRec(m : Map) : Map { switch m { case (#leaf) { #leaf }; - case (#red(l, xy, r)) { - #red(mapRec l, (xy.0, f xy), mapRec r) + case (#red(l, x, y, r)) { + #red(mapRec l, x, f(x,y), mapRec r) }; - case (#black(l, xy, r)) { - #black(mapRec l, (xy.0, f xy), mapRec r) + case (#black(l, x, y, r)) { + #black(mapRec l, x, f(x, y), mapRec r) }; } }; @@ -614,12 +614,12 @@ module { { switch (map) { case (#leaf) { base }; - case (#red(l, (k, v), r)) { + case (#red(l, k, v, r)) { let left = foldLeft(l, base, combine); let middle = combine(k, v, left); foldLeft(r, middle, combine) }; - case (#black(l, (k, v), r)) { + case (#black(l, k, v, r)) { let left = foldLeft(l, base, combine); let middle = combine(k, v, left); foldLeft(r, middle, combine) @@ -635,12 +635,12 @@ module { { switch (map) { case (#leaf) { base }; - case (#red(l, (k, v), r)) { + case (#red(l, k, v, r)) { let right = foldRight(r, base, combine); let middle = combine(k, v, right); foldRight(l, middle, combine) }; - case (#black(l, (k, v), r)) { + case (#black(l, k, v, r)) { let right = foldRight(r, base, combine); let middle = combine(k, v, right); foldRight(l, middle, combine) @@ -662,10 +662,10 @@ module { public func size(t : Map) : Nat { switch t { - case (#red(l, _, r)) { + case (#red(l, _, _, r)) { size(l) + size(r) + 1 }; - case (#black(l, _, r)) { + case (#black(l, _, _, r)) { size(l) + size(r) + 1 }; case (#leaf) { 0 } @@ -674,17 +674,17 @@ module { public func get(t : Map, compare : (K, K) -> O.Order, x : K) : ?V { switch t { - case (#red(l, xy, r)) { - switch (compare(x, xy.0)) { + case (#red(l, x1, y1, r)) { + switch (compare(x, x1)) { case (#less) { get(l, compare, x) }; - case (#equal) { ?xy.1 }; + case (#equal) { ?y1 }; case (#greater) { get(r, compare, x) } } }; - case (#black(l, xy, r)) { - switch (compare(x, xy.0)) { + case (#black(l, x1, y1, r)) { + switch (compare(x, x1)) { case (#less) { get(l, compare, x) }; - case (#equal) { ?xy.1 }; + case (#equal) { ?y1 }; case (#greater) { get(r, compare, x) } } }; @@ -694,8 +694,8 @@ module { func redden(t : Map) : Map { switch t { - case (#black (l, xy, r)) { - (#red (l, xy, r)) + case (#black (l, x, y, r)) { + (#red (l, x, y, r)) }; case _ { Debug.trap "RBTree.red" @@ -703,42 +703,42 @@ module { } }; - func lbalance(left : Map, xy : (K,V), right : Map) : Map { + func lbalance(left : Map, x : K, y : V, right : Map) : Map { switch (left, right) { - case (#red(#red(l1, xy1, r1), xy2, r2), r) { + case (#red(#red(l1, x1, y1, r1), x2, y2, r2), r) { #red( - #black(l1, xy1, r1), - xy2, - #black(r2, xy, r)) + #black(l1, x1, y1, r1), + x2, y2, + #black(r2, x, y, r)) }; - case (#red(l1, xy1, #red(l2, xy2, r2)), r) { + case (#red(l1, x1, y1, #red(l2, x2, y2, r2)), r) { #red( - #black(l1, xy1, l2), - xy2, - #black(r2, xy, r)) + #black(l1, x1, y1, l2), + x2, y2, + #black(r2, x, y, r)) }; case _ { - #black(left, xy, right) + #black(left, x, y, right) } } }; - func rbalance(left : Map, xy : (K,V), right : Map) : Map { + func rbalance(left : Map, x : K, y : V, right : Map) : Map { switch (left, right) { - case (l, #red(l1, xy1, #red(l2, xy2, r2))) { + case (l, #red(l1, x1, y1, #red(l2, x2, y2, r2))) { #red( - #black(l, xy, l1), - xy1, - #black(l2, xy2, r2)) + #black(l, x, y, l1), + x1, y1, + #black(l2, x2, y2, r2)) }; - case (l, #red(#red(l1, xy1, r1), xy2, r2)) { + case (l, #red(#red(l1, x1, y1, r1), x2, y2, r2)) { #red( - #black(l, xy, l1), - xy1, - #black(r1, xy2, r2)) + #black(l, x, y, l1), + x1, y1, + #black(r1, x2, y2, r2)) }; case _ { - #black(left, xy, right) + #black(left, x, y, right) }; } }; @@ -755,42 +755,42 @@ module { : Map{ func ins(tree : Map) : Map { switch tree { - case (#black(left, xy, right)) { - switch (compare (key, xy.0)) { + case (#black(left, x, y, right)) { + switch (compare (key, x)) { case (#less) { - lbalance(ins left, xy, right) + lbalance(ins left, x, y, right) }; case (#greater) { - rbalance(left, xy, ins right) + rbalance(left, x, y, ins right) }; case (#equal) { - let newVal = onClash({ new = val; old = xy.1 }); - #black(left, (key,newVal), right) + let newVal = onClash({ new = val; old = y }); + #black(left, key, newVal, right) } } }; - case (#red(left, xy, right)) { - switch (compare (key, xy.0)) { + case (#red(left, x, y, right)) { + switch (compare (key, x)) { case (#less) { - #red(ins left, xy, right) + #red(ins left, x, y, right) }; case (#greater) { - #red(left, xy, ins right) + #red(left, x, y, ins right) }; case (#equal) { - let newVal = onClash { new = val; old = xy.1 }; - #red(left, (key,newVal), right) + let newVal = onClash { new = val; old = y }; + #red(left, key, newVal, right) } } }; case (#leaf) { - #red(#leaf, (key,val), #leaf) + #red(#leaf, key, val, #leaf) } }; }; switch (ins m) { - case (#red(left, xy, right)) { - #black(left, xy, right); + case (#red(left, x, y, right)) { + #black(left, x, y, right); }; case other { other }; }; @@ -821,43 +821,43 @@ module { ) : Map = replace(m, compare, key, val).0; - func balLeft(left : Map, xy : (K,V), right : Map) : Map { + func balLeft(left : Map, x : K, y : V, right : Map) : Map { switch (left, right) { - case (#red(l1, xy1, r1), r) { + case (#red(l1, x1, y1, r1), r) { #red( - #black(l1, xy1, r1), - xy, + #black(l1, x1, y1, r1), + x, y, r) }; - case (_, #black(l2, xy2, r2)) { - rbalance(left, xy, #red(l2, xy2, r2)) + case (_, #black(l2, x2, y2, r2)) { + rbalance(left, x, y, #red(l2, x2, y2, r2)) }; - case (_, #red(#black(l2, xy2, r2), xy3, r3)) { + case (_, #red(#black(l2, x2, y2, r2), x3, y3, r3)) { #red( - #black(left, xy, l2), - xy2, - rbalance(r2, xy3, redden r3)) + #black(left, x, y, l2), + x2, y2, + rbalance(r2, x3, y3, redden r3)) }; case _ { Debug.trap "balLeft" }; } }; - func balRight(left : Map, xy : (K,V), right : Map) : Map { + func balRight(left : Map, x : K, y : V, right : Map) : Map { switch (left, right) { - case (l, #red(l1, xy1, r1)) { + case (l, #red(l1, x1, y1, r1)) { #red( l, - xy, - #black(l1, xy1, r1)) + x, y, + #black(l1, x1, y1, r1)) }; - case (#black(l1, xy1, r1), r) { - lbalance(#red(l1, xy1, r1), xy, r); + case (#black(l1, x1, y1, r1), r) { + lbalance(#red(l1, x1, y1, r1), x, y, r); }; - case (#red(l1, xy1, #black(l2, xy2, r2)), r3) { + case (#red(l1, x1, y1, #black(l2, x2, y2, r2)), r3) { #red( - lbalance(redden l1, xy1, l2), - xy2, - #black(r2, xy, r3)) + lbalance(redden l1, x1, y1, l2), + x2, y2, + #black(r2, x, y, r3)) }; case _ { Debug.trap "balRight" }; } @@ -867,39 +867,39 @@ module { switch (left, right) { case (#leaf, _) { right }; case (_, #leaf) { left }; - case (#red (l1, xy1, r1), - #red (l2, xy2, r2)) { + case (#red (l1, x1, y1, r1), + #red (l2, x2, y2, r2)) { switch (append (r1, l2)) { - case (#red (l3, xy3, r3)) { + case (#red (l3, x3, y3, r3)) { #red( - #red(l1, xy1, l3), - xy3, - #red(r3, xy2, r2)) + #red(l1, x1, y1, l3), + x3, y3, + #red(r3, x2, y2, r2)) }; case r1l2 { - #red(l1, xy1, #red(r1l2, xy2, r2)) + #red(l1, x1, y1, #red(r1l2, x2, y2, r2)) } } }; - case (t1, #red(l2, xy2, r2)) { - #red(append(t1, l2), xy2, r2) + case (t1, #red(l2, x2, y2, r2)) { + #red(append(t1, l2), x2, y2, r2) }; - case (#red(l1, xy1, r1), t2) { - #red(l1, xy1, append(r1, t2)) + case (#red(l1, x1, y1, r1), t2) { + #red(l1, x1, y1, append(r1, t2)) }; - case (#black(l1, xy1, r1), #black (l2, xy2, r2)) { + case (#black(l1, x1, y1, r1), #black (l2, x2, y2, r2)) { switch (append (r1, l2)) { - case (#red (l3, xy3, r3)) { + case (#red (l3, x3, y3, r3)) { #red( - #black(l1, xy1, l3), - xy3, - #black(r3, xy2, r2)) + #black(l1, x1, y1, l3), + x3, y3, + #black(r3, x2, y2, r2)) }; case r1l2 { balLeft ( l1, - xy1, - #black(r1l2, xy2, r2) + x1, y1, + #black(r1l2, x2, y2, r2) ) } } @@ -912,43 +912,43 @@ module { public func remove(tree : Map, compare : (K, K) -> O.Order, x : K) : (Map, ?V) { var y0 : ?V = null; - func delNode(left : Map, xy : (K, V), right : Map) : Map { - switch (compare (x, xy.0)) { + func delNode(left : Map, x1 : K, y1 : V, right : Map) : Map { + switch (compare (x, x1)) { case (#less) { let newLeft = del left; switch left { - case (#black(_, _, _)) { - balLeft(newLeft, xy, right) + case (#black(_, _, _, _)) { + balLeft(newLeft, x1, y1, right) }; case _ { - #red(newLeft, xy, right) + #red(newLeft, x1, y1, right) } } }; case (#greater) { let newRight = del right; switch right { - case (#black(_, _, _)) { - balRight(left, xy, newRight) + case (#black(_, _, _, _)) { + balRight(left, x1, y1, newRight) }; case _ { - #red(left, xy, newRight) + #red(left, x1, y1, newRight) } } }; case (#equal) { - y0 := ?xy.1; + y0 := ?y1; append(left, right) }; } }; func del(tree : Map) : Map { switch tree { - case (#red(left, xy, right)) { - delNode(left, xy, right) + case (#red(left, x, y, right)) { + delNode(left, x, y, right) }; - case (#black(left, xy, right)) { - delNode(left, xy, right) + case (#black(left, x, y, right)) { + delNode(left, x, y, right) }; case (#leaf) { tree @@ -956,8 +956,8 @@ module { }; }; switch (del(tree)) { - case (#red(left, xy, right)) { - (#black(left, xy, right), y0); + case (#red(left, x, y, right)) { + (#black(left, x, y, right), y0); }; case other { (other, y0) }; }; diff --git a/test/PersistentOrderedMap.test.mo b/test/PersistentOrderedMap.test.mo index 4c78c618..adcf9799 100644 --- a/test/PersistentOrderedMap.test.mo +++ b/test/PersistentOrderedMap.test.mo @@ -41,13 +41,13 @@ func blackDepth(node : Map.Map) : Nat { }; switch node { case (#leaf) 0; - case (#red(left, (key, _), right)) { + case (#red(left, key, _, right)) { let leftBlacks = checkNode(left, key, right); assert (not isRed(left)); assert (not isRed(right)); leftBlacks }; - case (#black(left, (key, _), right)) { + case (#black(left, key, _, right)) { checkNode(left, key, right) + 1 } } @@ -56,7 +56,7 @@ func blackDepth(node : Map.Map) : Nat { func isRed(node : Map.Map) : Bool { switch node { - case (#red(_, _, _)) true; + case (#red(_, _, _, _)) true; case _ false } }; @@ -64,10 +64,10 @@ func isRed(node : Map.Map) : Bool { func checkKey(node : Map.Map, isValid : Nat -> Bool) { switch node { case (#leaf) {}; - case (#red( _, (key, _), _)) { + case (#red( _, key, _, _)) { assert (isValid(key)) }; - case (#black( _, (key, _), _)) { + case (#black( _, key, _, _)) { assert (isValid(key)) } } From e0204cb0c8063115685c363e74624d4fe2e990b5 Mon Sep 17 00:00:00 2001 From: Pavel Golovin Date: Thu, 24 Oct 2024 18:08:54 +0200 Subject: [PATCH 20/20] PersistentOrderedMap: clean up comments + formatting typo --- src/PersistentOrderedMap.mo | 43 +++++++++++++++---------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo index 2dce47d7..3522b9c0 100644 --- a/src/PersistentOrderedMap.mo +++ b/src/PersistentOrderedMap.mo @@ -27,15 +27,6 @@ import List "List"; import Nat "Nat"; import O "Order"; -// TODO: a faster, more compact and less indirect representation would be: -// type Map = { -// #red : (Map, K, V, Map); -// #black : (Map, K, V, Map); -// #leaf -//}; -// (this inlines the colors into the variant, flattens a tuple, and removes a (now) redundant option, for considerable heap savings.) -// It would also make sense to maintain the size in a separate root for 0(1) access. - module { /// Red-black tree of nodes with key-value entries, ordered by the keys. @@ -47,6 +38,8 @@ module { #leaf }; + public type Direction = { #fwd; #bwd }; + /// Operations on `Map`, that require a comparator. /// /// The object should be created once, then used for all the operations @@ -54,7 +47,7 @@ module { /// /// `MapOps` contains methods that require `compare` internally: /// operations that may reshape a `Map` or should find something. - public class MapOps(compare : (K,K) -> O.Order) { + public class MapOps(compare : (K, K) -> O.Order) { /// Returns a new map, containing all entries given by the iterator `i`. /// If there are multiple entries with the same key the last one is taken. @@ -80,7 +73,7 @@ module { /// assuming that the `compare` function implements an `O(1)` comparison. /// /// Note: Creates `O(n * log(n))` temporary objects that will be collected as garbage. - public func fromIter(i : I.Iter<(K,V)>) : Map + public func fromIter(i : I.Iter<(K, V)>) : Map = Internal.fromIter(i, compare); /// Insert the value `value` with key `key` into the map `m`. Overwrites any existing entry with key `key`. @@ -148,7 +141,7 @@ module { /// assuming that the `compare` function implements an `O(1)` comparison. /// /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. - public func replace(m : Map, key : K, value : V) : (Map, ?V) + public func replace(m : Map, key : K, value : V) : (Map, ?V) = Internal.replace(m, compare, key, value); /// Creates a new map by applying `f` to each entry in the map `m`. For each entry @@ -276,7 +269,7 @@ module { /// assuming that the `compare` function implements an `O(1)` comparison. /// /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. - public func remove(m : Map, key : K) : (Map, ?V) + public func remove(m : Map, key : K) : (Map, ?V) = Internal.remove(m, compare, key); /// Create a new empty map. @@ -536,13 +529,11 @@ module { = Internal.foldRight(map, base, combine); }; - public type Direction = { #fwd; #bwd }; - module Internal { public func fromIter(i : I.Iter<(K,V)>, compare : (K, K) -> O.Order) : Map { - var map = #leaf : Map; + var map = #leaf : Map; for(val in i) { map := put(map, compare, val.0, val.1); }; @@ -698,12 +689,12 @@ module { (#red (l, x, y, r)) }; case _ { - Debug.trap "RBTree.red" + Debug.trap "PersistentOrderedMap.red" } } }; - func lbalance(left : Map, x : K, y : V, right : Map) : Map { + func lbalance(left : Map, x : K, y : V, right : Map) : Map { switch (left, right) { case (#red(#red(l1, x1, y1, r1), x2, y2, r2), r) { #red( @@ -723,7 +714,7 @@ module { } }; - func rbalance(left : Map, x : K, y : V, right : Map) : Map { + func rbalance(left : Map, x : K, y : V, right : Map) : Map { switch (left, right) { case (l, #red(l1, x1, y1, #red(l2, x2, y2, r2))) { #red( @@ -753,7 +744,7 @@ module { onClash : ClashResolver ) : Map{ - func ins(tree : Map) : Map { + func ins(tree : Map) : Map { switch tree { case (#black(left, x, y, right)) { switch (compare (key, x)) { @@ -802,7 +793,7 @@ module { key : K, val : V ) - : (Map, ?V) { + : (Map, ?V) { var oldVal : ?V = null; func onClash( clash : { old : V; new : V } ) : V { @@ -821,7 +812,7 @@ module { ) : Map = replace(m, compare, key, val).0; - func balLeft(left : Map, x : K, y : V, right : Map) : Map { + func balLeft(left : Map, x : K, y : V, right : Map) : Map { switch (left, right) { case (#red(l1, x1, y1, r1), r) { #red( @@ -842,7 +833,7 @@ module { } }; - func balRight(left : Map, x : K, y : V, right : Map) : Map { + func balRight(left : Map, x : K, y : V, right : Map) : Map { switch (left, right) { case (l, #red(l1, x1, y1, r1)) { #red( @@ -910,9 +901,9 @@ module { public func delete(m : Map, compare : (K, K) -> O.Order, key : K) : Map = remove(m, compare, key).0; - public func remove(tree : Map, compare : (K, K) -> O.Order, x : K) : (Map, ?V) { + public func remove(tree : Map, compare : (K, K) -> O.Order, x : K) : (Map, ?V) { var y0 : ?V = null; - func delNode(left : Map, x1 : K, y1 : V, right : Map) : Map { + func delNode(left : Map, x1 : K, y1 : V, right : Map) : Map { switch (compare (x, x1)) { case (#less) { let newLeft = del left; @@ -942,7 +933,7 @@ module { }; } }; - func del(tree : Map) : Map { + func del(tree : Map) : Map { switch tree { case (#red(left, x, y, right)) { delNode(left, x, y, right)