This is my third year participating in Advent of Code. I look forward to this all year. Last year I tried Mathematica and it was a great experience. This year: Julia!
Advent of Code is a wonderful way for experienced programmers to practice a new language. The challenges get extremely difficult. The complexity and variety of the challenges will stress test any programming language, showing you where the language (and the programmer) are weak and strong.
I loved the Intcode problems this year! This month of programming puzzles required most of what learned in my computer science degree and more.
It would be hard for me to choose a favorite. Day 7 was challenging. Day 9 was rewarding. Day 13 had a lot of room for creative solutions. Day 19 is a nice algorithmic learning opportunity. Day 23 was really fun for me, as I work in computer networking.
I did not finish every day. Most of the missing stars are due to time constraints, but a few of them are problems I looked at and didn't know how to solve. I may go back and try to finish all 50, but probably not. Below are my impressions of the major theme for each day.
**
Loops, functions, and recursion (map
/reduce
)**
Assembly language basics**
Procedural programming, Cartesian space, complex arithmetic (im
), key/value pairs (Dict
), Manhattan distance**
Conditions (filter
)**
Assembly language jumps and conditions**
Basic graphs, Dijkstra's algorithm**
Concurrency, pipelining, encapsulation**
Image processing, multidimensional arrays**
Software engineering, testing and verification**
Number theory or floating point arithmetic, sorting and searching**
Attractors, number theory, large problems**
Games, reverse engineering**
Depth-first search*
Signal processing, matrix multiplication*
ASCII, scripting (meta-metaprogramming with Intcode!)**
Linear programming, dynamic programming, quadratic complexity*
Dijkstra's algorithm*
Circular arrays, uhhh...**
Networking, concurrency*
Automata, binary tricks (fold
/nest
), recursion, whoa...
This year I set out to practice Julia. I did not bother with Jupyter notebooks at all. Rather, I did all of my work in Visual Studio Code and in Julia's REPL.
I experimented with Microsoft's new Windows Terminal. It's OK, but I found it kind of slow and the scrollbar does not always work.
Julia's REPL is a joy to work in. Emacs-style keyboard shortcuts work. The built-in help (?
) is useful. I can paste in half of my program and take a close look at what is going on.
I used GitHub Desktop to publish my programs. Someday I will get around to getting good at git
, but for now this is good enough.
There was one program that I reduced to my own JavaScript Dijkstra solver. Since then I have found the very robust LightGraphs library.
There was also one puzzle where I used Java for an interactive GUI. Julia could have done this, but I have no motivation to learn another windowing toolkit.
In previous years I sometimes modified the input to make it easier to parse. I never felt the need to do this in 2019.
Much of what I learned this year were basics and tricks of the Julia language. Most of my programming experience is in Java, JavaScript, and Mathematica. My Mathematica adventure last year has influenced my approach to programming more than I had realized. I eagerly reach for filter
, map
, and reduce
when possible. I use lots of tuples and matrices. Functions compose nicely in Mathematica and they compose equally well in Julia.
Things I really do not miss from Mathematica was the ugly double brackets for array indices (x[[1]]
), anonymous function arguments (#
and #1, #2, ...
), ampersand notation (&
) to denote pure functions, unweildy Module
syntax and scoping, underscores after function parameters, the :=
symbol, and slow performance. Mathematica's symbolic computation is very powerful but can be challenging to newcomers. I don't think I ever really felt like an Advent of Code problem could be better solved with symbolic computation.
The big things I did miss from Mathematica were the Nest
statement (to compute f ∘ f ∘ ... ∘ f(x)
) and the powerful visualizations (Plot
, GraphPlot
, ArrayPlot
, NumberLinePlot
, ListPlot
, Manipulate
). Julia has a Plots package that I used a few times, but not as easily or frequently as last year.
Julia is extremely expressive. I love the syntax. I frequently thought about how tedious some of these programs would have been if I were still using Java.
Julia is fast. I threw an 800×800ish matrix at a cubic time algorithm. Program completes in about four seconds. It's beautiful!
Julia does take second to "warm up." I assume this is all JIT. My day 7 solution (which I started to improve but did not finish) required 600 different Julia processes. Even on a modern computer, these processes require many minutes.
Julia does not have as rich a library as older and more popular languages (C++, Java, Python) but it has a modest and growing ecosystem. The modules I used were OffsetArrays, Sockets, LightGraphs, Combinatorics (permutations
), DelimitedFiles, Memoize, and LinearAlgebra (norm
).
Strong typing helped me a few times.
List comprehension is very powerful (example: [4 * row + col for row=0:5, col=1:4]
). I had encountered list comprehension in Python before, but it was not familiar enough to come to my mind quickly when solving puzzles. List comprehension is basically the same thing as Mathematica's Array
function.
I think I only defined a struct
once. The only module that I needed was to make my Intcode VM reusable. I did not learn Julia's exception handling mechanisms. I poked my little toe into Julia's concurrency and networking functions but I still have a lot to learn.
These are specific elements of the Julia language that I found particularly interesting or useful. I recommend reading The Julia Express if you are an experienced programmer intersted in getting started in Julia quickly.
- Occasionally I would miss Java/C-style
if (condition) statement;
one-liners. You can do this in Julia, but you still need theend
keyword at the end, such asif condition statement end
. I looks kind of strange, and there is a very nice alternative:condition && statement
! count(p, a)
is the same thing aslength(filter(p, a))
.findfirst(p, a)
takes a predicate function, such asfindfirst(<=(0.1), rand(5, 5))
. (Related:<=(0.1)
is a nice shorthand for the lambdax -> x <= 0.1
). This is in contrast tosearchsortedfirst(a, v)
which takes a value, such assearchsorted(sort(rand(100)), 0.8)
.0 + Inf
throws an error.0.0 + Inf
does not.- List comprehension is really the same thing as
map
. - The range operator (
:
) is very useful. If strongly typed functions will not accept aRange
, then you can convert a range to an array withcollect
. - Default parameter values can be specified in the function signature.
function f(x=10)
does exactly what you expect. - A
struct
is basically the same thing as a named tuple, but you can mark themmutable
and astruct
is easier to specify for strongly-typed functions. - Optional type annotations are wonderful. I really did feel like I was getting the performance of Java with the expressiveness of Python.
- Tuples are basically the same thing as arrays. They're wonderful! I frequently return two values from a function in Julia, thinking back to the bad old days where this was really inconvenient in Java and C.
- The Plots package is very easy to use.
- So is LightGraphs.
- The
global
keyword is mildly annoying. I think Python has similar syntax, semantics, and structure. The thing that really surprised me is that you have to specifyglobal
inside a loop, even if that loop was not part of a function. You do not need theglobal
keyword for structure or array members. - "
map
is not defined on sets." I guess this makes sense now that I think about it. It seems strange to get an array of arbitrary elements from a set of distinct elements. If you really want to usemap
over aSet
then you cancollect
the set first. - You can use
for
andforeach
withSet
andDict
. The values in a dictionary are passed as aPair
, which you can access by index (1 and 2) or withfirst
andlast
. - One-indexed arrays (values start at
a[1]
instead ofa[0]
) are often annoying. You can redefine your arrays with arbitrary dimensions using the OffsetArrays module. I found the documentation difficult to read, but the module is actually very easy to use. If you have a 50-element arraya
and want to access it with indices starting from 0, simply entera = OffsetArray(a, 0:49)
. - The
get
function is a nice way to give a default value for values not found in a data structure. The documentation shows this used for dictionaries butget
can also work with arrays. For example,get(rand(3), 4, -1)
(there is no position 4, so return -1). elseif
took me a while to get used to.- I miss the
switch
statement. While I can understand why no one wants to repeat the disastrous experience C gave us (see Gustavo Silva on -Wimplicit-fallthrough in Linux), I really liked the sensibleswitch
statement from Mathematica and Excel. - You can accidentally shadow builtin function names. Once, I used the word
values
for a variable, which gave a crazy error message when I tried to invoke thevalues
function on a dictionary. This made me more cautious about naming things than I ever needed to be in Java. - The dot syntax to vectorize functions is awesome! Just suffix any function with a
.
to apply that function element-wise to each member of an array. For example, to take the root of every member in a list you can entersqrt.(rand(8,4))
. This also works for binary operators, but for some reason you have to put the period first. For example,17 .* rand(8,4)
. You can even use the "dot" syntax with n-ary functions, such asstring.(rand(0:0xffffffff, 12), base=16, pad=8)
. Maybe this is just a shorthand formap
, but I still found it very convenient. NaN != NaN
is true, as it is in other languages. Julia has some nice helper functions likeisfinite
,isnan
, etc. Just typeis
into the REPL and press tab twice.- The Sockets library was very easy to use. The only language where I have any real networking experience to speak of is Java. Java's sockets are also easy to use, but they require lots of boilerplate code that you can (perhaps not safely...) ignore in Julia.
- Julia has a nice
@async begin ... end
statement that you can use to start an asynchronous task. These worked really well for my Intcode VM. I was happy to pass data over TCP sockets, although I know it could have been much faster if I had tried some form of shared memory.
Thank you so much to Eric Wastl and to the volunteers and the community who make Advent of Code the most wondeful time of the year! This was the best Advent of Code ever. I don't know what language I will try next year, maybe Clojure?