cm

November 9, 2024

My First Impression of the Julia Programming Language

Recently, I started learning about the Julia programming language. It's a programming language created in 2008 (or 2012 depending on how you want to define its start). Its official website can be found here: https://julialang.org.  In this post I'll describe some of what I love in more detail by looking at a few specific examples of Julia syntax: string concatenation using an operator, operators as functions, polynomial expressions, vectors and matrices, and function composition and piping.

To summarize my first impressions of Julia, it's like if Python became more sophisticated and academic. Personally, I haven't learned a language like Julia before and I'm having a lot of fun discovering things about it. That's what this blog post is about: how much fun I've had learning it! I'm not an expert in Julia and only know a small portion of the specification so far.

To set the stage, here is a quote from Julia's Wikipedia article outlining why Julia's creators have won some awards for the language:

Three of the Julia co-creators are the recipients of the 2019 James H. Wilkinson Prize for Numerical Software (awarded every four years) "for the creation of Julia, an innovative environment for the creation of high-performance tools that enable the analysis and solution of computational science problems." Also, Alan Edelman, professor of applied mathematics at MIT, has been selected to receive the 2019 IEEE Computer Society Sidney Fernbach Award "for outstanding breakthroughs in high-performance computing, linear algebra, and computational science and for contributions to the Julia programming language."

String Concatenation Using an Operator

I'm sure you're wondering why I'd start with something so basic and boring as the syntax for string concatenation by using an operator. Well, it turns out that Julia does this a little bit differently than most other languages. Instead of using the + operator, Julia makes us use *. Most interestingly, they have a justification for this which I'll quote below.

Here's Julia: 
greet = "Hello"
whom = " world!"
greet * whom

> Hello world!

For comparison, here's Python which uses the + operator instead:
greet = "Hello"
whom = " world!"
greet + whom

> Hello world!
While * may seem like a surprising choice to users of languages that provide + for string concatenation, this use of * has precedent in mathematics, particularly in abstract algebra.

In mathematics, + usually denotes a commutative operation, where the order of the operands does not matter. An example of this is matrix addition, where A + B == B + A for any matrices A and B that have the same shape. In contrast, * typically denotes a noncommutative operation, where the order of the operands does matter. An example of this is matrix multiplication, where in general A * B != B * A. As with matrix multiplication, string concatenation is noncommutative: greet * whom != whom * greet. As such, * is a more natural choice for an infix string concatenation operator, consistent with common mathematical use.

More precisely, the set of all finite-length strings S together with the string concatenation operator * forms a free monoid (S, *). The identity element of this set is the empty string, "". Whenever a free monoid is not commutative, the operation is typically represented as \cdot, *, or a similar symbol, rather than +, which as stated usually implies commutativity.

To me, that's a fun and well explicated choice made by the creators of Julia. I love it!

Operators As Functions

Another fun thing: Julia's operators are functions. For example, in the previous section we saw the * operator. Let's use it in some creative ways knowing they are functions.

If * is a function then we can provide it a number of arguments:
*("A", " ", "longer", " ", "hello", " ", "world!")

> A longer hello world!

Or we can even assign it to a new variable:
concat = *
concat("A", " ", "longer", " ", "hello", " ", "world!")

> A longer hello world!

In Python this would throw TypeError in the first example and SyntaxError in the second. That's a fine choice for Python: to not make operators be functions. Still I think what Julia does is fun and also useful. For example:
*("A", " ", "longer", " ", "hello", " ", "world!")
Is much more readable than:
"A" * " " * "longer" * " " * "hello" * " " * "world!"

Polynomial Expressions

Another really fun aspect of Julia is that it's easy to write polynomials or equations in general. On the one hand, this is fun and convenient. On the other hand, it does require understanding of what's happening underneath.

Take this polynomial for example:
f(x) = πx² + 3x + 2x³
That can be written as a function in Julia like this:
f(x) = π*x^6 + 3x + 2x^3
It looks pretty close to the regular symbolic math!

