I’m writing a wrapper for common generics, so that they can contain self-referential types without use of recursively-defined aliases. The code so far is below. I have two macros, recursive_generic1
to make things like Array(OneArgument)
and recursive_generic2
, to make things like Hash(Two, Arguments)
. I see that Iterable and Enumerable work with both Hash and Array, by using a tuple for the generic type argument. I don’t seem to be able to extract parts of a tuple to define types, though, code like this doesn’t work:
tuple = {Symbol, String}
h = Hash(tuple[0], MyWrappingType)
Is there a way I can pick a field out of a tuple and use it as a type in a declaration?
Here is my code so far:
# Implementation of a pseudo-generic that contains itself without use of
# recursively-defined aliases, which are problematical and/or broken
# in the compiler.
#
# Bruce Perens (@BrucePerens, bruce@perens.com)
# MIT license. Copyright (C) 2000 Algorithmic LLC. In addition, this may
# be a derivative work of the works cited below:
#
# Thanks for lessons from:
# * Ary Borenszweig (@asterite) explained the way to implement this
# in https://github.com/crystal-lang/crystal/issues/5155
# * Sijawusz Pur Rahnama (@sija) and his `any_hash` shard.
# * Johannes Müller (@straight-shoota) and his `crinja` shard.
# * The Crystal stdlib implementation of the wrapped types, for the
# the API, methods, some of the tests.
#
# name: The name of the new hash object.
# keytype: The type of keys in the hash.
# valuetype: The type of values in the hash.
#
# mutate_key: The name of a function that mutates the keys in the hash,
# and all values used to query keys in the hash.
# It takes the given key as
# its argument, and returns the mutated key. So, for example,
# this function would make all of the keys strings:
# ```crystal
# def mutate(key)
# key.to_s
# end
# ```
#
# mutate_value: The name of a function that mutates values as they are
# inserted in the hash. It takes the given value as
# its argument, and returns the mutated value. So, for example,
# this function would make all of the values strings as they
# are inserted in the hash:
# ```crystal
# def mutate(key)
# key.to_s
# end
# ```
macro recursive_generic1(name, generic, valuetype, mutate_key = nop, mutate_value = nop)
class {{name.id}}
{% verbatim do %}
RETURN_SELF = %w(clear)
{% end %}
alias WrappedValue = GenericWrapper::ValueWrapper({{valuetype.id}})
include GenericWrapper(({{valuetype}}),
::{{generic}}(WrappedValue),
WrappedValue)
module GenericWrapper::Mutate
private def mutate_key(v)
{{mutate_key}}(v)
end
private def mutate_value(v)
{{mutate_value}}(v)
end
end
end
end
macro recursive_generic2(name, generic, keytype, valuetype, mutate_key = nop, mutate_value = nop)
class {{name.id}}
{% verbatim do %}
RETURN_SELF = %w(clear)
{% end %}
alias WrappedValue = GenericWrapper::ValueWrapper({{valuetype.id}})
include GenericWrapper({({{keytype}}), ({{valuetype}})},
::{{generic}}({{keytype.id}}, WrappedValue),
WrappedValue)
module GenericWrapper::Mutate
private def mutate_key(v)
{{mutate_key}}(v)
end
private def mutate_value(v)
{{mutate_value}}(v)
end
end
end
end
module GenericWrapper(T, WrappedGeneric, WrappedValue)
include Enumerable(T)
include Iterable(T)
macro method_missing(call)
{% if RETURN_SELF.includes?((call).name.stringify) %}
@contained.{{call}}
self
{% else %}
@contained.{{call}}
{% end %}
end
@contained = WrappedGeneric.new
# Override the version in Enumerable, the one in the underlying generic
# is faster.
delegate size, to: @contained
def []=(key, value)
@contained[mutate_key(key)] = WrappedValue.new(mutate_value(value))
end
def [](key)
v = @contained[mutate_key(key)]
end
def []?(key)
@contained[mutate_key(key)]?
end
def each
WrappedValue::Iterator.new(@contained.each)
end
def each(&block)
i = @contained.each
while (v = i.next).is_a?(Tuple)
yield *extract_value(*v)
end
end
def to_h
self
end
private module ExtractValue
def extract_value(key, value)
{ key, value.value }
end
end
include ExtractValue
struct ValueWrapper(ValueType)
@value : ValueType
def initialize(@value)
end
def value
@value
end
def value=(n)
@value = mutate_value(n)
end
class Iterator
include ::Iterator(ValueWrapper)
include ExtractValue
@i : ::Iterator(ValueWrapper)
def initialize(@i)
super
end
def next
v = @i.next
if v == Iterator::Stop::INSTANCE
stop
else
extract_value(*v)
end
end
def rewind
@i.rewind
self
end
end
end
module Mutate
# This No-Operation method is the default for mutate_value and mutate_key
# It just returns its argument.
private def nop(value)
value
end
end
include Mutate
end
recursive_generic2(MyHash, Hash, Symbol, String|MyHash|Array(MyHash))
recursive_generic1(MyArray, Array, String|MyHash|Array(MyHash))
h = MyHash.new
h[:itself] = h
h[:array] = [h]
p h.inspect
p h.size