Reputation: 1104
I'm getting an interesting error when I use alias
to define a Hash:
alias MyHash = Hash(Symbol, String | Int32)
hash = {:one => 2}.as(MyHash)
If I run this the output is not what I expected:
Error in ffile2.cr:2: can't cast Hash(Symbol, Int32) to Hash(Symbol, Int32 | String)
hash = {:one => 2}.as(MyHash)
^
You can't? Why not? Isn't that what defining a unity type is for?
Note that if I put the type in a method signature, everything is fine:
def foo(h : Hash(Symbol, String | Int32) )
puts h[:bar]?.nil?
end
foo( {:one => 2} )
Update: This works, but it seems more than a little silly:
alias MyHash = Hash(Symbol, String | Int32)
hash = {:one => 2.as(String | Int32)}.as(MyHash)
Upvotes: 1
Views: 68
Reputation: 5661
This has nothing to do with alias. If you replace the alias in your original example with the aliased type, it will fail as well.
.as
cannot magically convert a Hash(Symbol, Int32)
to a Hash(Symbol, String | Int32)
. These are different types and behave differently. This might not be obvious at first because when retrieving an entry, both types return a value with a type matching String | Int32
. When storing an entry, they don't act the same: Hash(Symbol, String | Int32)
can receive a value of type String | Int32
, Hash(Symbol, Int32)
cannot.
The language design terms for this are covariance and contravariance.
To avoid having to specify the expected value type in the literal, you can also use a generic conversion, like this for example:
literal = {:one => 2}
mapped = literal.map do |key, value|
{key, value.as(String | Int32)}
end.to_h
typeof(mapped) # => Hash(Symbol, Int32 | String)
This will take any hash with matching types and convert it to Hash(Symbol, Int32 | String)
.
Types in method arguments are not "fine" they just behave differently. They match underspecified types as long as you don't do anything wrong with them. Try to set a value to a string, it will still fail:
def foo(h : Hash(Symbol, String | Int32) )
puts h[:bar] = "bar" # error: no overload matches 'Hash(Symbol, Int32)#[]=' with types Symbol, String
end
These different semantics are obviously not very intuitive but it's the current state of a work in progress. There is an issue which also explains in more detail what this is about: https://github.com/crystal-lang/crystal/issues/3803
Upvotes: 4