├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── package-set.dhall ├── reference └── HashMap.mo ├── src ├── ClassStableHashMap.mo └── FunctionalStableHashMap.mo ├── test ├── ClassStableHashMapTest.mo ├── FunctionalStableHashMapTest.mo ├── Makefile ├── package-set.dhall └── vessel.dhall └── vessel.dhall /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: "install wasmtime" 11 | run: | 12 | mkdir /home/runner/bin 13 | echo "/home/runner/bin" >> $GITHUB_PATH 14 | wget https://github.com/bytecodealliance/wasmtime/releases/download/v0.18.0/wasmtime-v0.18.0-x86_64-linux.tar.xz 15 | tar xf wasmtime-v0.18.0-x86_64-linux.tar.xz 16 | cp wasmtime-v0.18.0-x86_64-linux/wasmtime /home/runner/bin/wasmtime 17 | - name: "install vessel" 18 | run: | 19 | wget --output-document /home/runner/bin/vessel https://github.com/dfinity/vessel/releases/download/v0.6.3/vessel-linux64 20 | chmod +x /home/runner/bin/vessel 21 | - name: "check" 22 | run: make check-strict 23 | - name: "test" 24 | run: make test 25 | - name: "docs" 26 | run: make docs 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | generate-docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: "install vessel" 14 | run: | 15 | mkdir /home/runner/bin 16 | echo "/home/runner/bin" >> $GITHUB_PATH 17 | wget --output-document /home/runner/bin/vessel https://github.com/dfinity/vessel/releases/download/v0.6.3/vessel-linux64 18 | chmod +x /home/runner/bin/vessel 19 | - name: "docs" 20 | run: make docs 21 | - name: Upload docs 22 | uses: JamesIves/github-pages-deploy-action@releases/v3 23 | with: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | BRANCH: gh-pages # The branch the action should deploy to. 26 | FOLDER: docs/ # The folder the action should deploy. 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vessel 2 | *.wasm 3 | docs/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 Byron Becker 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: check docs test 2 | 3 | check: 4 | find src -type f -name '*.mo' -print0 | xargs -0 $(shell vessel bin)/moc $(shell vessel sources) --check 5 | 6 | all: check-strict docs test 7 | 8 | check-strict: 9 | find src -type f -name '*.mo' -print0 | xargs -0 $(shell vessel bin)/moc $(shell vessel sources) -Werror --check 10 | docs: 11 | $(shell vessel bin)/mo-doc 12 | test: 13 | make -C test 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StableHashMap 2 | 3 | Stable HashMaps in Motoko. 4 | 5 | ## Motivation 6 | Inspiration taken from [this back and forth in the Dfinity developer forums](https://forum.dfinity.org/t/clarification-on-stable-types-with-examples/11075). 7 | 8 | ## API Documentation 9 | 10 | API documentation for this library can be found at https://canscale.github.io/StableHashMap 11 | 12 | ## Implementation 13 | Two different StableHashMap implementations are accessible via this module. 14 | 15 | ### FunctionalStableHashMap (**Recommended**) 16 | This module is a direct deconstruction of the object oriented [HashMap.mo class in motoko-base] 17 | (https://github.com/dfinity/motoko-base/blob/master/src/HashMap.mo) 18 | into a series of functions and is meant to be persistent across updates, with the tradeoff 19 | being larger function signatures. 20 | 21 | One of the main additions/difference between the two modules at this time besides class deconstruction 22 | is the differing initialization methods if an initialCapacity is known (to prevent array doubling 23 | slowdown during map initialization) 24 | 25 | ### ClassStableHashMap 26 | **Note**: If using this module the `exportProps()` and `importProps()` class methods must be used in conjunction with the system pre/post-upgrade methods to assure stability. 27 | 28 | This module is nearly identical to [https://github.com/dfinity/motoko-base/blob/master/src/HashMap.mo](https://github.com/dfinity/motoko-base/blob/master/src/HashMap.mo), except public `exportProps()` and `importProps()` class methods were added to the original implementation in order to allow the hashtable and it's item count to be retrievable and therefore persistable across upgrades. 29 | 30 | ## License 31 | StableHashMap is distributed under the terms of the Apache License (Version 2.0). 32 | 33 | See LICENSE for details. -------------------------------------------------------------------------------- /package-set.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/dfinity/vessel-package-set/releases/download/mo-0.6.21-20220215/package-set.dhall sha256:b46f30e811fe5085741be01e126629c2a55d4c3d6ebf49408fb3b4a98e37589b 3 | 4 | in upstream 5 | -------------------------------------------------------------------------------- /reference/HashMap.mo: -------------------------------------------------------------------------------- 1 | /// The below is the reference HashMap.mo file from 2 | /// https://github.com/dfinity/motoko-base/blob/master/src/HashMap.mo 3 | /// that was used to create StableHashMap.mo 4 | /// 5 | /// Mutable hash map (aka Hashtable) 6 | /// 7 | /// This module defines an imperative hash map (hash table), with a general key and value type. 8 | /// 9 | /// It has a minimal object-oriented interface: `get`, `set`, `delete`, `count` and `entries`. 10 | /// 11 | /// The class is parameterized by the key's equality and hash functions, 12 | /// and an initial capacity. However, as with the `Buffer` class, no array allocation 13 | /// happens until the first `set`. 14 | /// 15 | /// Internally, table growth policy is very simple, for now: 16 | /// Double the current capacity when the expected bucket list size grows beyond a certain constant. 17 | 18 | import Prim "mo:⛔"; 19 | import P "Prelude"; 20 | import A "Array"; 21 | import Hash "Hash"; 22 | import Iter "Iter"; 23 | import AssocList "AssocList"; 24 | 25 | module { 26 | 27 | 28 | // key-val list type 29 | type KVs = AssocList.AssocList; 30 | 31 | /// An imperative HashMap with a minimal object-oriented interface. 32 | /// Maps keys of type `K` to values of type `V`. 33 | public class HashMap( 34 | initCapacity : Nat, 35 | keyEq : (K, K) -> Bool, 36 | keyHash : K -> Hash.Hash) { 37 | 38 | var table : [var K Vs] = [var]; 39 | var _count : Nat = 0; 40 | 41 | /// Returns the number of entries in this HashMap. 42 | public func size() : Nat = _count; 43 | 44 | /// Deletes the entry with the key `k`. Doesn't do anything if the key doesn't 45 | /// exist. 46 | public func delete(k : K) = ignore remove(k); 47 | 48 | /// Removes the entry with the key `k` and returns the associated value if it 49 | /// existed or `null` otherwise. 50 | public func remove(k : K) : ?V { 51 | let m = table.size(); 52 | if (m > 0) { 53 | let h = Prim.nat32ToNat(keyHash(k)); 54 | let pos = h % m; 55 | let (kvs2, ov) = AssocList.replace(table[pos], k, keyEq, null); 56 | table[pos] := kvs2; 57 | switch(ov){ 58 | case null { }; 59 | case _ { _count -= 1; } 60 | }; 61 | ov 62 | } else { 63 | null 64 | }; 65 | }; 66 | 67 | /// Gets the entry with the key `k` and returns its associated value if it 68 | /// existed or `null` otherwise. 69 | public func get(k : K) : ?V { 70 | let h = Prim.nat32ToNat(keyHash(k)); 71 | let m = table.size(); 72 | let v = if (m > 0) { 73 | AssocList.find(table[h % m], k, keyEq) 74 | } else { 75 | null 76 | }; 77 | }; 78 | 79 | /// Insert the value `v` at key `k`. Overwrites an existing entry with key `k` 80 | public func put(k : K, v : V) = ignore replace(k, v); 81 | 82 | /// Insert the value `v` at key `k` and returns the previous value stored at 83 | /// `k` or `null` if it didn't exist. 84 | public func replace(k : K, v : V) : ?V { 85 | if (_count >= table.size()) { 86 | let size = 87 | if (_count == 0) { 88 | if (initCapacity > 0) { 89 | initCapacity 90 | } else { 91 | 1 92 | } 93 | } else { 94 | table.size() * 2; 95 | }; 96 | let table2 = A.init>(size, null); 97 | for (i in table.keys()) { 98 | var kvs = table[i]; 99 | label moveKeyVals : () 100 | loop { 101 | switch kvs { 102 | case null { break moveKeyVals }; 103 | case (?((k, v), kvsTail)) { 104 | let h = Prim.nat32ToNat(keyHash(k)); 105 | let pos2 = h % table2.size(); 106 | table2[pos2] := ?((k,v), table2[pos2]); 107 | kvs := kvsTail; 108 | }; 109 | } 110 | }; 111 | }; 112 | table := table2; 113 | }; 114 | let h = Prim.nat32ToNat(keyHash(k)); 115 | let pos = h % table.size(); 116 | let (kvs2, ov) = AssocList.replace(table[pos], k, keyEq, ?v); 117 | table[pos] := kvs2; 118 | switch(ov){ 119 | case null { _count += 1 }; 120 | case _ {} 121 | }; 122 | ov 123 | }; 124 | 125 | /// An `Iter` over the keys. 126 | public func keys() : Iter.Iter 127 | { Iter.map(entries(), func (kv : (K, V)) : K { kv.0 }) }; 128 | 129 | /// An `Iter` over the values. 130 | public func vals() : Iter.Iter 131 | { Iter.map(entries(), func (kv : (K, V)) : V { kv.1 }) }; 132 | 133 | /// Returns an iterator over the key value pairs in this 134 | /// `HashMap`. Does _not_ modify the `HashMap`. 135 | public func entries() : Iter.Iter<(K, V)> { 136 | if (table.size() == 0) { 137 | object { public func next() : ?(K, V) { null } } 138 | } 139 | else { 140 | object { 141 | var kvs = table[0]; 142 | var nextTablePos = 1; 143 | public func next () : ?(K, V) { 144 | switch kvs { 145 | case (?(kv, kvs2)) { 146 | kvs := kvs2; 147 | ?kv 148 | }; 149 | case null { 150 | if (nextTablePos < table.size()) { 151 | kvs := table[nextTablePos]; 152 | nextTablePos += 1; 153 | next() 154 | } else { 155 | null 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | }; 163 | 164 | }; 165 | 166 | /// clone cannot be an efficient object method, 167 | /// ...but is still useful in tests, and beyond. 168 | public func clone ( 169 | h : HashMap, 170 | keyEq : (K, K) -> Bool, 171 | keyHash : K -> Hash.Hash 172 | ) : HashMap { 173 | let h2 = HashMap(h.size(), keyEq, keyHash); 174 | for ((k,v) in h.entries()) { 175 | h2.put(k,v); 176 | }; 177 | h2 178 | }; 179 | 180 | /// Clone from any iterator of key-value pairs 181 | public func fromIter( 182 | iter : Iter.Iter<(K, V)>, 183 | initCapacity : Nat, 184 | keyEq : (K, K) -> Bool, 185 | keyHash : K -> Hash.Hash 186 | ) : HashMap { 187 | let h = HashMap(initCapacity, keyEq, keyHash); 188 | for ((k, v) in iter) { 189 | h.put(k, v); 190 | }; 191 | h 192 | }; 193 | 194 | public func map( 195 | h : HashMap, 196 | keyEq : (K, K) -> Bool, 197 | keyHash : K -> Hash.Hash, 198 | mapFn : (K, V1) -> V2, 199 | ) : HashMap { 200 | let h2 = HashMap(h.size(), keyEq, keyHash); 201 | for ((k, v1) in h.entries()) { 202 | let v2 = mapFn(k, v1); 203 | h2.put(k, v2); 204 | }; 205 | h2 206 | }; 207 | 208 | public func mapFilter( 209 | h : HashMap, 210 | keyEq : (K, K) -> Bool, 211 | keyHash : K -> Hash.Hash, 212 | mapFn : (K, V1) -> ?V2, 213 | ) : HashMap { 214 | let h2 = HashMap(h.size(), keyEq, keyHash); 215 | for ((k, v1) in h.entries()) { 216 | switch (mapFn(k, v1)) { 217 | case null { }; 218 | case (?v2) { 219 | h2.put(k, v2); 220 | }; 221 | } 222 | }; 223 | h2 224 | }; 225 | 226 | } -------------------------------------------------------------------------------- /src/ClassStableHashMap.mo: -------------------------------------------------------------------------------- 1 | /// The below is the reference HashMap.mo file from 2 | /// https://github.com/dfinity/motoko-base/blob/master/src/HashMap.mo 3 | /// that was used to create StableHashMap.mo 4 | /// 5 | /// Mutable stable hash map (aka Hashtable) 6 | /// 7 | /// This module defines an imperative hash map (hash table), with a general key and value type. 8 | /// exportProps() and importProps() functions were added to the original implementation in order to allow 9 | /// the hashtable and it's item count to be retrievable and therefore persistable across upgrades. 10 | /// 11 | /// It has a minimal object-oriented interface: `get`, `set`, `delete`, `count` and `entries`. 12 | /// 13 | /// The class is parameterized by the key's equality and hash functions, 14 | /// and an initial capacity. However, as with the `Buffer` class, no array allocation 15 | /// happens until the first `set`. 16 | /// 17 | /// Internally, table growth policy is very simple, for now: 18 | /// Double the current capacity when the expected bucket list size grows beyond a certain constant. 19 | 20 | import Prim "mo:⛔"; 21 | import Nat "mo:base/Nat"; 22 | import A "mo:base/Array"; 23 | import Hash "mo:base/Hash"; 24 | import Iter "mo:base/Iter"; 25 | import AssocList "mo:base/AssocList"; 26 | 27 | module { 28 | 29 | type HashTable = [var KVs]; 30 | 31 | public type HashTableProps = { 32 | table: HashTable; 33 | _count: Nat; 34 | }; 35 | 36 | // key-val list type 37 | type KVs = AssocList.AssocList; 38 | 39 | /// An imperative StableHashMap with a minimal object-oriented interface. 40 | /// Maps keys of type `K` to values of type `V`. 41 | public class StableHashMap( 42 | initCapacity : Nat, 43 | keyEq : (K, K) -> Bool, 44 | keyHash : K -> Hash.Hash) { 45 | 46 | var table : HashTable = [var]; 47 | var _count : Nat = 0; 48 | 49 | // TODO: decide if want to enforce that the imported _count is equal to the populated size of the table 50 | // (populated size <= table.size()) 51 | public func importProps(props: HashTableProps): () { 52 | table := props.table; 53 | _count := 54 | if (props._count > props.table.size()) { props.table.size() } 55 | else { props._count } 56 | }; 57 | 58 | public func exportProps(): HashTableProps = { 59 | table = table; 60 | _count = _count; 61 | }; 62 | 63 | 64 | /// Returns the number of entries in this StableHashMap. 65 | public func size() : Nat = _count; 66 | 67 | /// Deletes the entry with the key `k`. Doesn't do anything if the key doesn't 68 | /// exist. 69 | public func delete(k : K) = ignore remove(k); 70 | 71 | /// Removes the entry with the key `k` and returns the associated value if it 72 | /// existed or `null` otherwise. 73 | public func remove(k : K) : ?V { 74 | let m = table.size(); 75 | if (m > 0) { 76 | let h = Prim.nat32ToNat(keyHash(k)); 77 | let pos = h % m; 78 | let (kvs2, ov) = AssocList.replace(table[pos], k, keyEq, null); 79 | table[pos] := kvs2; 80 | switch(ov){ 81 | case null { }; 82 | case _ { _count -= 1; } 83 | }; 84 | ov 85 | } else { 86 | null 87 | }; 88 | }; 89 | 90 | /// Gets the entry with the key `k` and returns its associated value if it 91 | /// existed or `null` otherwise. 92 | public func get(k : K) : ?V { 93 | let h = Prim.nat32ToNat(keyHash(k)); 94 | let m = table.size(); 95 | let v = if (m > 0) { 96 | AssocList.find(table[h % m], k, keyEq) 97 | } else { 98 | null 99 | }; 100 | }; 101 | 102 | /// Insert the value `v` at key `k`. Overwrites an existing entry with key `k` 103 | public func put(k : K, v : V) = ignore replace(k, v); 104 | 105 | /// Insert the value `v` at key `k` and returns the previous value stored at 106 | /// `k` or `null` if it didn't exist. 107 | public func replace(k : K, v : V) : ?V { 108 | if (_count >= table.size()) { 109 | let size = 110 | if (_count == 0) { 111 | if (initCapacity > 0) { 112 | initCapacity 113 | } else { 114 | 1 115 | } 116 | } else { 117 | table.size() * 2; 118 | }; 119 | let table2 = A.init>(size, null); 120 | for (i in table.keys()) { 121 | var kvs = table[i]; 122 | label moveKeyVals : () 123 | loop { 124 | switch kvs { 125 | case null { break moveKeyVals }; 126 | case (?((k, v), kvsTail)) { 127 | let h = Prim.nat32ToNat(keyHash(k)); 128 | let pos2 = h % table2.size(); 129 | table2[pos2] := ?((k,v), table2[pos2]); 130 | kvs := kvsTail; 131 | }; 132 | } 133 | }; 134 | }; 135 | table := table2; 136 | }; 137 | let h = Prim.nat32ToNat(keyHash(k)); 138 | let pos = h % table.size(); 139 | let (kvs2, ov) = AssocList.replace(table[pos], k, keyEq, ?v); 140 | table[pos] := kvs2; 141 | switch(ov){ 142 | case null { _count += 1 }; 143 | case _ {} 144 | }; 145 | ov 146 | }; 147 | 148 | /// An `Iter` over the keys. 149 | public func keys() : Iter.Iter 150 | { Iter.map(entries(), func (kv : (K, V)) : K { kv.0 }) }; 151 | 152 | /// An `Iter` over the values. 153 | public func vals() : Iter.Iter 154 | { Iter.map(entries(), func (kv : (K, V)) : V { kv.1 }) }; 155 | 156 | /// Returns an iterator over the key value pairs in this 157 | /// `StableHashMap`. Does _not_ modify the `StableHashMap`. 158 | public func entries() : Iter.Iter<(K, V)> { 159 | if (table.size() == 0) { 160 | object { public func next() : ?(K, V) { null } } 161 | } 162 | else { 163 | object { 164 | var kvs = table[0]; 165 | var nextTablePos = 1; 166 | public func next () : ?(K, V) { 167 | switch kvs { 168 | case (?(kv, kvs2)) { 169 | kvs := kvs2; 170 | ?kv 171 | }; 172 | case null { 173 | if (nextTablePos < table.size()) { 174 | kvs := table[nextTablePos]; 175 | nextTablePos += 1; 176 | next() 177 | } else { 178 | null 179 | } 180 | } 181 | } 182 | } 183 | } 184 | } 185 | }; 186 | 187 | }; 188 | 189 | /// clone cannot be an efficient object method, 190 | /// ...but is still useful in tests, and beyond. 191 | public func clone ( 192 | h : StableHashMap, 193 | keyEq : (K, K) -> Bool, 194 | keyHash : K -> Hash.Hash 195 | ) : StableHashMap { 196 | let h2 = StableHashMap(h.size(), keyEq, keyHash); 197 | for ((k,v) in h.entries()) { 198 | h2.put(k,v); 199 | }; 200 | h2 201 | }; 202 | 203 | /// Clone from any iterator of key-value pairs 204 | public func fromIter( 205 | iter : Iter.Iter<(K, V)>, 206 | initCapacity : Nat, 207 | keyEq : (K, K) -> Bool, 208 | keyHash : K -> Hash.Hash 209 | ) : StableHashMap { 210 | let h = StableHashMap(initCapacity, keyEq, keyHash); 211 | for ((k, v) in iter) { 212 | h.put(k, v); 213 | }; 214 | h 215 | }; 216 | 217 | public func map( 218 | h : StableHashMap, 219 | keyEq : (K, K) -> Bool, 220 | keyHash : K -> Hash.Hash, 221 | mapFn : (K, V1) -> V2, 222 | ) : StableHashMap { 223 | let h2 = StableHashMap(h.size(), keyEq, keyHash); 224 | for ((k, v1) in h.entries()) { 225 | let v2 = mapFn(k, v1); 226 | h2.put(k, v2); 227 | }; 228 | h2 229 | }; 230 | 231 | public func mapFilter( 232 | h : StableHashMap, 233 | keyEq : (K, K) -> Bool, 234 | keyHash : K -> Hash.Hash, 235 | mapFn : (K, V1) -> ?V2, 236 | ) : StableHashMap { 237 | let h2 = StableHashMap(h.size(), keyEq, keyHash); 238 | for ((k, v1) in h.entries()) { 239 | switch (mapFn(k, v1)) { 240 | case null { }; 241 | case (?v2) { 242 | h2.put(k, v2); 243 | }; 244 | } 245 | }; 246 | h2 247 | }; 248 | } -------------------------------------------------------------------------------- /src/FunctionalStableHashMap.mo: -------------------------------------------------------------------------------- 1 | /// Mutable hash map (aka Hashtable) 2 | /// 3 | /// This module is a direct deconstruction of the object oriented HashMap.mo class in motoko-base 4 | /// (https://github.com/dfinity/motoko-base/blob/master/src/HashMap.mo) 5 | /// into a series of functions and is meant to be persistent across updates, with the tradeoff 6 | /// being larger function signatures. 7 | /// 8 | /// One of the main additions/difference between the two modules at this time besides class deconstruction 9 | /// is the differing initialization methods if an initialCapacity is known (to prevent array doubling 10 | /// slowdown during map initialization) 11 | /// 12 | /// The rest of this documentation and code therefore follows directly from HashMap.mo, with minor 13 | /// modifications that do not attempt to change implementation. Please raise and issue if a 14 | /// discrepancy in implementation is found. 15 | /// 16 | /// The class is parameterized by the key's equality and hash functions, 17 | /// and an initial capacity. However, as with the `Buffer` class, no array allocation 18 | /// happens until the first `set`. 19 | /// 20 | /// Internally, table growth policy is very simple, for now: 21 | /// Double the current capacity when the expected bucket list size grows beyond a certain constant. 22 | 23 | import Prim "mo:⛔"; 24 | import Nat "mo:base/Nat"; 25 | import A "mo:base/Array"; 26 | import Hash "mo:base/Hash"; 27 | import Iter "mo:base/Iter"; 28 | import AssocList "mo:base/AssocList"; 29 | 30 | module { 31 | 32 | 33 | // key-val list type 34 | type KVs = AssocList.AssocList; 35 | 36 | /// Type signature for the StableHashMap object. 37 | public type StableHashMap = { 38 | initCapacity: Nat; 39 | var table: [var KVs]; 40 | var _count: Nat; 41 | }; 42 | 43 | /// Initializes a HashMap with initCapacity and table size zero 44 | public func init(): StableHashMap = { 45 | initCapacity = 0; 46 | var table = [var]; 47 | var _count = 0; 48 | }; 49 | 50 | /// Initializes a hashMap with given initCapacity. No array allocation will 51 | /// occur until the first item is inserted 52 | public func initPreSized(initCapacity: Nat): StableHashMap = { 53 | initCapacity = initCapacity; 54 | var table = [var]; 55 | var _count = 0; 56 | }; 57 | 58 | /// Returns the number of entries in this HashMap. 59 | public func size(map: StableHashMap): Nat { 60 | map._count; 61 | }; 62 | 63 | /// Deletes the entry with the key `k`. Doesn't do anything if the key doesn't 64 | /// exist. 65 | public func delete( 66 | map: StableHashMap, 67 | keyEq: (K, K) -> Bool, 68 | keyHash: K -> Hash.Hash, 69 | k: K 70 | ): () { 71 | ignore remove(map, keyEq, keyHash, k); 72 | }; 73 | 74 | /// Removes the entry with the key `k` and returns the associated value if it 75 | /// existed or `null` otherwise. 76 | public func remove( 77 | map: StableHashMap, 78 | keyEq: (K, K) -> Bool, 79 | keyHash: K -> Hash.Hash, 80 | k: K 81 | ): ?V { 82 | let m = map.table.size(); 83 | if (m > 0) { 84 | let h = Prim.nat32ToNat(keyHash(k)); 85 | let pos = h % m; 86 | let (kvs2, ov) = AssocList.replace(map.table[pos], k, keyEq, null); 87 | map.table[pos] := kvs2; 88 | switch(ov){ 89 | case null { }; 90 | case _ { map._count := map._count - 1; } 91 | }; 92 | ov 93 | } else { 94 | null 95 | }; 96 | }; 97 | 98 | /// Gets the entry with the key `k` and returns its associated value if it 99 | /// existed or `null` otherwise. 100 | public func get( 101 | map: StableHashMap, 102 | keyEq: (K, K) -> Bool, 103 | keyHash: K -> Hash.Hash, 104 | k: K 105 | ): ?V { 106 | let h = Prim.nat32ToNat(keyHash(k)); 107 | let m = map.table.size(); 108 | let v = if (m > 0) { 109 | AssocList.find(map.table[h % m], k, keyEq) 110 | } else { 111 | null 112 | }; 113 | }; 114 | 115 | /// Insert the value `v` at key `k`. Overwrites an existing entry with key `k` 116 | /// Does not return a value if updating an existing key 117 | public func put( 118 | map: StableHashMap, 119 | keyEq: (K, K) -> Bool, 120 | keyHash: K -> Hash.Hash, 121 | k: K, 122 | v: V 123 | ): () { 124 | ignore replace(map, keyEq, keyHash, k, v); 125 | }; 126 | 127 | /// Insert the value `v` at key `k` and returns the previous value stored at 128 | /// `k` or `null` if it didn't exist. 129 | public func replace( 130 | map: StableHashMap, 131 | keyEq: (K, K) -> Bool, 132 | keyHash: K -> Hash.Hash, 133 | k: K, 134 | v: V 135 | ): ?V { 136 | if (map._count >= map.table.size()) { 137 | let size = 138 | if (map._count == 0) { 139 | if (map.initCapacity > 0) { 140 | map.initCapacity 141 | } else { 142 | 1 143 | } 144 | } 145 | else { 146 | map.table.size() * 2; 147 | }; 148 | let table2 = A.init>(size, null); 149 | for (i in map.table.keys()) { 150 | var kvs = map.table[i]; 151 | label moveKeyVals: () 152 | loop { 153 | switch kvs { 154 | case null { break moveKeyVals }; 155 | case (?((k, v), kvsTail)) { 156 | let h = Prim.nat32ToNat(keyHash(k)); 157 | let pos2 = h % table2.size(); 158 | table2[pos2] := ?((k,v), table2[pos2]); 159 | kvs := kvsTail; 160 | }; 161 | } 162 | }; 163 | }; 164 | map.table := table2; 165 | }; 166 | let h = Prim.nat32ToNat(keyHash(k)); 167 | let pos = h % map.table.size(); 168 | let (kvs2, ov) = AssocList.replace(map.table[pos], k, keyEq, ?v); 169 | map.table[pos] := kvs2; 170 | switch(ov){ 171 | case null { map._count += 1 }; 172 | case _ {} 173 | }; 174 | ov 175 | }; 176 | 177 | /// An `Iter` over the keys. 178 | public func keys(map: StableHashMap): Iter.Iter { 179 | Iter.map(entries(map), func (kv: (K, V)): K { kv.0 }) 180 | }; 181 | 182 | /// An `Iter` over the values. 183 | public func vals(map: StableHashMap): Iter.Iter { 184 | Iter.map(entries(map), func (kv: (K, V)): V { kv.1 }) 185 | }; 186 | 187 | /// Returns an iterator over the key value pairs in this 188 | /// `HashMap`. Does _not_ modify the `HashMap`. 189 | public func entries(map: StableHashMap): Iter.Iter<(K, V)> { 190 | if (map.table.size() == 0) { 191 | object { public func next(): ?(K, V) { null } } 192 | } 193 | else { 194 | object { 195 | var kvs = map.table[0]; 196 | var nextTablePos = 1; 197 | public func next (): ?(K, V) { 198 | switch kvs { 199 | case (?(kv, kvs2)) { 200 | kvs := kvs2; 201 | ?kv 202 | }; 203 | case null { 204 | if (nextTablePos < map.table.size()) { 205 | kvs := map.table[nextTablePos]; 206 | nextTablePos += 1; 207 | next() 208 | } else { 209 | null 210 | } 211 | } 212 | } 213 | } 214 | } 215 | }; 216 | }; 217 | 218 | /// clone cannot be an efficient object method, 219 | /// ...but is still useful in tests, and beyond. 220 | public func clone ( 221 | h: StableHashMap, 222 | keyEq: (K, K) -> Bool, 223 | keyHash: K -> Hash.Hash 224 | ): StableHashMap { 225 | let h2 = init(); 226 | for ((k,v) in entries(h)) { 227 | put(h2, keyEq, keyHash, k, v); 228 | }; 229 | h2 230 | }; 231 | 232 | /// Clone from any iterator of key-value pairs 233 | public func fromIter( 234 | iter: Iter.Iter<(K, V)>, 235 | initCapacity: Nat, 236 | keyEq: (K, K) -> Bool, 237 | keyHash: K -> Hash.Hash 238 | ): StableHashMap { 239 | let h = initPreSized(initCapacity); 240 | for ((k, v) in iter) { 241 | put(h, keyEq, keyHash, k, v); 242 | }; 243 | h 244 | }; 245 | 246 | public func map( 247 | h: StableHashMap, 248 | keyEq: (K, K) -> Bool, 249 | keyHash: K -> Hash.Hash, 250 | mapFn: (K, V1) -> V2, 251 | ): StableHashMap { 252 | let h2 = init(); 253 | for ((k, v1) in entries(h)) { 254 | let v2 = mapFn(k, v1); 255 | put(h2, keyEq, keyHash, k, v2); 256 | }; 257 | h2 258 | }; 259 | 260 | public func mapFilter( 261 | h: StableHashMap, 262 | keyEq: (K, K) -> Bool, 263 | keyHash: K -> Hash.Hash, 264 | mapFn: (K, V1) -> ?V2, 265 | ): StableHashMap { 266 | let h2 = init(); 267 | for ((k, v1) in entries(h)) { 268 | switch (mapFn(k, v1)) { 269 | case null { }; 270 | case (?v2) { 271 | put(h2, keyEq, keyHash, k, v2); 272 | }; 273 | } 274 | }; 275 | h2 276 | }; 277 | } -------------------------------------------------------------------------------- /test/ClassStableHashMapTest.mo: -------------------------------------------------------------------------------- 1 | import Debug "mo:base/Debug"; 2 | import HM "../src/ClassStableHashMap"; 3 | import Hash "mo:base/Hash"; 4 | import Text "mo:base/Text"; 5 | import Nat "mo:base/Nat"; 6 | 7 | import List "mo:base/List"; 8 | 9 | Debug.print("class stable"); 10 | 11 | debug { 12 | let a = HM.StableHashMap(3, Text.equal, Text.hash); 13 | 14 | a.put("apple", 1); 15 | a.put("banana", 2); 16 | a.put("pear", 3); 17 | a.put("avocado", 4); 18 | a.put("Apple", 11); 19 | a.put("Banana", 22); 20 | a.put("Pear", 33); 21 | a.put("Avocado", 44); 22 | a.put("ApplE", 111); 23 | a.put("BananA", 222); 24 | a.put("PeaR", 333); 25 | a.put("AvocadO", 444); 26 | 27 | // need to resupply the constructor args; they are private to the object; but, should they be? 28 | let b = HM.clone(a, Text.equal, Text.hash); 29 | 30 | // ensure clone has each key-value pair present in original 31 | for ((k,v) in a.entries()) { 32 | Debug.print(debug_show (k,v)); 33 | switch (b.get(k)) { 34 | case null { assert false }; 35 | case (?w) { assert v == w }; 36 | }; 37 | }; 38 | 39 | // ensure original has each key-value pair present in clone 40 | for ((k,v) in b.entries()) { 41 | Debug.print(debug_show (k,v)); 42 | switch (a.get(k)) { 43 | case null { assert false }; 44 | case (?w) { assert v == w }; 45 | }; 46 | }; 47 | 48 | // ensure clone has each key present in original 49 | for (k in a.keys()) { 50 | switch (b.get(k)) { 51 | case null { assert false }; 52 | case (?_) { }; 53 | }; 54 | }; 55 | 56 | // ensure clone has each value present in original 57 | for (v in a.vals()) { 58 | var foundMatch = false; 59 | for (w in b.vals()) { 60 | if (v == w) { foundMatch := true } 61 | }; 62 | assert foundMatch 63 | }; 64 | 65 | // do some more operations: 66 | a.put("apple", 1111); 67 | a.put("banana", 2222); 68 | switch( a.remove("pear")) { 69 | case null { assert false }; 70 | case (?three) { assert three == 3 }; 71 | }; 72 | a.delete("avocado"); 73 | 74 | // check them: 75 | switch (a.get("apple")) { 76 | case (?1111) { }; 77 | case _ { assert false }; 78 | }; 79 | switch (a.get("banana")) { 80 | case (?2222) { }; 81 | case _ { assert false }; 82 | }; 83 | switch (a.get("pear")) { 84 | case null { }; 85 | case (?_) { assert false }; 86 | }; 87 | switch (a.get("avocado")) { 88 | case null { }; 89 | case (?_) { assert false }; 90 | }; 91 | 92 | // undo operations above: 93 | a.put("apple", 1); 94 | // .. and test that replace works 95 | switch (a.replace("apple", 666)) { 96 | case null { assert false }; 97 | case (?one) { assert one == 1; // ...and revert 98 | a.put("apple", 1) 99 | }; 100 | }; 101 | a.put("banana", 2); 102 | a.put("pear", 3); 103 | a.put("avocado", 4); 104 | 105 | // ensure clone has each key-value pair present in original 106 | for ((k,v) in a.entries()) { 107 | Debug.print(debug_show (k,v)); 108 | switch (b.get(k)) { 109 | case null { assert false }; 110 | case (?w) { assert v == w }; 111 | }; 112 | }; 113 | 114 | // ensure original has each key-value pair present in clone 115 | for ((k,v) in b.entries()) { 116 | Debug.print(debug_show (k,v)); 117 | switch (a.get(k)) { 118 | case null { assert false }; 119 | case (?w) { assert v == w }; 120 | }; 121 | }; 122 | 123 | 124 | // test fromIter method 125 | let c = HM.fromIter(b.entries(), 0, Text.equal, Text.hash); 126 | 127 | // c agrees with each entry of b 128 | for ((k,v) in b.entries()) { 129 | Debug.print(debug_show (k,v)); 130 | switch (c.get(k)) { 131 | case null { assert false }; 132 | case (?w) { assert v == w }; 133 | }; 134 | }; 135 | 136 | // b agrees with each entry of c 137 | for ((k,v) in c.entries()) { 138 | Debug.print(debug_show (k,v)); 139 | switch (b.get(k)) { 140 | case null { assert false }; 141 | case (?w) { assert v == w }; 142 | }; 143 | }; 144 | 145 | // Issue #228 146 | let d = HM.StableHashMap(50, Text.equal, Text.hash); 147 | switch(d.remove("test")) { 148 | case null { }; 149 | case (?_) { assert false }; 150 | }; 151 | 152 | // test exportProps 153 | let e = HM.StableHashMap(3, Text.equal, Text.hash); 154 | var tbl = e.exportProps(); 155 | assert tbl._count == 0; 156 | assert tbl.table.size() == 0; 157 | 158 | e.put("a", 0); 159 | tbl := e.exportProps(); 160 | assert tbl._count == 1; 161 | assert tbl.table.size() == 3; // table should be sized now 162 | 163 | e.put("b", 1); 164 | e.put("c", 2); 165 | e.put("d", 3); 166 | tbl := e.exportProps(); 167 | assert tbl._count == 4; 168 | assert tbl.table.size() == 6; // table should have doubled once 169 | 170 | let lst: List.List = ?(5, null); 171 | 172 | let asclst: List.List<(Text, Nat)> = ?(("a", 1), ?(("b", 2), null)); 173 | 174 | // test importProps 175 | e.importProps({ 176 | table = [var 177 | ?(("a", 2), null), ?(("b", 4),null) 178 | ]; 179 | _count = 2; 180 | }); 181 | tbl := e.exportProps(); 182 | assert tbl._count == 2; 183 | assert tbl.table.size() == 2; 184 | assert e.get("a") == ?2; 185 | assert e.get("b") == ?4; 186 | assert e.get("c") == null; 187 | assert e.get("d") == null; 188 | 189 | e.importProps({ 190 | table = [var 191 | ?(("a", 2), null), ?(("b", 4),null) 192 | ]; 193 | _count = 6; 194 | }); 195 | tbl := e.exportProps(); 196 | assert tbl._count == 2; 197 | 198 | }; 199 | -------------------------------------------------------------------------------- /test/FunctionalStableHashMapTest.mo: -------------------------------------------------------------------------------- 1 | import Debug "mo:base/Debug"; 2 | import HM "../src/FunctionalStableHashMap"; 3 | import Hash "mo:base/Hash"; 4 | import Text "mo:base/Text"; 5 | 6 | Debug.print("functional stable"); 7 | 8 | debug { 9 | let a = HM.init(); 10 | assert HM.size(a) == 0; 11 | 12 | func putHelper(map: HM.StableHashMap, t: Text, n: Nat): () { 13 | HM.put(map, Text.equal, Text.hash, t, n); 14 | }; 15 | 16 | func getHelper(map: HM.StableHashMap, t: Text): ?Nat { 17 | HM.get(map, Text.equal, Text.hash, t); 18 | }; 19 | 20 | func removeHelper(map: HM.StableHashMap, t: Text): ?Nat { 21 | HM.remove(map, Text.equal, Text.hash, t); 22 | }; 23 | 24 | func deleteHelper(map: HM.StableHashMap, t: Text): () { 25 | HM.delete(map, Text.equal, Text.hash, t); 26 | }; 27 | 28 | putHelper(a, "apple", 1); 29 | putHelper(a, "banana", 2); 30 | putHelper(a, "pear", 3); 31 | putHelper(a, "avocado", 4); 32 | putHelper(a, "Apple", 11); 33 | putHelper(a, "Banana", 22); 34 | putHelper(a, "Pear", 33); 35 | putHelper(a, "Avocado", 44); 36 | putHelper(a, "ApplE", 111); 37 | putHelper(a, "BananA", 222); 38 | putHelper(a, "PeaR", 333); 39 | putHelper(a, "AvocadO", 444); 40 | 41 | // need to resupply the constructor args; they are private to the object; but, should they be? 42 | assert HM.size(a) == 12; 43 | let b = HM.clone(a, Text.equal, Text.hash); 44 | assert HM.size(b) == 12; 45 | 46 | // ensure clone has each key-value pair present in original 47 | for ((k,v) in HM.entries(a)) { 48 | Debug.print(debug_show (k,v)); 49 | switch (getHelper(b, k)) { 50 | case null { assert false }; 51 | case (?w) { assert v == w }; 52 | }; 53 | }; 54 | 55 | // ensure original has each key-value pair present in clone 56 | for ((k,v) in HM.entries(b)) { 57 | Debug.print(debug_show (k,v)); 58 | switch (getHelper(a, k)) { 59 | case null { assert false }; 60 | case (?w) { assert v == w }; 61 | }; 62 | }; 63 | 64 | // ensure clone has each key present in original 65 | for (k in HM.keys(a)) { 66 | switch (getHelper(b, k)) { 67 | case null { assert false }; 68 | case (?_) { }; 69 | }; 70 | }; 71 | 72 | // ensure clone has each value present in original 73 | for (v in HM.vals(a)) { 74 | var foundMatch = false; 75 | for (w in HM.vals(b)) { 76 | if (v == w) { foundMatch := true } 77 | }; 78 | assert foundMatch 79 | }; 80 | 81 | // do some more operations: 82 | putHelper(a, "apple", 1111); 83 | putHelper(a, "banana", 2222); 84 | switch( removeHelper(a, "pear")) { 85 | case null { assert false }; 86 | case (?three) { assert three == 3 }; 87 | }; 88 | assert HM.size(a) == 11; 89 | deleteHelper(a, "avocado"); 90 | assert HM.size(a) == 10; 91 | 92 | // check them: 93 | switch (getHelper(a, "apple")) { 94 | case (?1111) { }; 95 | case _ { assert false }; 96 | }; 97 | switch (getHelper(a, "banana")) { 98 | case (?2222) { }; 99 | case _ { assert false }; 100 | }; 101 | switch (getHelper(a, "pear")) { 102 | case null { }; 103 | case (?_) { assert false }; 104 | }; 105 | switch (getHelper(a, "avocado")) { 106 | case null { }; 107 | case (?_) { assert false }; 108 | }; 109 | 110 | // undo operations above: 111 | putHelper(a, "apple", 1); 112 | // .. and test that replace works 113 | switch (HM.replace(a, Text.equal, Text.hash, "apple", 666)) { 114 | case null { assert false }; 115 | case (?one) { assert one == 1; // ...and revert 116 | putHelper(a, "apple", 1) 117 | }; 118 | }; 119 | putHelper(a, "banana", 2); 120 | putHelper(a, "pear", 3); 121 | putHelper(a, "avocado", 4); 122 | assert HM.size(a) == 12; 123 | 124 | // ensure clone has each key-value pair present in original 125 | for ((k,v) in HM.entries(a)) { 126 | Debug.print(debug_show (k,v)); 127 | switch (getHelper(b, k)) { 128 | case null { assert false }; 129 | case (?w) { assert v == w }; 130 | }; 131 | }; 132 | 133 | // ensure original has each key-value pair present in clone 134 | for ((k,v) in HM.entries(b)) { 135 | Debug.print(debug_show (k,v)); 136 | switch (getHelper(a, k)) { 137 | case null { assert false }; 138 | case (?w) { assert v == w }; 139 | }; 140 | }; 141 | 142 | 143 | // test fromIter method 144 | let c = HM.fromIter(HM.entries(b), 0, Text.equal, Text.hash); 145 | 146 | // c agrees with each entry of b 147 | for ((k,v) in HM.entries(b)) { 148 | Debug.print(debug_show (k,v)); 149 | switch (getHelper(c, k)) { 150 | case null { assert false }; 151 | case (?w) { assert v == w }; 152 | }; 153 | }; 154 | 155 | // b agrees with each entry of c 156 | for ((k,v) in HM.entries(c)) { 157 | Debug.print(debug_show (k,v)); 158 | switch (getHelper(b, k)) { 159 | case null { assert false }; 160 | case (?w) { assert v == w }; 161 | }; 162 | }; 163 | 164 | // Issue #228 165 | // let d = H.HashMap(50, Text.equal, Text.hash); 166 | let d = HM.init(); 167 | switch(removeHelper(d, "test")) { 168 | case null { }; 169 | case (?_) { assert false }; 170 | }; 171 | assert HM.size(d) == 0; 172 | }; -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | for file in *.mo; do \ 3 | $(shell vessel bin)/moc $(shell vessel sources) -wasi-system-api -o TestA.wasm "$$file" && wasmtime TestA.wasm && rm -f TestA.wasm; \ 4 | done -------------------------------------------------------------------------------- /test/package-set.dhall: -------------------------------------------------------------------------------- 1 | ../package-set.dhall 2 | -------------------------------------------------------------------------------- /test/vessel.dhall: -------------------------------------------------------------------------------- 1 | let mainVessel = ../vessel.dhall 2 | 3 | in mainVessel 4 | with dependencies = mainVessel.dependencies # [ "matchers" ] 5 | -------------------------------------------------------------------------------- /vessel.dhall: -------------------------------------------------------------------------------- 1 | { dependencies = [ "base" ], compiler = Some "0.6.21" } 2 | --------------------------------------------------------------------------------