Skip to content

Instantly share code, notes, and snippets.

@estum
Created December 7, 2021 07:22
Show Gist options
  • Save estum/d07672135ecf801b85dcd8aec4886d8c to your computer and use it in GitHub Desktop.
Save estum/d07672135ecf801b85dcd8aec4886d8c to your computer and use it in GitHub Desktop.
Map
# frozen_string_literal: true
require 'concurrent/map'
# {include:file:concurrent/map.rb}
class Map < Concurrent::Map
HashBuilder = proc do |hsh, default = nil, default_proc = nil|
hsh = ::Hash[hsh]
if !default.nil?
hsh.default = default
elsif !default_proc.nil?
hsh.default_proc = default_proc
end
hsh
end.freeze
ProcBuilder = proc { |m, *rest| m.get(*rest) }.curry(2).freeze
DefaultValueProc = proc { |h| h.default_value }.freeze
MapHash = begin
key_itself = proc { |_, k| k }.freeze
::Hash.new(&key_itself).freeze
end
private_constant :MapHash
# @!attribute [rw] default_value
# @return [Object, nil]
attr_accessor :default_value
# @!attribute [rw] default_proc
# @return [Proc, nil]
attr_accessor :default_proc
# Creates hash with default proc that returns a missing key
# @param hash [::Hash, nil]
# @return [::Hash] The new hash from the given with set default proc
def self.Hash(hash)
MapHash.merge(hash.to_h)
end
# @overload wrap(map)
# bypasses the given {Map} object
# @param map [Map]
#
# @overload wrap(any)
# @param hash [::Hash, nil]
#
# @return [Map]
# @see Map.new
def self.wrap(map_or_any)
self === map_or_any ? map_or_any : self[map_or_any]
end
# Makes a map from the given hash. If option `*` is given and
# it is not a proc, sets it as default value
# and {DefaultValueProc} as default proc
#
# @see Map.new
# @param hash [::Hash, nil]
# @option hash [Proc, Object, nil] :* Default proc or value
# @return [Map]
def self.[](hash = nil)
return new(hash) unless Hash === hash && !hash[:*].nil?
hash = hash.dup
star = hash.delete(:*)
if Proc === star
new(hash, &star)
else
new(hash, star, &DefaultValueProc)
end
end
# @param hash [::Hash, nil] A source hash
# @param default_value [Object, nil]
# @yield Default proc
# @yieldreturn [Object, nil] the default value
# @return [Map]
def self.new(hash = nil, default_value = nil, &block)
map = allocate
map.default_proc = block.to_proc if block_given?
map.default_value = default_value if default_value
map.marshal_load(hash)
map
end
# Creates empty map with default proc, that
# computes value for a missing key using the given proc.
# @yield [key] when key is absent
# @yieldparam [Any] key
# @yieldreturn [Any] value for an absent key to be stored then.
# @return [Map] with set {#default_proc}
def self.factory
new { |map, key| map.compute_if_absent(key) { yield(key) } }
end
# Populates the map with the given list of keys, computing values by yielding
# the block.
#
# @overload populate(map, *keys, &block)
# Uses given map to populate it
# @param map [Map]
#
# @overload populate(hash, *keys, &block)
# Creates map from the given hash to populate it.
# @param hash [::Hash]
#
# @overload populate(*keys, &block)
# Creates empty map to populate it.
#
# @param keys [Array<Object>] any number of keys to store into map
# @param method_name [Symbol] (see Concurrent::Map)
# @param _if [nil, :present, :absent] Method name suffix
# @yield computes and stores value for key, can pass old value
# @yieldparam [Object] key
# @yieldparam [Object, nil] old_value
# @yieldreturn [Object, nil] - computed value to store, key will be deteled if returns nil
# @return [Map] populated map
def self.populate(*keys, method_name: :compute, _if: nil, &block) # :yields:
map = case keys[0]
when ::Hash, self; wrap(keys.shift)
else new(); end
method_name = :"#{method_name}_if_#{_if}" if _if
keys.each do |key|
map.public_send(method_name, key) do |old_value|
block.arity > 1 ? block.call(key, old_value) : block.call(key)
end
end
return map
end
# @see Concurrent::Map#populate_from
# @param hash [::Hash] A source hash
# @return [self]
def marshal_load(hash)
initialize(&default_proc)
populate_from(hash) if hash
self
end
# Creates vanilla hash from self include default value/proc
# @see HashBuilder
# @return [::Hash]
def to_hash
HashBuilder[@backend, default_value, default_proc]
end
alias_method :to_h, :to_hash
# @return [Proc]
def to_proc
ProcBuilder[self]
end
# @param (see ::Hash#dig)
# @return [Object, nil]
def dig(key, *rest)
value = get(key)
if rest.size > 0
unless value.respond_to?(:dig)
raise TypeError, "#{type.class} does not have #dig method"
end
value.dig(*rest)
else
value
end
end
# @see Concurrent::Map#freeze
# @return [self]
def freeze
@backend.freeze
self
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment