struct Pf::Map(K, V)

Overview

A thread-safe, persistent, unordered hash map.

Value equality

Being a persistent map, Pf::Map requires path copying. However, path copying is unnecessary for equal values. If your values can be compared cheaply, you can make them include Pf::Eq. Then an additional comparison using #== will take place to figure out whether path copying should occur.

Methods that are marked with supports value equality guarantee to return exactly self if what they've done resulted in no change in self.

Note that out of the box, #== is called only when your value is of the types nil, Bool, Char, String, Symbol, or of a primitive number type. And #same? is called on all reference (Reference) types.

map = Pf::Map[foo: 100, bar: 200]
map.assoc("foo", 100).same?(map)             # => true, no change
map.update("foo", 0, &.succ.pred).same?(map) # => true, no change

map2 = Pf::Map[foo: 100, bar: 200]
map.merge(map2).same?(map)  # => true, no change
map2.merge(map).same?(map2) # => true, no change

If you want to enable #== for your value, you should make them include Pf::Eq.

record Info, first_name : String, last_name : String

people = Pf::Map
  .assoc(0, Info.new("John", "Doe"))
  .assoc(1, Info.new("Barbara", "Doe"))

people.assoc(0, Info.new("John", "Doe")).same?(people) # => false
# ^ Even though the value is the same the map is path-copied.

# v But if we include `Pf::Eq`...
record InfoEq, first_name : String, last_name : String do
  include Pf::Eq
end

people = Pf::Map
  .assoc(0, InfoEq.new("John", "Doe"))
  .assoc(1, InfoEq.new("Barbara", "Doe"))

# ... no path copying is going to be done at the expense of comparing
# the values too.
people.assoc(0, InfoEq.new("John", "Doe")).same?(people) # => true

Since BidiMap is backed by Map, the same applies to it. On the other hand, elements of a Set are keys so they are always compared using #== eventually.

Included Modules

Defined in:

permafrost/map.cr

Constructors

Class Method Summary

Instance Method Summary

Instance methods inherited from module Enumerable({K, V})

to_pf_bidi to_pf_bidi, to_pf_map(& : {K, V} -> Tuple(K, V)) : Pf::Map(K, V) forall K, V
to_pf_map
to_pf_map
, to_pf_set : Pf::Set({K, V}) to_pf_set

Constructor Detail

def self.new(enumerable : Enumerable(Tuple(K, V))) #

Constructs a Map from an enumerable of key-value pairs.


[View source]
def self.new : Map(K, V) #

Constructs an empty Map.


[View source]

Class Method Detail

def self.[](**entries) #

A shorthand syntax for creating a Map with string keys. The type of the map's values is the union of the types of values in entries.

map = Pf::Map[name: "John Doe", age: 25]
map["name"] # => "John Doe"
map["age"]  # => 25

typeof(map) # => Pf::Map(String, String | Int32)

[View source]
def self.assoc(key : K, value : V) : Map(K, V) #

A shorthand for new.assoc.


[View source]
def self.eqv?(v1 : V, v2 : V) : Bool forall V #

Returns true if two mapping values v1 and v2 are equal, taking Pf::Eq into account.


[View source]
def self.transaction(& : Commit(K, V) -> ) : Map(K, V) #

Shorthand for new.transaction.


[View source]

Instance Method Detail

def ==(other : Map) : Bool #

