The main difference between multiple dispatch and function overloading (esp. as implemented in C++/other OOP langs.) is that overloaded functions still generally have an implicit receiver of fixed type, which constrains method dispatch to only those variants defined for the receiver. Julia, on the other hand, being more of a functional language, dispatches on all arguments.
The classic way of demonstrating this is to imagine you are implementing a game of Asteroids™. As part of the implementation, you need to provide methods for what happens when two game objects collide. So, at first you define (in, for argument’s sake, Java):
public class Asteroid {
public void collideWith(Asteroid other) { /*...*/ }
public void collideWith(Ship other) { /*...*/ }
}
public class Ship {
public void collideWith(Asteroid asteroid) { /*...*/ }
}
Now, let’s imagine that you’re implementing the sequel: Moar Asteroids™. In this version you introduce bullets that the ship can fire. So now you have to add:
public class Asteroid {
/*...*/
public void collideWith(Bullet bullet) { /*...*/ }
}
public class Ship {
/*...*/
public void collideWith(Bullet bullet) { /*...*/ }
}
public class Bullet {
public void collideWith(Asteroid asteroid) { /*...*/ }
public void collideWith(Ship ship) { /*...*/ }
public void collideWith(Bullet other) { /*...*/ }
}
Or, in other words, 5 new methods for one new type. Let’s say the sequel is so successful that you are implementing the third game in the series: Space Rocks™, and in this version there are also missiles…
Ok, I won’t bore you with an even longer code listing, but if you work it out you’ll see that to add a 4th type of game object you now need to implement 8 new methods, and this number will continue to grow as you add more types.
Now consider the alternate implementation of in Julia, making smart use of multiple dispatch and optional typing:
# Asteroids™
abstract type GameObject end
struct Asteroid <: GameObject end
struct Ship <: GameObject end
# Collisions are symmetric
collide(a::GameObject, b::GameObject) = collide(b, a)
collide(first::Asteroid, second::Asteroid) = #...
collide(asteroid::Asteroid, ship::Ship) = #...
# Added for Moar Asteroids™
struct Bullet <: GameObject end
collide(first::Bullet, second::Bullet) = #...
collide(bullet::Bullet, asteroid::Asteroid) = #...
collide(bullet::Bullet, ship::Ship) = #...
# etc...
As you can see, the major difference is that we only need to consider each possible set of argument types once, since we can dispatch on all the arguments instead of just all-but-the-first as in Java.
Now, could you write an overloaded C++ global function that effectively does the same? Um…maybe? (Honestly, not sure, my C++ is a bit rusty in this regard.) Technically, I suppose the difference between “multiple dispatch” and “function overloading” is a bit semantic. The pragmatic difference is how each is used, as above.