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 …


This content originally appeared on DEV Community 👩‍💻👨‍💻 and was authored by Franciscello

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

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"

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"

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

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

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 (in Johannes' words):

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"

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 -> Array(String))
  block_result = block.call word.size
  puts block_result
end

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 -> Array(String))
  typeof(block) # => Proc(Int32, Array(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, Array(String)).new do |number|
  ["#{number}"]
end
transform_string("crystal", proc_int32_array)

The output:

Crystal
["7"]

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 -> Array(String))
  typeof(block) # => Proc(Int32, Array(String))
  puts block.call word.size
end

transform_string "crystal", Proc(String, String).new { |word| word.capitalize }
transform_string "crystal", Proc(Int32, Array(String)).new { |number| ["#{number}"] }

And it's working perfectly! 🤩

🤔 What if ... (part 2 🛹)

We got our code working but what if we could use a block to overload methods (ie. the original idea).

Maybe the compiler would need more information on the parameter's type restriction when defining the block 🤔. Something like this:

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

This way the compiler can "see" that we are defining a block, which 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

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! 😃


This content originally appeared on DEV Community 👩‍💻👨‍💻 and was authored by Franciscello


Print Share Comment Cite Upload Translate Updates
APA

Franciscello | Sciencx (2023-01-11T23:07:45+00:00) Blocks and methods overloading. Retrieved from https://www.scien.cx/2023/01/11/blocks-and-methods-overloading/

MLA
" » Blocks and methods overloading." Franciscello | Sciencx - Wednesday January 11, 2023, https://www.scien.cx/2023/01/11/blocks-and-methods-overloading/
HARVARD
Franciscello | Sciencx Wednesday January 11, 2023 » Blocks and methods overloading., viewed ,<https://www.scien.cx/2023/01/11/blocks-and-methods-overloading/>
VANCOUVER
Franciscello | Sciencx - » Blocks and methods overloading. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/01/11/blocks-and-methods-overloading/
CHICAGO
" » Blocks and methods overloading." Franciscello | Sciencx - Accessed . https://www.scien.cx/2023/01/11/blocks-and-methods-overloading/
IEEE
" » Blocks and methods overloading." Franciscello | Sciencx [Online]. Available: https://www.scien.cx/2023/01/11/blocks-and-methods-overloading/. [Accessed: ]
rf:citation
» Blocks and methods overloading | Franciscello | Sciencx | https://www.scien.cx/2023/01/11/blocks-and-methods-overloading/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.