How to Learn Julia When You Already Know Python | by DJ Passey | Towards Data Science

Julia is a newer, award-winning programming language that is simple to learn like Python but executes as fast as C. Don’t believe it? It’s really true. (Click here for a multiple language speed comparison.)
Julia offers more than just syntax and speed. To explain why they developed the language, the creators of Julia said:
“We want the speed of C with the dynamism of Ruby. We want a language that’s homoiconic, with true macros like Lisp, but with obvious, familiar mathematical notation like Matlab. We want something as usable for general programming as Python, as easy for statistics as R, as natural for string processing as Perl, as powerful for linear algebra as Matlab, as good at gluing programs together as the shell. Something that is dirt simple to learn, yet keeps the most serious hackers happy. [1]”
The jury is still out, but it feels like they delivered. When Julia 1.0 was released, the bones of a language with the potential to reach most, if not all, of their goals, was born.
At the same time, Julia has a long way to go before it reaches the maturity of mainstream programming languages. Julia’s packages need work and its documentation and learning resources can be improved. Luckily, an active (even zealous) developer community is working on these issues.
Even though the language is growing, there are a lot of reasons to learn Julia, especially if you are interested in machine learning, data science or scientific computing.
Python users will typically be able to pick up Julia syntax very quickly. The syntax is similar to Python and has many conventions that will be familiar to Python users.
However, programming in Julia is fundamentally different than programming Python. Chances are, the first Julia code written by Python users will look and act a lot like Python. While there are no huge problems with this approach, Julia that looks like Python will probably be inefficient and miss out on important aspects of the language.
Julia operates under different paradigms — generic functions, clever dispatch, and thoughtful typing (to name a few) and many of these ideas do not appear in Python at all. Therefore, the goal of this article is to teach three simple and important Julia concepts: The type hierarchy, multiple dispatch and user defined types. These concepts were chosen to help speed up Pythonic Julia programs, illustrate the ways that Julia is different than Python and introduce Python users to new programming ideas.
Therefore, because I am focusing on concepts, I won’t cover installing Julia and learning basic syntax here. For installation and syntax, I recommend the following resources:
The first chapter of Julia Programming for Operations Research by Changhyun Kwon contains an excellent installation and setup guide.
A comprehensive written guide to installation and syntax by J. Fernandez-Villaverde.
Learn syntax from Julia by Example.
Learn syntax from a video by Derek Banas.
Many of Julia’s speed, versatility and composability advantages are due, in part, to the typing system. In Python, types can be an afterthought, so thinking through typing may seem tedious. However, Julia keeps it simple and rewards careful thinking with a speed boost.
Concrete/Primitive Types
Julia uses two different kinds of types: concrete types and abstract types. Each one has different uses. Concrete types include the typical String, Bool, Int64, etc. and are used for standard computations. The type Float64 is a concrete type meaning that Float64 can be instantiated and used in computations.
Abstract Types
The types Union{}, AbstractFloat, Real, and Any are all abstract types. An abstract type cannot be instantiated. Instead, abstract types are containers for grouping together similar kinds of data. They are often used to indicate to the compiler that a function may be called on any subtype of the abstract type.
""" This function accepts Float16, Float32, Float64     because they are all subtypes of AbstractFloat"""function g(a::AbstractFloat)    return floor(Int, a) end
The types Any and Union{} are special. Union{} is predefined to be the subtype of all types. It is the bottom of the type hierarchy. Similarly, every type is a subtype of Any, making it the top of the type hierarchy.
Why Use Abstract Types?
Abstract types are useful because functions defined to act on an abstract type, are able to act on all subtypes of the abstract type.
As an example, suppose a developer needs an array-like data structure. In Julia they can define their own application specific structure and make sure it satisfies the requirements of an AbstractArray type. Then, all functions in the Julia ecosystem, defined to operate on AbstractArray data will work on the developer’s array-like data structure. Because of this feature, many of Julia’s packages work together smoothly, even though they were not designed together.
Contrast this with the Python packages. Almost every package that uses arrays is designed to work with numpy arrays. This creates a huge dependency on numpy. If a programmer wants to create their own array and call numpy functions on it, it will probably raise errors. Very few Python libraries will work with a self defined object. In contrast, abstract types in Julia give developers more flexibility and help make packages more composable.
Operators
The binary operator :: is used to assert that a variable is a certain type. More specifically, the operator can initialize a variable as a specific type, signify that a function argument must be a certain type, or assert that a predefined variable is a specific type. Each of these uses is demonstrated below.
# Initialize a Float64x::Float64 = 100# Argument z must be an Int64function f(z::Int64)   return z^2end# Assert x is a Float64 x::Float64 # (Does nothing)# Assert that x is an Int64 x::Int64 # (Raises error)
It is worth mentioning that we can declare variables or define functions without a type assert, e.g. x = 100. (The variable x will be an Int64 in this case.)
The subtype operator , <:, determines if one type is a subtype of another. If we want to compare the type of two variables x and y, evaluating the expression typeof(x) <: typeof(y) will return true if the type of variable x is a subtype of variable y’s type. As another example, consider the expression:
Union{} <: Float64 <: AbstractFloat <: Real <: Any
This evaluates to true signifying that we have ordered the types correctly in the type hierarchy. (The <: operator can compare these objects because they are types and not variables.)
More Reading On Types:
Julia documentation on types
A great tutorial on types from “Learn Julia the Hard Way”
One of the creators of Julia, (Stephan Karpinski) explaining the type system on Stack Overflow.
This concept is probably the most important concept to understand about Julia. From a development perspective, many of the advantages offered by Julia stem from multiple dispatch.
Multiple dispatch is when a function behaves differently depending on the types of its arguments. It is similar to function overloading but not exactly the same.
Multiple dispatch occurs when a programmer adds type annotations to a function definition. Consider the following example:
We need a function f to square its input and then compute its value mod 4. In Julia there are 3 equivalent ways to define f:
# Verbose definitionfunction f(x)    return x^2 % 4end# Mathematical notationf(x) = x^2 % 4 # Like a Python lambda f = x -> x^2 % 4
Assume that we always need f to output an integer, but its input, x, can be a String, Float64, or Int64, and we won’t know the type of x until runtime. In Python, this is solved with:
def f_py(x):    if type(x) == string:        x = float(x)    if type(x) == float:        x = ceil(x)    return x**2 % 4
In Julia, we could write a function that looked like the Python function above:
function f_py(x)    if isa(x, String)        x = parse(Float64, x)    end    if isa(x, Float64)        x = ceil(Int64,x)    end    x^2 % 4end
However, we would be better off writing:
f(x::Int64) = x^2 % 4f(x::Float64) = f(ceil(Int64, x))f(x::String) = f(parse(Float64, x))
This collection of definitions does the same thing as the Python function f_py. However, the action of f on x depends on the type of x. Each of the three definitions specifies what f will do with a particular type.
If f is passed an Int64, it will square it and mod by four.If f is passed a Float64, it will compute the integer ceiling above the float and call f on that integer. This invokes the integer version of f described in 1.If f is passed a String, it will convert it to a Float64, then call f on the float, which will invoke the float version of f, described in 2. As we already saw, the Float64 version converts to an Int64, and calls the Int64 version of f.
If f is passed an Int64, it will square it and mod by four.
If f is passed a Float64, it will compute the integer ceiling above the float and call f on that integer. This invokes the integer version of f described in 1.
If f is passed a String, it will convert it to a Float64, then call f on the float, which will invoke the float version of f, described in 2. As we already saw, the Float64 version converts to an Int64, and calls the Int64 version of f.
When these functions are broadcasted over an array with 3 million elements of mixed types, the dispatched function finishes in 0.039 seconds. The Python version of f_py is 50 times slower than f. Furthermore, the dispatched function f is twice as fast as the pythonic Julia.
On one hand, Julia is fundamentally faster than Python, but we also see that multiple dispatch is faster than pythonic Julia. This is because in Julia, the correct version of f is determined at runtime with a lookup table, and this avoids multiple if statement evaluations.
As you can see, multiple dispatch is fast, and can be an effective solution to a variety of programming challenges, making it one of the most useful tools of the Julia language.
More on Multiple Dispatch
How Julia Uses Multiple Dispatch To Beat Python. More examples of multiple dispatch from DJ Passey.
The Unreasonable Effectiveness of Multiple Dispatch. A presentation by Stephan Karpinski.
Some thoughts on generic programming blog post by Erik Schnetter.
It may be a shock, but as it turns out, Julia isn’t object oriented. There are no classes, and no objects with member functions. However, by leveraging multiple dispatch and the type system, Julia gains the advantages of object oriented programming languages plus added flexibility.
Instead of objects, Julia uses structs, which are user defined composite types. Structs have no internal functions. They are just collections of named types. In the example below, we define a NBA player struct:
struct NBAPlayer    name::String    height::Int        points::Float64    rebounds::Float64    assists::Float64end
The type NBAPlayer has a default constructor:
doncic = NBAPlayer("Luka Doncic", 79, 24.4, 8.5, 7.1)
Each field can be accessed with familiar dot notation: doncic.name, doncic.height, doncic.points, doncic.rebounds, and doncic.assists.
You can define additional constructors as long as they accept a different combination of types from the default. This is multiple dispatch at work:
# Constructor with no argumentsfunction NBAPlayer()    # Make an empty player    return NBAPlayer("", 0.0, 0.0, 0.0)end
With a struct defined, we can give new definitions to functions in the Julia base:
function Base.show(io::IO, player::NBAPlayer)    print(io, player.name)    print(io, ": ")    print(io, (player.points, player.rebounds, player.assists))end
This defines how the struct NBAPlayer is displayed when it is printed. It is similar to defining a __repr__() function for a class in Python. However, instead of defining internal functions like we would in Python, in Julia we provide new definitions for how external functions should act on the struct.
Python allows developers to determine how certain operators should act on a class with magic methods. Programmers can write their own definition for how +, -, += and others should act on a class. However, this is fundamentally limited by the list of operators with magic methods. In Julia, any function can be given a definition for any combination of types or structs.
Though Julia is easy to pick up, it can be tricky to master. Learning these concepts puts developers on the road to mastering Julia. By practicing and experimenting with these ideas, you can develop the skills necessary to write high quality Julia programs.
[1] J. Bezanson, S. Karpinski V. Shah, A. Edelman, Why We Created Julia (2012), JuliaLang.org