Operator Flexibility in Julia (AKA Abusing Operators in Julia)

Believe it or not but the following is actual Julia code which generates Fibonacci numbers:

julia> (n->((l->[l[2],l/+])^n)(0:1)[1]).(1:10)
10-element Array{Int64,1}:
  1
  1
  2
  3
  5
  8
 13
 21
 34
 55

Julia 0.6 introduced the new ∘ (\circ) function composition operator. It currcntly has one method:

julia> methods(∘)
# 1 method for generic function "∘":
∘(f, g) in Base at operators.jl:884

which has a rather straightforward definition:

∘(f,g) = (x...)->f(g(x...))

It takes functions f and g and composes them in the following way:

(f∘g)(x) == (f(g(x)))

For example

julia> sin(cos(2))
-0.4042391538522658

julia> (sin∘cos)(2)
-0.4042391538522658

julia> (sin∘cos∘log)(2)
0.6955886362231636

julia> sin(cos(log(2)))
0.6955886362231636

The operator provides a nice way of creating anonymous functions for broadcasting across arrays:

julia> map(reverse∘string, 230:240)
11-element Array{String,1}:
 "032"
 "132"
 "232"
 "332"
 "432"
 "532"
 "632"
 "732"
 "832"
 "932"
 "042"

Ultimately, the ∘ operator is a great demonstration of Julia's flexibility as a language. Which leads to the question: What else can we do with Julia's operators?

For starters, it may be nice to be able to compose functions and arguments so that (arg∘f)(x) is equivalent to f(arg,x). For example, the following does not work:

julia> map(10∘(-), 1:3)
ERROR: MethodError: objects of type Int64 are not callable
Stacktrace:
 [1] _collect(::UnitRange{Int64}, ::Base.Generator{UnitRange{Int64},Base.##55#56{Int64,Base.#-}}, ::Base.EltypeUnknown, ::Base.HasShape) at ./array.jl:454
 [2] map(::Function, ::UnitRange{Int64}) at ./abstractarray.jl:1865

However, thanks to Julia's overloading abilities, we can make it work ourselves. Here we define both arg∘f and f∘arg:

julia> import Base.∘

julia> (∘)(f::Function, arg) = (x...)->f(x...,arg)
∘ (generic function with 2 methods)

julia> (∘)(arg, f::Function) = (x...)->f(arg,x...)
∘ (generic function with 3 methods)
julia> map(10∘(-), 1:3)
3-element Array{Int64,1}:
 9
 8
 7

julia> map((-)∘10, 1:3)
3-element Array{Int64,1}:
 -9
 -8
 -7

We can now define some simple functions very easily:

julia> my_log2 = 2∘log
(::#7) (generic function with 1 method)

julia> my_log2(10)
3.3219280948873626

julia> log2(10) # Julia built-in
3.321928094887362

This is great but we created an additional problem. Our old code no longer works!

julia> (sin∘cos)(3)
ERROR: MethodError: ∘(::Base.#sin, ::Base.#cos) is ambiguous. Candidates:
  ∘(arg, f::Function) in Main at REPL[32]:1
  ∘(f::Function, arg) in Main at REPL[31]:1
Possible fix, define
  ∘(::Function, ::Function)

Julia doesn't know which of our functions to call. Since both arg and f are of type Function (typeof(sin) <: Funtion == true and typeof(cos) <: Function == true), it doesn't know which to use as f::Function and which to use as args::Any. In fact, we don't want it to use either, we want it to use the original definition if both arguments are functions. Luckily, this is a very easy fix in Julia. We can define a specialized version of the original definition like so:

julia> ∘(f::Function, g::Function) = (x...)->f(g(x...))
∘ (generic function with 4 methods)

Notice that this definition is exactly the same as the one above except that it includes specific types for its arguments f and g. Julia is now able to choose the proper function:

julia> (sin∘cos)(3)
-0.8360218615377305

Another way we can abuse- ahem demonstrate Julia's flexibility with operators is to use them to perform common complex operations, for example, here we define the / operator to run fold on a single dimensional list:

julia> import Base./

julia> /{T}(l::AbstractArray{T,1}, f::Function) = foldr(f,l)
/ (generic function with 74 methods)

Which can be used in the following way:

julia> foldr(+, [1,2,3])
6

julia> [1,2,3]/+
6

Or perhaps we want the ^ operator to repeat a function:

julia> import Base.^

julia> function ^(f::Function, iters::Int)
         fn = f
         for i in 1:(iters-1)
           fn = fn ∘ f
         end
         fn
       end
^ (generic function with 53 methods)

Which allows the following:

julia> sin(sin(2))
0.7890723435728884

julia> (sin^2)(2)
0.7890723435728884

Gauss would be proud.

Generating Fibonacci Numbers

Lets use our new operators to generate Fibonacci numbers. The first few terms of this sequence are the following:

0,1,1,2,3,5,8,13,21,34,55,...

The recursive definition for this sequence is:

fibn = fibn-1 + fibn-2

Lets say we have an array containing two adjacent numbers in the sequence:

julia> x = [8,13]

In order to generate the next Fibonacci number, we simply add the two items together.

julia> x[1] + x[2]
21

If we want to continue generating the next Fibonacci number, we will also need to save the previous one:

julia> next_x = [x[2], x[1] + x[2]]
[13,21]

Or equivalently, using our / operator to add l[1] and l[2]:

julia> next_x = [x[2], x/+]
2-element Array{Int64,1}:
 13
 21

Lets define a function so we can easily generate Fibonacci numbers:

julia> next_fib(x) = [x[2], x/+]
next_fib (generic function with 1 method)

julia> x = next_fib([8,13])
2-element Array{Int64,1}:
 13
 21

julia> x = next_fib(x)
2-element Array{Int64,1}:
 21
 34

julia> x = next_fib(x)
2-element Array{Int64,1}:
 34
 55

We can generate the nth Fibonacci number if we start at the first two Fibonacci numbers [0,1] and run the function n-1 times:

julia> function fib(n)
         x = [0,1]
         for i in 1:n-1
           x = next_fib(x)
         end
         return x[2]
       end
fib (generic function with 1 method)

julia> fib(10)
55

Or equivalently, using our ^ operator:

julia> fib(n) = (next_fib^(n-1))([0,1])[2]
fib (generic function with 1 method)

julia> fib(10)
55

Now we can swap out next_fib for it's definition:

julia> fib(n) = ((l -> [l[2], l/+])^(n-1))([0, 1])[2]
fib (generic function with 1 method)

And finally, to make the code a little more concise (why not?) we use 0:1 instead of [0,1] and index the first element of the nth list instead of the second element of the n-1th list.

julia> fib(n) = ((l->[l[2],l/+])^n)(0:1)[1]
fib (generic function with 1 method)

julia> fib(10)
55

julia> fib.(1:10)
10-element Array{Int64,1}:
  1
  1
  2
  3
  5
  8
 13
 21
 34
 55