100 Languages Speedrun: Episode 19: Julia

Julia might be the newest language covered so far, released just in 2015, and meant to replace older (and generally awful) languages for scientific computations. Let's see how well it does.

As an interesting side note, Jupyter Notebook is named after Julia, Python, and R, even though in practice it's used for Python, Python, and Python. I guess PyPyPy Notebook just doesn't sound right.

Hello, World!

Nothing surprising here:

println("Hello, World!")

Fibonacci

function fib(n)
  if n <= 2
    1
  else
    fib(n-1) + fib(n-2)
  end
end

for i=1:30
  println(fib(i))
end

The syntax is very close to Ruby and Python. Functions don't need explicit return.

FizzBuzz

function fizzbuzz(n)
  if n % 15 == 0
    "FizzBuzz"
  elseif n % 5 == 0
    "Buzz"
  elseif n % 3 == 0
    "Fizz"
  else
    n
  end
end

for i=1:100
  println(fizzbuzz(i))
end

Everything works just fine, and is perfectly clear. Time to do something a bit harder.

Matrices

Julia has builtin support for vectors and matrices, so we can do very fast Fibonacci formula with them, similar to what we did with Octave.

u = [1 1]
m = [1 1; 1 0]

for i=1:40
  result = (u * (m ^ (i-1)))[2]
  println("fib($i) = $result")
end

Which generates what we expect:

$ julia fibmatrix.jl
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
fib(10) = 55
fib(11) = 89
fib(12) = 144
fib(13) = 233
fib(14) = 377
fib(15) = 610
fib(16) = 987
fib(17) = 1597
fib(18) = 2584
fib(19) = 4181
fib(20) = 6765
fib(21) = 10946
fib(22) = 17711
fib(23) = 28657
fib(24) = 46368
fib(25) = 75025
fib(26) = 121393
fib(27) = 196418
fib(28) = 317811
fib(29) = 514229
fib(30) = 832040
fib(31) = 1346269
fib(32) = 2178309
fib(33) = 3524578
fib(34) = 5702887
fib(35) = 9227465
fib(36) = 14930352
fib(37) = 24157817
fib(38) = 39088169
fib(39) = 63245986
fib(40) = 102334155

Vectors and matrices use 1-based indexing, which is totally cringe, even if that's what pre-computer-era mathematicians are used to.

Julia supports string interpolation with "$x" for variables and "$(...)" for expressions. It's somewhat disappointing that we finally arrived in the world where every sensible language supports string interpolation, but every single one of them chose different syntax for it.

Unicode

strings = ["Hello", "Żółw", "🍩"]
println([length(s) for s in strings])
println([uppercase(s) for s in strings])

Given that it's a new language, it handles basic Unicode operations correctly as expected:

$ julia unicode.jl
[5, 4, 1]
["HELLO", "ŻÓŁW", "🍩"]

Also notice Python-style list comprehensions.

More Unicode

Take a guess which letter it would print:

s = "Żółw"
println(s[3])

Did you guess correctly?

$ julia unicode2.jl
ó

In every sensible language that would be w. Even JavaScript gets that right, and it rarely gets anything right.

I already mentioned that Julia uses 1-based indexing, even for strings, so you might have guessed ł instead, but how the hell did we get w?

It turns out that Julia indexes characters by their byte position, starting at 1. So for string "Żółw", these are the letters:

  • "Żółw"[1] is 'Ż'
  • "Żółw"[3] is 'ó'
  • "Żółw"[5] is 'ł'
  • "Żółw"[7] is 'w'

And every other index raises an exception like:

julia> "Żółw"[2]
ERROR: StringIndexError: invalid index [2], valid nearby indices [1]=>'Ż', [3]=>'ó'

We can call collect(eachindex("Żółw")) to get a list of the valid indices [1, 3, 5, 7].

Overall, I'm not sure if this is better or worse than JavaScript.

Equality

println([1 2] == [1 2])
println([1 2; 3 4] == [1 2; 3 4])
println((1,2) == (1,2))
println((10:20) == (10:20))

It's not much surprise that == works correctly on all complex types like vectors, matrices, tuples, ranges, and so on:

$ julia equality.jl
true
true
true
true

Blocks

Julia supports many different styles of functional programming:

numbers = (1:10)

println(
  [x for x in numbers if x % 2 == 0]
)
println(
  filter(iseven, numbers)
)
println(
  filter(x->x%2 == 0, numbers)
)
println(
  filter(numbers) do x
    x%2 == 0
  end
)

Notice that block argument goes last with do syntax, but it's translated to be first argument of the function.

Dot syntax

Like other mathematical programs, operators can be applied to collections element-wise. Julia also provides convenient syntax to use it with your own functions:

double(x) = 2*x
println(double(21))
println(double.(1:10))
println(10 .+ [1,2,3])

Which outputs:

$ julia dot.jl
42
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
[11, 12, 13]

.+ and .() are like implicit map for the appropriate types. This isn't something most general-purpose languages typically do, but it's much more common in mathematical code, so many mathematical languages have some degree of support for this.

Unicode operators

I mentioned back in Emojicode episode that languages really torture their ASCII symbols to avoid using anything outside ASCII. Operators often consist of 2-3 symbols, and the same combination is often overloaded to mean a lot of different things. Julia takes modest steps to use Unicode mathematical symbols.

# is subset
println([1,2][2,3,1])
# is element
println(3[2,3,1])
# some trigonometry and square roots
println(sin(π/3) * 3)

Which outputs:

true
true
1.4999999999999998

Julia doesn't go overboard, I think in the future this use will become more common.

Macros

Unusually for a non-Lisp, Julia has macros. They don't look quite the same as regular Julia syntax, but it's a thing:

macro unless(condition, expression)
  quote
    if !($condition)
      $expression
    end
  end |> esc
end

println("Enter number:")
num = parse(Int, readline())

if isodd(num)
  println("$num is odd")
end

@unless isodd(num) begin
  println("$num is even")
end

Which works just as expected:

$ julia macro.jl
Enter number:
69
69 is odd
$ julia macro.jl
Enter number:
420
420 is even

I don't know how much that matters in practice. A number of non-Lisp languages tried that, and none of those attempts really did much. Julia in general is very generous with the syntax. quote...end with $ is a "quasi-quote" in Lisp terms. |> is just Elixir style piping into another function and not specific to macros. You can do -42 |> abs if for some reason you don't want to type abs(-42).

As you can see as Julia isn't a Lisp, syntax introduced by macros doesn't look quite the same - there's extra @ and begin there. But maybe it would be good enough for some use cases.

Should you use Julia?

For the kinds of tasks Julia is aiming at, I'd generally recommend trying out Python first, but Julia isn't a bad choice.

It's a fairly modern language with limited baggage, friendly syntax similar to Python and Ruby, and you can use the familiar Jupyter Notebook style of development. Many new languages like Go, Rust, and TypeScript were obviously created by mental boomers enamored with distant past, and who rejected many of the lessons of modern programming language design. Julia doesn't feel like it - it feels like they got most of the things right, given what they were aiming for.

Most other "mathematical" languages have terrible quirks, massive baggage, awful syntax, are seriously underpowered once you step outside their core domain, and are often proprietary or semi-proprietary. By comparison, Julia does decently on all these criteria. If together with Python they ends up euthanizing some crappy closed source languages still used in the science world, that would make the world a better place.

I didn't investigate Julia's performance at all. Honestly I think the whole "performance" stuff is really overrated, and microbenchmarks are completely without any real world significance, but if you're into such things, Julia's performance claims are here.

Overall, I think it's highly promising, even if I'd probably still try Python first.

Code

28