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!

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

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)