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.
18.119.106.135