The Crystal Programming Language Forum

Defining types with tuples

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

You can use typeof

Sorry for the brevity but I can’t type more right now

What is your intention with the first line? Do you want a runtime tuple, or do you want to talk about a tuple type with those types? If it’s the latter case, you need to write that as Tuple(…).

I think I have figured this out. I can do {{ include “#{pattern}”.id }} to include my custom generic with parameters.