Julia for Python (and C++) programmers#
Why Julia?#
Julia is a relatively young (2012) programming language designed to be particularly effective for scientific workflows - the developers specifically call out Fortran and MATLAB as predecessors in this area.
Compared to these, Julia has much of the dynamic and interactive expressiveness of languages such as Python (including functionality that is only provided there by the third-party NumPy library), whilst leveraging just-in-time compilation and specialisation to allow performance approaching (and sometimes better than) high-performance compiled languages such as C, C++, Modern Fortran and Rust.
Some of Julia’s properties#
High-performance (usually within a factor of 0.5 of C; often at parity)
Fully Unicode supporting - including LaTeX markup support in REPL, and natural mathematical symbols for common operations (\(\sin^2\theta + cos^2\theta = 1\))
First-class support in Jupyter notebooks (it’s the Ju bit of the name)
Supports generic programming (with multiple dispatch), functional-style method chaining, and other modern paradigms
Dynamic typing, with efficient function specialisation (via JIT) and optional typing supported deeply in language (unlike Python)
Has an excellent, robust package manager making installation and reproducible environments a breeze
Julia is also just a really fun language to program in!
Example Julia Code#
function myfunc(x, y)
if x > y
"X is greater than Y\n"
else
"Y is greater than X\n"
end
end
print(myfunc(2,3))
print(myfunc(2.0, 1))
Y is greater than X
X is greater than Y
As you can see, Julia’s syntax is somewhere between that of Python and MATLAB in style and it’s easy to follow.
One subtle thing we’re using in this example is that all expressions return a value - the “value” of the if
chain is the string that is created as the only operation in that chain… and thus the “value” of the function, without an explicit return
statement, is simply the value of the last expression that was executed before the function ended.
We could write this more explicitly, but less naturally, as:
function verbosefunc(x, y)
if x > y
return "X is greater than Y\n"
else
return "Y is greater than X\n"
end
end
verbosefunc (generic function with 1 method)
Of course explicit return statements are necessary in some places for flow control, but it’s nice that often in Julia we can rely on implicit return value.
As we mentioned, Julia has a JIT which applies to all code written in it - the first time a unit of code is executed, the JIT will compile direct to host machine code.
We can see this better with a longer function that requires actual effort (thanks to @Moelf), using the @time
macro to get the execution time:
function go_faster(a)
trace = 0.0
for i in axes(a, 1) # Returns the dimension of array "a" along its first axis
trace += tanh(a[i, i])
end
return a .+ trace
end
go_faster (generic function with 1 method)
α = reshape(0:99, 10, 10) #Julia will tab-complete LaTeX markup - type \alpha and press tab to write this variable name
@time go_faster(α)
0.102960 seconds (269.89 k allocations: 18.393 MiB, 7.56% gc time, 99.98% compilation time)
10×10 Matrix{Float64}:
9.0 19.0 29.0 39.0 49.0 59.0 69.0 79.0 89.0 99.0
10.0 20.0 30.0 40.0 50.0 60.0 70.0 80.0 90.0 100.0
11.0 21.0 31.0 41.0 51.0 61.0 71.0 81.0 91.0 101.0
12.0 22.0 32.0 42.0 52.0 62.0 72.0 82.0 92.0 102.0
13.0 23.0 33.0 43.0 53.0 63.0 73.0 83.0 93.0 103.0
14.0 24.0 34.0 44.0 54.0 64.0 74.0 84.0 94.0 104.0
15.0 25.0 35.0 45.0 55.0 65.0 75.0 85.0 95.0 105.0
16.0 26.0 36.0 46.0 56.0 66.0 76.0 86.0 96.0 106.0
17.0 27.0 37.0 47.0 57.0 67.0 77.0 87.0 97.0 107.0
18.0 28.0 38.0 48.0 58.0 68.0 78.0 88.0 98.0 108.0
As we can see, over 99.9% of the time taken was by the JIT. However, as Julia caches the result, subsequent executions are much faster, using the previously compiled code:
β = reshape(1:100, 10, 10) #similarly, \beta and then tab
@time go_faster(β)
0.000008 seconds (1 allocation: 896 bytes)
10×10 Matrix{Float64}:
10.7616 20.7616 30.7616 40.7616 … 70.7616 80.7616 90.7616 100.762
11.7616 21.7616 31.7616 41.7616 71.7616 81.7616 91.7616 101.762
12.7616 22.7616 32.7616 42.7616 72.7616 82.7616 92.7616 102.762
13.7616 23.7616 33.7616 43.7616 73.7616 83.7616 93.7616 103.762
14.7616 24.7616 34.7616 44.7616 74.7616 84.7616 94.7616 104.762
15.7616 25.7616 35.7616 45.7616 … 75.7616 85.7616 95.7616 105.762
16.7616 26.7616 36.7616 46.7616 76.7616 86.7616 96.7616 106.762
17.7616 27.7616 37.7616 47.7616 77.7616 87.7616 97.7616 107.762
18.7616 28.7616 38.7616 48.7616 78.7616 88.7616 98.7616 108.762
19.7616 29.7616 39.7616 49.7616 79.7616 89.7616 99.7616 109.762
It’s possible to achieve similar effects using the various “JIT in Python” packages like numba, but those are additional add-ons to Python itself, and often require writing code in outwardly “unPythonic” ways.