Hash litterals with recursive alias issue

Working with hash tables in Crystal is not always ‘crystal’ clear to me :frowning_face:
So, given the following type:
alias Params = String | Int32 | Hash(Symbol, Params)

I want to define and populate a hash table with, for example, the following literal data:

foo = {
:bar1 => {:type => "I", :minval => 3, :maxval => 33}, 
:bar2 => {:type => "I", :minval => 4, :maxval => 44}, 
:bar3 => {}
}

Applying the advice given by @Blacksmoke16 in a previous post, I end up with the following code:

foo = {} of Symbol => Params
 
foo[:bar1] = {} of Symbol => Params
foo[:bar2] = {} of Symbol => Params
foo[:bar3] = {} of Symbol => Params
 
foo[:bar1].as(Hash(Symbol, Params))[:type] = "I"
foo[:bar1].as(Hash(Symbol, Params))[:minval] = 3
foo[:bar1].as(Hash(Symbol, Params))[:maxval] = 33
foo[:bar2].as(Hash(Symbol, Params))[:type] = "I"
foo[:bar2].as(Hash(Symbol, Params))[:minval] = 4
foo[:bar2].as(Hash(Symbol, Params))[:maxval] = 44

p! foo
p! typeof(foo)
p! typeof(foo[:bar1])
p! typeof(foo[:bar1].as(Hash(Symbol, Params))[:maxval])

and the result is what I expect.

foo # => {:bar1 => {:type => "I", :minval => 3, :maxval => 33}, :bar2 => {:type => "I", :minval => 4, :maxval => 44}, :bar3 => {}
typeof(foo) # => Hash(Symbol, Params)
typeof(foo[:bar1]) # => (Hash(Symbol, Params) | Int32 | String)
typeof((foo[:bar1].as(Hash(Symbol, Params)))[:maxval]) # => (Hash(Symbol, Params) | Int32 | String)

But the syntax is a bit heavy, I think. So I try a more compact version:

foo = {
  :bar1 => {:type => "I".as(Params), :minval => 3.as(Params)}.as(Params),
  # :bar2 => {:type => "I".as(Params), :minval => 4.as(Params), :maxval => 44.as(Params)}.as(Params),
}
p! foo
p! typeof(foo)
p! typeof(foo[:bar1])

This looks ok:

foo # => {:bar1 => {:type => "I", :minval => 3}}
typeof(foo) # => Hash(Symbol, Params)
typeof(foo[:bar1]) # => (Hash(Symbol, Params) | Int32 | String)

but, if I uncomment the second line (:bar2 …), I get the following compilation error:
Error: can't cast Hash(Symbol, Hash(Symbol, Params) | Int32 | String) to Params

Why is adding a 3rd key a problem? I read somewhere that recursive aliases are a bit buggy: is this the explanation of this error?

I would highly suggest not using recursive hashes like this and think of a different approach. Possibly something like how JSON::Any - Crystal 1.1.0-dev is implemented. I.e. a struct which stores an ivar that has a type of a big union of various primitive types, and other JSON::Any instances: JSON::Any::Type - Crystal 1.1.0-dev.

Or really, just do something like:

record Param, type : String, minval : Int32, maxval :Int32

foo = {
  "bar1" => Param.new("I", 3, 33),
  "bar2" => Param.new("I", 4, 44),
}

I’d also not advise using symbols like this as in Crystal land they don’t really have any benefit over String.

1 Like

It looks like a bug. But you can use { ... } of Symbol => Params instead of doing { ... }.as(Params). But you should probably avoid using recursive aliases at all.