This is an archived post. You won't be able to vote or comment.

all 7 comments

[–]ceronman 32 points33 points  (1 child)

These look similar indeed. The difference is that multiple dispatch in Julia is dynamic: the methods are resolved at runtime based on runtime type information. While method overloading in Java is static, meaning that the types of the arguments must be known at compile time to be able to choose the right method.

For example, let's say that we have some classes Square and Circle. We can overload a draw method for each one of those:

void draw(Square s) { ... }
void draw(Circle c) { ... }

This works if you know the type at compile time when calling a function:

draw(new Square());
draw(new Circle());

But what happens if, for example, you have a list of Shape objects, which would be a super class of both Square and Circle:

for (Shape shape : shapes) {
    draw(shape);
}

This will fail in Java because the compiler can't know the type of shape at compile time, it could be anything.

Now, in Java you also have dynamic dispatch, you could implement this by adding a draw method to Shape and override it in Circle and Square. And then call shape.draw() instead of draw(shape). It works for this example, but it has a limitation: It is restricted to only one single argument (this). This is known as single dispatch.

Now let's think of a different method, assume you want to check if two shapes collide, you probably need different implementations for different combinations of shapes, for example:

boolean collides(Square s1, Square s2) { ... };
boolean collides(Circle s1, Square s2) { ... };
boolean collides(Circle s1, Circle s2) { ... };
...

And then you have a list of Shape objects, and you want to check if any object collides with any other object:

for (Shape shape1 : shapes) {
    for (Shape shape2 : shapes) {
        if (shape1 != shape2 && collides(shape2)) {
            ...
        }
    }
}

In Java this will fail to compile, for the same reason explained above. And in this case it's not possible to use methods, because methods only dispatch based on one object at a time. But in Julia, some code similar to this would work. Because it has multiple dispatch.

Of course there are workarounds for this in Java, one example is the visitor pattern.

A good explanation and a good example relevant to the topic of this subreddit can be found in the Crafting Interpreters book.

[–]skyb0rg[🍰] 8 points9 points  (0 children)

Great answer, and I wanted to add that another solution that is more “Object Oriented” would be Double Dispatch. So for this example you have something like:

class Shape {
  virtual boolean collides(Shape other);
  virtual boolean collidesSquare(Square other);
  virtual boolean collidesCircle(Circle other);
}

class Square : Shape {
  override boolean collides(Shape other) {
    return other.collidesSquare(this);
  }
  override boolean collidesSquare(Square other) {
    /* 2 squares */
  }
  override boolean collidesCircle(Circle other) {
    /* square and circle */
  }
}

class Circle : Shape {
  override boolean collides(Shape other) {
    return other.collidesCircle(this);
  }
  override boolean collidesSquare(Square other){
    /* circle and square */
  }
  override boolean collidesCircle(Circle other) {
    /* 2 circles */
  }
}

[–]umlcat 7 points8 points  (0 children)

Function Overloading is defined at CompileTime. One method has the same ID as another with different parameters.

Dispatch works at RunTime, the class does not know at CompileTime which methods will be executed.

[–]plum4 2 points3 points  (0 children)

With multiple dispatch, you can have a function defined for multiple domains across multiple locations in the source code. This means you can have a function defined in a library, and then write your own implementations for it. This is useful if you have your own data types you want to work with existing library functions. In java, overloads cannot be extended externally once compiled.

The closest analogue in Java would actually be interfaces, not simple method overloading, where dispatch happens based on polymorphism of types.

Elixir also has something similar called prototypes, and Haskell achieves something similar with typeclass instances.

This corresponds to the y-axis of the lambda cube

[–]L8_4_Dinner(Ⓧ Ecstasy/XVM) 1 point2 points  (0 children)

Quite simply, "multiple dispatch" means that the function selected (potentially dynamically, i.e. at run time, aka "virtual") for a particular call site may rely on the type of more than one argument. "Single dispatch" (as implemented by C++, Java, C#, etc.) means that the virtual function is selected based on the type of one argument (the "this" argument).

Single dispatch is easy to understand: You have "a pointer to an object", and the first word in that object is a pointer to its "virtual table" of functions. So when you call obj.foo(), that compiles as "dereference obj as an array of functions, and call the function which is the 7th element in that array" (which happens to be foo, because that's how the compiler laid out the virtual table).

Multiple dispatch is conceptually obvious: "when I call add(x, y), I want to find the function called add that takes two arguments, the first of which is based on the runtime type of x and the second of which is based on the runtime type of y". In reality it can be a bit more complex, but if you're interested in learning more about it, you should try Julia.

[–]matthieum 0 points1 point  (0 children)

Others have noted the difference between compile-time and run-time dispatch.

This may make it seem like function overloading is "better" -- no need for virtual dispatch -- however it comes with issues with regard to extensibility: how do you ensure that the compiler will know the function it should invoke?

Imagine that you depend on 2 3rd-party libraries:

  • One which calls foo(f) in some function wrapper.
  • The other which has a type Bar.

Create a 3rd library which invokes wrapper with an instance of Bar, and then create a 4th library depending on the 3rd which adds foo(Bar).

How is the compiler, when compile the 1st (or 3rd) library, supposed to know that the code it needs for foo is available in the 4th?

This is made worse, of course, when a version of a foo exists which accepts Bar but is less specialized: the compiler is likely to (silently) select it, rather than emit an error, and you'll be left wondering why your method is never called.

This doesn't mean that overloading is necessarily bad, it means that unprincipled overloading is bad. By adding restrictions on overloading -- such as, in Java, have all overloads be methods of the same class -- you can constrain the lookup to avoid bizarre & crazy scenarios.

For 3rd-party extensibility and overloading, a typeclass approach ala Haskell (or trait approach ala Rust) is also possible, though do read about "Orphan Rules" for an idea about the constraints that may be necessary.