├── .ameba.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── benchmarks ├── benchmarks.cr ├── map │ ├── creation.cr │ ├── lookup.cr │ ├── merge.cr │ ├── traversal.cr │ └── update.cr └── vector │ ├── append.cr │ ├── concatenation.cr │ ├── creation.cr │ ├── lookup.cr │ ├── traversal.cr │ └── update.cr ├── shard.yml ├── spec ├── immutable │ ├── map │ │ ├── transient_spec.cr │ │ └── trie_spec.cr │ ├── map_spec.cr │ ├── vector │ │ ├── transient_spec.cr │ │ └── trie_spec.cr │ └── vector_spec.cr ├── immutable_spec.cr └── spec_helper.cr └── src ├── immutable.cr └── immutable ├── map.cr ├── map └── trie.cr ├── vector.cr ├── vector └── trie.cr └── version.cr /.ameba.yml: -------------------------------------------------------------------------------- 1 | Style/VerboseBlock: 2 | Enabled: false 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, workflow_dispatch] 3 | jobs: 4 | style: 5 | runs-on: ubuntu-latest 6 | container: 7 | image: crystallang/crystal 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Format 11 | run: crystal tool format --check 12 | - name: Lint 13 | uses: crystal-ameba/github-action@v0.2.12 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | test: 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: 21 | - ubuntu-latest 22 | - macos-latest 23 | crystal: 24 | - latest 25 | - nightly 26 | - 1.0.0 27 | - 0.36.1 28 | - 0.35.1 29 | runs-on: ${{matrix.os}} 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: oprypin/install-crystal@v1 33 | with: 34 | crystal: ${{matrix.crystal}} 35 | - run: shards install --ignore-crystal-version 36 | - run: crystal spec --error-trace -v 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /libs/ 3 | /.crystal/ 4 | /.shards/ 5 | .DS_Store 6 | /tmp/ 7 | 8 | 9 | # Libraries don't need dependency lock 10 | # Dependencies will be locked in application that uses them 11 | /shard.lock 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Luca Ongaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = $(shell crystal eval 'require "yaml"; s = YAML.parse(File.read("./shard.yml")); puts s["version"]') 2 | CONST_VERSION = $(shell crystal eval 'require "./src/immutable/version"; puts Immutable::VERSION') 3 | GIT_STATUS = $(shell git status -s) 4 | 5 | docsite: 6 | crystal docs && git checkout gh-pages && mkdir -p api && cp -r docs/. api && git add api && git commit -m "generate docs" && git push && git checkout master 7 | 8 | test: 9 | crystal spec 10 | 11 | release: test 12 | @test "$(VERSION)" == "$(CONST_VERSION)" || { echo "Error: version in shards.yml does not match version in code"; exit 1; } 13 | @test "$(GIT_STATUS)" == "" || { echo "Error: uncommitted changes"; exit 1; } 14 | git fetch && git tag v$(VERSION) origin/master && git push origin v$(VERSION) 15 | open https://github.com/lucaong/immutable/releases/new?tag=v$(VERSION) 16 | 17 | benchmark: 18 | mkdir -p ./tmp && crystal build -o ./tmp/benchmarks --release ./benchmarks/benchmarks.cr 19 | ./tmp/benchmarks 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/lucaong/immutable/workflows/CI/badge.svg)](https://github.com/lucaong/immutable/actions) 2 | 3 | # Immutable 4 | 5 | Efficient, thread-safe immutable data structures for Crystal. 6 | 7 | Whenever an `Immutable` data structure is "modified", the original remains 8 | unchanged and a modified copy is returned. However, the copy is efficient due to 9 | structural sharing. This makes `Immutable` data structures inherently 10 | thread-safe, garbage collector friendly and performant. 11 | 12 | At the moment, `Immutable` implements the following persistent data structures: 13 | 14 | - `Immutable::Vector`: array-like ordered, integer-indexed collection 15 | implementing efficient append, pop, update and lookup operations 16 | - `Immutable::Map`: hash-like unordered key-value collection implementing 17 | efficient lookup and update operations 18 | 19 | 20 | ## Installation 21 | 22 | Add this to your application's `shard.yml`: 23 | 24 | ```yaml 25 | dependencies: 26 | immutable: 27 | github: lucaong/immutable 28 | ``` 29 | 30 | 31 | ## Usage 32 | 33 | For a list of all classes and methods refer to the [API documentation](http://lucaong.github.io/immutable/api/) 34 | 35 | To use the immutable collections, require `immutable` in your code: 36 | 37 | ```crystal 38 | require "immutable" 39 | ``` 40 | 41 | ### Vector ([API docs](http://lucaong.github.io/immutable/api/Immutable/Vector.html)) 42 | 43 | ```crystal 44 | # Vector behaves mostly like an Array: 45 | vector = Immutable::Vector[1, 2, 3, 4, 5] # => Vector [1, 2, 3, 4, 5] 46 | vector[0] # => 1 47 | vector[-1] # => 5 48 | vector.size # => 5 49 | vector.each { |elem| puts elem } 50 | 51 | # Updating a Vector always returns a modified copy: 52 | vector2 = vector.set(2, 0) # => Vector [1, 2, 0, 4, 5] 53 | vector2 = vector2.push(42) # => Vector [1, 2, 0, 4, 5, 42] 54 | 55 | # The original vector is unchanged: 56 | vector # => Vector [1, 2, 3, 4, 5] 57 | 58 | # Bulk updates can be made faster by using `transient`: 59 | vector3 = vector.transient do |v| 60 | 1000.times { |i| v = v.push(i) } 61 | end 62 | ``` 63 | 64 | ### Map ([API docs](http://lucaong.github.io/immutable/api/Immutable/Map.html)) 65 | 66 | ```crystal 67 | # Map behaves mostly like a Hash: 68 | map = Immutable::Map[{:a => 1, :b => 2 }] # => Map {:a => 1, :b => 2} 69 | map[:a] # => 1 70 | 71 | # Updating a Map always returns a modified copy: 72 | map2 = map.set(:c, 3) # => Map {:a => 1, :b => 2, :c => 3} 73 | map2 = map2.delete(:b) # => Map {:a => 1, :c => 3} 74 | 75 | # The original map in unchanged: 76 | map # => Map {:a => 1, :b => 2} 77 | 78 | # Bulk updates can be made faster by using `transient`: 79 | map3 = Immutable::Map(String, Int32)[] 80 | map3 = map3.transient do |m| 81 | 1000.times { |i| m = m.set(i.to_s, i) } 82 | end 83 | ``` 84 | 85 | ### Nested structures 86 | 87 | ```crystal 88 | # Nested arrays/hashes can be turned into immutable versions with the `.from` 89 | # method: 90 | 91 | nested = Immutable.from({:name => "Ada", :colors => [:blue, :green, :red] }) 92 | nested # => Map {:name => "Ada", :colors => Vector [:blue, :green, :red]} 93 | ``` 94 | 95 | 96 | ## Implementation 97 | 98 | `Immutable::Vector` is implemented as a bit-partitioned vector trie with a block 99 | size of 32 bits, that guarantees O(Log32) lookups and updates, which is 100 | effectively constant time for practical purposes. Due to tail optimization, 101 | appends and pop are O(1) 31 times out of 32, and O(Log32) 1/32 of the times. 102 | 103 | `Immutable::Map` uses a bit-partitioned hash trie with a block size of 32 bits, 104 | that also guarantees O(Log32) lookups and updates. 105 | 106 | 107 | ## Contributing 108 | 109 | 1. Fork it ( https://github.com/lucaong/immutable/fork ) 110 | 2. Create your feature branch (git checkout -b my-new-feature) 111 | 3. Commit your changes (git commit -am 'Add some feature') 112 | 4. Push to the branch (git push origin my-new-feature) 113 | 5. Create a new Pull Request 114 | 115 | 116 | ## Contributors 117 | 118 | - [lucaong](https://github.com/lucaong) Luca Ongaro - creator, maintainer 119 | 120 | 121 | ## Acknowledgement 122 | 123 | Although not a port, this project takes inspiration from similar libraries and 124 | persistent data structure implementations like: 125 | 126 | - [Clojure persistent collections](http://clojure.org/reference/data_structures) 127 | - [The Hamster gem for Ruby](https://github.com/hamstergem/hamster) 128 | 129 | When researching on the topic of persistent data structure implementation, these 130 | blog posts have been of great help: 131 | 132 | - [Understanding Clojure's Persistent Vector](http://hypirion.com/musings/understanding-persistent-vector-pt-1) (also [Part 2](http://hypirion.com/musings/understanding-persistent-vector-pt-2), [Part 3](http://hypirion.com/musings/understanding-persistent-vector-pt-3) and [Understanding Clojure's Transients](http://hypirion.com/musings/understanding-clojure-transients)) 133 | - [Understanding Clojure's Persistent Hash Map](http://blog.higher-order.net/2009/09/08/understanding-clojures-persistenthashmap-deftwice.html) 134 | 135 | Big thanks to their authors for the great job explaining the internals of these 136 | data structures. 137 | -------------------------------------------------------------------------------- /benchmarks/benchmarks.cr: -------------------------------------------------------------------------------- 1 | require "benchmark" 2 | require "../src/immutable" 3 | require "./vector/*" 4 | require "./map/*" 5 | 6 | def banner(str) 7 | puts "-" * 80 8 | puts str 9 | puts 10 | end 11 | -------------------------------------------------------------------------------- /benchmarks/map/creation.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | banner "Map creation:" 3 | 4 | b.report("Hash creation") do 5 | x = {:foo => 1} 6 | 7 | 100.times { 8 | x = {:foo => 1, :bar => 2, :baz => 3, :qux => 4, :quux => 5} 9 | } 10 | end 11 | 12 | b.report("Map creation") do 13 | x = {:foo => 1} 14 | 15 | 100.times do 16 | x = Immutable.map({:foo => 1, :bar => 2, :baz => 3, :qux => 4, :quux => 5}) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /benchmarks/map/lookup.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | h = (0...100).map { |i| {i, i * 2} }.to_h 3 | m = Immutable.map(h) 4 | 5 | banner "Map lookup:" 6 | 7 | b.report("Hash#fetch") do 8 | 100.times { |i| h.fetch(i, 0) } 9 | end 10 | 11 | b.report("Map#fetch") do 12 | 100.times { |i| m.fetch(i, 0) } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /benchmarks/map/merge.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | h = (0...100).map { |i| {i, i * 2} }.to_h 3 | k = h.dup 4 | m = Immutable.map(h) 5 | o = {1 => 0, 101 => 0, 150 => 0} 6 | 7 | banner "Map merge:" 8 | 9 | b.report("Hash#merge!") do 10 | h.merge!(o) 11 | end 12 | 13 | b.report("Hash#merge") do 14 | k.merge(o) 15 | end 16 | 17 | b.report("Map#merge") do 18 | m.merge(o) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /benchmarks/map/traversal.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | h = (0...100).map { |i| {i, i * 2} }.to_h 3 | m = Immutable.map(h) 4 | 5 | banner "Map traversal:" 6 | 7 | b.report("Hash#each") do 8 | h.each { |_k, _v| nil } 9 | end 10 | 11 | b.report("Map#each") do 12 | m.each { |_k, _v| nil } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /benchmarks/map/update.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | h = {} of Int32 => Int32 3 | k = h.dup 4 | m = Immutable.map(h) 5 | 6 | banner "Map update:" 7 | 8 | b.report("Hash#[]=") do 9 | 100.times { |i| h[i] = 0 } 10 | end 11 | 12 | b.report("Hash#[]= with dup") do 13 | 100.times { |i| k = k.dup; k[i] = 0 } 14 | end 15 | 16 | b.report("Map#set") do 17 | 100.times { |i| m = m.set(i, 0) } 18 | end 19 | 20 | b.report("Map#set with Transient") do 21 | m.transient do |t| 22 | 100.times { |i| t = t.set(i, 0) } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /benchmarks/vector/append.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | banner "Vector append:" 3 | 4 | b.report("Array#push") do 5 | a = [] of Int32 6 | 100.times { |i| a.push(i) } 7 | end 8 | 9 | b.report("Array#push with dup") do 10 | a = [] of Int32 11 | 100.times { |i| a = a.dup.push(i) } 12 | end 13 | 14 | b.report("Vector#push") do 15 | v = Immutable::Vector(Int32).new 16 | 100.times { |i| v.push(i) } 17 | end 18 | 19 | b.report("Vector#push with Transient") do 20 | v = Immutable::Vector(Int32).new 21 | v.transient do |t| 22 | 100.times { |i| t.push(i) } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /benchmarks/vector/concatenation.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | a = [] of Int32 3 | a1 = (0..50).to_a 4 | a2 = (50..100).to_a 5 | v1 = Immutable::Vector.new(a1) 6 | v2 = Immutable::Vector.new(a2) 7 | 8 | banner "Vector concatenation:" 9 | 10 | b.report("Array#concat") do 11 | a1 = (0..50).to_a 12 | a2 = (50..100).to_a 13 | a = [0] 14 | 100.times { |_i| a = a1.concat(a2) } 15 | end 16 | 17 | b.report("Array#+") do 18 | 100.times { |_i| a = a1 + a2 } 19 | end 20 | 21 | b.report("Vector#+") do 22 | v1 = Immutable::Vector.new(a1) 23 | v2 = Immutable::Vector.new(a2) 24 | v = Immutable::Vector(Int32).new 25 | 100.times { |_i| v = v1 + v2 } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /benchmarks/vector/creation.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 3 | v = Immutable.vector(a) 4 | 5 | banner "Vector creation:" 6 | 7 | b.report("Array creation") do 8 | 100.times { a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } 9 | end 10 | 11 | b.report("Vector creation") do 12 | 100.times { v = Immutable.vector(a) } 13 | end 14 | 15 | b.report("Vector creation with .of") do 16 | 100.times { v = Immutable::Vector.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /benchmarks/vector/lookup.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | a = (0...100).to_a 3 | v = Immutable.vector(a) 4 | 5 | banner "Vector lookup:" 6 | 7 | b.report("Array#[]") do 8 | 100.times { |i| a[i] } 9 | end 10 | 11 | b.report("Vector#[]") do 12 | 100.times { |i| v[i] } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /benchmarks/vector/traversal.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | a = (0...100).to_a 3 | v = Immutable.vector(a) 4 | 5 | banner "Vector traversal:" 6 | 7 | b.report("Array#each") do 8 | a.each { |el| el } 9 | end 10 | 11 | b.report("Vector#each") do 12 | v.each { |el| el } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /benchmarks/vector/update.cr: -------------------------------------------------------------------------------- 1 | Benchmark.ips do |b| 2 | a = (0...100).to_a 3 | v = Immutable.vector(a) 4 | 5 | banner "Vector update:" 6 | 7 | b.report("Array#[]=") do 8 | 100.times { |i| a[i] = 0 } 9 | end 10 | 11 | b.report("Array#[]= with dup") do 12 | 100.times { |i| a = a.dup; a[i] = 0 } 13 | end 14 | 15 | b.report("Vector#set") do 16 | 100.times { |i| v = v.set(i, 0) } 17 | end 18 | 19 | b.report("Vector#set with Transient") do 20 | v.transient do |t| 21 | 100.times { |i| t = t.set(i, 0) } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: immutable 2 | version: 0.1.24 3 | 4 | crystal: ">= 0.35" 5 | 6 | authors: 7 | - Luca Ongaro 8 | 9 | license: MIT 10 | -------------------------------------------------------------------------------- /spec/immutable/map/transient_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe Immutable::Map::Transient do 4 | describe "#set" do 5 | it "sets a key-value pair" do 6 | t = Immutable::Map::Transient(Symbol, Int32).new 7 | t.set(:foo, 1)[:foo].should eq(1) 8 | end 9 | end 10 | 11 | describe "#delete" do 12 | it "deletes a key-value pair" do 13 | t = Immutable::Map::Transient(Symbol, Int32).new({:foo => 1, :bar => 2}) 14 | t.delete(:foo).fetch(:foo, nil).should eq(nil) 15 | end 16 | end 17 | 18 | describe "#merge" do 19 | it "merges key-value pairs" do 20 | t = Immutable::Map::Transient(Symbol, Int32).new({:foo => 1, :bar => 2}) 21 | t.merge({:baz => 3})[:baz].should eq(3) 22 | end 23 | end 24 | 25 | describe "#persist!" do 26 | it "returns a persistent immutable vector and invalidates the transient" do 27 | tr = Immutable::Map::Transient(Symbol, Int32).new({:foo => 1, :bar => 2}) 28 | m = tr.persist! 29 | m.should be_a(Immutable::Map(Symbol, Int32)) 30 | m.should_not be_a(Immutable::Map::Transient(Symbol, Int32)) 31 | expect_raises Immutable::Map::Transient::Invalid do 32 | tr.set(:baz, 3) 33 | end 34 | expect_raises Immutable::Map::Transient::Invalid do 35 | tr.delete(:foo) 36 | end 37 | expect_raises Immutable::Map::Transient::Invalid do 38 | tr.merge({:qux => 123}) 39 | nil 40 | end 41 | m.delete(:foo).set(:baz, 3).merge({:qux => 4, :quux => 5}) 42 | tr.to_h.should eq({:foo => 1, :bar => 2}) 43 | m.to_h.should eq({:foo => 1, :bar => 2}) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/immutable/map/trie_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe Immutable::Map::Trie do 4 | empty_trie = Immutable::Map::Trie(Symbol, Int32).empty 5 | trie = empty_trie.set(:foo, 42) 6 | 7 | describe "#set" do 8 | it "sets the value at the key" do 9 | t = empty_trie.set(:foo, 1).set(:bar, 2).set(:foo, 3) 10 | t.size.should eq(2) 11 | t.get(:foo).should eq(3) 12 | t.get(:bar).should eq(2) 13 | end 14 | 15 | it "does not modify the original" do 16 | trie.set(:foo, 0).set(:x, 5) 17 | trie.get(:foo).should eq(42) 18 | expect_raises(KeyError) { trie.get(:x) } 19 | end 20 | end 21 | 22 | describe "#get" do 23 | it "gets the value at key" do 24 | trie.get(:foo).should eq(42) 25 | end 26 | 27 | it "raises KeyError if the key is not associated with any value" do 28 | expect_raises(KeyError) { trie.get(:baz) } 29 | end 30 | end 31 | 32 | describe "#delete" do 33 | it "deletes the value at the key" do 34 | t = empty_trie.set(:foo, 1).set(:bar, 2) 35 | t2 = t.delete(:bar) 36 | t2.size.should eq(1) 37 | t.get(:foo).should eq(1) 38 | expect_raises(KeyError) { t2.get(:bar) } 39 | end 40 | 41 | it "does not modify the original" do 42 | trie.delete(:foo) 43 | trie.get(:foo).should eq(42) 44 | expect_raises(KeyError) { trie.get(:x) } 45 | end 46 | 47 | it "raises KeyError if the key does not exists" do 48 | expect_raises(KeyError) { trie.delete(:xxx) } 49 | end 50 | end 51 | 52 | describe "#fetch" do 53 | it "gets the value at key, if not set it evaluate the block" do 54 | trie.fetch(:foo) { |key| key }.should eq(42) 55 | trie.fetch(:baz) { |key| key }.should eq(:baz) 56 | end 57 | end 58 | 59 | describe "#has_key?" do 60 | it "returns true if the given key is associated to a value, else false" do 61 | trie.has_key?(:foo).should eq(true) 62 | trie.has_key?(:baz).should eq(false) 63 | end 64 | end 65 | 66 | describe "#each" do 67 | it "iterates through tuples of key and value" do 68 | t = empty_trie.set(:foo, 1).set(:bar, 2).set(:baz, 3) 69 | keyvals = [] of {Symbol, Int32} 70 | t.each do |kv| 71 | keyvals << kv 72 | end 73 | keyvals.sort.should eq([{:bar, 2}, {:baz, 3}, {:foo, 1}]) 74 | end 75 | 76 | it "returns an iterator if called without block" do 77 | t = empty_trie.set(:foo, 1).set(:bar, 2).set(:baz, 3) 78 | t.each.to_a.should eq(t.to_a) 79 | end 80 | end 81 | 82 | describe "in-place modifications" do 83 | describe "when modified by the owner" do 84 | it "#set! and #delete! modify in place" do 85 | t = Immutable::Map::Trie(Symbol, Int32).empty(42_u64) 86 | t.set!(:foo, 0, 42_u64) 87 | t.set!(:bar, 1, 42_u64) 88 | t.get(:foo).should eq(0) 89 | t.get(:bar).should eq(1) 90 | t.delete!(:foo, 42_u64) 91 | t.has_key?(:foo).should eq(false) 92 | end 93 | end 94 | 95 | describe "when modified by an object that is not the owner" do 96 | it "#set! and #delete! return a modified copy" do 97 | t = Immutable::Map::Trie(Symbol, Int32).empty(42_u64) 98 | x = t.set!(:foo, 0, 0_u64) 99 | t.has_key?(:foo).should eq(false) 100 | x.get(:foo).should eq(0) 101 | t.set!(:foo, 0, 42_u64) 102 | x = t.delete!(:foo, 0_u64) 103 | t.get(:foo).should eq(0) 104 | t.has_key?(:foo).should eq(true) 105 | x.has_key?(:foo).should eq(false) 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/immutable/map_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | require "json" 3 | 4 | describe Immutable do 5 | describe Immutable::Map do 6 | empty_map = Immutable::Map(String, Int32).new 7 | map = Immutable::Map.new({"foo" => 1, "bar" => 2, "baz" => 3}) 8 | 9 | describe ".new" do 10 | it "creates an empty map" do 11 | m = Immutable::Map(String, Int32).new 12 | m.is_a?(Immutable::Map(String, Int32)).should eq(true) 13 | m.size.should eq(0) 14 | end 15 | 16 | it "with a block, it creates a map with default" do 17 | m = Immutable::Map(String, Int32).new { |_k| 42 } 18 | m["xxx"].should eq(42) 19 | end 20 | 21 | it "with a hash, it creates and initialize an immutable map" do 22 | m = Immutable::Map.new({:foo => 123}) 23 | m[:foo].should eq(123) 24 | end 25 | 26 | it "with a hash and a block, it creates and initialize a map with default" do 27 | m = Immutable::Map.new({:foo => 123}) { |_k| 42 } 28 | m[:foo].should eq(123) 29 | m[:xxx].should eq(42) 30 | end 31 | 32 | it "with an Enumerable of {key, val} tuples, it creates and initialize an immutable map" do 33 | m = Immutable::Map.new([{:foo, 1}, {:bar, 2}]) 34 | m[:bar].should eq(2) 35 | end 36 | end 37 | 38 | describe ".[]" do 39 | it "it creates and initialize an immutable map" do 40 | m = Immutable::Map[{:foo => 123, :bar => 321}] 41 | m[:foo].should eq(123) 42 | end 43 | end 44 | 45 | describe "#fetch" do 46 | describe "with one argument and no block" do 47 | it "returns value associated with key if it exists, else raise KeyError" do 48 | map.fetch("bar").should eq(2) 49 | expect_raises(KeyError) { map.fetch("xxx") } 50 | end 51 | end 52 | 53 | describe "with one argument and a block" do 54 | it "returns value associated with key if it exists, else eval block" do 55 | map.fetch("bar") { |k| k }.should eq(2) 56 | map.fetch("xxx") { |k| k }.should eq("xxx") 57 | end 58 | end 59 | 60 | describe "with two arguments" do 61 | it "returns value associated with key if it exists, else the default" do 62 | map.fetch("bar", 123).should eq(2) 63 | map.fetch("xxx", 123).should eq(123) 64 | end 65 | end 66 | end 67 | 68 | describe "#[]" do 69 | it "returns value associated with key if it exists, else raise KeyError" do 70 | map["bar"].should eq(2) 71 | expect_raises(KeyError) { map["xxx"] } 72 | end 73 | end 74 | 75 | describe "#[]?" do 76 | it "returns value associated with key if it exists, else nil" do 77 | map["bar"].should eq(2) 78 | map["xxx"]?.should eq(nil) 79 | end 80 | end 81 | 82 | describe "#set" do 83 | it "returns a modified copy with the given key-value association" do 84 | m = map.set("abc", 1234) 85 | m["abc"].should eq(1234) 86 | m = map.set("abc", 4321) 87 | m["abc"].should eq(4321) 88 | map["abc"]?.should eq(nil) 89 | end 90 | end 91 | 92 | describe "#delete" do 93 | it "returns a modified copy with the key-value association removed" do 94 | m = map.delete("foo") 95 | m["foo"]?.should eq(nil) 96 | map["foo"].should eq(1) 97 | end 98 | 99 | it "raises KeyError if the key does not exist" do 100 | expect_raises(KeyError) do 101 | map.delete("abc") 102 | end 103 | end 104 | end 105 | 106 | describe "#merge" do 107 | it "returns a copy merged with the given hash" do 108 | m = map.merge({"foo" => 100, "qux" => true}) 109 | m.should eq(Immutable::Map.new({ 110 | "foo" => 100, 111 | "bar" => 2, 112 | "baz" => 3, 113 | "qux" => true, 114 | })) 115 | map["foo"].should eq(1) 116 | map["qux"]?.should eq(nil) 117 | end 118 | 119 | it "returns a copy merged with the given map" do 120 | m = map.merge(Immutable::Map.new({"foo" => 100, "qux" => true})) 121 | m.should eq(Immutable::Map.new({ 122 | "foo" => 100, 123 | "bar" => 2, 124 | "baz" => 3, 125 | "qux" => true, 126 | })) 127 | map["foo"].should eq(1) 128 | map["qux"]?.should eq(nil) 129 | end 130 | end 131 | 132 | describe "#each" do 133 | it "calls the block for each entry in the map" do 134 | entries = [] of Tuple(String, Int32) 135 | map.each do |entry| 136 | entries << entry 137 | end 138 | entries.sort.should eq([{"bar", 2}, {"baz", 3}, {"foo", 1}]) 139 | end 140 | 141 | it "returns an iterator if called with no block" do 142 | map.each.to_a.sort.should eq([{"bar", 2}, {"baz", 3}, {"foo", 1}]) 143 | end 144 | end 145 | 146 | describe "#each_key" do 147 | it "calls the block for each key in the map" do 148 | keys = [] of String 149 | map.each_key do |key| 150 | keys << key 151 | end 152 | keys.sort.should eq(["bar", "baz", "foo"]) 153 | end 154 | 155 | it "returns an iterator if called with no block" do 156 | map.each_key.to_a.sort.should eq(["bar", "baz", "foo"]) 157 | end 158 | end 159 | 160 | describe "#each_value" do 161 | it "calls the block for each value in the map" do 162 | vals = [] of Int32 163 | map.each_value do |val| 164 | vals << val 165 | end 166 | vals.sort.should eq([1, 2, 3]) 167 | end 168 | 169 | it "returns an iterator if called with no block" do 170 | map.each_value.to_a.sort.should eq([1, 2, 3]) 171 | end 172 | end 173 | 174 | describe "#keys" do 175 | it "returs an array of all the keys" do 176 | map.keys.sort!.should eq(["bar", "baz", "foo"]) 177 | end 178 | end 179 | 180 | describe "#values" do 181 | it "returs an array of all the values" do 182 | map.values.sort!.should eq([1, 2, 3]) 183 | end 184 | end 185 | 186 | describe "#==" do 187 | it "returns false for different types" do 188 | (map == 1).should eq(false) 189 | end 190 | 191 | it "returns true for equal maps" do 192 | m1 = Immutable::Map.new({:foo => 123}) 193 | m2 = Immutable::Map.new({:foo => 123}) 194 | (m1 == m2).should eq(true) 195 | end 196 | end 197 | 198 | describe "#inspect" do 199 | it "returns a string representation of the map" do 200 | m = Immutable::Map.new({:foo => 123, :bar => 321}) 201 | [ 202 | "Map {:foo => 123, :bar => 321}", 203 | "Map {:bar => 321, :foo => 123}", 204 | ].should contain(m.inspect) 205 | end 206 | end 207 | 208 | describe "#to_a" do 209 | it "returns an array of entries" do 210 | m = Immutable::Map.new({:foo => 123, :bar => 321}) 211 | a = m.to_a 212 | a.should contain({:foo, 123}) 213 | a.should contain({:bar, 321}) 214 | end 215 | end 216 | 217 | describe "#to_h" do 218 | it "returns a hash of the same entries" do 219 | m = Immutable::Map.new({:foo => 123, :bar => 321}) 220 | m.to_h.should eq({:foo => 123, :bar => 321}) 221 | end 222 | end 223 | 224 | describe "#to_json" do 225 | it "serializes as a JSON object" do 226 | m = Immutable::Map.new({:foo => 123, :bar => 321}) 227 | JSON.parse(m.to_json).should eq({"foo" => 123, "bar" => 321}) 228 | end 229 | end 230 | 231 | describe "#hash" do 232 | it "returns a different hash code for different maps" do 233 | m1 = Immutable::Map.new({"foo" => "bar"}) 234 | m2 = Immutable::Map.new({"baz" => "qux"}) 235 | m1.hash.should_not eq(m2.hash) 236 | end 237 | 238 | it "returns the same hash code for equal maps" do 239 | m1 = Immutable::Map.new({"foo" => "bar"}) 240 | m2 = Immutable::Map.new({"foo" => "bar"}) 241 | m1.hash.should eq(m2.hash) 242 | end 243 | end 244 | 245 | describe "transient" do 246 | it "yields a transient map and converts back to an immutable one" do 247 | map = empty_map.transient do |m| 248 | m.should be_a(Immutable::Map::Transient(String, Int32)) 249 | 100.times { |i| m = m.set(i.to_s, i) } 250 | end 251 | map.should be_a(Immutable::Map(String, Int32)) 252 | map.should_not be_a(Immutable::Map::Transient(String, Int32)) 253 | map.size.should eq(100) 254 | empty_map.size.should eq(0) 255 | end 256 | end 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /spec/immutable/vector/transient_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe Immutable::Vector::Transient do 4 | describe "#push" do 5 | it "pushes elements into the transient" do 6 | tr = Immutable::Vector::Transient(Int32).new 7 | 100.times { |i| tr = tr.push(i) } 8 | tr.size.should eq(100) 9 | end 10 | end 11 | 12 | describe "#set" do 13 | it "sets elements of the transient" do 14 | tr = Immutable::Vector::Transient.new([1, 2, 3]) 15 | tr.set(1, 0)[1].should eq(0) 16 | end 17 | end 18 | 19 | describe "#pop" do 20 | it "sets elements of the transient" do 21 | tr = Immutable::Vector::Transient.new([1, 2, 3]) 22 | elem, t = tr.pop 23 | elem.should eq(3) 24 | t.size.should eq(2) 25 | end 26 | end 27 | 28 | describe "#persist!" do 29 | it "returns a persistent immutable vector and invalidates the transient" do 30 | tr = Immutable::Vector::Transient(Int32).new 31 | 100.times { |i| tr = tr.push(i) } 32 | v = tr.persist! 33 | v.should be_a(Immutable::Vector(Int32)) 34 | v.should_not be_a(Immutable::Vector::Transient(Int32)) 35 | expect_raises Immutable::Vector::Transient::Invalid do 36 | tr.pop 37 | end 38 | expect_raises Immutable::Vector::Transient::Invalid do 39 | tr.push(100) 40 | end 41 | expect_raises Immutable::Vector::Transient::Invalid do 42 | tr.set(50, 0) 43 | end 44 | v.push(100).set(50, 0).set(98, 0) 45 | tr.size.should eq(100) 46 | tr.last.should eq(99) 47 | v.size.should eq(100) 48 | v[50].should eq(50) 49 | v.last.should eq(99) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/immutable/vector/trie_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe Immutable::Vector::Trie do 4 | empty_trie = Immutable::Vector::Trie(Int32).empty 5 | trie = Immutable::Vector::Trie.from((0..49).to_a) 6 | 7 | describe "#size" do 8 | it "returns the number of elements in the trie" do 9 | empty_trie.size.should eq(0) 10 | trie.size.should eq(50) 11 | end 12 | end 13 | 14 | describe "#get" do 15 | it "gets the element with the given index" do 16 | trie.get(0).should eq(0) 17 | trie.get(3).should eq(3) 18 | trie.get(49).should eq(49) 19 | end 20 | 21 | it "raises IndexError when accessing out of range" do 22 | expect_raises(IndexError) do 23 | trie.get(trie.size) 24 | end 25 | 26 | expect_raises(IndexError) do 27 | trie.get(-1) 28 | end 29 | end 30 | end 31 | 32 | describe "#update" do 33 | it "returns a modified copy of the trie" do 34 | t = trie.update(0, -1) 35 | t.get(0).should eq(-1) 36 | end 37 | 38 | it "does not modify the original" do 39 | trie.update(0, -1) 40 | trie.get(0).should eq(0) 41 | end 42 | 43 | it "raises IndexError when updating elements out of range" do 44 | expect_raises(IndexError) do 45 | trie.update(trie.size, -1) 46 | end 47 | 48 | expect_raises(IndexError) do 49 | trie.update(-1, -1) 50 | end 51 | end 52 | end 53 | 54 | describe "#push_leaf" do 55 | it "return a copy of the trie with the given values appended" do 56 | original = Immutable::Vector::Trie(Int32).empty 57 | t = original.push_leaf((0..31).to_a) 58 | t.get(0).should eq(0) 59 | t.get(31).should eq(31) 60 | t.size.should eq(32) 61 | original.size.should eq(0) 62 | end 63 | 64 | it "works across multiple levels" do 65 | t = Immutable::Vector::Trie(Int32).empty 66 | (0..1099).each_slice(Immutable::Vector::Trie::BLOCK_SIZE) do |leaf| 67 | t = t.push_leaf(leaf) 68 | end 69 | t.to_a.should eq((0..1099).to_a) 70 | end 71 | 72 | it "raises if the tree is partial" do 73 | expect_raises(ArgumentError) do 74 | trie.push_leaf((0..31).to_a) 75 | end 76 | end 77 | 78 | it "raises if the leaf has the wrong size" do 79 | expect_raises(ArgumentError) do 80 | empty_trie.push_leaf((0..35).to_a) 81 | end 82 | end 83 | end 84 | 85 | describe "#pop_leaf" do 86 | it "return a copy of the trie with the given values appended" do 87 | t = empty_trie 88 | block_size = Immutable::Vector::Trie::BLOCK_SIZE 89 | (0...block_size*5).each_slice(block_size) do |leaf| 90 | t = t.push_leaf(leaf) 91 | end 92 | t.pop_leaf.to_a.should eq(t.to_a[0...-1*block_size]) 93 | end 94 | 95 | it "works across multiple levels" do 96 | t = empty_trie 97 | block_size = Immutable::Vector::Trie::BLOCK_SIZE 98 | (0...block_size*(block_size + 1)).each_slice(block_size) do |leaf| 99 | t = t.push_leaf(leaf) 100 | end 101 | (block_size + 1).times do 102 | t = t.pop_leaf 103 | end 104 | t.size.should eq(0) 105 | end 106 | 107 | it "raises if the tree is partial" do 108 | expect_raises(ArgumentError) do 109 | trie.pop_leaf 110 | end 111 | end 112 | end 113 | 114 | describe "#each" do 115 | it "iterates through each element" do 116 | array = [] of Int32 117 | trie.each do |elem| 118 | array << elem 119 | end 120 | array.should eq((0...trie.size).to_a) 121 | end 122 | 123 | it "return an iterator if called with no arguments" do 124 | t = Immutable::Vector::Trie.from((0..100).to_a) 125 | iter = t.each 126 | iter.to_a.should eq(t.to_a) 127 | end 128 | end 129 | 130 | describe ".empty" do 131 | it "returns an empty trie" do 132 | t = Immutable::Vector::Trie(Int32).empty 133 | t.size.should eq(0) 134 | t.should be_a(Immutable::Vector::Trie(Int32)) 135 | end 136 | end 137 | 138 | describe ".from" do 139 | it "returns a trie containing the given elements" do 140 | t = Immutable::Vector::Trie.from((0..999).to_a) 141 | t.size.should eq(1000) 142 | t.should be_a(Immutable::Vector::Trie(Int32)) 143 | t.get(0).should eq(0) 144 | t.get(999).should eq(999) 145 | end 146 | end 147 | 148 | describe "in-place modifications" do 149 | describe "when modified by the owner" do 150 | it "#update! modifies in place" do 151 | t = Immutable::Vector::Trie.from((0..99).to_a, 42_u64) 152 | t.update!(50, 0, 42_u64) 153 | t.update!(99, 0, 42_u64) 154 | t.get(50).should eq(0) 155 | t.get(99).should eq(0) 156 | end 157 | 158 | it "#push_leaf! and #pop_leaf! modify in place" do 159 | block_size = Immutable::Vector::Trie::BLOCK_SIZE 160 | t = Immutable::Vector::Trie.new([] of Int32, 42_u64) 161 | not_in_place = 0 162 | # push_leaf! 163 | (block_size.to_i + 1).times do |i| 164 | x = t.push_leaf!((i*block_size...i * block_size + block_size).to_a, 42_u64) 165 | not_in_place += 1 unless x == t 166 | t = x 167 | end 168 | not_in_place.should eq(3) 169 | t.size.should eq(block_size * (block_size + 1)) 170 | # pop_leaf! 171 | not_in_place = 0 172 | block_size.times do |_i| 173 | x = t.pop_leaf!(42_u64) 174 | not_in_place += 1 unless x == t 175 | t = x 176 | end 177 | not_in_place.should eq(2) 178 | t.size.should eq(block_size) 179 | end 180 | end 181 | 182 | describe "when modified by an object who's not the owner" do 183 | it "#update! returns a modified copy" do 184 | t = Immutable::Vector::Trie.from((0..99).to_a, 42_u64) 185 | t.update!(50, 0, 2_u64) 186 | t.update!(99, 0, 2_u64) 187 | t.get(50).should eq(50) 188 | t.get(99).should eq(99) 189 | end 190 | 191 | it "#push_leaf! and #pop_leaf! return a modified copy" do 192 | block_size = Immutable::Vector::Trie::BLOCK_SIZE 193 | t = Immutable::Vector::Trie.new([] of Int32, 42_u64) 194 | 3.times do |_i| 195 | t = t.push_leaf!((0...block_size).to_a, 42_u64) 196 | end 197 | x = t.push_leaf!((0...block_size).to_a, 1_u64) 198 | x.should_not eq(t) 199 | x = t.pop_leaf!(1_u64) 200 | x.should_not eq(t) 201 | end 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /spec/immutable/vector_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | require "json" 3 | 4 | describe Immutable do 5 | describe Immutable::Vector do 6 | empty_vector = Immutable::Vector(Int32).new 7 | vector = Immutable::Vector.new((0..99).to_a) 8 | 9 | describe ".new" do 10 | describe "without arguments" do 11 | it "returns an empty Vector" do 12 | Immutable::Vector(Int32).new.size.should eq(0) 13 | end 14 | end 15 | 16 | describe "with an array of elements" do 17 | it "returns a Vector containing the elements" do 18 | vec = Immutable::Vector.new((0..99).to_a) 19 | vec.size.should eq(100) 20 | vec.first.should eq(0) 21 | vec.last.should eq(99) 22 | end 23 | end 24 | end 25 | 26 | describe ".[]" do 27 | it "returns a Vector of the arguments" do 28 | vec = Immutable::Vector[1, 2, 3] 29 | vec.size.should eq(3) 30 | vec.first.should eq(1) 31 | vec.last.should eq(3) 32 | end 33 | 34 | it "returns a Vector of the arguments, even when more than block size" do 35 | vec = Immutable::Vector[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 36 | 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 37 | 25, 26, 27, 28, 29, 30, 31, 32, 33] 38 | vec.size.should eq(33) 39 | vec.first.should eq(1) 40 | vec.last.should eq(33) 41 | end 42 | end 43 | 44 | describe "#size" do 45 | it "returns the correct size" do 46 | empty_vector.size.should eq(0) 47 | vector.size.should eq(100) 48 | end 49 | end 50 | 51 | describe "#push" do 52 | it "returns a modified copy without changing the original" do 53 | v = empty_vector.push(5) 54 | v.size.should eq(1) 55 | empty_vector.size.should eq(0) 56 | end 57 | 58 | it "work properly across leaves" do 59 | v = empty_vector 60 | 100.times do |i| 61 | v = v.push(i) 62 | end 63 | v.size.should eq(100) 64 | v.to_a.should eq((0..99).to_a) 65 | end 66 | end 67 | 68 | describe "#pop" do 69 | it "returns a tuple of last element and vector but last element" do 70 | l, v = vector.pop 71 | v.to_a.should eq(vector.to_a[0...-1]) 72 | l.should eq(vector.last) 73 | end 74 | 75 | it "work properly across leaves" do 76 | v = Immutable::Vector.new((0..99).to_a) 77 | v.size.times do 78 | _, v = v.pop 79 | end 80 | v.size.should eq(0) 81 | end 82 | 83 | it "raises IndexError if called on empty vector" do 84 | expect_raises(IndexError) do 85 | empty_vector.pop 86 | end 87 | end 88 | end 89 | 90 | describe "#pop?" do 91 | it "behaves like `pop` if called on non-empty vector" do 92 | vector.pop?.should eq(vector.pop) 93 | end 94 | 95 | it "returns { nil, self } if called on empty vector" do 96 | l, v = empty_vector.pop? 97 | l.should eq(nil) 98 | v.should eq(empty_vector) 99 | end 100 | end 101 | 102 | describe "#at" do 103 | it "returns the element at the given index" do 104 | vector.at(10).should eq(10) 105 | end 106 | 107 | it "works with negative indexes" do 108 | vector.at(-1).should eq(vector.at(vector.size - 1)) 109 | vector.at(-1 * vector.size).should eq(vector.at(0)) 110 | end 111 | 112 | it "raises IndexError if accessing indexes out of range" do 113 | expect_raises(IndexError) { vector.at(vector.size) } 114 | expect_raises(IndexError) { vector.at(-1 * (vector.size + 1)) } 115 | end 116 | 117 | it "evaluates block if given and accessing indexes out of range" do 118 | vector.at(vector.size) { 42 }.should eq(42) 119 | vector.at(-1 * (vector.size + 1)) { 46 }.should eq(46) 120 | end 121 | end 122 | 123 | describe "#[]" do 124 | it "returns the element at the given index" do 125 | vector[10].should eq(10) 126 | end 127 | 128 | it "works with negative indexes" do 129 | vector[-1].should eq(vector[vector.size - 1]) 130 | vector[-1 * vector.size].should eq(vector[0]) 131 | end 132 | 133 | it "raises IndexError if accessing indexes out of range" do 134 | expect_raises(IndexError) { vector[vector.size] } 135 | expect_raises(IndexError) { vector[-1 * (vector.size + 1)] } 136 | end 137 | end 138 | 139 | describe "#[]?" do 140 | it "returns the element at the given index" do 141 | vector[10]?.should eq(10) 142 | end 143 | 144 | it "works with negative indexes" do 145 | vector[-1]?.should eq(vector[vector.size - 1]) 146 | vector[-1 * vector.size]?.should eq(vector[0]) 147 | end 148 | 149 | it "returns nil if accessing indexes out of range" do 150 | vector[vector.size]?.should eq(nil) 151 | vector[-1 * (vector.size + 1)]?.should eq(nil) 152 | end 153 | end 154 | 155 | describe "#first,#last" do 156 | it "returns the first/last element of the vector" do 157 | vector.first.should eq(0) 158 | vector.last.should eq(99) 159 | end 160 | 161 | it "raises IndexError if the list is empty" do 162 | expect_raises(IndexError) { empty_vector.first } 163 | expect_raises(IndexError) { empty_vector.last } 164 | end 165 | end 166 | 167 | describe "#first?,#last?" do 168 | it "returns the first/last element of the vector" do 169 | vector.first?.should eq(0) 170 | vector.last?.should eq(99) 171 | end 172 | 173 | it "returns nil if the list is empty" do 174 | empty_vector.first?.should eq(nil) 175 | empty_vector.last?.should eq(nil) 176 | end 177 | end 178 | 179 | describe "#set" do 180 | it "returns a copy with the value set at given index" do 181 | v = vector.set(10, -1) 182 | v[10].should eq(-1) 183 | vector[10].should eq(10) 184 | end 185 | 186 | it "works with negative indexes" do 187 | v = vector.set(-1, -1) 188 | v[-1].should eq(-1) 189 | vector[-1].should eq(vector[vector.size - 1]) 190 | v = vector.set(-1 * vector.size, -1) 191 | v[0].should eq(-1) 192 | vector[0].should eq(0) 193 | end 194 | 195 | it "raises IndexError if accessing indexes out of range" do 196 | expect_raises(IndexError) { vector[vector.size] } 197 | expect_raises(IndexError) { vector[-1 * (vector.size + 1)] } 198 | end 199 | end 200 | 201 | describe "#each" do 202 | it "iterates through each element" do 203 | array = [] of Int32 204 | vector.each do |elem| 205 | array << elem 206 | end 207 | array.should eq((0...vector.size).to_a) 208 | end 209 | 210 | it "returns self" do 211 | v = vector.each do |_elem|; end 212 | v.should eq(vector) 213 | end 214 | 215 | it "returns an iterator if called with no arguments" do 216 | iter = vector.each 217 | iter.to_a.should eq((0...vector.size).to_a) 218 | end 219 | end 220 | 221 | describe "#<=>" do 222 | it "returns 0 if vectors are equal" do 223 | v1 = Immutable::Vector.new([1, 2, 3]) 224 | v2 = Immutable::Vector.new([1, 2, 3]) 225 | (v1 <=> v2).should eq(0) 226 | end 227 | 228 | it "returns -1 if the first mismatched element is smaller than the other" do 229 | v1 = Immutable::Vector.new([4, 1, 5]) 230 | v2 = Immutable::Vector.new([4, 2, 3, 7]) 231 | (v1 <=> v2).should eq(-1) 232 | end 233 | 234 | it "returns 1 if the first mismatched element bigger than the other" do 235 | v1 = Immutable::Vector.new([4, 3, 2]) 236 | v2 = Immutable::Vector.new([4, 2, 3, 7]) 237 | (v1 <=> v2).should eq(1) 238 | end 239 | 240 | describe "when equal up to the shorter Vector" do 241 | it "returns -1 if shorter than the other" do 242 | v1 = Immutable::Vector.new([4, 2, 3]) 243 | v2 = Immutable::Vector.new([4, 2, 3, 7]) 244 | (v1 <=> v2).should eq(-1) 245 | end 246 | 247 | it "returns 1 if longer than the other" do 248 | v1 = Immutable::Vector.new([4, 2, 3, 7]) 249 | v2 = Immutable::Vector.new([4, 2, 3]) 250 | (v1 <=> v2).should eq(1) 251 | end 252 | end 253 | end 254 | 255 | describe "#==" do 256 | it "returns false for different types" do 257 | (vector == 1).should eq(false) 258 | end 259 | 260 | it "returns true for equal vectors" do 261 | v1 = Immutable::Vector.new([4, 2, 3]) 262 | v2 = Immutable::Vector.new([4, 2, 3]) 263 | (v1 == v2).should eq(true) 264 | end 265 | end 266 | 267 | describe "#+" do 268 | it "concatenates vectors" do 269 | v1 = Immutable::Vector.new((0..99).to_a) 270 | v2 = Immutable::Vector.new((100..149).to_a) 271 | v3 = v1 + v2 272 | v3.size.should eq(150) 273 | v1.size.should eq(100) 274 | v2.size.should eq(50) 275 | v3.to_a.should eq(v1.to_a + v2.to_a) 276 | end 277 | end 278 | 279 | describe "#-" do 280 | it "subtract the given vector from self" do 281 | v1 = Immutable::Vector.new([1, 2, 3, 4]) 282 | v2 = Immutable::Vector.new([3, 2, 5]) 283 | v3 = v1 - v2 284 | v3.to_a.should eq(v1.to_a - v2.to_a) 285 | end 286 | end 287 | 288 | describe "#&" do 289 | it "returns the intersection between vectors" do 290 | v1 = Immutable::Vector.new([1, 2, 3, 2, 4, 0]) 291 | v2 = Immutable::Vector.new([0, 2, 1]) 292 | (v1 & v2).should eq(Immutable::Vector.of(1, 2, 0)) 293 | end 294 | 295 | it "returns an empty vector if self or other is empty" do 296 | v1 = Immutable::Vector(Int32).new 297 | v2 = Immutable::Vector.new([0, 2, 1]) 298 | (v1 & v2).should eq(Immutable::Vector(Int32).new) 299 | (v2 & v1).should eq(Immutable::Vector(Int32).new) 300 | end 301 | end 302 | 303 | describe "#|" do 304 | it "returns the union between vectors" do 305 | v1 = Immutable::Vector.new([1, 2, 3, 2, 4, 0]) 306 | v2 = Immutable::Vector.new([0, 2, 0, 7, 1]) 307 | (v1 | v2).should eq(Immutable::Vector.of(1, 2, 3, 4, 0, 7)) 308 | end 309 | end 310 | 311 | describe "#uniq" do 312 | it "returns a vector of unique elements" do 313 | v = Immutable::Vector.new([1, 2, 3, 2, 4, 1, 0]) 314 | v.uniq.should eq(Immutable::Vector.of(1, 2, 3, 4, 0)) 315 | end 316 | end 317 | 318 | describe "#inspect and #to_s" do 319 | it "return a human-readable string representation" do 320 | vec = Immutable::Vector.of(1, 2, 3) 321 | vec.inspect.should eq("Vector [1, 2, 3]") 322 | vec.to_s.should eq("Vector [1, 2, 3]") 323 | end 324 | end 325 | 326 | describe "hash" do 327 | it "returns the same value for identical vectors" do 328 | v1 = Immutable::Vector.of(1, 2, 3) 329 | v2 = Immutable::Vector.of(1, 2, 3) 330 | v1.hash.should eq(v2.hash) 331 | end 332 | 333 | it "returns a different value for different vectors" do 334 | v1 = Immutable::Vector.of(1, 2, 3) 335 | v2 = Immutable::Vector.of(3, 2, 1) 336 | v1.hash.should_not eq(v2.hash) 337 | end 338 | end 339 | 340 | describe "to_json" do 341 | it "serializes to JSON in the same way as array" do 342 | empty_vector.to_json.should eq("[]") 343 | vector.to_json.should eq(vector.to_a.to_json) 344 | end 345 | end 346 | 347 | describe "transient" do 348 | it "yields a transient vector and converts back to an immutable one" do 349 | vec = empty_vector.transient do |v| 350 | v.should be_a(Immutable::Vector::Transient(Int32)) 351 | 100.times { |i| v = v.push(i) } 352 | end 353 | vec.should be_a(Immutable::Vector(Int32)) 354 | vec.should_not be_a(Immutable::Vector::Transient(Int32)) 355 | vec.size.should eq(100) 356 | empty_vector.size.should eq(0) 357 | end 358 | end 359 | end 360 | end 361 | -------------------------------------------------------------------------------- /spec/immutable_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Immutable do 4 | it "has a version number" do 5 | Immutable::VERSION.should_not be(nil) 6 | end 7 | 8 | describe ".vector" do 9 | it "creates a vector of the given elements" do 10 | Immutable.vector([1, 2, 3]).should eq(Immutable::Vector.new([1, 2, 3])) 11 | end 12 | end 13 | 14 | describe ".map" do 15 | it "creates a map of the given key-values" do 16 | Immutable.map({:foo => "bar"}).should eq(Immutable::Map.new({:foo => "bar"})) 17 | end 18 | end 19 | 20 | describe ".from" do 21 | it "creates immutable objects by deeply traversing the given object" do 22 | x = Immutable.from({:foo => [1, 2, 3], :bar => {:baz => 1}}) 23 | y = Immutable.map({ 24 | :foo => Immutable.vector([1, 2, 3]), 25 | :bar => Immutable.map({:baz => 1}), 26 | }) 27 | x.should eq(y) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/immutable" 3 | -------------------------------------------------------------------------------- /src/immutable.cr: -------------------------------------------------------------------------------- 1 | require "./immutable/*" 2 | 3 | module Immutable 4 | # Construct an `Immutable::Vector` of the given elements 5 | def self.vector(elements : Array(T)) forall T 6 | Vector.new(elements) 7 | end 8 | 9 | # Construct an `Immutable::Map` of the given key-values 10 | def self.map(keyvals : Hash(K, V)) forall K, V 11 | Map.new(keyvals) 12 | end 13 | 14 | # Recursively traverses the given object and turns hashes into 15 | # `Immutable::Map` and arrays into `Immutable::Vector` 16 | def self.from(object) 17 | case object 18 | when Array 19 | Vector.new(object.map { |elem| from(elem) }) 20 | when Hash 21 | Map.new(object.map { |k, v| {k, from(v)} }) 22 | else 23 | object 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /src/immutable/map.cr: -------------------------------------------------------------------------------- 1 | # A map is an immutable mapping of keys (of type K) to values (of type V). 2 | # 3 | # Similarly to a hash, a map can use any type as keys, as long as it implements 4 | # a valid `#hash` method. 5 | # 6 | # A map can be constructed from a hash: 7 | # 8 | # ``` 9 | # Immutable::Map(Symbol, Int32).new # => Map {} 10 | # Immutable::Map.new({:foo => 1, :bar => 2}) # => Map {:foo => 1, :bar => 2} 11 | # Immutable::Map[{:foo => 1, :bar => 2}] # => Map {:foo => 1, :bar => 2} 12 | # ``` 13 | # 14 | # `Immutable::Map` works similarly to a regular hash, except it never mutates in 15 | # place, but rather return modified copies. However, this copies are using 16 | # structural sharing, ensuring performant operations and memory efficiency. 17 | # Under the hood, `Immutable::Map` uses a bit-partitioned hash trie, ensuring 18 | # lookups and updates with a complexity of O(Log32), which for practical 19 | # purposes is equivalent to O(1): for maps of viable sizes (even up to billions 20 | # of elements) the complexity is bound by a small constant. 21 | # 22 | require "./map/trie" 23 | 24 | module Immutable 25 | class Map(K, V) 26 | @trie : Trie(K, V) 27 | @block : (K -> V)? 28 | 29 | include Enumerable(Tuple(K, V)) 30 | 31 | # Creates a map with the given key-values 32 | # 33 | # ``` 34 | # m = Immutable::Map.new({:a => 1, :b => true}) # Map {:a => 1, :b => true} 35 | # ``` 36 | def initialize(hash : Hash(K, V) = {} of K => V) 37 | @trie = hash.reduce(Trie(K, V).empty(object_id)) do |trie, (k, v)| 38 | trie.set!(k, v, object_id) 39 | end 40 | @trie.clear_owner! 41 | @block = nil 42 | end 43 | 44 | # Creates a map with the given key-values. When getting a key-value that is 45 | # not in the map, the given block is executed passing the key, and the 46 | # return value is returned. 47 | # 48 | # ``` 49 | # m = Immutable::Map.new({:a => 123, :b => 321}) # Map {:a => 123, :b => 321} 50 | # ``` 51 | def initialize(hash : Hash(K, V) = {} of K => V, &block : K -> V) 52 | @trie = hash.reduce(Trie(K, V).empty(object_id)) do |trie, (k, v)| 53 | trie.set!(k, v, object_id) 54 | end 55 | @trie.clear_owner! 56 | @block = block 57 | end 58 | 59 | # :nodoc: 60 | def initialize(@trie : Trie(K, V), @block : (K -> V)? = nil) 61 | end 62 | 63 | # Creates a map with the given key-values. 64 | # 65 | # ``` 66 | # m = Immutable::Map.new([{:a, 123}, {:b, 321}]) # Map {:a => 123, :b => 321} 67 | # ``` 68 | def self.new(e : Enumerable({_, _})) 69 | Transient.new(e).persist! 70 | end 71 | 72 | # Creates a map with the given key-values 73 | # 74 | # ``` 75 | # m = Immutable::Map[{:a => 123, :b => 321}] # Map {:a => 123, :b => 321} 76 | # ``` 77 | def self.[](hash : Hash(K, V) = {} of K => V) 78 | new(hash) 79 | end 80 | 81 | # Executes the given block passing a transient version of the map, then 82 | # converts the transient map back to an immutable one and returns it. 83 | # 84 | # This is useful to perform several updates on a map in an efficient way: as 85 | # the transient map supports the same API of map, but performs updates in 86 | # place, avoiding unnecessary object allocations. 87 | # 88 | # ``` 89 | # map = Immutable::Map(Int32, Int32).new 90 | # m2 = map.transient do |m| 91 | # 100.times { |i| m = m.set(i, i * 2) } 92 | # end 93 | # m2.size # => 100 94 | # ``` 95 | # 96 | # Note that, as the transient is mutable, it is not thread-safe. 97 | def transient 98 | t = Transient.new(@trie, @block) 99 | yield t 100 | t.persist! 101 | end 102 | 103 | # Returns the number of key-value pairs in the map 104 | def size 105 | @trie.size 106 | end 107 | 108 | # Returns the value associated with the given key, if existing, else raises 109 | # `KeyError` 110 | def fetch(key : K) 111 | fetch(key) do 112 | if b = @block 113 | next b.call(key) 114 | end 115 | raise KeyError.new("Missing map key: #{key.inspect}") 116 | end 117 | end 118 | 119 | # Returns the value associated with the given key, if existing, else 120 | # executes the given block and returns its value 121 | def fetch(key : K, &block : K -> _) 122 | @trie.fetch(key, &block) 123 | end 124 | 125 | # Returns the value associated with the given key, if existing, else 126 | # it returns the provided default value 127 | def fetch(key : K, default) 128 | fetch(key) { default } 129 | end 130 | 131 | # Returns the value associated with the given key, if existing, else raises 132 | # `KeyError`. See also `fetch` 133 | def [](key : K) 134 | fetch(key) 135 | end 136 | 137 | # Returns the value associated with the given key, if existing, else nil 138 | def []?(key : K) 139 | fetch(key, nil) 140 | end 141 | 142 | # Returns a modified copy of the map where key is associated to value 143 | # 144 | # ``` 145 | # m = Immutable::Map[{:foo => 123}] 146 | # m2 = m.set(:bar, 321) # => Map {:foo => 123, :bar => 321} 147 | # m # => Map {:foo => 123} 148 | # ``` 149 | def set(key : K, value : V) 150 | Map.new(@trie.set(key, value), @block) 151 | end 152 | 153 | # Returns a modified copy of the map with the key-value pair removed. If the 154 | # key is not existing, it raises `KeyError` 155 | # 156 | # ``` 157 | # m = Immutable::Map[{:foo => 123, :bar => 321}] 158 | # m2 = m.delete(:bar) # => Map {:foo => 123} 159 | # m # => Map {:foo => 123, bar: 321} 160 | # ``` 161 | def delete(key : K) 162 | Map.new(@trie.delete(key), @block) 163 | end 164 | 165 | # Returns a new map with the keys and values of this map and the given hash 166 | # combined. 167 | # A value in the given hash takes precedence over the one in this map. 168 | # 169 | # ``` 170 | # map = Immutable::Map[{"foo" => "bar"}] 171 | # merged = map.merge({"baz" => "qux"}) 172 | # merged # => Map {"foo" => "bar", "baz" => "qux"} 173 | # map # => Map {"foo" => "bar"} 174 | # ``` 175 | def merge(hash : Hash(K, V)) 176 | trie = hash.reduce(@trie) do |t, (key, value)| 177 | t.set(key, value) 178 | end 179 | Map.new(trie, @block) 180 | end 181 | 182 | # Returns a new map with the keys and values of this map and the given map 183 | # combined. 184 | # A value in the given map takes precedence over the one in this map. 185 | # 186 | # ``` 187 | # map = Immutable::Map[{"foo" => "bar"}] 188 | # merged = map.merge(Immutable::Map[{"baz" => "qux"}]) 189 | # merged # => Map {"foo" => "bar", "baz" => "qux"} 190 | # map # => Map {"foo" => "bar"} 191 | # ``` 192 | def merge(map : Map(K, V)) 193 | trie = map.reduce(@trie) do |t, (key, value)| 194 | t.set(key, value) 195 | end 196 | Map.new(trie, @block) 197 | end 198 | 199 | def merge(hash : Hash(L, W)) forall L, W 200 | Transient(K | L, V | W).new.merge(self).merge(hash).persist! 201 | end 202 | 203 | def merge(map : Map(L, W)) forall L, W 204 | Transient(K | L, V | W).new.merge(self).merge(map).persist! 205 | end 206 | 207 | # Calls the given block for each key-value and passes in a tuple of key and 208 | # value. The order of iteration is not specified. 209 | # 210 | # ``` 211 | # m = Immutable::Map[{"foo" => "bar"}] 212 | # m.each do |keyval| 213 | # keyval[0] # => "foo" 214 | # keyval[1] # => "bar" 215 | # end 216 | # ``` 217 | def each 218 | @trie.each { |keyval| yield keyval } 219 | self 220 | end 221 | 222 | # Returns an iterator over the map entries, returning a `Tuple` of the key 223 | # and value. The order of iteration is not specified. 224 | # 225 | # ``` 226 | # map = Immutable::Map[{"foo" => "bar", "baz" => "qux"}] 227 | # iterator = map.each 228 | # 229 | # entry = iterator.next 230 | # entry[0] # => "foo" 231 | # entry[1] # => "bar" 232 | # 233 | # entry = iterator.next 234 | # entry[0] # => "baz" 235 | # entry[1] # => "qux" 236 | # ``` 237 | def each 238 | @trie.each 239 | end 240 | 241 | # Calls the given block for each key-value pair and passes in the key. 242 | # 243 | # ``` 244 | # m = Immutable::Map[{"foo" => "bar"}] 245 | # m.each_key do |key| 246 | # key # => "foo" 247 | # end 248 | # ``` 249 | def each_key(&block : K ->) 250 | each do |keyval| 251 | block.call(keyval.first) 252 | end 253 | end 254 | 255 | # Returns an iterator over the map keys. The order is not guaranteed. 256 | # 257 | # ``` 258 | # map = Immutable::Map[{"foo" => "bar", "baz" => "qux"}] 259 | # iterator = map.each_key 260 | # 261 | # key = iterator.next 262 | # key # => "foo" 263 | # 264 | # key = iterator.next 265 | # key # => "baz" 266 | # ``` 267 | def each_key 268 | each.map(&.first) 269 | end 270 | 271 | # Calls the given block for each key-value pair and passes in the value. 272 | # 273 | # ``` 274 | # m = Immutable::Map[{"foo" => "bar"}] 275 | # m.each_value do |val| 276 | # val # => "bar" 277 | # end 278 | # ``` 279 | def each_value(&block : V ->) 280 | each do |keyval| 281 | block.call(keyval.last) 282 | end 283 | end 284 | 285 | # Returns an iterator over the map values. The order is not specified. 286 | # 287 | # ``` 288 | # map = Immutable::Map[{"foo" => "bar", "baz" => "qux"}] 289 | # iterator = map.each_value 290 | # 291 | # val = iterator.next 292 | # val # => "bar" 293 | # 294 | # val = iterator.next 295 | # val # => "qux" 296 | # ``` 297 | def each_value 298 | each.map(&.last) 299 | end 300 | 301 | # Returns only the keys as an `Array`. The order is not specified. 302 | # 303 | # ``` 304 | # m = Immutable::Map[{"foo" => "bar", "baz" => "qux"}] 305 | # m.keys # => ["foo", "bar"] 306 | # ``` 307 | def keys 308 | each_key.to_a 309 | end 310 | 311 | # Returns only the values as an `Array`. The order is not specified. 312 | # 313 | # ``` 314 | # m = Immutable::Map[{"foo" => "bar", "baz" => "qux"}] 315 | # m.values # => ["bar", "qux"] 316 | # ``` 317 | def values 318 | each_value.to_a 319 | end 320 | 321 | # Appends a `String` representation of this object to the given IO object. 322 | def inspect(io : IO) 323 | to_s(io) 324 | end 325 | 326 | # Appends a String representation of this map 327 | # to the given IO object. 328 | def to_s(io : IO) 329 | io << "Map " 330 | to_h.to_s(io) 331 | end 332 | 333 | # Returns the JSON serialization of this map 334 | def to_json(json : JSON::Builder) 335 | json.object do 336 | each do |key, value| 337 | json.field key do 338 | value.to_json(json) 339 | end 340 | end 341 | end 342 | end 343 | 344 | # See `Object#hash`. 345 | # 346 | # ``` 347 | # map = Immutable::Map[{"foo" => "bar"}] 348 | # map.hash # => 63502 349 | # ``` 350 | def hash 351 | reduce(size &* 43) do |memo, (key, value)| 352 | 43 &* memo &+ (key.hash ^ value.hash) 353 | end 354 | end 355 | 356 | def ==(other : Map) 357 | return true if @trie.same?(other.trie) 358 | return false unless size == other.size 359 | all? do |kv| 360 | entry = other.trie.find_entry(kv[0]) 361 | entry && (entry.value == kv[1]) 362 | end 363 | end 364 | 365 | # :nodoc: 366 | def ==(other) 367 | false 368 | end 369 | 370 | protected def trie : Trie(K, V) 371 | @trie 372 | end 373 | 374 | class Transient(K, V) < Map(K, V) 375 | @valid : Bool 376 | 377 | def initialize(hash : Hash(K, V) = {} of K => V) 378 | @valid = true 379 | @trie = hash.reduce(Trie(K, V).empty(object_id)) do |t, (k, v)| 380 | t.set!(k, v, object_id) 381 | end 382 | @block = nil 383 | end 384 | 385 | def initialize(hash : Hash(K, V) = {} of K => V, &block : K -> V) 386 | @valid = true 387 | @trie = hash.reduce(Trie(K, V).empty(object_id)) do |t, (k, v)| 388 | t.set!(k, v, object_id) 389 | end 390 | @block = block 391 | end 392 | 393 | # :nodoc: 394 | def initialize(@trie : Trie(K, V), @block : (K -> V)? = nil) 395 | @valid = true 396 | end 397 | 398 | def self.new(e : Enumerable({L, W})) forall L, W 399 | e.reduce(Transient(L, W).new) do |m, (k, v)| 400 | m.set(k, v) 401 | end 402 | end 403 | 404 | def persist! 405 | check_validity! 406 | @valid = false 407 | @trie.clear_owner! 408 | Map.new(@trie, @block) 409 | end 410 | 411 | def set(key : K, value : V) 412 | check_validity! 413 | @trie = @trie.set!(key, value, object_id) 414 | self 415 | end 416 | 417 | def delete(key : K) 418 | check_validity! 419 | @trie = @trie.delete!(key, object_id) 420 | self 421 | end 422 | 423 | def merge(hash : Hash(K, V)) 424 | check_validity! 425 | @trie = hash.reduce(@trie) do |trie, (key, value)| 426 | trie.set!(key, value, object_id) 427 | end 428 | self 429 | end 430 | 431 | def merge(map : Map(K, V)) 432 | check_validity! 433 | @trie = map.reduce(@trie) do |trie, (key, value)| 434 | trie.set!(key, value, object_id) 435 | end 436 | self 437 | end 438 | 439 | def merge(hash : Hash(L, W)) forall L, W 440 | check_validity! 441 | Transient(K | L, V | W).new.merge(self).merge(hash) 442 | end 443 | 444 | def merge(map : Map(L, W)) forall L, W 445 | check_validity! 446 | Transient(K | L, V | W).new.merge(self).merge(map) 447 | end 448 | 449 | private def check_validity! 450 | unless @valid 451 | raise Invalid.new("Attempt to use transient map after persisting it") 452 | end 453 | end 454 | 455 | class Invalid < Exception; end 456 | end 457 | end 458 | end 459 | -------------------------------------------------------------------------------- /src/immutable/map/trie.cr: -------------------------------------------------------------------------------- 1 | module Immutable 2 | class Map(K, V) 3 | class Trie(K, V) 4 | BITS_PER_LEVEL = 5_u64 5 | BLOCK_SIZE = 2_u64 ** BITS_PER_LEVEL 6 | INDEX_MASK = BLOCK_SIZE - 1_u64 7 | BITMAP_MASK = (2_u64 ** BLOCK_SIZE - 1_u64) 8 | 9 | include Enumerable(Tuple(K, V)) 10 | 11 | getter :size, :levels 12 | 13 | @children : Array(Trie(K, V)) 14 | @bitmap : UInt64 15 | @values : Values(K, V) 16 | @size : Int32 17 | @levels : Int32 18 | @owner : UInt64? 19 | 20 | def initialize( 21 | @children : Array(Trie(K, V)), 22 | @values : Values(K, V), 23 | @bitmap : UInt64, 24 | @levels : Int32, 25 | @owner : UInt64? = nil 26 | ) 27 | children_size = @children.reduce(0) { |size, child| size + child.size } 28 | @size = children_size + @values.size 29 | end 30 | 31 | def self.empty(owner : UInt64? = nil) 32 | children = [] of Trie(K, V) 33 | values = Values(K, V).new 34 | new(children, values, 0_u32, 0, owner) 35 | end 36 | 37 | def get(key : K) : V 38 | lookup(key.hash) { |hash| hash[key] } 39 | end 40 | 41 | def fetch(key : K, &block : K -> _) 42 | lookup(key.hash) { |hash| hash.fetch(key, &block) } 43 | end 44 | 45 | def has_key?(key : K) : Bool 46 | lookup(key.hash) { |hash| hash.has_key?(key) } 47 | end 48 | 49 | def find_entry(key : K) : Entry(K, V)? 50 | lookup(key.hash) { |hash| hash.find_entry(key) } 51 | end 52 | 53 | def set(key : K, value : V) : Trie(K, V) 54 | set_at_index(key.hash, key, value) 55 | end 56 | 57 | def set!(key : K, value : V, from : UInt64) : Trie(K, V) 58 | set_at_index!(key.hash, key, value, from) 59 | end 60 | 61 | def delete(key : K) : Trie(K, V) 62 | delete_at_index(key.hash, key) 63 | end 64 | 65 | def delete!(key : K, from : UInt64) : Trie(K, V) 66 | delete_at_index!(key.hash, key, from) 67 | end 68 | 69 | def each 70 | each.each { |entry| yield entry } 71 | self 72 | end 73 | 74 | def each 75 | children_iter = @children.each.map do |child| 76 | child.each.as(Iterator({K, V})) 77 | end 78 | @values.each.chain(EntryIterator(K, V).new(children_iter)) 79 | end 80 | 81 | def empty? 82 | size == 0 83 | end 84 | 85 | def clear_owner! 86 | @owner = nil 87 | self 88 | end 89 | 90 | protected def set_at_index(index : UInt64, key : K, value : V, from : UInt64? = nil) : Trie(K, V) 91 | if leaf_of?(index) 92 | set_leaf(index, key, value, from) 93 | else 94 | set_branch(index, key, value, from) 95 | end 96 | end 97 | 98 | protected def set_at_index!(index : UInt64, key : K, value : V, from : UInt64) : Trie(K, V) 99 | return set_at_index(index, key, value, from) unless from == @owner 100 | if leaf_of?(index) 101 | @values[key] = value 102 | else 103 | set_branch!(index, key, value, from) 104 | end 105 | @size = calculate_size 106 | self 107 | end 108 | 109 | protected def delete_at_index(index : UInt64, key : K) : Trie(K, V) 110 | if leaf_of?(index) 111 | delete_in_leaf(index, key) 112 | else 113 | delete_in_branch(index, key) 114 | end 115 | end 116 | 117 | protected def delete_at_index!(index : UInt64, key : K, from : UInt64) : Trie(K, V) 118 | return delete_at_index(index, key) unless from == @owner 119 | if leaf_of?(index) 120 | @values.delete(key) 121 | else 122 | delete_in_branch!(index, key, from) 123 | end 124 | @size = calculate_size 125 | self 126 | end 127 | 128 | protected def lookup(index : UInt64, &block : Values(K, V) -> _) 129 | if leaf_of?(index) 130 | yield @values 131 | else 132 | return yield(Values(K, V).new) unless i = child_index?(bit_index(index)) 133 | @children[i].lookup(index, &block) 134 | end 135 | end 136 | 137 | private def leaf_of?(index) 138 | (index.to_u32! >> (@levels * BITS_PER_LEVEL)) == 0 139 | end 140 | 141 | private def set_leaf(index : UInt64, key : K, value : V, from : UInt64?) : Trie(K, V) 142 | values = @values.dup.tap do |vs| 143 | vs[key] = value 144 | end 145 | Trie.new(@children, values, @bitmap, @levels, from) 146 | end 147 | 148 | private def set_branch(index : UInt64, key : K, value : V, from : UInt64?) : Trie(K, V) 149 | i = bit_index(index) 150 | if idx = child_index?(i) 151 | children = @children.dup.tap do |cs| 152 | cs[idx] = cs[idx].set_at_index(index, key, value) 153 | end 154 | Trie.new(children, @values, @bitmap, @levels, from) 155 | else 156 | child = Trie.new(([] of Trie(K, V)), Values(K, V).new, 0_u32, @levels + 1, from) 157 | .set_at_index(index, key, value) 158 | bitmap = @bitmap | bitpos(i) 159 | Trie.new(@children.dup.insert(child_index(i, bitmap), child), @values, bitmap, @levels, from) 160 | end 161 | end 162 | 163 | private def set_branch!(index : UInt64, key : K, value : V, from : UInt64) 164 | i = bit_index(index) 165 | if idx = child_index?(i) 166 | @children[idx] = @children[idx].set_at_index!(index, key, value, from) 167 | else 168 | child = Trie.new(([] of Trie(K, V)), Values(K, V).new, 0_u32, @levels + 1) 169 | .set_at_index!(index, key, value, from) 170 | @bitmap = @bitmap | bitpos(i) 171 | @children.insert(child_index(i, @bitmap), child) 172 | end 173 | end 174 | 175 | private def delete_in_leaf(index : UInt64, key : K) 176 | values = @values.dup.tap do |vs| 177 | vs.delete(key) 178 | end 179 | Trie.new(@children, values, @bitmap, @levels) 180 | end 181 | 182 | private def delete_in_branch(index : UInt64, key : K) 183 | i = bit_index(index) 184 | raise KeyError.new("key #{key.inspect} not found") unless idx = child_index?(i) 185 | child = @children[idx].delete_at_index(index, key) 186 | if child.empty? 187 | children = @children.dup.tap { |cs| cs.delete_at(idx) } 188 | bitmap = @bitmap & (bitpos(i) ^ BITMAP_MASK) 189 | else 190 | children = @children.dup.tap { |cs| cs[idx] = child } 191 | bitmap = @bitmap 192 | end 193 | Trie.new(children, @values, bitmap, @levels) 194 | end 195 | 196 | private def delete_in_branch!(index : UInt64, key : K, from : UInt64) 197 | i = bit_index(index) 198 | raise KeyError.new("key #{key.inspect} not found") unless idx = child_index?(i) 199 | child = @children[idx].delete_at_index!(index, key, from) 200 | if child.empty? 201 | @children.delete_at(idx) 202 | @bitmap = @bitmap & (bitpos(i) ^ BITMAP_MASK) 203 | else 204 | @children[idx] = child 205 | end 206 | end 207 | 208 | private def bitpos(i : UInt64) : UInt64 209 | 1_u64 << i 210 | end 211 | 212 | private def child_index?(bidx : UInt64) : UInt64? 213 | pos = bitpos(bidx) 214 | return nil unless (pos & @bitmap) == pos 215 | (@bitmap & (pos - 1)).popcount.to_u64 216 | end 217 | 218 | private def child_index(bidx : UInt64, bitmap : UInt64) : UInt64 219 | pos = bitpos(bidx) 220 | (bitmap & (pos - 1)).popcount.to_u64 221 | end 222 | 223 | private def bit_index(index : UInt64) : UInt64 224 | (index.to_u64 >> (@levels * BITS_PER_LEVEL)) & INDEX_MASK 225 | end 226 | 227 | private def calculate_size 228 | children_size = @children.reduce(0) { |size, child| size + child.size } 229 | children_size + @values.size 230 | end 231 | 232 | struct Entry(K, V) 233 | @key : K 234 | @value : V 235 | 236 | getter :key, :value 237 | 238 | def initialize(@key : K, @value : V) 239 | end 240 | end 241 | 242 | struct Values(K, V) 243 | @entries : Array(Entry(K, V)) 244 | 245 | def initialize 246 | @entries = [] of Entry(K, V) 247 | end 248 | 249 | def initialize(@entries : Array(Entry(K, V))) 250 | end 251 | 252 | def size 253 | @entries.size 254 | end 255 | 256 | def each 257 | @entries.map { |entry| {entry.key, entry.value} }.each 258 | end 259 | 260 | def [](key : K) : V 261 | fetch(key) { raise KeyError.new } 262 | end 263 | 264 | def []?(key : K) : V? 265 | fetch(key) { nil } 266 | end 267 | 268 | def fetch(key : K) 269 | @entries.each do |entry| 270 | return entry.value if entry.key == key 271 | end 272 | yield key 273 | end 274 | 275 | def fetch(key : K, default) 276 | fetch(key) { default } 277 | end 278 | 279 | def delete(key : K) : V 280 | @entries.each_with_index do |entry, i| 281 | if entry.key == key 282 | @entries.delete_at(i) 283 | return entry.value 284 | end 285 | end 286 | raise KeyError.new 287 | end 288 | 289 | def find_entry(key : K) : Entry(K, V)? 290 | @entries.each do |entry| 291 | return entry if entry.key == key 292 | end 293 | nil 294 | end 295 | 296 | def has_key?(key : K) : Bool 297 | return true if find_entry(key) 298 | false 299 | end 300 | 301 | def []=(key : K, value : V) 302 | @entries.each_with_index do |entry, i| 303 | if entry.key == key 304 | return @entries[i] = Entry(K, V).new(key, value) 305 | end 306 | end 307 | 308 | @entries.push(Entry(K, V).new(key, value)) 309 | value 310 | end 311 | 312 | def dup 313 | Values(K, V).new(@entries.dup) 314 | end 315 | end 316 | 317 | class EntryIterator(K, V) 318 | include Iterator(Tuple(K, V)) 319 | 320 | @iterator : Iterator(Iterator({K, V})) 321 | @generator : Iterator({K, V}) | Iterator(Iterator({K, V})) 322 | @top : Bool 323 | 324 | def initialize(@iterator) 325 | @generator = @iterator 326 | @top = true 327 | end 328 | 329 | def next : Tuple(K, V) | Stop 330 | value = @generator.next 331 | if value.is_a?(Stop) 332 | return stop if @top 333 | @generator = @iterator 334 | @top = true 335 | self.next 336 | else 337 | flatten(value) 338 | end 339 | end 340 | 341 | private def flatten(value) : Tuple(K, V) | Stop 342 | if value.is_a? Iterator 343 | @generator = value 344 | @top = false 345 | self.next 346 | else 347 | value 348 | end 349 | end 350 | end 351 | end 352 | end 353 | end 354 | -------------------------------------------------------------------------------- /src/immutable/vector.cr: -------------------------------------------------------------------------------- 1 | # A vector is an ordered, immutable, integer-indexed collection of objects of 2 | # type T. 3 | # 4 | # Similar to an array, vector indexing starts at 0 and negative indexes are 5 | # assumed to be relative to the end of the vector. 6 | # 7 | # A vector can be constructed from an array of its elements: 8 | # 9 | # ``` 10 | # Immutable::Vector(Int32).new # => Vector [] 11 | # Immutable::Vector.new([1, 42, 5, 46]) # => Vector [1, 42, 5, 46] 12 | # Immutable::Vector[1, 2, 3] # => Vector [1, 2, 3] 13 | # ``` 14 | # 15 | # When a vector is modified, the original remans unchanged and a modified copy 16 | # is returned. However, due to structural sharing, the copy is efficient. This 17 | # makes vector inherently thread-safe and at the same time fast: 18 | # 19 | # ``` 20 | # vector = Immutable::Vector[1, 2, 3] # => Vector [1, 2, 3] 21 | # vector2 = vector.push(4) # => Vector [1, 2, 3, 4] 22 | # vector # => Vector [1, 2, 3] 23 | # ``` 24 | # 25 | # Vector is implemented as a "persistent bit-partitioned vector trie" with a 26 | # ranching factor of 32. This means that updates and lookups are performed have 27 | # a complexity of O(Log32), which for practical purposes is effectively 28 | # equivalent to O(1): in a vector of 1 billion elements these operations take no 29 | # more than 6 steps. Due to tail optimization, appends are O(1) 31 times out of 30 | # 32, and O(Log32) 1/32 of times. 31 | require "./vector/trie" 32 | 33 | module Immutable 34 | class Vector(T) 35 | include Enumerable(T) 36 | include Iterable(T) 37 | include Comparable(Vector) 38 | 39 | @trie : Trie(T) 40 | @tail : Array(T) 41 | 42 | # Alias for `Vector.[]` 43 | def self.of(*elems : T) 44 | self[*elems] 45 | end 46 | 47 | # Creates a new vector from the given arguments 48 | # 49 | # ``` 50 | # vec = Immutable::Vector[1, 2, 3, 4] 51 | def self.[](*elems : T) 52 | if (elems.size <= Trie::BLOCK_SIZE) 53 | new(Trie(T).empty, elems.to_a) 54 | else 55 | new(elems.to_a) 56 | end 57 | end 58 | 59 | # Creates a new empty vector 60 | def initialize 61 | @trie = Trie(T).empty 62 | @tail = [] of T 63 | end 64 | 65 | # :nodoc: 66 | def initialize(@trie : Trie(T), @tail : Array(T)) 67 | end 68 | 69 | # Creates a vector filled with the elements from the given array, in the 70 | # same position. 71 | def initialize(elems : Array(T)) 72 | leaves = elems.size - (elems.size % Trie::BLOCK_SIZE) 73 | @trie = Trie(T).from(elems[0...leaves], object_id).clear_owner! 74 | @tail = elems[leaves..-1] 75 | end 76 | 77 | # Executes the given block passing a transient version of the vector, then 78 | # converts the transient vector back to an immutable one and returns it. 79 | # 80 | # This is useful to perform several updates on a vector in an efficient way: 81 | # as the transient vector supports the same API of vector, but performs 82 | # updates in place, avoiding unnecessary object allocations. 83 | # 84 | # ``` 85 | # vec = Immutable::Vector(Int32).new 86 | # v2 = vec.transient do |v| 87 | # 100.times { |i| v = v.push(i) } 88 | # end 89 | # v2.size # => 100 90 | # ``` 91 | # 92 | # Note that, as the transient is mutable, it is not thread-safe. 93 | def transient 94 | t = Transient.new(@trie, @tail.dup) 95 | yield t 96 | t.persist! 97 | end 98 | 99 | # Returns the number of elements in the vector 100 | def size 101 | @trie.size + @tail.size 102 | end 103 | 104 | # Calls the given block once for each element in this vector, passing that 105 | # element as a parameter. 106 | # 107 | # ``` 108 | # v = Immutable::Vector["a", "b", "c"] 109 | # v.each { |x| print x, " -- " } 110 | # ``` 111 | # 112 | # produces: 113 | # 114 | # ```text 115 | # a -- b -- c -- 116 | # ``` 117 | def each 118 | @trie.each { |elem| yield elem } 119 | @tail.each { |elem| yield elem } 120 | self 121 | end 122 | 123 | # Returns an `Iterator` for the elements of this vector. 124 | # 125 | # ``` 126 | # v = Immutable::Vector["a", "b", "c"] 127 | # iter = v.each 128 | # iter.next # => "a" 129 | # iter.next # => "b" 130 | # ``` 131 | def each 132 | @trie.each.chain(@tail.each) 133 | end 134 | 135 | # Calls the given block once for each index in this vector, passing that 136 | # index as a parameter. 137 | # 138 | # ``` 139 | # v = Immutable::Vector["a", "b", "c"] 140 | # v.each_index { |x| print x, " -- " } 141 | # ``` 142 | # 143 | # produces: 144 | # 145 | # ```text 146 | # 0 -- 1 -- 2 -- 147 | # ``` 148 | def each_index 149 | i = 0 150 | while i < size 151 | yield i 152 | i += 1 153 | end 154 | self 155 | end 156 | 157 | # Returns a new vector with the given value appended to the end, given that the type of 158 | # the value is T (which might be a type or a union of types). 159 | # 160 | # ``` 161 | # v = Immutable::Vector["a", "b"] 162 | # v.push("c") # => Vector ["a", "b", "c"] 163 | # v.push(1) # => Errors, because the vector only accepts String 164 | # 165 | # # The original vector remains unchanged: 166 | # v # => Vector ["a", "b"] 167 | # ``` 168 | def push(elem : T) 169 | new_tail = @tail + [elem] 170 | if new_tail.size == Trie::BLOCK_SIZE 171 | Vector.new(@trie.push_leaf(new_tail), [] of T) 172 | else 173 | Vector.new(@trie, new_tail) 174 | end 175 | end 176 | 177 | # Return a tuple of two things: the last element of the vector and a copy of 178 | # the vector with the last element removed. Raises `IndexError` if the 179 | # vector is empty. 180 | # 181 | # ``` 182 | # v = Immutable::Vector[1, 2, 3, 4] 183 | # last, v2 = v.pop 184 | # last # => 4 185 | # v2 # => Vector [1, 2, 3] 186 | # ``` 187 | def pop : Tuple(T, Vector(T)) 188 | vec = drop_last { raise IndexError.new("cannot pop empty vector") } 189 | {last, vec} 190 | end 191 | 192 | # Like `pop`, but returns a tuple of nil and empty vector if called on an 193 | # empty vector 194 | def pop? : Tuple(T?, Vector(T)) 195 | vec = drop_last { self } 196 | {last?, vec} 197 | end 198 | 199 | # Alias for `push` 200 | def <<(elem : T) 201 | push(elem) 202 | end 203 | 204 | # Returns the element at the given index. 205 | # 206 | # Negative indices can be used to start counting from the end of the vector. 207 | # Raises `IndexError` if trying to access an element outside the vector's range. 208 | # 209 | # ``` 210 | # vec = Immutable::Vector['a', 'b', 'c'] 211 | # vec[0] # => 'a' 212 | # vec[2] # => 'c' 213 | # vec[-1] # => 'c' 214 | # vec[-2] # => 'b' 215 | # 216 | # vec[3] # raises IndexError 217 | # vec[-4] # raises IndexError 218 | # ``` 219 | def [](i : Int) 220 | at(i) 221 | end 222 | 223 | # Returns the element at the given index. 224 | # 225 | # Negative indices can be used to start counting from the end of the vector. 226 | # Returns `nil` if trying to access an element outside the vector's range. 227 | # 228 | # ``` 229 | # vec = Immutable::Vector['a', 'b', 'c'] 230 | # vec[0]? # => 'a' 231 | # vec[2]? # => 'c' 232 | # vec[-1]? # => 'c' 233 | # vec[-2]? # => 'b' 234 | # 235 | # vec[3]? # nil 236 | # vec[-4]? # nil 237 | # ``` 238 | def []?(i : Int) 239 | at(i) { nil } 240 | end 241 | 242 | # Returns the element at the given index, if in bounds, 243 | # otherwise raises `IndexError` 244 | # 245 | # ``` 246 | # v = Immutable::Vector[:foo, :bar] 247 | # v.at(0) { :baz } # => :foo 248 | # v.at(2) { :baz } # => IndexError 249 | # ``` 250 | def at(i : Int) 251 | at(i) { raise IndexError.new } 252 | end 253 | 254 | # Returns the element at the given index, if in bounds, 255 | # otherwise executes the given block and returns its value. 256 | # 257 | # ``` 258 | # v = Immutable::Vector[:foo, :bar] 259 | # v.at(0) { :baz } # => :foo 260 | # v.at(2) { :baz } # => :baz 261 | # ``` 262 | def at(i : Int) 263 | i = size + i if i < 0 264 | return yield if i < 0 || i >= size 265 | return @tail[i - @trie.size] if in_tail?(i) 266 | @trie.get(i) 267 | end 268 | 269 | # Returns a modified copy of the vector with the element at the given index 270 | # set to the given value. 271 | # 272 | # Negative indices can be used to start counting from the end of the vector. 273 | # Raises `IndexError` if trying to set an element outside the vector's range. 274 | # 275 | # ``` 276 | # vec = Immutable::Vector[1, 2, 3] 277 | # vec.set(0, 5) # Vector [5, 2, 3] 278 | # vec # Vector [1, 2, 3] 279 | # 280 | # vec.set(3, 5) # => IndexError 281 | # ``` 282 | def set(i : Int, value : T) 283 | i = size + i if i < 0 284 | raise IndexError.new if i < 0 || i >= size 285 | if in_tail?(i) 286 | new_tail = @tail.dup.tap { |t| t[i - @trie.size] = value } 287 | return Vector.new(@trie, new_tail) 288 | end 289 | Vector.new(@trie.update(i, value), @tail) 290 | end 291 | 292 | # Returns the first element in the vector, if not empty, else raises 293 | # `IndexError` 294 | def first 295 | self[0] 296 | end 297 | 298 | # Returns the first element in the vector, if not empty, else nil 299 | def first? 300 | self[0]? 301 | end 302 | 303 | # Returns the last element in the vector, if not empty, else raises 304 | # `IndexError` 305 | def last 306 | self[-1] 307 | end 308 | 309 | # Returns the last element in the vector, if not empty, else nil 310 | def last? 311 | self[-1]? 312 | end 313 | 314 | # Determines if this vector equals *other* according to a comparison 315 | # done by the given block. 316 | # 317 | # If this vector's size is the same as *other*'s size, this method yields 318 | # elements from this vector and *other* in tandem: if the block returns true 319 | # for all of them, this method returns `true`. Otherwise it returns `false`. 320 | # 321 | # ``` 322 | # a = Immutable::Vector[1, 2, 3] 323 | # b = Immutable::Vector["a", "ab", "abc"] 324 | # a.equals?(b) { |x, y| x == y.size } # => true 325 | # a.equals?(b) { |x, y| x == y } # => false 326 | # ``` 327 | def equals?(other : Vector) 328 | return false if size != other.size 329 | each.zip(other.each).all? do |tuple| 330 | yield(tuple.first, tuple.last) 331 | end 332 | end 333 | 334 | # Equality. Returns true if it is passed a Vector and `equals?` 335 | # returns true for both vectors, the caller and the argument. 336 | # 337 | # ``` 338 | # vec = Immutable::Vector[1, 2, 3] 339 | # vec == Immutable::Vector[1, 2, 3] # => true 340 | # vec == Immutable::Vector[2, 3] # => false 341 | # ``` 342 | def ==(other : Vector) 343 | return true if @trie.same?(other.trie) && @tail == other.tail 344 | equals?(other) { |x, y| x == y } 345 | end 346 | 347 | # :nodoc: 348 | def ==(other) 349 | false 350 | end 351 | 352 | # Combined comparison operator. Returns 0 if the first vector equals the 353 | # second, 1 if the first is greater than the second and -1 if the first is 354 | # smaller than the second. 355 | # 356 | # It compares the elements of both vectors in the same position using the 357 | # `<=>` operator, as soon as one of such comparisons returns a non zero 358 | # value, that result is the return value of the whole comparison. 359 | # 360 | # If all elements are equal, the comparison is based on the size of the 361 | # vectors. 362 | # 363 | # ``` 364 | # Immutable::Vector[8] <=> Immutable::Vector[1, 2, 3] # => 1 365 | # Immutable::Vector[2] <=> Immutable::Vector[4, 2, 3] # => -1 366 | # Immutable::Vector[1, 2] <=> Immutable::Vector[1, 2] # => 0 367 | # ``` 368 | def <=>(other : Vector) 369 | each.zip(other.each).each do |tuple| 370 | n = tuple.first <=> tuple.last 371 | return n if n != 0 372 | end 373 | size <=> other.size 374 | end 375 | 376 | # Returns true if the vector is empty, else false 377 | def empty? 378 | size == 0 379 | end 380 | 381 | # Returns true if the vector contains at least one element, else false 382 | def any? 383 | size != 0 384 | end 385 | 386 | # Concatenation. Returns a new vector built by concatenating self with 387 | # other. The type of the new vector is the union of the types of self and 388 | # other. 389 | # 390 | # ``` 391 | # v1 = Immutable::Vector[1, 2] 392 | # v2 = Immutable::Vector[2, 3] 393 | # v3 = Immutable::Vector["a"] 394 | # v1 + v2 # => Vector [1, 2, 2, 3] 395 | # v1 + v3 # => Vector [1, 2, "a"] 396 | # ``` 397 | def +(other : Vector(U)) forall U 398 | trie = @trie.as(Trie(T | U)) 399 | tail = @tail.as(Array(T | U)) 400 | other.each_slice(Trie::BLOCK_SIZE) do |slice| 401 | leaf_tail = (tail + slice) 402 | trie = trie.push_leaf!(leaf_tail.first(Trie::BLOCK_SIZE), object_id) 403 | tail = leaf_tail.skip(Trie::BLOCK_SIZE) 404 | end 405 | Vector.new(trie.clear_owner!, tail) 406 | end 407 | 408 | # Difference. Returns a new vector that is a copy of the original, removing 409 | # any items that appear in `other`. The order of the original vector is 410 | # preserved. 411 | # 412 | # ``` 413 | # v1 = Immutable::Vector[1, 2, 3] 414 | # v2 = Immutable::Vector[2, 1] 415 | # v1 - v2 => Vector [3] 416 | # ``` 417 | def -(other : Vector(_)) 418 | set = other.to_lookup_set 419 | elems = reject do |elem| 420 | set.includes?(elem) 421 | end 422 | Vector(T).new(elems) 423 | end 424 | 425 | # Set intersection: returns a new array containing elements common to the two 426 | # vectors, excluding any duplicates. The order is preserved from the original 427 | # vector. 428 | # 429 | # ``` 430 | # v1 = Immutable::Vector[1, 1, 3, 5] 431 | # v2 = Immutable::Vector[1, 2, 3] 432 | # v1 & v2 # => Vector [1, 3] 433 | # ``` 434 | def &(other : Vector(_)) 435 | return Vector(T).new if empty? || other.empty? 436 | set = other.to_lookup_set 437 | intersection = self.select do |elem| 438 | in_set = set.includes?(elem) 439 | set.delete(elem) 440 | in_set 441 | end 442 | Vector(T).new(intersection) 443 | end 444 | 445 | # Set union: returns a new vector by joining self with other, excluding 446 | # any duplicates and preserving the order from the original vector. 447 | # 448 | # ``` 449 | # v1 = Immutable::Vector["a", "b", "c"] 450 | # v2 = Immutable::Vector["c", "d", "a"] 451 | # v1 | v2 # => Vector [1, 3] # => Vector ["a", "b", "c", "d"] 452 | # ``` 453 | def |(other : Vector(U)) forall U 454 | set = Set(T | U).new 455 | union = reduce([] of T | U) do |u, elem| 456 | u << elem unless set.includes?(elem) 457 | set.add(elem) 458 | u 459 | end 460 | union = other.reduce(union) do |u, elem| 461 | u << elem unless set.includes?(elem) 462 | set.add(elem) 463 | u 464 | end 465 | Vector(T | U).new(union) 466 | end 467 | 468 | # Returns a new vector by removing duplicate values in self. 469 | # 470 | # ``` 471 | # v = Immutable::Vector["a", "a", "b", "b", "c"] 472 | # v.uniq # => Vector ["a", "b", "c"] 473 | # v # => Vector ["a", "a", "b", "b", "c"] 474 | # ``` 475 | def uniq 476 | hash = {} of T => Bool 477 | elems = reject do |elem| 478 | next true if in_hash = hash.has_key?(elem) 479 | hash[elem] = true 480 | in_hash 481 | end 482 | Vector(T).new(elems) 483 | end 484 | 485 | # Appends a String representation of this vector 486 | # to the given IO object. 487 | def to_s(io : IO) 488 | io << "Vector " 489 | to_a.to_s(io) 490 | end 491 | 492 | # Appends a JSON string representation of this vector to the given 493 | # io object 494 | def to_json(json : JSON::Builder) 495 | json.array do 496 | each &.to_json(json) 497 | end 498 | end 499 | 500 | # Appends a `String` representation of this object to the given IO object. 501 | def inspect(io : IO) 502 | to_s(io) 503 | end 504 | 505 | # Returns a hash code based on this vector's size and elements. 506 | # 507 | # See `Object#hash`. 508 | def hash 509 | reduce(41 &* size) do |memo, elem| 510 | 41 &* memo &+ elem.hash 511 | end 512 | end 513 | 514 | private def in_tail?(index) 515 | index >= @trie.size && index < size 516 | end 517 | 518 | private def drop_last 519 | return yield if empty? 520 | return Vector.new(@trie.pop_leaf, @trie.last_leaf) if @tail.size == 1 && size > 1 521 | Vector.new(@trie, @tail[0...-1]) 522 | end 523 | 524 | protected def to_lookup_set 525 | reduce(Set(T).new) do |set, elem| 526 | set.add(elem) 527 | end 528 | end 529 | 530 | protected def trie 531 | @trie 532 | end 533 | 534 | protected def tail 535 | @tail 536 | end 537 | 538 | class Transient(T) < Vector(T) 539 | @valid : Bool 540 | 541 | def initialize(@trie : Trie(T), @tail : Array(T)) 542 | @valid = true 543 | end 544 | 545 | def initialize 546 | @trie = Trie(T).empty(object_id) 547 | @tail = Array(T).new(Trie::BLOCK_SIZE) 548 | @valid = true 549 | end 550 | 551 | def initialize(elems : Array(T)) 552 | leaves = elems.size - (elems.size % Trie::BLOCK_SIZE) 553 | @trie = Trie(T).from(elems[0...leaves], object_id) 554 | @tail = elems[leaves..-1] 555 | @valid = true 556 | end 557 | 558 | def persist! 559 | @trie.clear_owner! 560 | @valid = false 561 | Vector.new(@trie, @tail.dup) 562 | end 563 | 564 | def push(elem : T) 565 | check_validity! 566 | @tail << elem 567 | if @tail.size == Trie::BLOCK_SIZE 568 | @trie = @trie.push_leaf!(@tail, object_id) 569 | @tail = Array(T).new(Trie::BLOCK_SIZE) 570 | end 571 | self 572 | end 573 | 574 | def pop : Tuple(T, Transient(T)) 575 | check_validity! 576 | raise IndexError.new("cannot pop empty vector") if empty? 577 | {last, drop_last { self }} 578 | end 579 | 580 | def pop? : Tuple(T?, Transient(T)) 581 | check_validity! 582 | {last?, drop_last { self }} 583 | end 584 | 585 | def set(i : Int, value : T) 586 | check_validity! 587 | i = size + i if i < 0 588 | raise IndexError.new if i < 0 || i >= size 589 | if in_tail?(i) 590 | @tail[i - @trie.size] = value 591 | else 592 | @trie = @trie.update!(i, value, object_id) 593 | end 594 | self 595 | end 596 | 597 | private def drop_last 598 | return yield if empty? 599 | if @tail.size == 1 && size > 1 600 | @tail = @trie.last_leaf 601 | @trie = @trie.pop_leaf!(object_id) 602 | else 603 | @tail.pop 604 | end 605 | self 606 | end 607 | 608 | private def check_validity! 609 | unless @valid 610 | raise Invalid.new("Attempt to use transient vector after persisting it") 611 | end 612 | end 613 | 614 | class Invalid < Exception; end 615 | end 616 | end 617 | end 618 | -------------------------------------------------------------------------------- /src/immutable/vector/trie.cr: -------------------------------------------------------------------------------- 1 | module Immutable 2 | class Vector(T) 3 | class Trie(T) 4 | BITS_PER_LEVEL = 5_u32 5 | BLOCK_SIZE = (2 ** BITS_PER_LEVEL).to_u32 6 | INDEX_MASK = BLOCK_SIZE - 1 7 | 8 | include Enumerable(T) 9 | 10 | getter :size, :levels 11 | 12 | @children : Array(Trie(T)) 13 | @values : Array(T) 14 | @size : Int32 15 | @levels : Int32 16 | @owner : UInt64? 17 | 18 | def initialize(@children : Array(Trie(T)), @levels : Int32, @owner : UInt64? = nil) 19 | @size = @children.reduce(0) { |sum, child| sum + child.size } 20 | @values = [] of T 21 | end 22 | 23 | def initialize(@values : Array(T), @owner : UInt64? = nil) 24 | @size = @values.size 25 | @levels = 0 26 | @children = [] of Trie(T) 27 | end 28 | 29 | def at(index : Int) 30 | return yield if index < 0 || index >= size 31 | lookup(index) 32 | end 33 | 34 | def get(index : Int) 35 | at(index) { raise IndexError.new } 36 | end 37 | 38 | def update(index : Int, value : T) : Trie(T) 39 | raise IndexError.new if index < 0 || index >= size 40 | set(index, value) 41 | end 42 | 43 | def update!(index : Int, value : T, from : UInt64) : Trie(T) 44 | raise IndexError.new if index < 0 || index >= size 45 | set!(index, value, from) 46 | end 47 | 48 | def each 49 | i = 0 50 | while i < size 51 | leaf_values = leaf_for(i).values 52 | leaf_values.each do |value| 53 | yield value 54 | i += 1 55 | end 56 | end 57 | self 58 | end 59 | 60 | def each 61 | FlattenLeaves.new((0...size).step(BLOCK_SIZE).map do |i| 62 | leaf_for(i).values.each.as(Iterator(T)) 63 | end) 64 | end 65 | 66 | def push_leaf(leaf : Array(T), from : UInt64? = nil) : Trie(T) 67 | raise ArgumentError.new if leaf.size > BLOCK_SIZE || size % 32 != 0 68 | return Trie.new([self], @levels + 1, from).push_leaf(leaf, from) if full? 69 | return Trie.new(leaf, from) if empty? && leaf? 70 | Trie.new(@children.dup.tap do |cs| 71 | if @levels == 1 72 | cs.push(Trie.new(leaf)) 73 | else 74 | if cs.empty? || cs.last.full? 75 | cs << Trie.new([] of Trie(T), @levels - 1) 76 | end 77 | cs[-1] = cs[-1].push_leaf(leaf, from) 78 | end 79 | end, @levels, from) 80 | end 81 | 82 | def push_leaf!(leaf : Array(T), from : UInt64) : Trie(T) 83 | raise ArgumentError.new if leaf.size > BLOCK_SIZE || size % 32 != 0 84 | return push_leaf(leaf, from) unless from == @owner 85 | return Trie.new([self], @levels + 1, from).push_leaf!(leaf, from) if full? 86 | return Trie.new(leaf, @owner) if empty? && leaf? 87 | if @levels == 1 88 | @children.push(Trie.new(leaf, from)) 89 | else 90 | if @children.empty? || @children.last.full? 91 | @children << Trie.new([] of Trie(T), @levels - 1, from) 92 | end 93 | @children[-1] = @children[-1].push_leaf!(leaf, from) 94 | end 95 | @size = calculate_size 96 | self 97 | end 98 | 99 | def pop_leaf(from : UInt64? = nil) : Trie(T) 100 | raise ArgumentError.new if empty? || size % 32 != 0 101 | return Trie.new([] of T, from) if leaf? 102 | child = @children.last.pop_leaf 103 | if child.empty? 104 | return @children.first if @children.size == 2 105 | return Trie.new(@children[0...-1], @levels, from) 106 | end 107 | Trie.new(@children[0...-1].push(child), @levels, from) 108 | end 109 | 110 | def pop_leaf!(from : UInt64) : Trie(T) 111 | raise ArgumentError.new if empty? || size % 32 != 0 112 | return pop_leaf(from) unless from == @owner 113 | return Trie.new([] of T, from) if leaf? 114 | @children[-1] = @children[-1].pop_leaf!(from) 115 | if @children[-1].empty? 116 | return @children.first if @children.size == 2 117 | @children.pop 118 | end 119 | @size = calculate_size 120 | self 121 | end 122 | 123 | def last 124 | get(size - 1) 125 | end 126 | 127 | def last_leaf 128 | leaf_for(@size - 1).values 129 | end 130 | 131 | def empty? 132 | @size == 0 133 | end 134 | 135 | def leaf? 136 | @levels == 0 137 | end 138 | 139 | def inspect 140 | return @values.inspect if leaf? 141 | "[#{@children.map { |c| c.inspect }.join(", ")}]" 142 | end 143 | 144 | def clear_owner! 145 | @owner = nil 146 | self 147 | end 148 | 149 | def self.empty(owner : UInt64? = nil) 150 | Trie.new([] of T, owner) 151 | end 152 | 153 | def self.from(elems : Array(T)) 154 | trie = Trie(T).empty 155 | elems.each_slice(BLOCK_SIZE) do |leaf| 156 | trie = trie.push_leaf(leaf) 157 | end 158 | trie 159 | end 160 | 161 | def self.from(elems : Array(T), owner : UInt64) 162 | trie = Trie(T).empty(owner) 163 | elems.each_slice(BLOCK_SIZE) do |leaf| 164 | trie = trie.push_leaf!(leaf, owner) 165 | end 166 | trie 167 | end 168 | 169 | protected def set(index : Int, value : T, from : UInt64? = nil) : Trie(T) 170 | child_idx = child_index(index) 171 | if leaf? 172 | values = @values.dup.tap { |vs| vs[child_idx] = value } 173 | Trie.new(values, from) 174 | else 175 | children = @children.dup.tap do |cs| 176 | cs[child_idx] = cs[child_idx].set(index, value) 177 | end 178 | Trie.new(children, @levels, from) 179 | end 180 | end 181 | 182 | protected def set!(index : Int, value : T, from : UInt64) : Trie(T) 183 | return set(index, value, from) unless from == @owner 184 | child_idx = child_index(index) 185 | if leaf? 186 | @values[child_idx] = value 187 | else 188 | @children[child_idx] = @children[child_idx].set!(index, value, from) 189 | end 190 | @size = calculate_size 191 | self 192 | end 193 | 194 | private def calculate_size 195 | if leaf? 196 | @values.size 197 | else 198 | @children.reduce(0) { |sum, child| sum + child.size } 199 | end 200 | end 201 | 202 | protected def lookup(index : Int) 203 | child_idx = child_index(index) 204 | return @values[child_idx] if leaf? 205 | @children[child_idx].lookup(index) 206 | end 207 | 208 | protected def leaf_for(index : Int) 209 | return self if leaf? 210 | @children[child_index(index)].leaf_for(index) 211 | end 212 | 213 | protected def values 214 | @values 215 | end 216 | 217 | private def child_index(index) 218 | (index >> (@levels * BITS_PER_LEVEL)) & INDEX_MASK 219 | end 220 | 221 | protected def full? 222 | return @values.size == BLOCK_SIZE if leaf? 223 | @size >> ((@levels + 1) * BITS_PER_LEVEL) > 0 224 | end 225 | 226 | # :nodoc: 227 | class FlattenLeaves(T) 228 | include Iterator(T) 229 | 230 | @chunk : Iterator(T) | Iterator::Stop 231 | 232 | def initialize(@generator : Iterator(Iterator(T))) 233 | @chunk = @generator.next 234 | end 235 | 236 | def next : T | Iterator::Stop 237 | chunk = @chunk 238 | if chunk.is_a?(Iterator::Stop) 239 | Iterator::Stop.new 240 | else 241 | elem = chunk.next 242 | if elem.is_a?(Iterator::Stop) 243 | @chunk = @generator.next 244 | self.next 245 | else 246 | elem 247 | end 248 | end 249 | end 250 | end 251 | end 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /src/immutable/version.cr: -------------------------------------------------------------------------------- 1 | module Immutable 2 | VERSION = "0.1.24" 3 | end 4 | --------------------------------------------------------------------------------