The Crystal Programming Language Forum

Confusing type error

First time caller, long time listener.
I have an issue that could use some elucidation.

The intent is to traverse a structure and transform a set of primitive types to string, whilst preserving structure. Given this example code…

def to_stringly(value)
  case value
  when Array, Tuple
    value.map { |v| to_stringly(v) }
  when Hash
    value.transform_values { |v| to_stringly(v) }
  when NamedTuple
    to_stringly(value.to_h)
  when String, Bool
    value
  else
    value.to_s
  end
end


body = {
  :compare => [
    {
      :key    => "key",
      :value  => 0,
      :target => "VERSION",
      :result => "EQUAL",
    },
  ],
  :success => [
    {
      :request_put => {
        :key   => "key",
        :value => "value",
      },
    },
  ],
  :failure => [] of Nil,
}

to_stringly(body)

Results in the following error…

error in line 47
Error: instantiating 'to_stringly(Hash(Symbol, Array(Hash(Symbol, Hash(Symbol, String))) | Array(Hash(Symbol, Int32 | String)) | Array(Nil)))'


error in line 14
Error: instantiating 'to_stringly((Array(Hash(Symbol, Hash(Symbol, String))) | Array(Hash(Symbol, Int32 | String)) | Array(Nil)))'


error in line 8
Error: instantiating 'to_stringly((Hash(Symbol, Hash(Symbol, String)) | Hash(Symbol, Int32 | String)))'


error in line 10
Error: type must be Hash(Symbol, String), not (Hash(Symbol, String) | String)

Shouldn’t the union be an acceptable argument given the type refinement of the case statement?

Thanks!

1 Like

This was the solution. Any ideas why the type coercion for the String case was necessary?

def to_stringly(value)
    case value
    when Array, Tuple
      value.map { |v| to_stringly(v) }
    when Hash
      value.transform_values { |v| to_stringly(v) }
    when NamedTuple
      to_stringly(value.to_h)
    when String
      value.as(String)
    when Bool
      value
    else
      value.to_s
    end
  end
1 Like

The issue is related to how the method is instantiated for each type.

A workaround is to use split the case in different methods and use forall. The ordering is important.

def to_stringly(value)
  value.to_s
end

def to_stringly(value : Nil)
  value
end

def to_stringly(value : Array(T)) forall T
  value.map { |v| to_stringly(v) }
end

def to_stringly(value : Hash(K, V)) forall K, V
  value.transform_values { |v| to_stringly(v) }
end

Note that if Array and Hash are defined with no forall you will end up in the same issue:

def to_stringly(value : Array)
  value.map { |v| to_stringly(v) }
end

def to_stringly(value : Hash)
  value.transform_values { |v| to_stringly(v) }
end
 38 | value.transform_values { |v| to_stringly(v) }
                             ^
Error: type must be Hash(Symbol, String), not (Hash(Symbol, String) | String)
3 Likes