0

I'm learning crystal (just for fun) and trying to implement kind of []= method for a struct. Here is the first attempt:

struct Foo
  @str : String | Int32 # Have to share all the types, NOT OK
  @int : Int32 | String # Have to share all the types, NOT OK

  def initialize(@str = "foo", @int = 111)
  end

  def []=(variable, value)
    {% for ivar in @type.instance_vars %}
      @{{ivar}} = value if {{ivar.id.symbolize}} == variable
    {% end %}
  end
end

foo = Foo.new
foo[:str] = "bar" # OK
foo[:int] = 222   # OK

It works, but the disadvantage is all the instance variables should share the same type, otherwise the compiller will complain. My second attempt was to reimplement the method like this:

def []=(variable, value)
  {% for ivar in @type.instance_vars %}
    {% if ivar.id.symbolize == variable %} # ERROR: undefined macro variable 'variable'
      @{{ivar}} = value
    {% end %}
  {% end %}
end

But this doesn't work, as variable inside {%%} syntax can't be resolved. How to overcome this problem? Maybe there is other idiomatic approach to implement such method at all?

WPeN2Ic850EU
  • 995
  • 4
  • 8

1 Answers1

1

You don't need the instance variables to have a union of all possible types, you can restrict to the matching type in the setter method:

struct Foo
  @str : String
  @int : Int32

  def initialize(@str = "foo", @int = 111)
  end

  def []=(variable, value)
    {% for ivar in @type.instance_vars %}
      if {{ivar.id.symbolize}} == variable
        if value.is_a?({{ ivar.type.id }})
          @{{ivar}} = value
        else
          raise "Invalid type #{value.class} for {{ivar.id.symbolize}} (expected {{ ivar.type.id }})"
        end
      end
    {% end %}
  end
end

foo = Foo.new
foo[:str] = "bar"
foo[:int] = 222
foo[:int] = "string"  # => Invalid type String for :int (expected Int32)

While that is certainly possible, it comes at the disadvantage that these type mismatches are only detected at runtime. You'll get better type safety if you access the properties directly (foo.int = "string" will not even compile). It should be possible to implement []= as a macro which could complain at compile time about type mismatches, but I'm not sure that's what you're looking for.

A better solution might be to just use a NamedTuple. Or maybe you don't need to access the fields using symbols at all. I don't know if there is a real use case for this.

Johannes Müller
  • 5,581
  • 1
  • 11
  • 25
  • Thank you! I think, there should be real usecases for this, e.g. Ruby has such method: http://ruby-doc.org/core-2.5.0/Struct.html#method-i-5B-5D-3D – WPeN2Ic850EU Feb 08 '18 at 12:15
  • Yeah, but "because Ruby has it", doesn't mean there's a real use case for this in Crystal. Ruby is a dynamically typed language, Crystal statically typed. And there is a great benefit in having static type checking. – Johannes Müller Feb 08 '18 at 12:25