Instantiation of a generic method

In following program, compiler instantiate a call of f(h : Hash(String, Float64)) , but not for f(h : Hash(String, Int32)), and emits no overload matches error.

if rand < 0.5
  h = Hash(String, Float64).new
else
  h = Hash(String, Int32).new
end

def f(h : Hash(String, T)) forall T
  h["test"] = 1.as(T)
end

f(h)
Showing last frame. Use --error-trace for full trace.

In test.cr:11:1

 11 | f(h)
      ^
Error: no overload matches 'f' with type (Hash(String, Float64) | Hash(String, Int32))

Overloads are:
 - f(h : Hash(String, T))
Couldn't find overloads for these types:
 - f(h : Hash(String, Int32))

Instantiating f(h : Hash(String, Int32)) explicitly (adding f(Hash(String, Int32).new) before calling f) did not work.

Why it won’t match?

I thought someone asked this sort of question before, but I could not find it.

Hm, this is a bit of an edge case. Here h is a union type of Hash(String, Float64) | Hash(String, Int32) (as pointed out by the compiler) and even though it can match Hash(String, T) where T would be Float64 | Int32, the compiler doesn’t do that yet… and I’m not sure it should (it’s a bit confusing). It works if it’s Hash(String, Float64 | Int32), though. So that’s the reason: the compiler will try to match a single type Hash(String, T), where T can be a union, but it won’t match an entire union and form a union from the T that match.

But most of the time you don’t need types in method signatures, this works fine:

if rand < 0.5
  h = Hash(String, Float64).new
else
  h = Hash(String, Int32).new
end

def f(h)
  h["test"] = 1
end

f(h)

Also maybe worth asking: what are you trying to do or accomplish?

This solution segfaults when Hash(String, Float64) is reached.

if true 
  h = Hash(String, Float64).new
else
  h = Hash(String, Int32).new
end

def f(h)
  h["test"] = 1
end

p h.class
f(h)

Interesting, I didn’t see that when I run the program, but yeah, seems like a bug. Most probably related to automatic casting.

There is the wrapper solution.

struct Wrapper(T)
  getter hash : Hash(String, T) = Hash(String, T).new
  def f
    @hash["test"] = T.new 1
  end
end

if rand < 0.5
  w = Wrapper(Float64).new
else
  w = Wrapper(Int32).new
end

w.f
p w.hash
1 Like

In the original example Float64 couldn’t be a valid type for T because it can’t match 1.as(T) (at least currently, automatic casting can’t do that). That’s not the reason why this failed, but we could pretend the compiler was smart enough to see this error coming up for Hash(String, Float64) ;)

I’m writing a program that computes the minimum, maximum and average each row
across CSV files.

I was using Hash(String, Array(Float64)) or Hash(String, Array(Int32)) for the dataset, but fetching and updating is something complicated, so I wrote a generic method.

require "csv"

minimum = Hash(String, Array(Float64)).new
maximum = Hash(String, Array(Float64)).new
average = Hash(String, Array(Float64)).new
number  = Hash(String, Array(Int32)).new

def update(set : Hash(String, Array(T)), index, key, value) forall T
  v = value.as(T)
  if (a = set[key]?)
    if a[index]?
      a[index] = v
    else
      a << v
    end
  else
    set[key] = [v]
  end
end

def compute(key, index, *sets)
  arrays = sets.map { |d| d[key]? }
  values = arrays.map { |a| a ? a[index]? : nil }
  ret = yield *values
  sets.zip(ret) do |d, r|
    update(d, index, key, r)
  end
end

csv_files = %w[foo.csv bar.csv hoge.csv fuga.csv]
csv_files.each do |file|
  File.open(file, "r") do |fp|
    index = 0
    CSV.new(fp, headers: true) do |csv|
      val = csv.row["value of foo"].to_f64
      num = csv.row["number of foo"].to_i32
      compute("foo", index, minimum, maximum) do |omin, omax|
        [omin, omax, val].compact.minmax
      end
      compute("foo", index, average, number) do |oave, onum|
        # not yet implemented, but something like
        { 1.0, 0 }
      end
      index += 1
    end
  end
  first_file = false
end

I’ll refactor the class of datasets something like:

struct A
  property min : Array(Float64)
  property max : Array(Float64)
  property ave : Array(Float64)
  property num : Array(Int32)

  def add(index, value, number)
     # ...
  end
end

dataset = Hash(String, A).new

csv_files = %w[foo.csv bar.csv hoge.csv fuga.csv]
csv_files.each do |file|
  File.open(file, "r") do |fp|
    index = 0
    CSV.new(fp, headers: true) do |csv|
      val = csv.row["value of foo"].to_f64
      num = csv.row["number of foo"].to_i32
      dataset["foo"].add(index, val, num)
      index += 1
    end
  end
  first_file = false
end

Thanks.

1 Like