Joshua Ballanco
Posted on January 9, 2018
This post was first published on my personal blog as Five (More) Reasons to Check Out Julia.
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.
#1: do...end
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.
julia> function mymap(f, coll)
println(typeof(f))
map(f, coll)
end
mymap (generic function with 1 method)
# With a `do...end` block
julia> mymap(range(1, 4)) do x
x * 2
end
##1#2
4-element Array{Int64,1}:
2
4
6
8
# Using an anonymous function
julia> mymap(x -> x * 2, range(1, 4))
##3#4
4-element Array{Int64,1}:
2
4
6
8
# Passing a function directly
julia> double(x) = x * 2
double (generic function with 1 method)
julia> mymap(double, range(1, 4))
#double
4-element Array{Int64,1}:
2
4
6
8
(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.
#2: Unicode Operators
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.
#3: dot Broadcast
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:
julia> square(x) = x * x
square (generic function with 1 method)
julia> square(2)
4
julia> a = reshape(range(1, 9), 3, 3)
3×3 Base.ReshapedArray{Int64,2,UnitRange{Int64},Tuple{}}:
1 4 7
2 5 8
3 6 9
julia> square(a)
3×3 Array{Int64,2}:
30 66 102
36 81 126
42 96 150
julia> square.(a)
3×3 Array{Int64,2}:
1 16 49
4 25 64
9 36 81
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:
julia> dotjoin(a, b) = "$a.$b"
dotjoin (generic function with 1 method)
julia> searchengines = ["google", "bing", "duckduckgo"]
3-element Array{String,1}:
"google"
"bing"
"duckduckgo"
julia> dotjoin.(searchengines, "com")
3-element Array{String,1}:
"google.com"
"bing.com"
"duckduckgo.com"
julia> map(dotjoin, searchengines, "com")
3-element Array{String,1}:
"google.c"
"bing.o"
"duckduckgo.m"
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.
julia> httpify(a) = "http://$a"
httpify (generic function with 1 method)
julia> wwwify(a) = "www.$a"
wwwify (generic function with 1 method)
julia> comify(a) = "$a.com"
comify (generic function with 1 method)
julia> httpify.(wwwify.(comify.(searchengines)))
3-element Array{String,1}:
"http://www.google.com"
"http://www.bing.com"
"http://www.duckduckgo.com"
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.
julia> searchengines .|> comify .|> wwwify .|> httpify
3-element Array{String,1}:
"http://www.google.com"
"http://www.bing.com"
"http://www.duckduckgo.com"
#4: Pkg.generate()
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.
julia> Pkg.add("PkgDev")
julia> using PkgDev
julia> PkgDev.generate("MyAwesomeLib", "MIT")
INFO: Initializing MyAwesomeLib repo: /Users/jballanc/.julia/v0.6/MyAwesomeLib
INFO: Origin: https://github.com/jballanc/MyAwesomeLib.jl.git
INFO: Generating LICENSE.md
INFO: Generating README.md
INFO: Generating src/MyAwesomeLib.jl
INFO: Generating test/runtests.jl
INFO: Generating REQUIRE
INFO: Generating .gitignore
INFO: Generating .travis.yml
INFO: Generating appveyor.yml
INFO: Generating .codecov.yml
INFO: Committing MyAwesomeLib generated files
julia> using MyAwesomeLib
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.
julia> Pkg.checkout("Requests")
INFO: Checking out Requests master...
INFO: Pulling Requests latest master...
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.
#5: String macros
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.
julia> struct Person
firstname::String
lastname::String
end
julia> macro person_str(fullname)
nameparts = split(fullname, " ")
Person(nameparts[1], nameparts[end])
end
@person_str (macro with 1 method)
julia> person"Joshua Ballanco"
Person("Joshua", "Ballanco")
String macros can also accept additional arguments following the string their invoked with (with apologies for the contrived example):
julia> macro person_str(fullname, suffix)
strippedname = replace(fullname, Regex(" $suffix\.?\$", "i"), "")
nameparts = split(strippedname, " ")
Person(nameparts[1], nameparts[end])
end
@person_str (macro with 1 method)
julia> person"Sammy Davis Jr."jr
Person("Sammy", "Davis")
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.
Bonus Reason
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!
Posted on January 9, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.