I’ve recently become interested in branching out from my primarily python-centric development workflow. A recent conversation with a colleague piqued my interest in Julia. Julia is JIT compiled, can be very performant, and has several interesting language features that make it an exciting language for use in scientific computing applications including multiple dispatch. In this post I’ll run through some toy problems solved in Julia. At first blush writing Julia code feels fairly similar to python, especially for those well versed in numpy. However there are some pretty interesting syntactic differences, not to mention to significantly different design patterns used in Julia that would be difficult or impossible to implement in python.
Newton’s method for approximating the square root of a number
Recall from calculus Newton’s clever method for approximating the roots of a function \(f\). We can use this method to approximate the square root of a number. Consider the function \(f(x) = x^2 - k\). Under this setup, finding the square root of k is equivalent to finding the positive valued root of \(f\). Here’s the graph of \(f\) setting k = 2:
Newton’s method essentially follows the algorithm outlined below: - Set \(x_{0} = g\) where g is some initial guess - Set \(x_{n} = x_{n-1} - \frac{f(x_{n-1})}{f'(x_{n-1})}\) - Continue until \(f(x_{n}) < \epsilon\), where \(\epsilon\) is the desired tolerance level (note this convergence criteria may not be sufficient for arbitrary functions, but suffices for this example)
The \(f'\) portion of the algorithm presents an interesting challenge when implementing this in a language like python where most objects are not differentiable. In Julia, however, most objects are differentiable, so you get a trivial implementation using the ForwardDiff package. Also note in the implementation below- we are using arbitrary unicode characters! This definitely makes it feel like you are coding in math.
usingForwardDiffusingDataFramesfunctionnewton_roots(k, ϵ)functionf(x)return x^2- kend x₀ = k xₙ = x₀# Sufficient convergence criteriawhilef(xₙ) > ϵ# Update step xₙ = xₙ - (f(xₙ) / ForwardDiff.derivative(f, xₙ))endreturn xₙendresult =DataFrame( x = [k for k in1:10], sqrt_approx = [newton_roots(k, 0.001) for k in1:10], sqrt_true = [sqrt(k) for k in1:10])result[!, "error"] = result.sqrt_true - result.sqrt_approxresult
10 rows × 4 columns
x
sqrt_approx
sqrt_true
error
Int64
Real
Float64
Float64
1
1
1
1.0
0.0
2
2
1.41422
1.41421
-2.1239e-6
3
3
1.73214
1.73205
-9.20496e-5
4
4
2.0
2.0
-9.29223e-8
5
5
2.23607
2.23607
-9.18144e-7
6
6
2.44949
2.44949
-4.62882e-6
7
7
2.64577
2.64575
-1.57331e-5
8
8
2.82847
2.82843
-4.14471e-5
9
9
3.00009
3.0
-9.15541e-5
10
10
3.16228
3.16228
-5.0073e-9
Not bad! You can see our approximation quickly converged to a reasonably accurate estimate of the standard implementation of sqrt function within Julia. If we need a lower error we can simply lower the tolerance threshold. Let’s move on to another interesting example- Sierpinski Triangles!
Computing the area of Sierpinski Triangles recursively in Julia
usingComposetriangle() =compose(context(), polygon([(1, 1), (0, 1), (1/2, 0)]))functionplace_in_3_corners(t)# Uses the Compose library to place 3 copies of t# in the 3 corners of a triangle.# treat this function as a black box,# or learn how it works from the Compose documentation here https://giovineitalia.github.io/Compose.jl/latest/tutorial/#Compose-is-declarative-1compose(context(), (context(1/4, 0, 1/2, 1/2), t), (context(0, 1/2, 1/2, 1/2), t), (context(1/2, 1/2, 1/2, 1/2), t))endfunctionsierpinski(n)if n ==0triangle()else t =sierpinski(n -1) # recursively construct a smaller sierpinski's triangleplace_in_3_corners(t) # place it in the 3 corners of a triangleendendsierpinski(4)
Now suppose we wanted to compute the area of a Sierpinski triangle of order \(n\) assuming that the area of \(Sierpinski(0)\) is 1. It turns out this lends itself nicely to a recursive solution.
functionarea_sierpinski(n) area =1.0if n ==0return1.0* areaelse area =0.75*area_sierpinski(n-1)endend[area_sierpinski(i) for i in0:10]
You can see that the recursive solution is very succinct, even elegant. While this could have been implemented just as easily in python, this was a fun reason to give Julia a test drive.