Understanding overload ordering

Trying to understand the order methods will be recognized in the case of overloading.

I have this test case:

class Foo
  def bar(a, b, c)
    "bar1"
  end

  def bar(a, b, c = 1)
    "bar2"
  end

  def bar(a, b = 1, c = 2)
    "bar3"
  end

  def bar(a = 1, b = 2, c = 3)
    "bar4"
  end

  def boo(a) # restriction 4 (least restrictive, but its never used actually)
    "boo1"
  end

  def boo(a : String) # restriction 1 (most restrictive)
    "boo2"
  end

  def boo(a : String | Int32) # restriction 2
    "boo3"
  end

  def boo(a : String | Int32 | Bool) # restriction 3 <- this will be defined last, and so will get used in all cases?
    "boo4"
  end
end

puts Foo.new.bar(1, 2, 3)
puts Foo.new.bar

puts Foo.new.boo(12)
puts Foo.new.boo(true)

which produces:

bar4
bar4
boo3
boo4

okay, now if I switch things around, I would still expect the same result.

but instead I get an error (wrong number of params) unless the definition with all three default params of bar is last.

More over, if I switch the two definitions of boo around it prints boo4 twice. I.e. it doesn’t seem that the ordering is actually doing anything.

class Foo
  def bar(a, b, c)
    "bar1"
  end

  def bar(a, b, c = 1)
    "bar2"
  end

  def bar(a = 1, b = 2, c = 3)
    "bar4"
  end

  def bar(a, b = 1, c = 2)
    "bar3"
  end

  def boo(a) # restriction 4 (least restrictive, but its never used actually)
    "boo1"
  end

  def boo(a : String) # restriction 1 (most restrictive)
    "boo2"
  end

  def boo(a : String | Int32 | Bool) # restriction 3 <- this will be defined last, and so will get used in all cases?
    "boo4"
  end

  def boo(a : String | Int32) # restriction 2
    "boo3"
  end
end

puts Foo.new.bar(1, 2, 3)
# puts Foo.new.bar <- this no won't work

puts Foo.new.boo(12)
puts Foo.new.boo(true)

produces

bar3
boo4
boo4

Hi there!

Good questions. I took the liberty of editing your post to include code sections, otherwise it was very hard to understand.

  def bar(a, b, c)
    "bar1"
  end

  def bar(a, b, c = 1)
    "bar2"
  end

The second overload overrides the first one. Give that you are defining a method with 2 required arguments and one optional, if you pass 2 arguments it will call that method. If you pass 3, it will call that method too!

I mean, if you pass three arguments, if the first overload is chosen by the compiler, what’s the point of having a default value for the third argument if it’s never going to be used?

The same logic applies to the following overloads:

  def bar(a, b = 1, c = 2)
    "bar3"
  end

now you have two optional arguments, that overrides the previous overload.

  def bar(a = 1, b = 2, c = 3)
    "bar4"
  end

Same. This is the only overload that remains.

def boo(a) # restriction 4 (least restrictive, but its never used actually)
    "boo1"
  end

  def boo(a : String) # restriction 1 (most restrictive)
    "boo2"
  end

  def boo(a : String | Int32) # restriction 2
    "boo3"
  end

  def boo(a : String | Int32 | Bool) # restriction 3 <- this will be defined last, and so will get used in all cases?
    "boo4"
  end

When you have String | Int32 vs String | Int32 | Bool, if you pass an Int32, which one should win? It’s not clear. That’s why the compiler just leaves them in order and when you invoke the method with Int32 it will check each of them. Of course the first one wins.

When you pass bool, the last one is the one that has the Bool restriciton.

The first overload without restrictions will be called if you pass a different type, like Char:

Foo.new.bar('a') # => "boo1"

okay, now if I switch things around, I would still expect the same result.

This is incorrect. The order matters. In case there’s no clear “this is more strict than this”, the compiler will try to match overloads in order.

In general, my advice is to avoid such code because it’s confusing for you and for everyone. There’s really no need to define such overloads.

@asterite Thanks for the reply, and sorry for the bad formatting (I was typing the original message on a cell phone :-) )

The reason I am asking these questions is that I am working away on a Crystal -> JS compiler, and so need to understand these semantics. Even though as you say in real life these kind of overloads should be avoided, its important that the code runs the same regardless of the code gen backend!

Anyway after digging a bit more I think I have better clarity on what is going on here with the “restrictions” and “restriction_of” methods.

Thanks!

1 Like