diff --git a/compiler/test/stdlib/immutablemap.test.gr b/compiler/test/stdlib/immutablemap.test.gr new file mode 100644 index 0000000000..41783affc3 --- /dev/null +++ b/compiler/test/stdlib/immutablemap.test.gr @@ -0,0 +1,331 @@ +import ImmutableMap from "immutablemap" +import List from "list" +import Array from "array" + +// Data types used in multiple tests +enum Resource { + Grain, + Sheep, + Brick, + Wood, +} +record ResourceData { + name: String, + emoji: String, +} + +let strKeys = ImmutableMap.fromList([("🌾", 1), ("🐑", 2), ("🧱", 3)]) +let numKeys = ImmutableMap.fromList([(1, "🌾"), (2, "🐑"), (3, "🧱")]) +let varKeys = ImmutableMap.fromList( + [(Grain, "🌾"), (Sheep, "🐑"), (Brick, "🧱")] +) +let recordKeys = ImmutableMap.fromList( + [ + ({ name: "Grain", emoji: "🌾" }, 1), + ({ name: "Sheep", emoji: "🐑" }, 2), + ({ name: "Brick", emoji: "🧱" }, 3), + ] +) + +// ImmutableMap.isEmpty() + +let mut e = ImmutableMap.empty + +assert ImmutableMap.isEmpty(e) +e = ImmutableMap.remove("🌾", e) +assert ImmutableMap.isEmpty(e) +let newE = ImmutableMap.set("🌾", "🌾", e) +assert !ImmutableMap.isEmpty(newE) +assert ImmutableMap.isEmpty(e) + +// ImmutableMap.size() + +let m = strKeys + +assert ImmutableMap.size(m) == 3 + +// ImmutableMap.contains() + +assert ImmutableMap.contains("🌾", m) +assert ImmutableMap.contains("🐑", m) +assert ImmutableMap.contains("🧱", m) +assert !ImmutableMap.contains("🌳", m) + +// ImmutableMap.set() & ImmutableMap.get() + +// With Number keys +let nums = numKeys + +assert ImmutableMap.get(1, nums) == Some("🌾") +assert ImmutableMap.get(2, nums) == Some("🐑") +assert ImmutableMap.get(3, nums) == Some("🧱") +assert ImmutableMap.get(4, nums) == None + +// With String keys +let mut strs = strKeys + +assert ImmutableMap.get("🌾", strs) == Some(1) +assert ImmutableMap.get("🐑", strs) == Some(2) +assert ImmutableMap.get("🧱", strs) == Some(3) +assert ImmutableMap.get("🌳", strs) == None + +// With variant keys +let vars = varKeys + +assert ImmutableMap.get(Grain, vars) == Some("🌾") +assert ImmutableMap.get(Sheep, vars) == Some("🐑") +assert ImmutableMap.get(Brick, vars) == Some("🧱") +assert ImmutableMap.get(Wood, vars) == None + +// With record keys +let recs = recordKeys + +assert ImmutableMap.get({ name: "Grain", emoji: "🌾" }, recs) == Some(1) +assert ImmutableMap.get({ name: "Sheep", emoji: "🐑" }, recs) == Some(2) +assert ImmutableMap.get({ name: "Brick", emoji: "🧱" }, recs) == Some(3) +assert ImmutableMap.get({ name: "Wood", emoji: "🌳" }, recs) == None + +// Overwriting data +let mut o = ImmutableMap.empty + +o = ImmutableMap.set(1, "🐑", o) +o = ImmutableMap.set(1, "🌾", o) + +assert ImmutableMap.get(1, o) == Some("🌾") + +// ImmutableMap.remove() + +let mut r = strKeys + +assert ImmutableMap.size(r) == 3 + +r = ImmutableMap.remove("🐑", r) + +assert ImmutableMap.size(r) == 2 +assert ImmutableMap.get("🐑", r) == None + +r = ImmutableMap.remove("🌳", r) + +assert ImmutableMap.size(r) == 2 + +r = ImmutableMap.remove("🌾", r) +assert ImmutableMap.get("🌾", r) == None + +r = ImmutableMap.remove("🧱", r) +assert ImmutableMap.get("🧱", r) == None + +assert ImmutableMap.isEmpty(r) + +// ImmutableMap.forEach() + +let fe = varKeys + +let mut called = 0 + +ImmutableMap.forEach((key, value) => { + called += 1 + match (key) { + Grain => assert value == "🌾", + Sheep => assert value == "🐑", + Brick => assert value == "🧱", + _ => fail "ImmutableMap.forEach() should not contain this value.", + } +}, fe) + +assert called == 3 + +// ImmutableMap.reduce() + +let mut r = ImmutableMap.empty + +r = ImmutableMap.set(Grain, 1, r) +r = ImmutableMap.set(Sheep, 2, r) +r = ImmutableMap.set(Brick, 3, r) + +let mut called = 0 + +let result = ImmutableMap.reduce((acc, key, value) => { + called += 1 + match (key) { + Grain => assert value == 1, + Sheep => assert value == 2, + Brick => assert value == 3, + _ => fail "ImmutableMap.reduce() should not contain this value.", + } + acc + value +}, 0, r) + +assert called == 3 +assert result == 6 + +// ImmutableMap.keys() & ImmutableMap.values(); + +let kvs = varKeys + +let keys = ImmutableMap.keys(kvs) + +assert List.contains(Grain, keys) +assert List.contains(Sheep, keys) +assert List.contains(Brick, keys) +assert !List.contains(Wood, keys) + +let vals = ImmutableMap.values(kvs) + +assert List.contains("🌾", vals) +assert List.contains("🐑", vals) +assert List.contains("🧱", vals) +assert !List.contains("🌳", vals) + +// ImmutableMap.toList() + +let tl = varKeys + +let lis = ImmutableMap.toList(tl) + +// No order is guaranteed +assert List.contains((Grain, "🌾"), lis) +assert List.contains((Sheep, "🐑"), lis) +assert List.contains((Brick, "🧱"), lis) +assert !List.contains((Wood, "🌳"), lis) + +// ImmutableMap.fromList() + +let fl = ImmutableMap.fromList( + [(Grain, "🌾"), (Sheep, "🐑"), (Brick, "🧱")] +) + +assert ImmutableMap.contains(Grain, fl) +assert ImmutableMap.contains(Sheep, fl) +assert ImmutableMap.contains(Brick, fl) +assert !ImmutableMap.contains(Wood, fl) + +// ImmutableMap.toArray() + +let ta = varKeys + +let arr = ImmutableMap.toArray(ta) + +// No order is guaranteed +assert Array.contains((Grain, "🌾"), arr) +assert Array.contains((Sheep, "🐑"), arr) +assert Array.contains((Brick, "🧱"), arr) +assert !Array.contains((Wood, "🌳"), arr) + +// ImmutableMap.fromArray() + +let fa = ImmutableMap.fromArray( + [> (Grain, "🌾"), (Sheep, "🐑"), (Brick, "🧱")] +) + +assert ImmutableMap.contains(Grain, fa) +assert ImmutableMap.contains(Sheep, fa) +assert ImmutableMap.contains(Brick, fa) +assert !ImmutableMap.contains(Wood, fa) + +// ImmutableMap.filter() + +let makeFilterTestImmutableMap = () => + ImmutableMap.fromList([(Grain, "g"), (Sheep, "s"), (Brick, "b")]) + +let mut filterTestImmutableMap = makeFilterTestImmutableMap() + +filterTestImmutableMap = ImmutableMap.filter((key, value) => + key == Sheep, filterTestImmutableMap) + +assert !ImmutableMap.contains(Grain, filterTestImmutableMap) +assert ImmutableMap.contains(Sheep, filterTestImmutableMap) +assert !ImmutableMap.contains(Brick, filterTestImmutableMap) + +let mut filterTestImmutableMap = makeFilterTestImmutableMap() + +filterTestImmutableMap = ImmutableMap.filter((key, value) => + value == "b" || value == "s", filterTestImmutableMap) + +assert !ImmutableMap.contains(Grain, filterTestImmutableMap) +assert ImmutableMap.contains(Sheep, filterTestImmutableMap) +assert ImmutableMap.contains(Brick, filterTestImmutableMap) + +let mut filterTestImmutableMap = makeFilterTestImmutableMap() + +filterTestImmutableMap = ImmutableMap.filter((key, value) => + value == "invalid", filterTestImmutableMap) + +assert ImmutableMap.size(filterTestImmutableMap) == 0 + +let mut filterTestImmutableMap = makeFilterTestImmutableMap() + +filterTestImmutableMap = ImmutableMap.filter((key, value) => + true, filterTestImmutableMap) + +assert ImmutableMap.size(filterTestImmutableMap) == 3 + +// ImmutableMap.reject() + +let mut rejectTestImmutableMap = makeFilterTestImmutableMap() + +rejectTestImmutableMap = ImmutableMap.reject((key, value) => + key == Sheep, rejectTestImmutableMap) + +assert ImmutableMap.contains(Grain, rejectTestImmutableMap) +assert !ImmutableMap.contains(Sheep, rejectTestImmutableMap) +assert ImmutableMap.contains(Brick, rejectTestImmutableMap) + +let mut rejectTestImmutableMap = makeFilterTestImmutableMap() + +rejectTestImmutableMap = ImmutableMap.reject((key, value) => + value == "b" || value == "s", rejectTestImmutableMap) + +assert ImmutableMap.contains(Grain, rejectTestImmutableMap) +assert !ImmutableMap.contains(Sheep, rejectTestImmutableMap) +assert !ImmutableMap.contains(Brick, rejectTestImmutableMap) + +let mut rejectTestImmutableMap = makeFilterTestImmutableMap() + +rejectTestImmutableMap = ImmutableMap.reject((key, value) => + true, rejectTestImmutableMap) + +assert ImmutableMap.size(rejectTestImmutableMap) == 0 + +let mut rejectTestImmutableMap = makeFilterTestImmutableMap() + +rejectTestImmutableMap = ImmutableMap.reject((key, value) => + false, rejectTestImmutableMap) + +assert ImmutableMap.size(rejectTestImmutableMap) == 3 + +// ImmutableMap.update() + +let mut toUpdate = ImmutableMap.fromList([("a", 1), ("b", 2), ("c", 3)]) + +toUpdate = ImmutableMap.update( + "b", + old => { + assert old == Some(2) + Some(4) + }, + toUpdate +) + +assert ImmutableMap.get("b", toUpdate) == Some(4) + +toUpdate = ImmutableMap.update( + "d", + old => { + assert old == None + Some(10) + }, + toUpdate +) + +assert ImmutableMap.get("d", toUpdate) == Some(10) + +toUpdate = ImmutableMap.update( + "c", + old => { + assert old == Some(3) + None + }, + toUpdate +) + +assert ImmutableMap.contains("c", toUpdate) == false diff --git a/compiler/test/stdlib/immutableset.test.gr b/compiler/test/stdlib/immutableset.test.gr new file mode 100644 index 0000000000..d31074f160 --- /dev/null +++ b/compiler/test/stdlib/immutableset.test.gr @@ -0,0 +1,197 @@ +import ImmutableSet from "immutableset" +import List from "list" +import Array from "array" + +// Data types used in multiple tests +enum Resource { + Grain, + Sheep, + Brick, + Wood, +} +record ResourceData { + name: String, + emoji: String, +} + +// ImmutableSet.isEmpty + +let mut e = ImmutableSet.empty + +assert ImmutableSet.isEmpty(e) +e = ImmutableSet.remove("🌾", e) +assert ImmutableSet.isEmpty(e) +let newE = ImmutableSet.add("🌾", e) +assert !ImmutableSet.isEmpty(newE) +assert ImmutableSet.isEmpty(e) + +// ImmutableSet.size + +let s = ImmutableSet.fromList(["🌾", "🐑", "🧱"]) + +assert ImmutableSet.size(s) == 3 + +// ImmutableSet.contains + +let h = ImmutableSet.fromList(["🌾", "🐑", "🧱"]) + +assert ImmutableSet.contains("🌾", h) +assert ImmutableSet.contains("🐑", h) +assert ImmutableSet.contains("🧱", h) +assert !ImmutableSet.contains("🌳", h) + +// ImmutableSet.add + +let mut vars = ImmutableSet.empty + +vars = ImmutableSet.add(Grain, vars) +vars = ImmutableSet.add(Sheep, vars) +vars = ImmutableSet.add(Grain, vars) + +assert ImmutableSet.size(vars) == 2 + +let mut recs = ImmutableSet.empty + +recs = ImmutableSet.add({ name: "Grain", emoji: "🌾" }, recs) +recs = ImmutableSet.add({ name: "Sheep", emoji: "🐑" }, recs) +recs = ImmutableSet.add({ name: "Brick", emoji: "🧱" }, recs) +recs = ImmutableSet.add({ name: "Grain", emoji: "🌾" }, recs) +recs = ImmutableSet.add({ name: "Sheep", emoji: "🐑" }, recs) +recs = ImmutableSet.add({ name: "Brick", emoji: "🧱" }, recs) + +assert ImmutableSet.size(recs) == 3 + +// ImmutableSet.remove + +let mut r = ImmutableSet.fromList(["🌾", "🐑", "🧱"]) + +assert ImmutableSet.size(r) == 3 + +r = ImmutableSet.remove("🌾", r) + +assert ImmutableSet.size(r) == 2 +assert !ImmutableSet.contains("🌾", r) + +r = ImmutableSet.remove("🐑", r) +r = ImmutableSet.remove("🧱", r) + +assert ImmutableSet.isEmpty(r) + +// ImmutableSet.filter + +let makeTestSet = () => ImmutableSet.fromList([Grain, Sheep, Brick]) + +let mut filterTestSet = makeTestSet() + +ImmutableSet.filter(key => fail "Shouldn't be called", ImmutableSet.empty) +filterTestSet = ImmutableSet.filter(key => key == Sheep, filterTestSet) + +assert !ImmutableSet.contains(Grain, filterTestSet) +assert ImmutableSet.contains(Sheep, filterTestSet) +assert !ImmutableSet.contains(Brick, filterTestSet) + +// ImmutableSet.reject + +let mut rejectTestSet = makeTestSet() + +ImmutableSet.reject(key => fail "Shouldn't be called", ImmutableSet.empty) +rejectTestSet = ImmutableSet.reject(key => key == Sheep, rejectTestSet) + +assert ImmutableSet.contains(Grain, rejectTestSet) +assert !ImmutableSet.contains(Sheep, rejectTestSet) +assert ImmutableSet.contains(Brick, rejectTestSet) + +// ImmutableSet.reduce + +let reduceTestSet = ImmutableSet.fromList([1, 3, 2, 5, 4]) + +let result = ImmutableSet.reduce((acc, key) => fail "Shouldn't be called", +0, +ImmutableSet.empty +) + +assert result == 0 + +let mut called = 0 + +let result = ImmutableSet.reduce((acc, key) => { + called += 1 + [key, ...acc] +}, [], reduceTestSet) + +assert called == 5 +assert result == [5, 4, 3, 2, 1] + +// ImmutableSet.forEach + +let forEachTestSet = makeTestSet() + +ImmutableSet.forEach(key => fail "Shouldn't be called", ImmutableSet.empty) + +let mut called = 0 + +ImmutableSet.forEach(key => { + called += 1 + match (key) { + Grain => void, + Sheep => void, + Brick => void, + _ => fail "ImmutableSet.forEach() should not contain this value.", + } +}, forEachTestSet) + +assert called == 3 + +// ImmutableSet.diff + +let d = ImmutableSet.fromList([0, 1, 2, 3, 4, 5, 6]) +let e = ImmutableSet.fromList([4, 2, 1, 3, 0, -2, -1, -3]) + +let diffSet = ImmutableSet.diff(d, e) + +assert ImmutableSet.size(diffSet) == 5 +assert [-3, -2, -1, 5, 6] == ImmutableSet.toList(diffSet) + +// ImmutableSet.union + +let unionSet = ImmutableSet.union(d, e) + +assert ImmutableSet.size(unionSet) == 10 +assert [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6] == ImmutableSet.toList(unionSet) + +// ImmutableSet.intersect + +let intersectSet = ImmutableSet.intersect(d, e) + +assert ImmutableSet.size(intersectSet) == 5 +assert [0, 1, 2, 3, 4] == ImmutableSet.toList(intersectSet) + +// ImmutableSet.fromList + +let k = ImmutableSet.fromList([1, 1, 1]) + +assert ImmutableSet.size(k) == 1 +assert ImmutableSet.contains(1, k) + +// ImmutableSet.toList + +let o = ImmutableSet.fromList([0, 2, 1, 3, 0, 4, 2]) + +let l = ImmutableSet.toList(o) + +assert l == [0, 1, 2, 3, 4] + +// ImmutableSet.fromArray + +let q = ImmutableSet.fromArray([> 0, 0, 0]) + +assert ImmutableSet.size(q) == 1 +assert ImmutableSet.contains(0, q) + +// ImmutableSet.toArray + +let p = ImmutableSet.fromArray([> 0, 1, 2, 3, 4, 3, 2, 1, 0]) + +let r = ImmutableSet.toArray(p) + +assert r == [> 0, 1, 2, 3, 4] diff --git a/compiler/test/suites/stdlib.re b/compiler/test/suites/stdlib.re index 86efe51522..0dd5f260bd 100644 --- a/compiler/test/suites/stdlib.re +++ b/compiler/test/suites/stdlib.re @@ -100,6 +100,7 @@ describe("stdlib", ({test, testSkip}) => { assertStdlib("int64.test"); assertStdlib("list.test"); assertStdlib("map.test"); + assertStdlib("immutablemap.test"); assertStdlib("marshal.test"); assertStdlib("number.test"); assertStdlib("option.test"); @@ -108,6 +109,7 @@ describe("stdlib", ({test, testSkip}) => { assertStdlib("range.test"); assertStdlib("result.test"); assertStdlib("set.test"); + assertStdlib("immutableset.test"); assertStdlib("regex.test"); assertStdlib("stack.test"); assertStdlib("priorityqueue.test"); diff --git a/stdlib/immutablemap.gr b/stdlib/immutablemap.gr new file mode 100644 index 0000000000..9dcb6e72c6 --- /dev/null +++ b/stdlib/immutablemap.gr @@ -0,0 +1,493 @@ +/** + * @module ImmutableMap: An ImmutableMap holds key-value pairs. Any value may be used as a key or value. Operations on an ImmutableMap do not mutate the map's internal state. + * @example import ImmutableMap from "immutablemap" + * + * @since v0.5.4 + */ + +import List from "list" +import Array from "array" +import Option from "option" + +// implementation based on the paper "Implementing Sets Efficiently in a +// Functional Language" by Stephen Adams +record Node { + key: k, + val: v, + size: Number, + left: ImmutableMap, + right: ImmutableMap, +}, +/** + * @section Types: Type declarations included in the ImmutableMap module. + */ +enum ImmutableMap { + Empty, + Tree(Node), +} + +/** + * @section Values: Functions and constants for working with ImmutableMaps. + */ + +// semi-arbitrary value chosen for algorithm for determining when to balance +// trees; no tree can have a left subtree containing this number of times +// more elements than its right subtree or vice versa +let weight = 4 + +/** + * An empty map + * + * @since v0.5.4 + */ +export let empty = Empty + +// returns the key-value pair of the minimum key in a tree +let rec min = node => { + match (node) { + Tree({ key, val, left: Empty, _ }) => (key, val), + Tree({ left, _ }) => min(left), + Empty => fail "Impossible: min of empty element in ImmutableMap", + } +} + +/** + * Provides the count of key-value pairs stored within the map. + * + * @param map: The map to inspect + * @returns The count of key-value pairs in the map + * + * @since v0.5.4 + */ +export let size = map => { + match (map) { + Empty => 0, + Tree({ size, _ }) => size, + } +} + +/** + * Determines if the map contains no key-value pairs. + * + * @param map: The map to inspect + * @returns `true` if the given map is empty or `false` otherwise + * + * @since v0.5.4 + */ +export let isEmpty = map => { + match (map) { + Empty => true, + Tree(_) => false, + } +} + +let unwrapTree = node => { + match (node) { + Empty => fail "Impossible: ImmutableMap unwrapTree got an empty tree node", + Tree(tree) => tree, + } +} + +// helper function for creating a tree node with correct size from +// two balanced trees +let makeNode = (key, val, left, right) => { + Tree({ key, val, size: 1 + size(left) + size(right), left, right }) +} + +// note: see Figure 1 of paper referenced above for visual illustration of +// the rotations below + +// node rotation moving the left subtree of the right node to the left side +let singleL = (key, val, left, right) => { + let { key: rKey, val: rVal, left: rl, right: rr, _ } = unwrapTree(right) + makeNode(rKey, rVal, makeNode(key, val, left, rl), rr) +} + +// node rotation moving left child of right tree to the root +let doubleL = (key, val, left, right) => { + let { key: rKey, val: rVal, left: rl, right: rr, _ } = unwrapTree(right) + let { key: rlKey, val: rlVal, left: rll, right: rlr, _ } = unwrapTree(rl) + makeNode( + rlKey, + rlVal, + makeNode(key, val, left, rll), + makeNode(rKey, rVal, rlr, rr) + ) +} + +// node rotation moving the right subtree of the left node to the right side +let singleR = (key, val, left, right) => { + let { key: lKey, val: lVal, left: ll, right: lr, _ } = unwrapTree(left) + makeNode(lKey, lVal, ll, makeNode(key, val, lr, right)) +} + +// node rotation moving right child of left tree to the root +let doubleR = (key, val, left, right) => { + let { key: lKey, val: lVal, left: ll, right: lr, _ } = unwrapTree(left) + let { key: lrKey, val: lrVal, left: lrl, right: lrr, _ } = unwrapTree(lr) + makeNode( + lrKey, + lrVal, + makeNode(lKey, lVal, ll, lrl), + makeNode(key, val, lrr, right) + ) +} + +// creates a new node after either the left or right trees have just had an +// element inserted or removed from them, maintaining balance in the tree +let balancedNode = (key, val, left, right) => { + let makeNodeFn = if (size(left) + size(right) < 2) { + makeNode + } else if (size(right) > weight * size(left)) { + // if the right tree is too much larger than the left then move part of + // the right tree to the left side + let { left: rl, right: rr, _ } = unwrapTree(right) + if (size(rl) < size(rr)) singleL else doubleL + } else if (size(left) > weight * size(right)) { + // if the left tree is too much larger than the right then move part of + // the left tree to the right side + let { left: ll, right: lr, _ } = unwrapTree(left) + if (size(lr) < size(ll)) singleR else doubleR + } else { + // if neither tree is too much larger than the other then simply create + // a new node + makeNode + } + + makeNodeFn(key, val, left, right) +} + +/** + * Produces a new map containing a new key-value pair. If the key already exists in the map, the value is replaced. + * + * @param key: The unique key in the map + * @param value: The value to store + * @param map: The base map + * @returns A new map containing the new key-value pair + * + * @since v0.5.4 + */ +export let rec set = (key, val, map) => { + match (map) { + Empty => Tree({ key, val, size: 1, left: Empty, right: Empty }), + Tree({ key: nodeKey, val: nodeVal, left, right, _ }) => { + match (compare(key, nodeKey)) { + cmp when cmp < 0 => + balancedNode(nodeKey, nodeVal, set(key, val, left), right), + cmp when cmp > 0 => + balancedNode(nodeKey, nodeVal, left, set(key, val, right)), + _ => makeNode(key, val, left, right), + } + }, + } +} + +/** + * Retrieves the value for the given key. + * + * @param key: The key to access + * @param map: The map to access + * @returns `Some(value)` if the key exists in the map or `None` otherwise + * + * @since v0.5.4 + */ +export let rec get = (key, map) => { + match (map) { + Empty => None, + Tree({ key: nodeKey, val: nodeVal, left, right, _ }) => { + match (compare(key, nodeKey)) { + cmp when cmp < 0 => get(key, left), + cmp when cmp > 0 => get(key, right), + _ => Some(nodeVal), + } + }, + } +} + +/** + * Determines if the map contains the given key. In such a case, it will always contain a value for the given key. + * + * @param key: The key to search for + * @param map: The map to search + * @returns `true` if the map contains the given key or `false` otherwise + * + * @since v0.5.4 + */ +export let rec contains = (key, map) => { + Option.isSome(get(key, map)) +} + +// removes the minimum element from a tree +let rec removeMin = node => { + match (node) { + Tree({ left: Empty, right, _ }) => right, + Tree({ key, val, left, right, _ }) => + balancedNode(key, val, removeMin(left), right), + _ => fail "Impossible: ImmutableMap removeMin on empty node", + } +} + +// helper function for removing a node by creating a new node containing the +// removed node's left and right subtrees +let removeInner = (left, right) => { + match ((left, right)) { + (Empty, node) | (node, Empty) => node, + (left, right) => { + let (minKey, minVal) = min(right) + balancedNode(minKey, minVal, left, removeMin(right)) + }, + } +} + +/** + * Produces a new map without the key-value pair corresponding to the given + * key. If the key doesn't exist in the map, the map will be returned unmodified. + * + * @param key: The key to exclude + * @param map: The map to exclude from + * @returns A new map without the given key + * + * @since v0.5.4 + */ +export let rec remove = (key, map) => { + match (map) { + Empty => Empty, + Tree({ key: nodeKey, val: nodeVal, left, right, _ }) => { + match (compare(key, nodeKey)) { + cmp when cmp < 0 => + balancedNode(nodeKey, nodeVal, remove(key, left), right), + cmp when cmp > 0 => + balancedNode(nodeKey, nodeVal, left, remove(key, right)), + _ => removeInner(left, right), + } + }, + } +} + +/** + * Produces a new map by calling an updater function that receives the + * previously stored value as an `Option` and returns the new value to be + * stored as an `Option`. If the key didn't exist previously, the value + * will be `None`. If `None` is returned from the updater function, the + * key-value pair is excluded. + * + * @param key: The unique key in the map + * @param fn: The updater function + * @param map: The base map + * @returns A new map with the value at the given key modified according to the function's output + * + * @since v0.5.4 + */ +export let update = (key, fn, map) => { + let val = get(key, map) + match (fn(val)) { + Some(next) => set(key, next, map), + None => remove(key, map), + } +} + +/** + * Iterates the map, calling an iterator function with each key and value. + * + * @param fn: The iterator function to call with each key and value + * @param map: The map to iterate + * + * @since v0.5.4 + */ +export let forEach = (fn, map) => { + let rec forEachInner = node => { + match (node) { + Empty => void, + Tree({ key, val, left, right, _ }) => { + forEachInner(left) + fn(key, val): Void + forEachInner(right) + }, + } + } + forEachInner(map) +} + +/** + * Combines all key-value pairs of a map using a reducer function. + * + * @param fn: The reducer function to call on each key and value, where the value returned will be the next accumulator value + * @param init: The initial value to use for the accumulator on the first iteration + * @param map: The map to iterate + * @returns The final accumulator returned from `fn` + * + * @since v0.5.4 + */ +export let reduce = (fn, init, map) => { + let rec reduceInner = (acc, node) => { + match (node) { + Empty => acc, + Tree({ key, val, left, right, _ }) => { + let newAcc = fn(reduceInner(acc, left), key, val) + reduceInner(newAcc, right) + }, + } + } + reduceInner(init, map) +} + +// joins two trees with a value, preserving the BST property of left children +// being less the node and right children being greater than the node +let rec concat3 = (key, val, left, right) => { + match ((left, right)) { + (Empty, node) | (node, Empty) => set(key, val, node), + (Tree(left) as leftOpt, Tree(right) as rightOpt) => { + if (weight * left.size < right.size) { + balancedNode( + right.key, + right.val, + concat3(key, val, leftOpt, right.left), + right.right + ) + } else if (weight * right.size < left.size) { + balancedNode( + left.key, + left.val, + left.left, + concat3(key, val, left.right, rightOpt) + ) + } else { + makeNode(key, val, leftOpt, rightOpt) + } + }, + } +} + +// concatenates two trees of arbitrary size +let concat = (node1, node2) => { + match (node2) { + Empty => node1, + _ => { + let (minKey, minVal) = min(node2) + concat3(minKey, minVal, node1, removeMin(node2)) + }, + } +} + +let reduceRight = (fn, init, map) => { + let rec reduceInner = (acc, node) => { + match (node) { + Empty => acc, + Tree({ key, val, left, right, _ }) => { + let newAcc = fn(reduceInner(acc, right), key, val) + reduceInner(newAcc, left) + }, + } + } + reduceInner(init, map) +} + +/** + * Enumerates all keys in the given map. + * + * @param map: The map to enumerate + * @returns A list containing all keys from the given map + * + * @since v0.5.4 + */ +export let keys = map => { + reduceRight((list, key, _) => [key, ...list], [], map) +} + +/** + * Enumerates all values in the given map. + * + * @param map: The map to enumerate + * @returns A list containing all values from the given map + * + * @since v0.5.4 + */ +export let values = map => { + reduceRight((list, _, value) => [value, ...list], [], map) +} + +/** + * Produces a new map excluding the key-value pairs where a predicate function returns `false`. + * + * @param fn: The predicate function to indicate which key-value pairs to exclude from the map, where returning `false` indicates the key-value pair should be excluded + * @param map: The map to iterate + * @returns A new map excluding the key-value pairs not fulfilling the predicate + * + * @since v0.5.4 + */ +export let filter = (fn, map) => { + let rec filterInner = node => { + match (node) { + Empty => Empty, + Tree({ key, val, left, right, _ }) => { + if (fn(key, val)) { + concat3(key, val, filterInner(left), filterInner(right)) + } else { + concat(filterInner(left), filterInner(right)) + } + }, + } + } + filterInner(map) +} + +/** + * Produces a new map excluding the key-value pairs where a predicate function returns `true`. + * + * @param fn: The predicate function to indicate which key-value pairs to exclude from the map, where returning `true` indicates the key-value pair should be excluded + * @param map: The map to iterate + * @returns A new map excluding the key-value pairs fulfilling the predicate + * + * @since v0.5.4 + */ +export let reject = (fn, map) => { + filter((key, val) => !fn(key, val), map) +} + +/** + * Creates a map from a list. + * + * @param list: The list to convert + * @returns A map containing all key-value pairs from the list + * + * @since v0.5.4 + */ +export let fromList = list => { + List.reduce((map, (key, val)) => set(key, val, map), empty, list) +} + +/** + * Enumerates all key-value pairs in the given map. + * + * @param map: The map to enumerate + * @returns A list containing all key-value pairs from the given map + * + * @since v0.5.4 + */ +export let toList = map => { + reduceRight((list, key, val) => [(key, val), ...list], [], map) +} + +/** + * Creates a map from an array. + * + * @param array: The array to convert + * @returns A map containing all key-value pairs from the array + * + * @since v0.5.4 + */ +export let fromArray = array => { + Array.reduce((map, (key, val)) => set(key, val, map), empty, array) +} + +/** + * Converts a map into an array of its key-value pairs. + * + * @param map: The map to convert + * @returns An array containing all key-value pairs from the given map + * + * @since v0.5.4 + */ +export let toArray = map => { + Array.fromList(toList(map)) +} diff --git a/stdlib/immutablemap.md b/stdlib/immutablemap.md new file mode 100644 index 0000000000..6260cd1f6c --- /dev/null +++ b/stdlib/immutablemap.md @@ -0,0 +1,479 @@ +--- +title: ImmutableMap +--- + +An ImmutableMap holds key-value pairs. Any value may be used as a key or value. Operations on an ImmutableMap do not mutate the map's internal state. + +
+Added in next +No other changes yet. +
+ +```grain +import ImmutableMap from "immutablemap" +``` + +## Types + +Type declarations included in the ImmutableMap module. + +### ImmutableMap.**ImmutableMap** + +```grain +type ImmutableMap +``` + +## Values + +Functions and constants for working with ImmutableMaps. + +### ImmutableMap.**empty** + +
+Added in next +No other changes yet. +
+ +```grain +empty : ImmutableMap +``` + +An empty map + +### ImmutableMap.**size** + +
+Added in next +No other changes yet. +
+ +```grain +size : ImmutableMap -> Number +``` + +Provides the count of key-value pairs stored within the map. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`map`|`ImmutableMap`|The map to inspect| + +Returns: + +|type|description| +|----|-----------| +|`Number`|The count of key-value pairs in the map| + +### ImmutableMap.**isEmpty** + +
+Added in next +No other changes yet. +
+ +```grain +isEmpty : ImmutableMap -> Bool +``` + +Determines if the map contains no key-value pairs. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`map`|`ImmutableMap`|The map to inspect| + +Returns: + +|type|description| +|----|-----------| +|`Bool`|`true` if the given map is empty or `false` otherwise| + +### ImmutableMap.**set** + +
+Added in next +No other changes yet. +
+ +```grain +set : (a, b, ImmutableMap) -> ImmutableMap +``` + +Produces a new map containing a new key-value pair. If the key already exists in the map, the value is replaced. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`key`|`a`|The unique key in the map| +|`value`|`b`|The value to store| +|`map`|`ImmutableMap`|The base map| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableMap`|A new map containing the new key-value pair| + +### ImmutableMap.**get** + +
+Added in next +No other changes yet. +
+ +```grain +get : (a, ImmutableMap) -> Option +``` + +Retrieves the value for the given key. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`key`|`a`|The key to access| +|`map`|`ImmutableMap`|The map to access| + +Returns: + +|type|description| +|----|-----------| +|`Option`|`Some(value)` if the key exists in the map or `None` otherwise| + +### ImmutableMap.**contains** + +
+Added in next +No other changes yet. +
+ +```grain +contains : (a, ImmutableMap) -> Bool +``` + +Determines if the map contains the given key. In such a case, it will always contain a value for the given key. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`key`|`a`|The key to search for| +|`map`|`ImmutableMap`|The map to search| + +Returns: + +|type|description| +|----|-----------| +|`Bool`|`true` if the map contains the given key or `false` otherwise| + +### ImmutableMap.**remove** + +
+Added in next +No other changes yet. +
+ +```grain +remove : (a, ImmutableMap) -> ImmutableMap +``` + +Produces a new map without the key-value pair corresponding to the given +key. If the key doesn't exist in the map, the map will be returned unmodified. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`key`|`a`|The key to exclude| +|`map`|`ImmutableMap`|The map to exclude from| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableMap`|A new map without the given key| + +### ImmutableMap.**update** + +
+Added in next +No other changes yet. +
+ +```grain +update : + (a, (Option -> Option), ImmutableMap) -> ImmutableMap +``` + +Produces a new map by calling an updater function that receives the +previously stored value as an `Option` and returns the new value to be +stored as an `Option`. If the key didn't exist previously, the value +will be `None`. If `None` is returned from the updater function, the +key-value pair is excluded. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`key`|`a`|The unique key in the map| +|`fn`|`Option -> Option`|The updater function| +|`map`|`ImmutableMap`|The base map| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableMap`|A new map with the value at the given key modified according to the function's output| + +### ImmutableMap.**forEach** + +
+Added in next +No other changes yet. +
+ +```grain +forEach : (((a, b) -> Void), ImmutableMap) -> Void +``` + +Iterates the map, calling an iterator function with each key and value. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`fn`|`(a, b) -> Void`|The iterator function to call with each key and value| +|`map`|`ImmutableMap`|The map to iterate| + +### ImmutableMap.**reduce** + +
+Added in next +No other changes yet. +
+ +```grain +reduce : (((a, b, c) -> a), a, ImmutableMap) -> a +``` + +Combines all key-value pairs of a map using a reducer function. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`fn`|`(a, b, c) -> a`|The reducer function to call on each key and value, where the value returned will be the next accumulator value| +|`init`|`a`|The initial value to use for the accumulator on the first iteration| +|`map`|`ImmutableMap`|The map to iterate| + +Returns: + +|type|description| +|----|-----------| +|`a`|The final accumulator returned from `fn`| + +### ImmutableMap.**keys** + +
+Added in next +No other changes yet. +
+ +```grain +keys : ImmutableMap -> List +``` + +Enumerates all keys in the given map. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`map`|`ImmutableMap`|The map to enumerate| + +Returns: + +|type|description| +|----|-----------| +|`List`|A list containing all keys from the given map| + +### ImmutableMap.**values** + +
+Added in next +No other changes yet. +
+ +```grain +values : ImmutableMap -> List +``` + +Enumerates all values in the given map. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`map`|`ImmutableMap`|The map to enumerate| + +Returns: + +|type|description| +|----|-----------| +|`List`|A list containing all values from the given map| + +### ImmutableMap.**filter** + +
+Added in next +No other changes yet. +
+ +```grain +filter : (((a, b) -> Bool), ImmutableMap) -> ImmutableMap +``` + +Produces a new map excluding the key-value pairs where a predicate function returns `false`. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`fn`|`(a, b) -> Bool`|The predicate function to indicate which key-value pairs to exclude from the map, where returning `false` indicates the key-value pair should be excluded| +|`map`|`ImmutableMap`|The map to iterate| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableMap`|A new map excluding the key-value pairs not fulfilling the predicate| + +### ImmutableMap.**reject** + +
+Added in next +No other changes yet. +
+ +```grain +reject : (((a, b) -> Bool), ImmutableMap) -> ImmutableMap +``` + +Produces a new map excluding the key-value pairs where a predicate function returns `true`. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`fn`|`(a, b) -> Bool`|The predicate function to indicate which key-value pairs to exclude from the map, where returning `true` indicates the key-value pair should be excluded| +|`map`|`ImmutableMap`|The map to iterate| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableMap`|A new map excluding the key-value pairs fulfilling the predicate| + +### ImmutableMap.**fromList** + +
+Added in next +No other changes yet. +
+ +```grain +fromList : List<(a, b)> -> ImmutableMap +``` + +Creates a map from a list. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`list`|`List<(a, b)>`|The list to convert| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableMap`|A map containing all key-value pairs from the list| + +### ImmutableMap.**toList** + +
+Added in next +No other changes yet. +
+ +```grain +toList : ImmutableMap -> List<(a, b)> +``` + +Enumerates all key-value pairs in the given map. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`map`|`ImmutableMap`|The map to enumerate| + +Returns: + +|type|description| +|----|-----------| +|`List<(a, b)>`|A list containing all key-value pairs from the given map| + +### ImmutableMap.**fromArray** + +
+Added in next +No other changes yet. +
+ +```grain +fromArray : Array<(a, b)> -> ImmutableMap +``` + +Creates a map from an array. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`array`|`Array<(a, b)>`|The array to convert| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableMap`|A map containing all key-value pairs from the array| + +### ImmutableMap.**toArray** + +
+Added in next +No other changes yet. +
+ +```grain +toArray : ImmutableMap -> Array<(a, b)> +``` + +Converts a map into an array of its key-value pairs. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`map`|`ImmutableMap`|The map to convert| + +Returns: + +|type|description| +|----|-----------| +|`Array<(a, b)>`|An array containing all key-value pairs from the given map| + diff --git a/stdlib/immutableset.gr b/stdlib/immutableset.gr new file mode 100644 index 0000000000..b7ff63d412 --- /dev/null +++ b/stdlib/immutableset.gr @@ -0,0 +1,498 @@ +/** + * @module ImmutableSet: An ImmutableSet is a collection of unique values. Operations on an ImmutableSet do not mutate the set's internal state. + * @example import ImmutableSet from "immutableset" + * + * @since v0.5.4 + */ + +import List from "list" +import Array from "array" + +// implementation based on the paper "Implementing Sets Efficiently in a +// Functional Language" by Stephen Adams + +record Node
{ + key: a, + size: Number, + left: ImmutableSet, + right: ImmutableSet, +}, +/** + * @section Types: Type declarations included in the ImmutableSet module. + */ +enum ImmutableSet { + Empty, + Tree(Node), +} + +/** + * @section Values: Functions and constants for working with ImmutableSets. + */ + +// semi-arbitrary value chosen for algorithm for determining when to balance +// trees; no tree can have a left subtree containing this number of times +// more elements than its right subtree or vice versa +let weight = 4 + +/** + * An empty set + * + * @since v0.5.4 + */ +export let empty = Empty + +// returns the minimum value in a tree +let rec min = node => { + match (node) { + Tree({ key, left: Empty, _ }) => key, + Tree({ left, _ }) => min(left), + Empty => fail "Impossible: min of empty element in ImmutableSet", + } +} + +/** + * Provides the count of values within the set. + * + * @param set: The set to inspect + * @returns The count of elements in the set + * + * @since v0.5.4 + */ +export let size = set => { + match (set) { + Empty => 0, + Tree({ size, _ }) => size, + } +} + +/** + * Determines if the set contains no elements. + * + * @param set: The set to inspect + * @returns `true` if the given set is empty or `false` otherwise + * + * @since v0.5.4 + */ +export let isEmpty = set => { + match (set) { + Empty => true, + Tree(_) => false, + } +} + +let unwrapTree = node => { + match (node) { + Empty => fail "Impossible: ImmutableSet unwrapTree got an empty tree node", + Tree(tree) => tree, + } +} + +// helper function for creating a tree node with correct size from +// two balanced trees +let makeNode = (key, left, right) => { + Tree({ key, size: 1 + size(left) + size(right), left, right }) +} + +// note: see Figure 1 of paper referenced above for visual illustration of +// the rotations below + +// node rotation moving the left subtree of the right node to the left side +let singleL = (key, left, right) => { + let { key: rKey, left: rl, right: rr, _ } = unwrapTree(right) + makeNode(rKey, makeNode(key, left, rl), rr) +} + +// node rotation moving left child of right tree to the root +let doubleL = (key, left, right) => { + let { key: rKey, left: rl, right: rr, _ } = unwrapTree(right) + let { key: rlKey, left: rll, right: rlr, _ } = unwrapTree(rl) + makeNode(rlKey, makeNode(key, left, rll), makeNode(rKey, rlr, rr)) +} + +// node rotation moving the right subtree of the left node to the right side +let singleR = (key, left, right) => { + let { key: lKey, left: ll, right: lr, _ } = unwrapTree(left) + makeNode(lKey, ll, makeNode(key, lr, right)) +} + +// node rotation moving right child of left tree to the root +let doubleR = (key, left, right) => { + let { key: lKey, left: ll, right: lr, _ } = unwrapTree(left) + let { key: lrKey, left: lrl, right: lrr, _ } = unwrapTree(lr) + makeNode(lrKey, makeNode(lKey, ll, lrl), makeNode(key, lrr, right)) +} + +// creates a new node after either the left or right trees have just had an +// element inserted or removed from them, maintaining balance in the tree +let balancedNode = (key, left, right) => { + let makeNodeFn = if (size(left) + size(right) < 2) { + makeNode + } else if (size(right) > weight * size(left)) { + // if the right tree is too much larger than the left then move part of + // the right tree to the left side + let { left: rl, right: rr, _ } = unwrapTree(right) + if (size(rl) < size(rr)) singleL else doubleL + } else if (size(left) > weight * size(right)) { + // if the left tree is too much larger than the right then move part of + // the left tree to the right side + let { left: ll, right: lr, _ } = unwrapTree(left) + if (size(lr) < size(ll)) singleR else doubleR + } else { + // if neither tree is too much larger than the other then simply create + // a new node + makeNode + } + + makeNodeFn(key, left, right) +} + +/** + * Produces a new set by inserting the given value into the set. If the value + * already exists, the new set will have the same elements as the input set. + * + * @param key: The value to add + * @param set: The base set + * @returns A new set containing the new element + * + * @since v0.5.4 + */ +export let rec add = (key, set) => { + match (set) { + Empty => Tree({ key, size: 1, left: Empty, right: Empty }), + Tree({ key: nodeKey, left, right, _ }) => { + match (compare(key, nodeKey)) { + cmp when cmp < 0 => balancedNode(nodeKey, add(key, left), right), + cmp when cmp > 0 => balancedNode(nodeKey, left, add(key, right)), + _ => makeNode(key, left, right), + } + }, + } +} + +/** + * Determines if the set contains the given value. + * + * @param key: The value to search for + * @param set: The set to search + * @returns `true` if the set contains the given value or `false` otherwise + * + * @since v0.5.4 + */ +export let rec contains = (key, set) => { + match (set) { + Empty => false, + Tree({ key: nodeKey, left, right, _ }) => { + match (compare(key, nodeKey)) { + cmp when cmp < 0 => contains(key, left), + cmp when cmp > 0 => contains(key, right), + _ => true, + } + }, + } +} + +// removes the minimum element from a tree +let rec removeMin = node => { + match (node) { + Tree({ left: Empty, right, _ }) => right, + Tree({ key, left, right, _ }) => balancedNode(key, removeMin(left), right), + _ => fail "Impossible: ImmutableSet removeMin on empty node", + } +} + +// helper function for removing a node by creating a new node containing the +// removed node's left and right subtrees +let removeInner = (left, right) => { + match ((left, right)) { + (Empty, node) | (node, Empty) => node, + (left, right) => { + balancedNode(min(right), left, removeMin(right)) + }, + } +} + +/** + * Produces a new set without the given element. If the value doesn't exist in + * the set, the set will be returned unmodified. + * + * @param key: The value to exclude + * @param set: The set to exclude from + * @returns A new set without the excluded element + * + * @since v0.5.4 + */ +export let rec remove = (key, set) => { + match (set) { + Empty => Empty, + Tree({ key: nodeKey, left, right, _ }) => { + match (compare(key, nodeKey)) { + cmp when cmp < 0 => balancedNode(nodeKey, remove(key, left), right), + cmp when cmp > 0 => balancedNode(nodeKey, left, remove(key, right)), + _ => removeInner(left, right), + } + }, + } +} + +/** + * Iterates the set, calling an iterator function on each element. + * + * @param fn: The iterator function to call with each element + * @param set: The set to iterate + * + * @since v0.5.4 + */ +export let forEach = (fn, set) => { + let rec forEachInner = node => { + match (node) { + Empty => void, + Tree({ key, left, right, _ }) => { + forEachInner(left) + fn(key): Void + forEachInner(right) + }, + } + } + forEachInner(set) +} + +/** + * Combines all elements of a set using a reducer function. + * + * @param fn: The reducer function to call on each element, where the value returned will be the next accumulator value + * @param init: The initial value to use for the accumulator on the first iteration + * @param set: The set to iterate + * @returns The final accumulator returned from `fn` + * + * @since v0.5.4 + */ +export let reduce = (fn, init, set) => { + let rec reduceInner = (acc, node) => { + match (node) { + Empty => acc, + Tree({ key, left, right, _ }) => { + let newAcc = fn(reduceInner(acc, left), key) + reduceInner(newAcc, right) + }, + } + } + reduceInner(init, set) +} + +// joins two trees with a value, preserving the BST property of left children +// being less the node and right children being greater than the node +let rec concat3 = (key, left, right) => { + match ((left, right)) { + (Empty, node) | (node, Empty) => add(key, node), + (Tree(left) as leftOpt, Tree(right) as rightOpt) => { + if (weight * left.size < right.size) { + balancedNode(right.key, concat3(key, leftOpt, right.left), right.right) + } else if (weight * right.size < left.size) { + balancedNode(left.key, left.left, concat3(key, left.right, rightOpt)) + } else { + makeNode(key, leftOpt, rightOpt) + } + }, + } +} + +// returns a tree containing all of the nodes in the input tree whose values +// are less than the given value +let rec splitLt = (splitKey, node) => { + match (node) { + Empty => Empty, + Tree({ key, left, right, _ }) => { + match (compare(key, splitKey)) { + // we want this node, join it to the output + cmp when cmp < 0 => concat3(key, left, splitLt(splitKey, right)), + cmp when cmp > 0 => splitLt(splitKey, left), + _ => left, + } + }, + } +} + +// returns a tree containing all of the nodes in the input tree whose values +// are greater than the given value +let rec splitGt = (splitKey, node) => { + match (node) { + Empty => Empty, + Tree({ key, left, right, _ }) => { + match (compare(key, splitKey)) { + // we want this node, join it to the output + cmp when cmp > 0 => concat3(key, splitGt(splitKey, left), right), + cmp when cmp < 0 => splitGt(splitKey, right), + _ => right, + } + }, + } +} + +// concatenates two trees of arbitrary size +let concat = (node1, node2) => { + match (node2) { + Empty => node1, + _ => concat3(min(node2), node1, removeMin(node2)), + } +} + +/** + * Produces a new set without the elements from the input set where a predicate function returns `false`. + * + * @param fn: The predicate function to indicate which elements to exclude from the set, where returning `false` indicates the value should be excluded + * @param set: The set to iterate + * @returns A new set excluding the elements not fulfilling the predicate + * + * @since v0.5.4 + */ +export let filter = (fn, set) => { + let rec filterInner = node => { + match (node) { + Empty => Empty, + Tree({ key, left, right, _ }) => { + if (fn(key)) { + concat3(key, filterInner(left), filterInner(right)) + } else { + concat(filterInner(left), filterInner(right)) + } + }, + } + } + filterInner(set) +} + +/** + * Produces a new set without the elements from the input set where a predicate function returns `true`. + * + * @param fn: The predicate function to indicate which elements to exclude from the set, where returning `true` indicates the value should be excluded + * @param set: The set to iterate + * @returns A new set excluding the elements fulfilling the predicate + * + * @since v0.5.4 + */ +export let reject = (fn, set) => { + filter(key => !fn(key), set) +} + +/** + * Combines two sets into a single set containing all elements from both sets. + * + * @param set1: The first set to combine + * @param set2: The second set to combine + * @returns A set containing all elements of both sets + * + * @since v0.5.4 + */ +export let rec union = (set1, set2) => { + match ((set1, set2)) { + (Empty, node) | (node, Empty) => node, + (node1, Tree(node2)) => { + let l = splitLt(node2.key, node1) + let r = splitGt(node2.key, node1) + concat3(node2.key, union(l, node2.left), union(r, node2.right)) + }, + } +} + +/** + * Combines two sets into a single set containing only the elements not shared between both sets. + * + * @param set1: The first set to combine + * @param set2: The second set to combine + * @returns A set containing only unshared elements from both sets + * + * @since v0.5.4 + */ +export let diff = (set1, set2) => { + let rec diffInner = (node1, node2) => { + match ((node1, node2)) { + (Empty, node) | (node, Empty) => node, + (node1, Tree(node2)) => { + let l = splitLt(node2.key, node1) + let r = splitGt(node2.key, node1) + concat(diffInner(l, node2.left), diffInner(r, node2.right)) + }, + } + } + union(diffInner(set1, set2), diffInner(set2, set1)) +} + +/** + * Combines two sets into a single set containing only the elements shared between both sets. + * + * @param set1: The first set to combine + * @param set2: The second set to combine + * @returns A set containing only shared elements from both sets + * + * @since v0.5.4 + */ +export let rec intersect = (set1, set2) => { + match ((set1, set2)) { + (Empty, _) | (_, Empty) => Empty, + (node1, Tree(node2)) => { + let l = splitLt(node2.key, node1) + let r = splitGt(node2.key, node1) + if (contains(node2.key, node1)) { + concat3(node2.key, intersect(l, node2.left), intersect(r, node2.right)) + } else { + concat(intersect(l, node2.left), intersect(r, node2.right)) + } + }, + } +} + +/** + * Creates a set from a list. + * + * @param list: The list to convert + * @returns A set containing all list values + * + * @since v0.5.4 + */ +export let fromList = list => { + List.reduce((set, key) => add(key, set), empty, list) +} + +/** + * Converts a set into a list of its elements. + * + * @param set: The set to convert + * @returns A list containing all set values + * + * @since v0.5.4 + */ +export let toList = set => { + let rec toListInner = (acc, node) => { + match (node) { + Empty => acc, + Tree({ key, left, right, _ }) => { + toListInner([key, ...toListInner(acc, right)], left) + }, + } + } + toListInner([], set) +} + +/** + * Creates a set from an array. + * + * @param array: The array to convert + * @returns A set containing all array values + * + * @since v0.5.4 + */ +export let fromArray = array => { + Array.reduce((set, key) => add(key, set), empty, array) +} + +/** + * Converts a set into an array of its elements. + * + * @param set: The set to convert + * @returns An array containing all set values + * + * @since v0.5.4 + */ +export let toArray = set => { + Array.fromList(toList(set)) +} diff --git a/stdlib/immutableset.md b/stdlib/immutableset.md new file mode 100644 index 0000000000..0e56706088 --- /dev/null +++ b/stdlib/immutableset.md @@ -0,0 +1,449 @@ +--- +title: ImmutableSet +--- + +An ImmutableSet is a collection of unique values. Operations on an ImmutableSet do not mutate the set's internal state. + +
+Added in next +No other changes yet. +
+ +```grain +import ImmutableSet from "immutableset" +``` + +## Types + +Type declarations included in the ImmutableSet module. + +### ImmutableSet.**ImmutableSet** + +```grain +type ImmutableSet
+``` + +## Values + +Functions and constants for working with ImmutableSets. + +### ImmutableSet.**empty** + +
+Added in next +No other changes yet. +
+ +```grain +empty : ImmutableSet
+``` + +An empty set + +### ImmutableSet.**size** + +
+Added in next +No other changes yet. +
+ +```grain +size : ImmutableSet
-> Number +``` + +Provides the count of values within the set. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`set`|`ImmutableSet`|The set to inspect| + +Returns: + +|type|description| +|----|-----------| +|`Number`|The count of elements in the set| + +### ImmutableSet.**isEmpty** + +
+Added in next +No other changes yet. +
+ +```grain +isEmpty : ImmutableSet
-> Bool +``` + +Determines if the set contains no elements. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`set`|`ImmutableSet`|The set to inspect| + +Returns: + +|type|description| +|----|-----------| +|`Bool`|`true` if the given set is empty or `false` otherwise| + +### ImmutableSet.**add** + +
+Added in next +No other changes yet. +
+ +```grain +add : (a, ImmutableSet
) -> ImmutableSet +``` + +Produces a new set by inserting the given value into the set. If the value +already exists, the new set will have the same elements as the input set. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`key`|`a`|The value to add| +|`set`|`ImmutableSet`|The base set| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableSet`|A new set containing the new element| + +### ImmutableSet.**contains** + +
+Added in next +No other changes yet. +
+ +```grain +contains : (a, ImmutableSet
) -> Bool +``` + +Determines if the set contains the given value. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`key`|`a`|The value to search for| +|`set`|`ImmutableSet`|The set to search| + +Returns: + +|type|description| +|----|-----------| +|`Bool`|`true` if the set contains the given value or `false` otherwise| + +### ImmutableSet.**remove** + +
+Added in next +No other changes yet. +
+ +```grain +remove : (a, ImmutableSet
) -> ImmutableSet +``` + +Produces a new set without the given element. If the value doesn't exist in +the set, the set will be returned unmodified. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`key`|`a`|The value to exclude| +|`set`|`ImmutableSet`|The set to exclude from| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableSet`|A new set without the excluded element| + +### ImmutableSet.**forEach** + +
+Added in next +No other changes yet. +
+ +```grain +forEach : ((a -> Void), ImmutableSet
) -> Void +``` + +Iterates the set, calling an iterator function on each element. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`fn`|`a -> Void`|The iterator function to call with each element| +|`set`|`ImmutableSet`|The set to iterate| + +### ImmutableSet.**reduce** + +
+Added in next +No other changes yet. +
+ +```grain +reduce : (((a, b) -> a), a, ImmutableSet) -> a +``` + +Combines all elements of a set using a reducer function. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`fn`|`(a, b) -> a`|The reducer function to call on each element, where the value returned will be the next accumulator value| +|`init`|`a`|The initial value to use for the accumulator on the first iteration| +|`set`|`ImmutableSet`|The set to iterate| + +Returns: + +|type|description| +|----|-----------| +|`a`|The final accumulator returned from `fn`| + +### ImmutableSet.**filter** + +
+Added in next +No other changes yet. +
+ +```grain +filter : ((a -> Bool), ImmutableSet
) -> ImmutableSet +``` + +Produces a new set without the elements from the input set where a predicate function returns `false`. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`fn`|`a -> Bool`|The predicate function to indicate which elements to exclude from the set, where returning `false` indicates the value should be excluded| +|`set`|`ImmutableSet`|The set to iterate| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableSet`|A new set excluding the elements not fulfilling the predicate| + +### ImmutableSet.**reject** + +
+Added in next +No other changes yet. +
+ +```grain +reject : ((a -> Bool), ImmutableSet
) -> ImmutableSet +``` + +Produces a new set without the elements from the input set where a predicate function returns `true`. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`fn`|`a -> Bool`|The predicate function to indicate which elements to exclude from the set, where returning `true` indicates the value should be excluded| +|`set`|`ImmutableSet`|The set to iterate| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableSet`|A new set excluding the elements fulfilling the predicate| + +### ImmutableSet.**union** + +
+Added in next +No other changes yet. +
+ +```grain +union : (ImmutableSet
, ImmutableSet) -> ImmutableSet +``` + +Combines two sets into a single set containing all elements from both sets. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`set1`|`ImmutableSet`|The first set to combine| +|`set2`|`ImmutableSet`|The second set to combine| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableSet`|A set containing all elements of both sets| + +### ImmutableSet.**diff** + +
+Added in next +No other changes yet. +
+ +```grain +diff : (ImmutableSet
, ImmutableSet) -> ImmutableSet +``` + +Combines two sets into a single set containing only the elements not shared between both sets. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`set1`|`ImmutableSet`|The first set to combine| +|`set2`|`ImmutableSet`|The second set to combine| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableSet`|A set containing only unshared elements from both sets| + +### ImmutableSet.**intersect** + +
+Added in next +No other changes yet. +
+ +```grain +intersect : (ImmutableSet
, ImmutableSet) -> ImmutableSet +``` + +Combines two sets into a single set containing only the elements shared between both sets. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`set1`|`ImmutableSet`|The first set to combine| +|`set2`|`ImmutableSet`|The second set to combine| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableSet`|A set containing only shared elements from both sets| + +### ImmutableSet.**fromList** + +
+Added in next +No other changes yet. +
+ +```grain +fromList : List
-> ImmutableSet +``` + +Creates a set from a list. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`list`|`List`|The list to convert| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableSet`|A set containing all list values| + +### ImmutableSet.**toList** + +
+Added in next +No other changes yet. +
+ +```grain +toList : ImmutableSet
-> List +``` + +Converts a set into a list of its elements. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`set`|`ImmutableSet`|The set to convert| + +Returns: + +|type|description| +|----|-----------| +|`List`|A list containing all set values| + +### ImmutableSet.**fromArray** + +
+Added in next +No other changes yet. +
+ +```grain +fromArray : Array
-> ImmutableSet +``` + +Creates a set from an array. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`array`|`Array`|The array to convert| + +Returns: + +|type|description| +|----|-----------| +|`ImmutableSet`|A set containing all array values| + +### ImmutableSet.**toArray** + +
+Added in next +No other changes yet. +
+ +```grain +toArray : ImmutableSet
-> Array +``` + +Converts a set into an array of its elements. + +Parameters: + +|param|type|description| +|-----|----|-----------| +|`set`|`ImmutableSet`|The set to convert| + +Returns: + +|type|description| +|----|-----------| +|`Array`|An array containing all set values| + diff --git a/stdlib/set.gr b/stdlib/set.gr index 9551b57807..142e760934 100644 --- a/stdlib/set.gr +++ b/stdlib/set.gr @@ -2,7 +2,7 @@ * @module Set: A Set is an unordered collection of unique values. Operations on a Set mutate the internal state, so it never needs to be re-assigned. * @example import Set from "set" * - * @since 0.3.0 + * @since v0.3.0 */ import List from "list" import Array from "array" @@ -33,7 +33,7 @@ record Set { * @param size: The initial storage size of the set * @returns An empty set with the given initial storage size * - * @since 0.3.0 + * @since v0.3.0 */ export let makeSized = size => { let buckets = Array.make(size, None) @@ -44,7 +44,7 @@ export let makeSized = size => { * * @returns An empty set * - * @since 0.3.0 + * @since v0.3.0 */ export let make = () => { makeSized(16) @@ -121,7 +121,7 @@ let rec nodeInBucket = (key, node) => { * @param key: The value to add * @param set: The set to update * - * @since 0.3.0 + * @since v0.3.0 */ export let add = (key, set) => { let buckets = set.buckets @@ -154,7 +154,7 @@ export let add = (key, set) => { * @param set: The set to search * @returns `true` if the set contains the given value or `false` otherwise * - * @since 0.3.0 + * @since v0.3.0 */ export let contains = (key, set) => { let buckets = set.buckets @@ -186,7 +186,7 @@ let rec removeInBucket = (key, node) => { * @param key: The value to remove * @param set: The set to update * - * @since 0.3.0 + * @since v0.3.0 */ export let remove = (key, set) => { let buckets = set.buckets @@ -214,7 +214,7 @@ export let remove = (key, set) => { * @param set: The set to inspect * @returns The count of elements in the set * - * @since 0.3.0 + * @since v0.3.0 */ export let size = set => { set.size @@ -226,7 +226,7 @@ export let size = set => { * @param set: The set to inspect * @returns `true` if the given set is empty or `false` otherwise * - * @since 0.3.0 + * @since v0.3.0 */ export let isEmpty = set => { size(set) == 0 @@ -237,7 +237,7 @@ export let isEmpty = set => { * * @param set: The set to reset * - * @since 0.3.0 + * @since v0.3.0 */ export let clear = set => { set.size = 0 @@ -263,7 +263,7 @@ let rec forEachBucket = (fn, node) => { * @param fn: The iterator function to call with each element * @param set: The set to iterate * - * @since 0.3.0 + * @since v0.3.0 * @history v0.5.0: Ensured the iterator function return type is always `Void` */ export let forEach = (fn, set) => { @@ -288,7 +288,7 @@ let rec reduceEachBucket = (fn, node, acc) => { * @param set: The set to iterate * @returns The final accumulator returned from `fn` * - * @since 0.3.0 + * @since v0.3.0 */ export let reduce = (fn, init, set) => { let buckets = set.buckets @@ -305,10 +305,10 @@ export let reduce = (fn, init, set) => { * @param fn: The predicate function to indicate which elements to remove from the set, where returning `false` indicates the value should be removed * @param set: The set to iterate * - * @since 0.3.0 + * @since v0.3.0 */ -export let filter = (predicate, set) => { - let keysToRemove = reduce((list, key) => if (!predicate(key)) { +export let filter = (fn, set) => { + let keysToRemove = reduce((list, key) => if (!fn(key)) { [key, ...list] } else { list @@ -324,10 +324,10 @@ export let filter = (predicate, set) => { * @param fn: The predicate function to indicate which elements to remove from the set, where returning `true` indicates the value should be removed * @param set: The set to iterate * - * @since 0.3.0 + * @since v0.3.0 */ -export let reject = (predicate, set) => { - filter(key => !predicate(key), set) +export let reject = (fn, set) => { + filter(key => !fn(key), set) } /** @@ -336,7 +336,7 @@ export let reject = (predicate, set) => { * @param set: The set to convert * @returns A list containing all set values * - * @since 0.3.0 + * @since v0.3.0 */ export let toList = set => { reduce((list, key) => [key, ...list], [], set) @@ -348,7 +348,7 @@ export let toList = set => { * @param list: The list to convert * @returns A set containing all list values * - * @since 0.3.0 + * @since v0.3.0 */ export let fromList = list => { let set = make() @@ -364,7 +364,7 @@ export let fromList = list => { * @param set: The set to convert * @returns An array containing all set values * - * @since 0.3.0 + * @since v0.3.0 */ export let toArray = set => { Array.fromList(toList(set)) @@ -376,7 +376,7 @@ export let toArray = set => { * @param array: The array to convert * @returns A set containing all array values * - * @since 0.3.0 + * @since v0.3.0 */ export let fromArray = array => { let set = make() @@ -393,7 +393,7 @@ export let fromArray = array => { * @param set2: The second set to combine * @returns A set containing all elements of both sets * - * @since 0.3.0 + * @since v0.3.0 */ export let union = (set1, set2) => { let set = make() @@ -413,7 +413,7 @@ export let union = (set1, set2) => { * @param set2: The second set to combine * @returns A set containing only unshared elements from both sets * - * @since 0.3.0 + * @since v0.3.0 */ export let diff = (set1, set2) => { let set = make() @@ -437,7 +437,7 @@ export let diff = (set1, set2) => { * @param set2: The second set to combine * @returns A set containing only shared elements from both sets * - * @since 0.3.0 + * @since v0.3.0 */ export let intersect = (set1, set2) => { let set = make() @@ -461,7 +461,7 @@ export let intersect = (set1, set2) => { * @param set: The set to inspect * @returns The internal state of the set * - * @since 0.3.0 + * @since v0.3.0 */ export let getInternalStats = set => { (set.size, Array.length(set.buckets))