Overloading and Multiple Dispatch

Crystal allows you to have different versions of a method with the same name. This is called overloading. More Safety Through Types showed how you can use argument types to restrict methods.

Code written without types can be more generic and reusable, but specifying types can often make code safer. Details such as the number of arguments, their names, and whether the method accepts a code block let you craft different versions of a method that work with slightly different contexts or arguments.

The compiler creates separate executable code for each version of a method. Then, when the compiler encounters calls to that method name, the compiler attaches the right code version, based on the best-matching type of the parameter(s). You can have code specialization based on type without needing any checks at runtime, which adds considerably to execution speed.

To see this for yourself, try out the following. Copy all the add methods and the tests from overloading1234.cr (see More Safety Through Types). Then write and test an add method that takes:

  • A number and a Boolean, returning the number if the Boolean is true, otherwise returning 0.

  • Two strings, converts them to integers and then adds them.

What happens if you execute the previous tests? Use what you learned in Getting Input to handle this.

There’s still a challenge to sort out. After defining a method, add(x : String, y : String), the test case, add("Hello ", "Crystal"), will now take this new method for execution instead of the generic add. The reason is that the types in this call better match the new method. But now to_i will fail on these arguments, giving rise to an exception.

You can protect your new method against this:

 def​ ​add​(x : String, y : String)
 if​ x.​to_i?​ && y.​to_i?
  add x, y ​# calls version 1
 end
 end

But add("Hello ", "Crystal") will then return nil, so you need an else branch for that:

 def​ ​add​(x : String, y : String)
 if​ x.​to_i?​ && y.​to_i?
  add x, y ​# calls version 1
 else
  x + y
 end
 end

Here’s the complete code of this example:

 # version 1:
 def​ ​add​(x : Int, y : Int)
  x + y
 end
 
 # version 2:
 def​ ​add​(x : Number, y : Number)
  x + y
 end
 # version 3:
 def​ ​add​(x : Number, y : String)
  x.​to_s​ + y ​# convert a number to a string with to_s method
 end
 
 # version 4:
 def​ ​add​(x, y)
  x + y
 end
 
 # new methods:
 # version 5:
 def​ ​add​(x : Number, y : Bool)
  y ? x : 0
 end
 
 # version 6:
 def​ ​add​(x : String, y : String)
 if​ x.​to_i?​ && y.​to_i?
  add x.​to_i​, y.​to_i​ ​# calls version 1
 else
  x + y
 end
 end
 
 add(2, 3) ​# => 5
 add(1.0, 3.14) ​# => 4.14
 add(​"Hello "​, ​"Crystal"​) ​# => "Hello Crystal"
 add(42, ​" times"​) ​# => "42 times"
 add 5, ​true​ ​# => 5
 add 13, ​false​ ​# => 0
 add(​"12"​, ​"13"​) ​# => 25

You can also make this work with a union type on the return value. In a new script, make an add(x, y) method that returns nil if y is equal to 0, and x + y otherwise. Test with n = add(2, 3). What is n’s type? What happens when you try to execute n + 10? Add an if test that prevents this.

When the arguments have union types, the compiler doesn’t know which method version to call. Only at runtime is the real type of the arguments known and the right method version called accordingly. This is called multiple dispatch. But this doesn’t hamper speed too much because versions for all the possible types were compiled ahead of time. You can see this here:

 def​ ​display​(x : Number) ​# overloading 1
  puts ​"​​#{​x​}​​ is a number"
 end
 
 def​ ​display​(x : String) ​# overloading 2
  puts ​"​​#{​x​}​​ is a string"
 end
 
 n = 42
 display n ​# => 42 is a number
 
 str = ​"magic"
 display str ​# => magic is a string
 
 r = rand < 0.5 ? n : str
 typeof(r) ​# => (Int32 | String)
 display r

In the first display, call overloading 1 is used. In the second, overloading 2, both decided at compile time.

But the situation is different for the third call: the compile-time type of r is (Int32 | String). Which of these types r will become is only known at runtime because of rand, so which display method to call can only be decided then!

You can also use type restrictions for a splat argument, as you can see here:

 def​ ​method1​(*args : Int32)
 end
 
 def​ ​method1​(*args : String)
 end
 
 method1 41, 42, 43 ​# OK, invokes first overload
 method1 ​"Crystal"​, ​"Ruby"​, ​"Go"​ ​# OK, invokes second overload
 method1 1, 2, ​"No"
 # Error: no overload matches 'method1' with types Int32, Int32, String
 method1() ​# Error: no overload matches 'method1'

Note that the last two calls of method1 are rejected by the compiler.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.106.135