Functional One-Liners in Julia
Practical everyday tricks to improve your functional programming skills in Julia
Julia is such an expressive language, that when writing Julia code it is easy to start thinking:
Isn’t there an even more conscience and elegant way of solving this problem?
Plenty of programming challenges can be solved straightforward with approaches familiar to you from other languages. However, in Julia, there is often an even shorter and more elegant way of doing things.
Partial Function Application
Partial application of a function (currying) is something that was popularized by Haskell. It is achieved by omitting one or more arguments to a function call. Instead of returning a value, the function will return a new function taking the remaining arguments.
Does that sound bizarre? No worries, a code example should clarify it. Normally, you would do a comparison between numbers like this:
julia> 3 < 4
true
Which is identical to the following code because almost everything in Julia is a function:
julia> <(3, 4)
true
What happens if you don’t provide all the arguments when calling the function?
julia> <(4)
(::Base.Fix2{typeof(<),Int64}) (generic function with 1 method)
What you get back instead is a callable object. We can store this object, and use it later:
julia> f = <(4);
julia> f(3)
true
How is this trick useful? It simplifies working with higher-order functions such as map
, filter
and reduce
. Let us work through an example to demonstrate the benefits.
Find all Elements Which are Less Than Value
Find elements in a list or range which are less than a five.
Naturally, you can compare with any value, not just five.
julia> filter(<(5), 1:10)
4-element Array{Int64,1}:
1
2
3
4
Alternatively we could look for all values which are larger than five:
julia> filter(>(5), 1:10)
5-element Array{Int64,1}:
6
7
8
9
10
Find the Index of an Element
We can find the index of every occurrence of the number four:
julia> findall(==(4), [4, 8, 4, 2, 1, 5])
2-element Array{Int64,1}:
1
3
Or we can just look for the first occurrence of the number four:
julia> findfirst(==(4), [4, 8, 4, 2, 1, 5])
1
This approach, of course, works equally well with strings:
julia> findlast(==("foo"), ["bar", "foo", "qux", "foo"])
4
Filter Out Particular File Types
Say you want to get a list of all the .png
files in the current directory. How do you do that? We can use the endswith
function. It takes two arguments. Here we check if the string "somefile.png"
ends with the string ".png"
julia> endswith("somefile.png", ".png")
true
Like many other functions endswith
can be used in partial application which makes it very useful in combination with filters:
pngs = filter(endswith(".png"), readdir())
Predicate Function Negation
This is a trick I sadly discovered quite late. But it turns out that you can place !
in front of a function to produce a new function which inverts its output. This is actually not built into the Julia language but just a function itself defined as:
!(f::Function) = (x...)->!f(x...)
Again it may not be entirely clear what I am getting at, so let us look at some examples.
Removing Empty Lines from a File
Say you read all the lines in a file given by filename
and you want to strip out the empty lines, you could provide an anonymous function taking a line as argument:
filter(line -> !isempty(line), readlines(filename))
But the following code is a more elegant approach. We use the exlamation mark !
to negate the boolean value returned from the isempty
function.
filter(!isempty, readlines(filename))
Here is an example of using it in the REPL with some dummy data:
julia> filter(!isempty, ["foo", "", "bar", ""])
2-element Array{String,1}:
"foo"
"bar"
Applying the Broadcast and Map Functions to an Array
Julia has the broadcast
function which you can think of as a fancy version of the map
function. You can even use it in similar fashion:
julia> map(sqrt, [9, 16, 25])
3-element Array{Float64,1}:
3.0
4.0
5.0
julia> broadcast(sqrt, [9, 16, 25])
3-element Array{Float64,1}:
3.0
4.0
5.0
The real power comes when dealing with functions taking multiple arguments and you want one of the arguments to be reused and the others to be changed. Let us walk through some examples.
Converting a List of Strings to Numbers
To convert say a string to a number you use the parse
function like this:
julia> parse(Int, "42")
42
The naive way to apply this to multiple text strings would be to write:
julia> map(s -> parse(Int, s), ["7", "42", "1331"])
3-element Array{Int64,1}:
7
42
1331
We can simplify this with the broadcast
function:
julia> broadcast(parse, Int, ["7", "42", "1331"])
3-element Array{Int64,1}:
7
42
1331
In fact this is so useful and common to do in Julia, that there is an even shorter version using the dot .
suffix:
julia> parse.(Int, ["7", "42", "1331"])
3-element Array{Int64,1}:
7
42
1331
You can even chain this:
julia> sqrt.(parse.(Int, ["9", "16", "25"]))
3-element Array{Float64,1}:
3.0
4.0
5.0
Convert Snake Case to Camel Case
In programming we often have identifiers written like hello_how_are_you
which we may want to covert to camel case written like HelloHowAreYou
. Turns out you can do this conversion easily with just one line of code in Julia.
julia> greeting = "hello_how_are_you"
"hello_how_are_you"
julia> join(uppercasefirst.(split(greeting, '_')))
"HelloHowAreYou"
Avoid Deep Nesting With Pipe Operator
A common complaint against more functional oriented language like Julia from OOP fans is that it is hard to read deeply nested function calls. However, we can avoid deep nesting by using the pipe operator |>.
Just to give a simple flavor of what it does. Here is an example of equivalent expressions:
julia> string(sqrt(16))
"4.0"
julia> 16 |> sqrt |> string
"4.0"
This approach also works with the broadcast
function, so you can use it to pipe multiple values between stages in a sort of pipeline.
julia> [16, 4, 9] .|> sqrt .|> string
3-element Vector{String}:
"4.0"
"2.0"
"3.0"
With this technique we can simplify our snake-case to camel-case example.
julia> split(greeting, '_') .|> uppercasefirst |> join
"HelloHowAreYou"