How Julia Perfected Multiple Dispatch | by Emmett Boudreau | Towards Data Science

Although the Julia programming language is new and not quite popular, at least for now, this does not stop the language from achieving some pretty spectacular feats. The Julia programming language is a lot more unique than many users or viewers even understand, as it resides in its own paradigm. This paradigm is awesome in my subjective opinion, and I have really fell in love with the fantastic type system and parametric polymorphism in the programming language. Julia Computing has dubbed this paradigm
“ The Multiple Dispatch Paradigm”
Okay, so not the most creative naming, but there are so many great reasons to use this paradigm, and with it the Julia programming language. For me, multiple dispatch is a very natural way to program, especially when it comes to scientific computing. On top of that though, there are several things that the language has done to make multiple dispatch even better than it has ever been in any language prior. Before we get into multiple dispatch, one more thing; all of the examples in this article are going to be taken from a package I am currently working to release, it is an object-oriented DataFrames package that makes a bridge between options like DataFrames.jl and Pandas.py in Julia. If you would like to learn more about, contribute, or support this package (thank you,) you may check the package out here:
github.com
Before we touch on what is so great about the way the Julia language has expanded and worked with multiple dispatch across the board, let us first touch on what exactly multiple dispatch is. Multiple dispatch is a generic programming concept that was originally introduced to solve a problem in the functional programming paradigm. This problem was that there were simply too many method names, when methods are not private to the types we are working with, we must have a separate call for each type, even if said functions do the same thing.
For example, let us pretend we have our own functional programming language, and we want to get the length of an iterable array, we would call:
length(array)
Now let us say we also wanted to get the length of a the chars in a string — since functional programming primarily focuses on declarative, and more vitally — GLOBAL — programming, we can only have one definition of length. Since we have already used this definition on the array, we would need to change this function’s alias:
length_str(string)
Needless to say, specifying what type you are about to pass inside of a method call is just a tedious and disgusting interaction between programmer, methods, and types. This is where multiple dispatch comes in. With multiple dispatch, the methods can be defined specifically to work with the type of provided arguments, like our example before:
length(array)length(string)
And all we need to do is add the specific type we are writing to into our method definitions in order to facilitate this. If you would like to learn a bit more about why multiple dispatch is so cool, and a few more technicalities on the subject, I have an article with more detail one may read here:
towardsdatascience.com
Also, that was published almost two years ago — why.
One thing that definitely makes multiple dispatch convenient is one-line definitions. In Julia, the function key-word is optional. Julia can tell we are calling a method whenever we put parenthesis after an alias, e.g.
example()
If we were to run this, we would get the same error as without the parenthesis, however with a stack-trace, because the Julia compiler has now noticed that we are not necessarily working within the global scope, as it has evaluated that we are working with a function that has its own scope. This is an interesting difference, check it out:
All of this creates the ability to not only quickly create functions in one line, but also change function calls in one line. Consider the following example from OddFrames.jl where our functions that will become children of our type are defined:
head(x::Int64) = _head(labels, columns, coldata, x)head() = _head(labels, columns, coldata, 5)# Dropdrop(x) = _drop(x, columns)drop(x::Symbol) = _drop(x, labels, columns, coldata)drop(x::String) = _drop(Symbol(x), labels, columns, coldata)dropna() = _dropna(columns)dtype(x::Symbol) = typeof(coldata[findall(x->x == x, labels)[1]][1])dtype(x::Symbol, y::Type) = _dtype(columns[findall(x->x == x,labels)[1]], y)
In many of these examples, we see that the arguments change from the first to the second definition. In the first instance, we provide data from the type into the function, along with the provided value. In the second instance, we provide a default value for x so there are no input arguments. What is so valuable about these examples is that some of these input arguments even need to be changed before we can actually provide them. For example, consider the dtype() function.
dtype(x::Symbol) = typeof(coldata[findall(x->x == x, labels)[1]][1])
In this example, if we were to simply provide the symbol to get the data-type, we would have a huge problem. As the symbol we need is not only in a different array to the data, but we also do not know which array! Fortunately, we can do all of the writing for this in one line, and the resulting code even looks clean!
Probably my favorite implementation of multiple dispatch into the Julia programming language is that of dispatch. Consider the constructor for our Immutable OddFrame (I chose this because it is a lot shorter.)
struct ImmutableOddFrame <: AbstractOddFramelabels::Array{Symbol}columns::Array{Any}coldata::Array{Pair}head::Function
This is what is called the outer constructor. We can call this outer constructor directly, and the return will always be the type ImmutableOddFrame. In order to make such a call, however, we would need to provide
labels
columns
coldata
head
all by ourselves. How on Earth are we supposed to do that? Especially given that head() is a function. Are we seriously meant to write our own functions in order to use this type? Of course not. Furthermore, the data inside of this type is essentially only for the actual module to utilize in such a way, not the end-user. The end-user simply needs to provide some much more high-level declarative data. For example, the file-path of the file they would like to load data from:
function OddFrame(file_path::String)# Labels/Columnsextensions = Dict("csv" => read_csv)extension = split(file_path, '.')[2]labels, columns = extensions[extension](file_path)
This is all done through dispatch, and the calls are essentially the same for both the inner constructor and the outer constructor — just the types passed are different. Needless to say, this brings constructors to an entirely new level of power! If you would like to learn more about constructors, you may either read this article or watch this video if you desire:
towardsdatascience.com
Needless to say, Julia has changed the game completely, not just in the much smaller world of multiple dispatch applications of software, but also in the world of programming overall! The multiple dispatch paradigm really does bring multiple dispatch programming to an entirely new level. The concept is taken so much further and with so much power with the abilities of constructors. From this whole article, it is probably the constructors that make the most significant impact here. Thank you very much for reading, it really does mean the world to me! I hope this article was interesting, and filled you in a bit more on the multiple dispatch paradigm — which is awesome!