Andy Jones
Andy Jones

Reputation: 1104

How do I cast a Hash as an aliased type without getting this error message?

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

Answers (1)

Johannes Müller
Johannes Müller

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

Related Questions