Why is multiple dispatch a feature?

Multiple Dispatch is prominently advertised as one of the main selling points of Julia. But I am not even sure if I would consider it a feature at all. So maybe I don’t quite understand it yet.

My background: I am a Maths (especially Probability Theory and Statistics) student, and since statisticians love R and think of Julia as R’s Successor (not Python), I had a look at Julias documentation and stumbled upon Multiple Dispatch.

And people seem to rave about Multiple Dispatch, even though it is one of the things I hate about R. You see, having a lot of contact with R, I started to dislike programming itself until I did an Internship were I spent two months programming in Java. After which I thought: Programming isn’t that bad, let me pick up a small R project. And I realized that I seem to simply dislike R. So I tried to figure out why.

So here is one example of a particularly frustrating moment with R, and I will discuss why I think that multiple dispatch is at fault.

In an exercise, we were supposed to use a package and simulate random variables according to a model.
So I would download the package with

library(package)

Now in an object oriented language, you could just type

package.

and the IDE could suggest you things included in the package. In R you type

?package

and get the documentation. Then you have to hope, that the thing you want to use is documented there. In my case it is not. But thankfully the exercise suggested the function I was supposed to try so this was not a problem for me. Anyway, so I figure out how to generate random variables according to a model and save it into a variable.

y<-simulate(model, x)

So now I want to see what I have generated. So I type

y

and get

Object of class gridDataFrame
Grid topology:
 cellcentre.offset cellsize cells.dim
                 1        1       400
Points:
    coordinates   variable1
1             1 -0.04098571
2             2 -0.09039331
3             3 -1.42573822
4             4 -0.60306120
5             5  0.47389710
6             6 -1.63860592

Okay neat, I want to plot it! Let’s extract the columns. No idea what this object is, but let’s just try the usual:

> y[2]
Error in `[.data.frame`(x@data, i) : undefined columns selected
> y$coordinates
Error in y$coordinates : $ operator not defined for this S4 class

hm, a class? how about

> y.coordinates
Error: object 'y.coordinates' not found

Shit, I guess google might help. Oh, apparently getSlots can be used on S4 classes.

> getSlots(y)
Error in getSlots(y) : 
  no slot of name "slots" for this object of class "gridDataFrame"

Right… Back to google… Oh, so how about:

getSlots("gridDataFrame")
          data           grid      .params 
  "data.frame" "GridTopology"         "list" 

What is this supposed to mean?
Okay, let’s try

?gridDataFrame

Oh, there is a method called “coordinates”, which returns the coordinates. Great!

> coordinates(y)
       [,1]
  [1,]    1
  [2,]    2
  [3,]    3
  [4,]    4

So let’s get the other column too then:

> variable1(y)
Error in variable1(y) : could not find function "variable1"

WTF? Okay, I guess back to the documentation. Hm, there seems to be a method called “as.data.frame”

> as.data.frame(y)
      variable1
1   -0.04098571
2   -0.09039331
3   -1.42573822
4   -0.60306120

Weird it seems to have deleted the column with the coordinates. I guess it doesn’t matter I know how to extract the coordinates and I know how to get a column from a dataframe!

plot(coordinates(y), as.data.frame(y)$variable1)

In total it took me half an hour to simply extract the columns from an unknown class. This is incredibly frustrating! So why did that happen? I think it is because R does not have proper encapsulation.

If objects were actually boxes of things like in other programming languages, then I could just write

object.

and the IDE could start guessing what I would possibly want to do. The IDE can not guess a method though, if you have to write the method before the object.

So due to Multiple Dispatch and Dynamic Typing, the IDE is pretty much helpless and leaves you on your own. And since all methods are just floating about not belonging to one object, everything becomes a soup of unstructured dread.

So with this realization about R I looked at Julia (talked about as the successor to R) and was surprised that people raved about multiple dispatch which caused so much pain. So what makes that pain worth it?

