Method call depending on block type

It seems that method overloading with the only difference being the type of block passed as a parameter is not possible.
For example, with the following code:

def add_col2(&extractor : String -> _)
  extractor.call("test1")
end
def add_col2(&extractor : (String, Int32) -> _)
  extractor.call("test2", 3)
end
add_col2 {|x, y| puts "y=#{y}, x=#{x}" }
add_col2 {|x| puts "x=#{x}"}

output is:

y=3, x=test2
x=test2

when I would like:

y=3, x=test2
x=test1

So the first method is simply overwritten by the second.
I have tried many ways to solve this problem, but without success.

Any ideas ?

Correct. Pretty sure this is by design, as pointed out in the reference:

Overloads simply take if it accepts a block or not into account. Would have to also change one of the other criteria to make it correctly be an overload and not a redefinition.

Overloading based on block type restriction does not work, you’re essentially overriding the previous definition.
I’m combining the excellent answers from @ysbaddaden and @RX14 in Allow method overloading via block type · Issue #5090 · crystal-lang/crystal · GitHub to explain why:

Block arguments are defined by the method that yields, not by how the method is called; the yielding method can’t behave differently depending on the block. You need to find the method before you look at the block, which means block arguments cannot be used to find the method.
It would be technically possible to determine the method based on the number of block arguments, but that’d require removing the ability to skip block arguments (currently yield 1, 2, 3 with a block { |x, y| puts "#{x}, #{y}" } works and prints 1, 2). It’d also be quite confusing, since overloading currently works with both args length and args type, whereas this overloading would only work with length.

So you can use either different names, different regular parameters to create different signatures for both methods.

Another option for this example would be to use regular parameters with different Proc types instead of capturing block parameters. It’s a bit more verbose but semantically completely equivalent:

def add_col2(extractor : String -> _)
  extractor.call("test1")
end
def add_col2(extractor : (String, Int32) -> _)
  extractor.call("test2", 3)
end
add_col2 Proc(String, Int32, Nil).new{|x, y| puts "y=#{y}, x=#{x}" }
add_col2 Proc(String, Nil).new{|x| puts "x=#{x}"}
2 Likes

Hi @hutou (and everyone)! :wave: Here is a (just published) post about this very topic. Hope it’s a good read! :nerd_face:

3 Likes

Reading your answers and their associated links was very interesting and I found a solution to my problem. Thank you all.