DEV Community

Cover image for Blocks and methods overloading
Franciscello
Franciscello

Posted on • Edited on

Blocks and methods overloading

The other day while working on the Blocks section for the Crystal's tutorial, I came across something interesting about blocks, methods and overloading.

Overloading

Let's start by reviewing the concept of method overloading in Crystal.

In Crystal we can define methods with the same name but some difference and for the compiler they will be seen as different methods.

Here is an example:

def transform(str : String) : Int32
  str.size
end

def transform(n : Int32) : Int32
  n + 1
end

transform("Crystal") # => 7
transform(41) # => 42
Enter fullscreen mode Exit fullscreen mode

In the above example we have two methods with the same name transform but the difference is the type restriction of the parameter.

Here is another example:

def transform(str : String)
  yield str
end

def transform(str : String)
  str.capitalize
end

transform("Crystal") { |str| "Hello #{str}" } # => "Hello Crystal"
transform("crystal") # => "Crystal"
Enter fullscreen mode Exit fullscreen mode

In this last example, the difference between both methods is that the first one receives a block, in addition to the String parameter. To make this difference more clear, we can add the block parameter explicitly:

def transform(str : String, & : String -> String)
  yield str
end

def transform(str : String)
  str.capitalize
end

transform("Crystal") { |str| "Hello #{str}" } # => "Hello Crystal"
transform("crystal") # => "Crystal"
Enter fullscreen mode Exit fullscreen mode

The following is the list of differences that allows overloading a method:

  • The number of parameters
  • The type restrictions applied to parameters (first example)
  • The names of required named parameters
  • Whether the method accepts a block or not (last example)

πŸ€” What if ...

As I was writing about blocks, I wonder if the type restrictions over a block parameter could allow method overloading.

For example, are the following transform_string methods considered different?

def transform_string(word : String, & : String -> String)
  block_result = yield word
  puts block_result
end

def transform_string(word : String, & : Int32 -> String)
  block_result = yield word.size
  puts block_result
end

transform_string "crystal" do |word|
  word.capitalize
end

transform_string "crystal" do |number|
  "#{number}"
end
Enter fullscreen mode Exit fullscreen mode

Note: The first method defines a block parameter of type String -> String. And the second method defines a block of type Int32 -> String.

The output was:

Error: undefined method 'capitalize' for Int32
Enter fullscreen mode Exit fullscreen mode

Oops! Not the expected output πŸ˜…

The problem is that, for the compiler, the second method is the only definition for transform_string (meaning, the first definition is simply overridden by the second one). And this happens because the compiler does not use blocks for method overloading. Here is the why, explained in a comment by Johannes:

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. The compiler needs to find the method before it looks at the block, which means block arguments cannot be used to find the method.

But, what if ...

Blocks and Procs

After talking to Beta and Johannes, they proposed a solution very close to what we are trying to implement:

We can use a Proc (created from a captured block) as the methods' argument.

🀯

First, let's see an example on how a Proc is created from a captured block:

proc = Proc(Int32, String).new { |x| x.to_s } 
typeof(proc) # Proc(Int32, String)

# when can invoke it using `call`:
proc.call 42 # => "42"
Enter fullscreen mode Exit fullscreen mode

So now we need to change the transform_string definitions and implementations, like this:

def transform_string(word : String, block : String -> String)
  block_result = block.call word
  puts block_result
end

def transform_string(word : String, block : Int32 -> String)
  block_result = block.call word.size
  puts block_result
end
Enter fullscreen mode Exit fullscreen mode

We have replaced the block parameter with a Proc parameter, and related to this change, we are using the method call to invoke the Proc 😎.

Let's see if this works as expected:

def transform_string(word : String, block : String -> String)
  typeof(block) # => Proc(String, String)
  block_result = block.call word
  puts block_result
end

def transform_string(word : String, block : Int32 -> String)
  typeof(block) # => Proc(Int32, String)
  block_result = block.call word.size
  puts block_result
end

proc_string_string = Proc(String, String).new do |word| 
  word.capitalize
end
transform_string("crystal", proc_string_string)

proc_int32_array = Proc(Int32, String).new do |number|
  "#{number}"
end
transform_string("crystal", proc_int32_array)
Enter fullscreen mode Exit fullscreen mode

The output:

Crystal
"7"
Enter fullscreen mode Exit fullscreen mode

It worked! Yeah! πŸ€“πŸŽ‰

Let's rewrite the code in a more concise way:

def transform_string(word : String, block : String -> String)
  typeof(block) # => Proc(String, String)
  puts block.call word
end

def transform_string(word : String, block : Int32 -> String)
  typeof(block) # => Proc(Int32, String)
  puts block.call word.size
end

transform_string "crystal", Proc(String, String).new { |word| word.capitalize }
transform_string "crystal", Proc(Int32, String).new { |number| "#{number}" }
Enter fullscreen mode Exit fullscreen mode

And it's working perfectly! 🀩

πŸ€” What if ... (part 2 πŸ›Ή)

We got our code working but at the same time we discovered that the compiler can not distinguish two methods differing only in the block’s type.

So let’s think on how we could improve Crystal to allow it.

Here is a proposal: maybe we can give more information to the compiler about the parameter's type restriction when defining the block πŸ€”. Something like this:

transform_string "crystal" do |word : String| 
  word.capitalize
end
Enter fullscreen mode Exit fullscreen mode

This way the compiler can "see" that we are defining a block, whose input is a String and also returns a String and so can select the correct implementation for transform_string:

def transform_string(word : String, &block : String -> String)
  puts yield word
end

transform_string "crystal" do |word : String| 
  word.capitalize 
end
Enter fullscreen mode Exit fullscreen mode

Farewell and see you later

Let's recap:

We have reviewed some interesting concepts (method overloading, Blocks and Procs) and we have a way to overload methods using the type restrictions of the Blocks Proc parameter.

Hope you enjoyed it! πŸ˜ƒ

Thanks, thanks, thanks to Beta and Johannes for reviewing this post and improving the code and text!! πŸ‘πŸ‘πŸ‘

Top comments (0)