You can pass different objects to it? So why not just call it a function in those cases instead of a method? (not belonging to an object?). You just need the concept of an interface like you have in Java, and you could pass anything which implements that interface into the function. The popular example seems to be collisions. So why not write an interface: collidable and pass collidable object into the function

collide(collidable s, collidable b)

What is the advantage of multiple dispatch over that? And more applicable things: Everything could just implement the plotable interface. And then you could just use

object.plot()

on an object and you would know whether or not it will work once you type

object.p

as the IDE will start suggesting you .plot() if available. While you have to actually try it (and let the error message clutter your console if it does does not work) in a multiple dispatch language.

3 Likes

Multiple dispatch does not mean no name space. What you are complaining is a combination of dynamic typing and lack of namespace and it’s also about a issue with ide rather than the language. In another word, what you are complaining about is neither about multiple dispatch or Julia. It’s just that multiple dispatch makes the support for namespace less necessary for the language.

Multiple dispatch does have many other nice aspect and that’s what’s some people are selling. It doesn’t mean it’s flawless. I don’t think anyone (or many of them) likes multiple dispatch due to the problems you have and there are issues acknowledging the problems you are talking about. It’s certainly not purely bad as someone that only read what you’ve said would have believed.

19 Likes

I don’t think I can say it better than Stefan:

In short: it’s linguistic (detangles nouns vs. verbs), solves the expression problem, and facilitates code reuse beyond what we’d imagined.

27 Likes

I also use R a lot from a a non-computer scientist background and while I learned to like R for data analysis, programming with it was always challenging. Julia is great for its syntax and for the level of code abstraction you can get, and that’s, I think, were multiple dispatch really shines. However, it’s a different paradigm, specially for people like me who doesn’t know a bunch of different languages and have very little to compare Julia too. But once you start realizing that your problem needs to be approached a little differently than what you normally do with R, you can basically avoid exactly what you mentioned in R, which is that you can inherit different methods once you define the type and Julia just makes that work. And it’s crazy efficient.

So, for example, in R you get dataframes, tibbles and data.tables. And all of them are basically the same data structure, but since they were defined differently by different developers, methods don’t always work the way you expect. Multiple dispatch in Julia allows you to define basically an abstract table, add instructions for some basic methods that are important for tables, and then you might have a bunch of “free” other methods that other developers have created for tables, you just need to make sure the function knows your new type is an abstract table. And once you get enough people following the same instructions, things work as you’d expect with your example in R.

There’s an aspect of community that’s important here, and those abstract frameworks usually need groups of people defining what’s the basis of a data structure, what methods are the minimum requirement, etc, but that’s already happening. And new developers quickly realize than you don’t have to reinvent the wheel every time you create a new type and that saves a lot of time for them.

Again, I’m not an expert, but I think you need to play with it a little more and find the power that comes with it.

Also, if I said something wrong about my understanding of multiple dispatch, please correct me. And yes, Stephan’s talk in JuliaCon 2019 is pretty good.

6 Likes

I agree, dynamic typing is part of the problem, but I can still get suggestions from the IDE in python, if the variable is already filled with an object (i.e. after variable assignment).
But since multiple dispatch means, that method names can be reused and different objects can define behavior of that method if they get passed to it, multiple dispatch seems to necessitate the lack of namespaces. So I don’t see how that is not related.

And I disagree that the IDE is the issue, since the IDE can not possibly guess what method I want to use if the method comes before the object. So the syntax causes the problems the IDE has. And it is not just a problem of the IDE.

And the question is exactly what those nice aspects are.

