Day 2 of Julia takes a wander through control flow, types, concurrency and multiple dispatch. Multiple dispatch is something I have not played with much before, and it seems very useful indeed. It works like overridden methods in OOP. But rather than picking which function to call based on just the object, in multiple dispatch all the types are taken into account. Pretty cool.
I enjoyed playing with the parallel processing parts of the language too. It is remarkably straightforward to parallellize tasks, which has got to be pretty critical for a language that is optimized for use in scientific computing.
Exercises
Easy exercises
Write a for loop that counts backward using Julia’s range notation.
We can use the start:step:end notation like this.
julia> for i in 10:-1:1
println(i)
end
10
9
8
7
6
5
4
3
2
1
julia> for i in 10:-2:5
println(i)
end
10
6
8
Write an iteration over a multidimensional array like [1 2 3; 4 5 6; 7 8 9]. In what order does it get printed out?
Seems trivial to get rows out thus.
julia> m = [1 2 3; 4 5 6; 7 8 9]
3x3 Array{Int64,2}:
1 2 3
4 5 6
7 8 9
julia> for i in m
println(m[i])
end
1
2
3
4
5
6
7
8
9
Use pmap to take an array of trial counts and produce the number of heads found for each element.
I spent a really long time on this and could not find a sensible way to do it. This seems very clearly a job for a fold/reduce rather than a map. I came up with something that "works" but not sure if it is what the authors were after.
julia> heads=[1 0 1 1; 1 1 0 1; 1 0 0 1; 0 1 0 1]
4x4 Array{Int64,2}:
1 0 1 1
1 1 0 1
1 0 0 1
0 1 0 1
julia> pmap(x->sum(heads[x,:]), 1:4)
4-element Array{Any,1}:
3
3
2
2
Medium exercises
Write the fatorial function as a parallel for loop
Well, there is a much prettier way to do it that I realized earlier which is just.
julia> *(1:5...)
120
However, to make a parallel loop you could
julia> function fac(n)
@parallel (*) for i in 1:n
i
end
end
fac (generic function with 1 method)
julia> fac(5)
120
julia> fac(15)
1307674368000
julia> fac(25)
7034535277573963776
Add a method for concat that can concatenate an integer with a matrix — concat(5, [1 2; 3 4]) should produce [5 5 1 2; 5 5 3 4]
That is a bit weird. I would have thought it would produce [5 1 2; 5 3 4], so I implemented it that way. I use the library function hcat once I have constructed a vercor of the correct dimension.
julia> q = [1 2 5; 2 2 2; 6 6 6]
3x3 Array{Int64,2}:
1 2 5
2 2 2
6 6 6
julia> function concat(n, m)
tocat = fill(n,size(m)[1])
hcat(tocat,m)
end
concat (generic function with 1 method)
julia> concat(5,q)
3x4 Array{Int64,2}:
5 1 2 5
5 2 2 2
5 6 6 6
You can extend built-in functions with new methods too. Add a new method for + to make "jul"+"ia" work.
I assumed that + ought to mean string concatnenation rather than anything more exotic here.
julia> function +(a::String, b::String)
"$a$b"
end
+ (generic function with 118 methods)
julia> "jul"+"ia"
"julia"
Hard exercises
Parallel for loops dispatch loop bodies to other processes. Depending on the size of the loop body, this can have a noticeable overhead. See if you can beat Julia’s parallel for loop version of pflip_coins by writing something using the lower-level primitives like @spawn or remotecall.
I wasn’t able to have a major impact on execution time — the variance between runs was more than any saving in time over the original function. However taking a more functional approach allowed me to use significantly less memory. The code is a little more complex syntactically and there is a potential bug if the test number is not divisible by 4 (I found that running 4 processes gave me the best results).
I suppose that the saving is because of the count_flip function body being added to the global environment with @everywhere but I don’t have any evidence for this.
Incidentally an optimization that sometimes works in Haskell — combining the functions in the 2 maps into one map with 2 functions in it — which in this case would mean composing fetch and spawn in the same map call actually made things somewhat slower.
julia> @everywhere function count_flip(n)
count = 0
for i in 1:n
count += int(randbool())
end
count
end
julia> function mycoins(n)
sum( map( fetch, map(x -> (@spawn count_flip(n/4)), 2:5) ) )
end
mycoins (generic function with 1 method)
julia> @time pflip_coins(1000000000)
elapsed time: 2.150548463 seconds (6429340 bytes allocated)
500020611
julia> @time pflip_coins(1000000000)
elapsed time: 1.713294895 seconds (183048 bytes allocated)
499988013
julia> @time mycoins(1000000000)
elapsed time: 1.681171545 seconds (20912 bytes allocated)
444429739
julia> @time mycoins(1000000000)
elapsed time: 1.638956568 seconds (18944 bytes allocated)
444437840
Wrapping up
Julia is looking like a really nice language so far.
The syntax is straightforward. There are types but they don’t get in your way. The REPL works nicely — in particular the standard readline facilities work in a more sensible way than other REPLs when you’re writing multiline functions. The concurrency tools I have looked at are clean and easy to use. Not bad at all.