In Python we might write:
import math

pi = math.pi

def f(x):
    return (pi * x**6) + (3*x) + (2 * x**3)

Of course, if we expect the function to accept an iterable as the argument x, then we need to refactor it. However, in Julia it's as simple as using dot vectorization, literally adding a dot like so f.(x) when you call it! This is another really neat aspect of Julia. In Python we have to refactor the function to handle a list, perhaps by using a list comprehension (see the appendix below). Anyway, for dot vectorization reference check out https://docs.julialang.org/en/v1/manual/functions/#man-vectorized and their excellent Performance Tips documentation, especially the section on fused vectorized operations: https://docs.julialang.org/en/v1/manual/performance-tips/#More-dots:-Fuse-vectorized-operations .

Vectors and Matrices

Another thing I love about Julia is that their Array type differentiates between Vectors, which are one dimensional arrays, and Matrices, which are arrays of more than one dimension:
v = [1, 2, 3]
v

> 3-element Vector{Int64}:
 1
 2
 3
The following would all produce the same matrix:
m = [1 2
     3 4]
n = [1:3 2:4]
o = [[1,3] [2,4]]
m

> 2×2 Matrix{Int64}:
 1  2
 3  4

Function Composition and Piping

The final thing I want to geek out about in this blog post is another math-inspired syntax: the function composition operator ∘ . Say you have a function f and a function g. You can specify their composition like so:
f  g

We can also chain functions in a visual way by using the pipe syntax |> :
f |> g

Let's take a look at some fun and simple examples using composition, piping, and some of the other syntax we've learned in this blog post!

Instead of composing + and sqrt like this:
sqrt(+(5, 20))

> 5
We can do:
(sqrt  +)(5, 20)

> 5

Here are three equivalent ways in Julia to take a range of integers, sum them, and take the square root of the resulting sum:
x = 1:10
y = sum(x)
z = sqrt(y)
z

> 7.416198487095663

(sqrt  sum)(1:10)

> 7.416198487095663

1:10 |> sum |> sqrt

> 7.416198487095663

Here are some fun examples from their documentation ( https://docs.julialang.org/en/v1/manual/functions/#Function-composition-and-piping ), including how to use dot vectorization to broadcast an iterable to a piped function and an anonymous function:
map(first  reverse  uppercase, split("you can compose functions like this"))

> 6-element Vector{Char}:
 'U': ASCII/Unicode U+0055 (category Lu: Letter, uppercase)
 'N': ASCII/Unicode U+004E (category Lu: Letter, uppercase)
 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

1:3 .|> (x -> x^2) |> sum |> sqrt

> 3.7416573867739413

["a", "list", "of", "strings"] .|> [uppercase, reverse, titlecase, length]

> 4-element Vector{Any}:
  "A"
  "tsil"
  "Of"
  7
So much fun!

Stay tuned! I might make a follow up blog post or two as I learn more about Julia. There's a lot more to it than the little bit I shared here.

Appendix: A Brief Performance Comparison

Let's do a simple timing of the vectorized polynomial we saw earlier: 
f(x) = π*x^6 + 3x + 2x^3
large_random_vector = rand(10^6)

@time f.(large_random_vector)
Here's an example output:
> 0.019226 seconds (34.67 k allocations: 10.004 MiB, 74.61% compilation time: 100% of which was compilation time.

In Python we do a little refactoring so the function can handle a list:
import math
import random
import time

pi = math.pi
random_floats_list = [random.uniform(0, 1) for _ in range(1000000)]

def f(rf_list):
    return [ ((pi * x**6) + (3*x) + (2 * x**3)) for rf in rf_list ]

start = time.time()
f(random_floats_list)
end = time.time()
elapsed = end - start
print(f'{elapsed} seconds.')
Here's an example output:
> 0.34940171241760254 seconds.

About cm

Das Lied schläft in der Maschine

Published articleshttps://aiptcomics.com/author/julianskeptic