There is definitely a valid point here about discoverability of methods in OOP vs multiple dispatch. Being able to do obj.<tab> and have immediately the list of methods attached to that object is amazing, and is one of the strong points of OOP. It’s definitely not as easy to do in julia; methodswith does not answer the problem. It could conceivably be mitigated with some tooling however. For instance, one could hack the REPL completion code so that if on write (obj, put the point before the parenthesis and press tab, it shows the list of methods for that object. I remember I played around with that some time ago, but didn’t follow through.

7 Likes

Yes, lets not waste time arguing against the downsides.
And why they may or may not be real, or caused by the things you think they are.
That has been discussed many times, and we can just link to the last discussion.

More interesting is explaining the upsides.

2 Likes

Since he claims that the first thing is easy in OO anyway, no need to talk about that.
So is it hard to apply new operations to existing types? I mean if it is statically typed, then it could mean that you need to create a common interface for all those existing types, which could be annoying. But in a dynamic programming language, you can also just create a function which just does things to these objects. Of course you need to assume some similar interface.

But you also have to do that in case of multiple dispatch. If there is no common interface, then you also can’t really define a new operation on existing types with multiple dispatch. Right?

So I guess this would be a case for dynamic programming. Although I am not sure if it is really that good if you can simply define new operations on existing types, essentially forcing those types to keep their interface in order to allow you to do that. If you would then change the interface of that type, a lot of things would break. And if you haven’t even defined such an interface like in a static programming language, you would not even notice immediately.

So in some sense this just makes things more entangled not less. Or am I wrong?

Yes, which is why I said a combination of two factors. And I mentioned it because you mentioned it…

No, it only means methods/verbs that doesn’t belong to an object can’t be namespaced. In another word, it means that you have to have your problem when you write something that’s hard to express without multiple dispatch. For things that works for you in other languages there doesn’t have to be a difference.

Well, that’s not the case. The ide can be made to autocomplete a function after you typed part of the argument. It could even be made to not require moving cursor. That’s a ui issue.

I don’t really want to repeat what you’ve read elsewhere and I just want to point out that your problem is definitely not tied to multiple dispatch. I personally find multiple mainly needed for a small percentage of cases outside Base for the kind of programming that I do and I won’t say it’s easy to appreciate before you experience an case yourself.

2 Likes

Agreed. The following idea came up on Slack some time ago:

You type (foo, bar, <tab> or (foo, bar)<tab> and get a list of methods that take foo, bar, … as arguments. If you select one of those we insert the method before the parenthesis and return the cursor to where it was, so you’d end up with somemethod(foo, bar, | or somemethod(foo, bar)|.

That should be fairly easy to implement, but there still is the issue that the list of methods you are going to get will be big – simply because duck typing is so prevalent in Julia.

9 Likes

That just sounds like inheritance. You would get all the free functionality too. Since you can pass children into function parameters requiring a parent.

What you describe sounds like an interface specification. If you code for a certain interface, then you can pass your new types into existing functions and expect them to work. This interface specification can also be more easily enforced, if an interface is an actual thing in the programming language like in java.

Lets consider NamedDims.jl

NamedDims.jl difines NamedDimArray where each dimension has a name.

The rule of NamedDImsArray’s is that if yuou do some binary operation between two NamedDimArrays
(Say +, or -, or broadcasted (elementwise) multiplication),
then it is an error unless the names agree.
But if you do it between a NamedDimArray and any other array, then it works and the result is a NamedDimsArray.

So there are 2 dispatches there (ignoring order), one for (::NamedDImsArray, ::NamedDimsArray) and one for (::NamedDImsArray, ::AbstractArray)
And that is great and works in general pretty well.

But then say you had a package that was similar to NamedDimsArray but also has SpecialIndicies for each dimension as well as names, and both have to agree for binary operations.
AxisArrays.jl is like that
So it has similarly (::AxisArray, ::AxisArray) and one for (::AxisArray, ::AbstractArray)

And if you want to use them together they will argue about who wins.
But we can just add a dispatch for (::AxisArray, ::NamedDimsArray) that says what to do.
And the answer here is: It should check the names agree, but not worry about the specialindicies.

Similar Tracker.jl adds a TrackedArray which is like an Array but also tracks what operations have been done for purposes of Automatic differnetiation.
And so it overloads binary operations too, (::TrackedArray, ::TrackedArray) and one for (::TrackedArray, ::AbstractArray).
It will also not know what to do, if used with a NamedDimsArray
but we can add a dispatch for (::TrackedArray, ::NamedDimsArray)
and tell it that it should track the operation and give the result the names.

Sure it might sound bad that have to implement methods saying how to do this things to make them work together.
But without multiple dispatch it would be basically impossible to have these work together.
It would not be extensible, so
It would all have to be in one package that supported everything (or written very very cleverly).

And this is why the NamedTensors in Pytorch is such an impressive achievement,
whereas NamedDims.jl is something I cobbled together to help trackdown a bug.

This is what multiple dispatch gives you.
Everything can be made orthogonally, and stil also the corner cases (where those orthogonal likes cross) can be solved.

21 Likes

You need to then specialize for each concrete type of collider vs other type by adding code to each of those classes. i.e. add 1 new collider type => edit n collider files. Way more code, need access to all collider code ( you may not have written it ), must recompile, compiler can’t optimize easily.

compare julia to java here: Multiple dispatch - Wikipedia

3 Likes

In addition to the great explanations provided by others: multiple dispatch in R is not user- or developer friendly. S4 is probably closest to Julia (because it was inspired by Dylan), but it is just one of the 3 similar mechanisms, and is considered an advanced topic. Consequently, many package authors don’t invest in learning about it and most R users are unaware of it.

So if that’s your first encounter with the idea, then I am not surprised that you are surprised how Julia users relate to the feature. In comparison, Julia’s multiple dispatch is seamlessly integrated into the language and has zero overhead.

Instead of discussing the concept in the abstract, I would recommend just watching Stefan’s video linked above, and using Julia. You will soon be one of the people who rave about it (hopefully 1/c here, and not 1/a).

1 Like

I don’t completely get this. What happens if the objects in question haven’t implemented the collidable interface?

to add on this narrative, often, OO language would have a class collidable_obj, and then say, car and bus inherit from it. The problem is that if you want to build things on top, you either have to convince author to add something to handle your class, or you copy source code and do it yourself.

But in multi-dispatch paradigm, you can just extend the collide function to collode(a::collidable_obj, b::your_obj), which boosts the re-usability in science domain dramatically.

The assumption here is that you care for data flow (i.e. outcome of function rather than change of internal states), so clearly if we’re talking about collision in game engine or something, OO would make more sense to a lot of people.

5 Likes

But couldn’t multiple dispatch handle changes in internal state just as well as OO?

2 Likes

not as ‘intuitive’, in terms of mental picture and code base separation, some people would argue.

1 Like

I’m not quite convinced. More familiar, probably.

1 Like

That is an interesting idea and would certainly alleviate this problem a bit. But it won’t be quite as good:

  1. This discoverability feature is itself not very discoverable. If you just write normal code, then you would never notice that this feature is there. Since it only kicks in, if you don’t write the method and just start with a parenthesis, and know that you can use tab to get suggestions. In the OO case, you just write the object as if you did not want to use any feature and the suggestion list just pops up when you write the dot.
    This might sound trivial, but using LaTeX for example, I don’t really use macros where you need to type some shorthand for something and hit a certain button so that it creates the \begin{…} \end{…} wrapping for it. Because I always forget them, and they aren’t really suggested to me. But I do take the suggestion of adding the corresponding \end{} to the \begin I typed.
    So whether or not the feature presents itself to you, or whether you need to find it somewhere is a big difference. In a sense it is the difference between vim and other editors. And there aren’t that many vim users.
  2. You need to decide when to trigger the suggestions. In the OO case, the suggestions start appearing when using the dot. If the list is too long, you can try guessing the first letter and see if the list is now short enough. You can also delete that letter and go back to the larger list and try a different letter. If you have to press tab, you can’t quite play with it as interactively.
    I guess you could move the cursor into a searchbar of the list when you press tab to help with that though.

So I would say, that while it would certainly help, it won’t be quite as natural as the OO version. And the second issue I have with this is: It does not exist yet.

2 Likes