Compares self with other. Returns true if all associations are the same (values are compared using #==).


[View source]
def [](key : K) : V #

Returns the value associated with key. Raises KeyError if key is absent.

map = Pf::Map[foo: 10]
map["foo"] # => 10
map["bar"] # raises KeyError

[View source]
def [](key : K, *subkeys) #

Traverses nested maps/Hashes and returns the value. Raises KeyError if there is no value.

map = Pf::Map[foo: Pf::Map[bar: {100 => Pf::Map[baz: "Yay!"]}]]
map["foo", "bar", 100, "baz"] # => "Yay!"
map["foo", "bar", 200]        # raises KeyError

[View source]
def []?(key : K) : V | Nil #

Returns the value associated with key, or nil if key is absent.

map = Pf::Map[foo: 10, bar: 20]
map["foo"]? # => 10
map["bar"]? # => 20
map["baz"]? # => nil

[View source]
def []?(key : K, *subkeys) #

Traverses nested maps/Hashes and returns the value, or nil if the value is absent.

map = Pf::Map[foo: Pf::Map[bar: {100 => Pf::Map[baz: "Yay!"]}]]
map["foo", "bar", 100, "baz"]? # => "Yay!"
map["foo", "bar", 200]?        # => nil

[View source]
def assoc(key : K, value : V) : Map(K, V) #

Returns a copy of self that contains the association between key and value.

Supports value equality.

map = Pf::Map(String, Int32).new

branch1 = map.assoc("foo", 100)
branch2 = map.assoc("foo", 200)

map = map.assoc("bar", 300)

map["foo"]? # => nil
map["bar"]? # => 300

branch1["foo"]? # => 100
branch1["bar"]? # => nil

branch2["foo"]? # => 200
branch2["bar"]? # => nil

[View source]
def clone : Map(K, V) #

Returns a new Map whose values are deeply cloned versions of those from self. That is, returns a deep copy of self.

Keys are not cloned (if you need to clone keys then the last thing to help you is a persistent immutable map!).

map = Pf::Map[foo: [1, 2, 3], bar: [4, 5, 6]]
map2 = map.clone

map["foo"][0] = 100

map  # => Pf::Map{"foo" => [100, 2, 3], "bar" => [4, 5, 6]}
map2 # => Pf::Map{"foo" => [1, 2, 3], "bar" => [4, 5, 6]}

[View source]
def compact #

Returns a copy of self without nil values.

map = Pf::Map[foo: nil, bar: 123]
map.compact # => Pf::Map{"bar" => 123}

typeof(map)         # => Pf::Map(String, Int32?)
typeof(map.compact) # => Pf::Map(String, Int32)

[View source]
def concat(other : Enumerable(Tuple(K, V))) : Map(K, V) #

Returns a new map with associations from self and other combined, where other is an enumerable of key-value pairs.

Supports value equality.

map = Pf::Map[foo: 100, bar: 200, baz: 300]
map.concat([{"x", 123}, {"y", 456}]) # => Pf::Map[foo: 100, bar: 200, baz: 300, x: 123, y: 456]

[View source]
def dissoc(key : K) : Map(K, V) #

Returns a copy of self that is guaranteed not to contain an association with the given key.

map = Pf::Map[foo: 100, bar: 200]

branch1 = map.dissoc("foo")
branch2 = map.dissoc("bar")

map["foo"]? # => 100
map["bar"]? # => 200

branch1["foo"]? # => nil
branch1["bar"]? # => 200

branch2["foo"]? # => 100
branch2["bar"]? # => nil

[View source]
def each(& : Tuple(K, V) -> ) : Nil #

Yields each association to the block.


[View source]
def each_key(& : K -> ) : Nil #

Yields each key to the block.


[View source]
def each_value(& : V -> ) : Nil #

Yields each value to the block.


[View source]
def empty? : Bool #

Returns true if this map contains no associations.


[View source]
def fetch(key : K, & : -> {K, V}) : V | {K, V} forall T #

Returns the value mapped to key, or yields if key is absent. This method mainly exists to circumvent nil as in value vs. nil as in absence issue.


[View source]
def fetch?(key : K) : Tuple(V) | Nil #

Returns the value associated with key, or nil if the value is absent. The value is wrapped in a tuple to differentiate between nil as value and nil as absence.

map = Pf::Map[name: "John Doe", job: nil]
map.fetch?("job")  # => {nil}
map.fetch?("name") # => {"John Doe"}
map.fetch?("age")  # => nil

if name_t = map.fetch?("name")
  name, *_ = name_t
  name # => "John Doe"
end

[View source]
def fmap(& : Tuple(K, V) -> Tuple(K2, V2)) : Map(K2, V2) forall K2, V2 #

Same as map, but returns Map instead of an array.

If the block returns more than one value for the same key, the last yielded value is preferred.

Supports value equality if K == K2 and V == V2.

map = Pf::Map[foo: "John Doe", bar: "Samantha Doe"]
map.fmap { |k, v| {k.upcase, v.upcase} } # => Pf::Map{"FOO" => "JOHN DOE", "BAR" => "SAMANTHA DOE"}

[View source]
def has_key?(key) : Bool #

Alias of #includes?.


[View source]
def hash(hasher) #

See Object#hash(hasher)


[View source]
def includes?(key : K) : Bool #

Returns true if key is present in this map.

map = Pf::Map[foo: 100, bar: 200]
"foo".in?(map) # => true
"bar".in?(map) # => true
"baz".in?(map) # => false

[View source]
def inspect(io) #

[View source]
def keys : Array(K) #

Returns an array with all keys from this map. There is no guaranteed order of keys.

map = Pf::Map[foo: 10, bar: 20]
map.keys # => ["foo", "bar"]

[View source]
def map_key(& : K -> K2) : Map(K2, V) forall K2 #

Transforms keys: same as #fmap, but only yields keys from this map.

Supports value equality if K == K2.

map = Pf::Map[foo: "John Doe", bar: "Samantha Doe"]
map.map_key(&.upcase) # => Pf::Map{"FOO" => "John Doe", "BAR" => "Samantha Doe"}

[View source]
def map_value(& : V -> V2) : Map(K, V2) forall V2 #

Transforms values: same as #fmap, but only yields values from this map.

Supports value equality if V == V2.

map = Pf::Map[foo: "John Doe", bar: "Samantha Doe"]
map.map_value(&.upcase) # => Pf::Map{"foo" => "JOHN DOE", "bar" => "SAMANTHA DOE"}

[View source]
def merge(other : Map(K2, V2)) : Map(K | K2, V | V2) forall K2, V2 #

Returns a new map with associations from self and other combined.

If some key is common both to self and other, other's value is preferred.

Supports value equality if K == K2 and V == V2.

a = Pf::Map[foo: 100, bar: 200]
b = Pf::Map[foo: "hello", baz: true, boo: 500]

map = a.merge(b)
map # => Pf::Map{"foo" => "hello", "bar" => 200, "baz" => true, "boo" => 500}

typeof(map) # => Pf::Map(String, String | Int32 | Bool)

[View source]
def merge(other : Map(K2, V2), & : K, V, V2 -> V | V2) : Map(K | K2, V | V2) forall K2, V2 #

Returns a new map with assocations from self and other combined.

If some key is common both to self and other, that key is yielded to the block together with the two values. The return value of the block is used as the final value.

a = Pf::Map[foo: 100, bar: 200, baz: 300]
b = Pf::Map[foo: 200, bar: 300.8, boo: 1000.5]

map = a.merge(b) { |k, v1, v2| v1 + v2 }
map # => Pf::Map{"foo" => 300, "bar" => 500.8, "baz" => 300, "boo" => 1000.5}

typeof(map) # => Pf::Map(String, Int32 | Float64)

[View source]
def pretty_print(pp) : Nil #

[View source]
def reject(& : Tuple(K, V) -> Bool) : Map(K, V) #

Returns a copy of self which includes only associations for which the block is falsey.

Supports value equality.

map = Pf::Map[foo: 2, bar: 3, baz: 4, boo: 5]
map.reject { |_, v| v.even? } # => Pf::Map{"bar" => 3, "boo" => 5}

[View source]
def reject(keys : Enumerable) #

Returns a new map which is guaranteed not to include the given keys.

map = Pf::Map[foo: 2, bar: 3, baz: 4, boo: 5]
map.reject({"foo", "boo"}) # => Pf::Map{"bar" => 3, "baz" => 4}
map.reject("foo", "boo")   # => Pf::Map{"bar" => 3, "baz" => 4}

[View source]
def reject(*keys) #

Returns a new map which is guaranteed not to include the given keys.

map = Pf::Map[foo: 2, bar: 3, baz: 4, boo: 5]
map.reject({"foo", "boo"}) # => Pf::Map{"bar" => 3, "baz" => 4}
map.reject("foo", "boo")   # => Pf::Map{"bar" => 3, "baz" => 4}

[View source]
def same?(other : Map(K, V)) : Bool #

Returns true if self and other refer to the same map in memory.

Due to the way Map is implemented, this method can be used as a cheap way to detect changes.

map1 = Pf::Map[foo: 123, bar: 456]
map2 = map1.assoc("foo", 123)
map1.same?(map2) # => true

[View source]
def select(& : Tuple(K, V) -> Bool) : Map(K, V) #

Returns a copy of self which includes only associations for which the block is truthy.

Supports value equality.

map = Pf::Map[foo: 2, bar: 3, baz: 4, boo: 5]
map.select { |_, v| v.even? } # => Pf::Map{"foo" => 2, "baz" => 4}

[View source]
def select(keys : Enumerable) #

Returns a new map which includes only associations with the given keys.

map = Pf::Map[foo: 2, bar: 3, baz: 4, boo: 5]
map.select({"foo", "boo"}) # => Pf::Map{"foo" => 2, "boo" => 5}
map.select("foo", "boo")   # => Pf::Map{"foo" => 2, "boo" => 5}

[View source]
def select(*keys) #

Returns a new map which includes only associations with the given keys.

map = Pf::Map[foo: 2, bar: 3, baz: 4, boo: 5]
map.select({"foo", "boo"}) # => Pf::Map{"foo" => 2, "boo" => 5}
map.select("foo", "boo")   # => Pf::Map{"foo" => 2, "boo" => 5}

[View source]
def size : Int32 #

Returns the number of associations in this map.


[View source]
def to_s(io) #

[View source]
def transaction(& : Commit(K, V) -> ) : Map(K, V) #

Yields a Commit object which allows you to mutate a copy of self.

  • The commit object is marked as resolved after the block. You should not retain it. If you do, all operations on the object (including readonly ones) will raise ResolvedError.

  • If you pass the commit object to another fiber in the block, e.g. via a channel, and fiber yield immediately after that, the commit obviously would not be marked as resolved as the resolution code would not have been reached yet. However, if you then attempt to call mutation methods on the commit, another error, ReadonlyError, will be raised. In other words, the yielded commit object is readonly for any other fiber except for the fiber that it was originally yielded to.

Returns self if the transaction did not touch the map. If the map was changed but then the changes were reverted this method will return a new map.

map1 = Pf::Map(String, Int32).new
map2 = map1.transaction do |commit|
  commit.assoc("John Doe", 12)
  commit.assoc("Susan Doe", 34)
  commit.dissoc("John Doe")
  if "John Doe".in?(commit)
    commit.assoc("Mark Doe", 21)
  else
    commit.assoc("John Doe", 456)
    commit.assoc("Susan Doe", commit["Susan Doe"] + 1)
  end
end
map1 # => Pf::Map[]
map2 # => Pf::Map["John Doe" => 456, "Susan Doe" => 35]

[View source]
def update(key : K, default : V, & : V -> V) #

Returns an updated copy of self.

  • If there is no association for key, the copy contains an association between key and default.

  • If there is an association for key, its value is yielded to the block and the return value of the block is used as the next value of key.

Supports value equality.

map = Pf::Map[foo: 100, bar: 200]
map.update("foo", 0, &.succ) # => Pf::Map{"foo" => 101, "bar" => 200}
map.update("baz", 0, &.succ) # => Pf::Map{"foo" => 100, "bar" => 200, "baz" => 0}

[View source]
def update(key : K, & : V -> V) : Map(K, V) #

Returns an updated copy of self.

  • If there is no association for key, returns self.
  • If there is an association for key, its value is yielded to the block and the return value of the block is used as the next value of key.

Supports value equality.

map = Pf::Map[foo: 100, bar: 200]
map.update("foo", &.succ) # => Pf::Map{"foo" => 101, "bar" => 200}
map.update("baz", &.succ) # => Pf::Map{"foo" => 100, "bar" => 200}

[View source]
def values : Array(V) #

Returns an array with all values from this map. There is no guaranteed order of values.

map = Pf::Map[foo: 10, bar: 20]
map.values # => [10, 20]

[View source]