As 2018 gets under way, chances are that if you’ve heard of Julia you know of it as a programming language for science, finance, and AI research. It is true that each of these fields have adopted Julia and benefitted greatly from its features, but I feel that Julia represents an interesting, new approach to programming that could improve the way we all write code. Much has already been written (especially in the official documentation) about Julia’s take on types, optional typing of functions, and organization around type-based multiple dispatch. With this post, I want to look at five lesser known features that I believe make Julia a joy to work with, even as a generalist developer.
Among the languages that inspired Julia is Ruby, so it should come as no
surprise that Julia adopted one of Ruby’s more iconic features: the do...end
block. As is the case with many other features that Julia has adopted, its
implementation of do...end
is simpler and more flexible than Ruby’s.
In Ruby using do...end
creates a Block object that is callable by invoking the
yield
keyword or is made available as an argument to the method if the
terminal method parameter is decorated with an &
. Julia instead defines an
anonymous function from the do...end
block and passes it as the first argument
to the immediately preceding function call in all cases. Since Julia, unlike
Ruby (but like Python), can operate on functions directly, this works quite
simply.
One other slight difference with blocks in Ruby is the dropping of the pipe
characters to delimit the block’s parameters. Instead, everything from do
to
the end of the line is parsed as the block’s parameter list. This does mean that
single line do...end
blocks are not possible (though they were always
uncomfortable, at best, in Ruby). Julia makes up for this by also adopting an
anonymous function literal form almost identical to Java 8’s.
(Note that ##1#2
is just Julia’s way of representing anonymous functions.)
All of these forms are equivalent and, since the release of Julia v0.6, perform identically.
For many programming languages, support of the full Unicode character set in identifiers (variable and function names) is useful for little more than the occasional joke or obfuscated code competition. Julia, on the other hand, has done an admirable job of supporting Unicode, not only in identifiers but also, for certain characters, as operators.
To understand why this is an advantage, consider the case of division. In Ruby,
Python, and even C, the value of 4 / 3
is 1
! If you want something closer to
the actual value of 4/3, one or both of the values must be a floating point
number to start: 4 / 3.0
results in 1.3333...
. One might argue that this is
a long-standing tradition in programming languages, and so it’s “no big deal”,
but it’s wrong and complicated and leads to bugs. If, instead of number
literals, you’re evaluating a / b
you can’t know the type of the result
without knowing the explicit type of each argument.
Julia avoids this whole morass by having two division operators: /
and ÷
.
Using /
will always give the closest floating point approximation of the
division, even if both operands are Integer
types going in. The thought being
that this is most often what programmers expect from a / b
. Using ÷
performs
truncated Integer
division. (It’s worth noting that there are two more
division operators: //
which performs Rational
division, and \
that
performs a left division.)
Of course, Unicode operators aren’t going to be much use if you have to go
hunting in some Emoji pallet each time you want to insert one. Here, Julia is
also extremely helpful. At the REPL, inserting a Unicode character is as simple
as entering a \
followed by the symbol’s abbreviation, and pressing <TAB>
.
So for the division example above, you’d start with 4 \div
, press <TAB>
, and
then enter 3
for: 4 ÷ 3
. The Julia packages for Vim, Emacs, Atom, and more
(all available from the excellent
JuliaEditorSupport GitHub organization)
support the same ability so that entering Unicode characters in your editor
should work the same as the REPL.
Even though Julia is much more generally useful for non-numeric programming
tasks than languages such as R and Matlab, it does still include a number of
features that make working with matrices convenient (such as being able to
trivially transpose a matrix with the '
operator). In keeping with Julia’s
design philosophy, though, even these conveniences are simply implemented in the
most flexible way possible (to see just how flexible, consider a recent
proposal to repurpose the transpositon operator for method
currying).
The most recent example of a feature originally intended for numeric programming
that turned out to be more generally useful is that of the “dot” broadcast. If
you’re familiar with the functional programming concept of map
, then
broadcast
will seem familiar…but different. The essential difference between
map
and broadcast
is that when broadcast
is applied to combinations of
iterable and scalar values, it effectively repeats the scalars to match the
dimensions of the iterables. (This will become clearer, I hope, with some
examples below.)
Julia has had a broadcast
method for quite some time. What was added recently
was the ability to turn any operator or any function into a broadcast function
with the addition of a humble .
. For functions, the .
goes between the
function name and the open parentheses. For operators, the .
goes before the
operator. To understand why broadcasting is important, consider a simple
function to square a value:
Squaring a matrix is a very different operation than squaring each value in a
matrix. Broadcasting the square
function allows us to easily do the later.
Bringing scalars into the mix, we can see the clear difference between
broadcasting and map
ing:
The true power of the dot broadcast comes, however, when you begin forming chains. It takes a bit of getting used to, but it has the potential to drastically simplify any code you write that operates on collections.
To take it one more level, the |>
operator in Julia (currently) does function
chaining. Combining this with the dot broadcast reveals Julia’s true power and beauty.
There is a good reason that, despite it’s relatively young age and modest
community size, Julia already sports a healthy package ecosystem. It is because
Julia has the development, distribution, and support of packages at its heart.
Out of the box, Julia includes the notion of downloading and installing 3rd
party packages. It will even helpfully tell you the first time you try using
Foo
that you probably first need to run Pkg.add("Foo")
.
What really makes Julia special is that the default Pkg
module is extended by
the PkgDev
package to include facilities for developing new packages.
Notice that this doesn’t just generate a project skeleton, but prepopulates that skeleton with files to manage Git ignores, CI for macOS, Linux, and Windows, and code coverage metrics. For this reason, it should come as no surprise that Julia packages tend to be extensively tested with significant coverage. Newly generated packages are also automatically placed in the correct location to be used immediately.
If you happen upon a bug in someone else’s package, Julia also makes it easy to contribute back.
Calling checkout
will temporarily decouple the package in question from the
normal dependency and version resolution process, so that you can make whatever
changes are necessary and submit a pull-request with your fixes. Once that’s
done, calling Pkg.free("Requests")
returns the package to normal version
control.
One of the more powerful and under appreciated features of other LISPs is the concept of a reader macro. These allow a developer to manipulate how the language is parsed at a fundamental level, enabling the creation of custom literal forms. While Julia doesn’t have proper reader macros, it does have the next best thing: string macros.
Essentially how these work is that, if you define a macro whose name ends in
_str
, then it can be used to decorate a string literal. Instead of parsing
that literal directly as a string, the string is first passed to the
corresponding macro and whatever type it returns is substituted in place.
String macros can also accept additional arguments following the string their invoked with (with apologies for the contrived example):
In fact, although Julia’s use of r"foo.*bar"i
to construct Regular Expression
literals is very reminiscent of Python, this is implemented as a simple string
macro, not a parser hack. A number of Julia packages have already made great use
of this feature. For example, the Cxx.jl package has a string macro for
compiling C++ code within a Julia function.
Hopefully by now I’ve convinced you to give Julia another look as 2018 gets underway, but if you need some additional motivation, here’s one more reason: it is highly likely that Julia will hit v1.0 in 2018.
Predicting software milestones is notoriously fraught, doubly so when it’s open source. That said, Julia is well into the home stretch. One more pre-1.0 version will be released (v0.7) with all the same features as v1.0 but retaining deprecation warnings for everything that’s changed since v0.6. Then, once everyone has had a chance to validate their packages and eliminate deprecated code, the warnings will be removed and v1.0 will be christened…or, at least that’s the plan.
The core team have announced their intention to adhere strictly to semantic versioning of Julia, so any library developed against v1.0 will continue to work for the duration of Julia v1.X’s lifetime. This does not mean that v1.0 will be perfect, but it will be stable. In fact, in a number of instances the core team has opted not to include features in v1.0 with the reasoning that it is easier to add features in v1.1 than to have to wait until v2.0 to remove them.
The Julia community is still on the small side, but it is growing. There is a healthy collection of libraries already, and relatively simple means for incorporating libraries from C, Python, and Java (and slightly less simple means for calling out to C++). If you’re looking for a new language to learn in 2018 that will both help you grow as a programmer and provide an opportunity for you to have a non-trivial impact on the future of the language, Julia is definitely worth a look!