Julia

Julia is developed as an easy to use scientific language. It is a high-level, multi-paradigm language that Prides it's self for being efficient to both code and during execution.

  • mulit dispatch
  • Dynamic typing
  • Just-In-Time (JIT) complier
  • Distributed parallel computation

Ease

Julia uses coding styles that are familiar to many programmers who have experince with other languages. A lot of julia code could be read by programmers not even familiar with julia. It seemlessly adds some of the stragies of a functional programming language in a way that is easy to understand.

In [1]:
x = 1
Out[1]:
1
In [2]:
typeof(x)
Out[2]:
Int64

You can use user defined types in order to expand the language and make object that better fit your purposes. By having different types of Types in Julia you are able to build the language to do what ever you need it to do. A lot of Julia is built to be able to work seemlessly with user defined types

immutable (struct) type (mutable struct) abstract (abstract type)
In [3]:
type classmates ##mutable Struct
    name::String
    ID::Int
end
In [4]:
b = push!([], classmates("Bob.", 95))
Out[4]:
1-element Array{Any,1}:
 classmates("Bob.",95)
In [5]:
push!(b, classmates("Sarah", 90))
push!(b, classmates("Alex", 100))
Out[5]:
3-element Array{Any,1}:
 classmates("Bob.",95) 
 classmates("Sarah",90)
 classmates("Alex",100)

Julia is able to make closures around functions automatically.

-------- counter -------- first.jl
In [54]:
function makecounter(in::Int)
    c = in
    println("made counter")
    return function()
        c = c +1
        return c
    end
end
WARNING: Method definition makecounter(Int64) in module Main at In[53]:2 overwritten at In[54]:2.
Out[54]:
makecounter (generic function with 1 method)
In [55]:
count1 = makecounter(0)
made counter
Out[55]:
(::#11) (generic function with 1 method)
In [58]:
count1()
Out[58]:
3

Multi Dispatch


Allows you to "overload" functions in the Just-In-Time Compliler at run time.

In [7]:
function volume(s::Real) #volume of a cube
    return s * s * s
end
Out[7]:
volume (generic function with 1 method)
In [8]:
function volume{T <: Real}(l::T, w::T, h::T) #Volume of a box
    return l * w * w * h
end
Out[8]:
volume (generic function with 2 methods)
In [9]:
volume(2, 3, 4)
Out[9]:
72

Can overload the Julia's built in functions to be more expansive especially if you define your own types

In [61]:
type weirdnum
    num::Int32
end
In [62]:
foo = weirdnum(2)
Out[62]:
weirdnum(2)
In [63]:
faa = weirdnum(3)
Out[63]:
weirdnum(3)
In [64]:
foo + faa
MethodError: no method matching +(::weirdnum, ::weirdnum)
Closest candidates are:
  +(::Any, ::Any, ::Any, ::Any...) at operators.jl:138
In [65]:
import Base.+
function +(a::weirdnum, b::weirdnum)
    return weirdnum(a.num + b.num)
end
Out[65]:
+ (generic function with 164 methods)
In [67]:
foo + faa
Out[67]:
weirdnum(5)
--------generic function--------

function generic(args) return args

In [72]:
function generic(a)
    println(a)
end
WARNING: Method definition generic(Any) in module Main at In[68]:2 overwritten at In[72]:2.
Out[72]:
generic (generic function with 3 methods)
In [71]:
function generic(num::Number)
    println(num^2)
end
WARNING: Method definition generic(Number) in module Main at In[69]:2 overwritten at In[71]:2.
Out[71]:
generic (generic function with 3 methods)
In [76]:
function generic(a, Varargs...)
    println(a, " followed by ", Varargs, "as a tuple")
end
WARNING: Method definition generic(Any, Any...) in module Main at In[70]:2 overwritten at In[76]:2.
Out[76]:
generic (generic function with 4 methods)
In [74]:
function generic(b::Array{Any,1})
    println("::",b,"::")
end
WARNING: Method definition generic(Array{Any, 1}) in module Main at In[73]:2 overwritten at In[74]:2.
Out[74]:
generic (generic function with 4 methods)
In [77]:
generic("Hello World")
generic(2)
generic("first call", 2, 3, 4, 5)
generic([1, 2, 3])
Hello World
4
first call followed by (2,3,4,5)as a tuple
[1,2,3]

Arrays


Julia has fairly expansive array handling capabilities for the scientific uses that it has. A cleaner syntax for Multidimensionl arrays means that it can be easily used for the linear algebra that is required in some fields. While the syntax between the different demensions of arrays are slightly different both are very nice seperately. you just have to get used to changing the syntax.

Julia's multidemensional arrays are limited to being a consistent width and height across different columns and rows. For that you have to go back to older languages arrays of arrays.

In [24]:
one_D_array = ["a","b","c"]
Out[24]:
3-element Array{String,1}:
 "a"
 "b"
 "c"
In [25]:
two_D_array = ["a" "b"; "c" "d"]
Out[25]:
2×2 Array{String,2}:
 "a"  "b"
 "c"  "d"
In [26]:
typeof(one_D_array)
Out[26]:
Array{String,1}
In [27]:
typeof(two_D_array)
Out[27]:
Array{String,2}

Indexing these arrays is done via square brackets just like in many languages, It can also be done via iterators over ranges. In the below example the default iterator is used but you can actually change that.

In [78]:
one_D_array[1]
UndefVarError: one_D_array not defined
In [29]:
for i in 1:length(one_D_array)
    print(one_D_array[i], " ")
end
a b c 
In [30]:
for i in 1:length(two_D_array)
    print(two_D_array[i], " ")
end
a c b d 
In [31]:
two_D_array[1,2]
Out[31]:
"b"

Multidimensional-Arrays have lots of convient constructors that are needed for the common features of linear algebra.

In [32]:
identity = eye(3)
Out[32]:
3×3 Array{Float64,2}:
 1.0  0.0  0.0
 0.0  1.0  0.0
 0.0  0.0  1.0
In [33]:
one = ones(2,3)
Out[33]:
2×3 Array{Float64,2}:
 1.0  1.0  1.0
 1.0  1.0  1.0
In [34]:
zero = zeros(3,2)
Out[34]:
3×2 Array{Float64,2}:
 0.0  0.0
 0.0  0.0
 0.0  0.0

Existing arrays can have different parts selected by using square brackets. In place of the numbers within these arrays you can use ranges (format of 2:3) in order to select a specific subset of the array or simply an : in order to select the whole dimension.

Additionally Julia has a lot of built in functions for manipulating Arrays and the data within. For many of the basic math functionality of arrays such as addition, and multiplication can be used via dot operators (format of .+).

In [35]:
alpha = [8 4 2;4 2 1; 2 1 0] #2D syntactic Sugar
Out[35]:
3×3 Array{Int64,2}:
 8  4  2
 4  2  1
 2  1  0
In [36]:
alpha[2:3,:]
Out[36]:
2×3 Array{Int64,2}:
 4  2  1
 2  1  0
In [37]:
alpha[1,:]
Out[37]:
3-element Array{Int64,1}:
 8
 4
 2
In [40]:
n = zeros(3,3,3)
Out[40]:
3×3×3 Array{Float64,3}:
[:, :, 1] =
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

[:, :, 2] =
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

[:, :, 3] =
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0
In [41]:
n[:,2, 1:2] = 1
Out[41]:
1
In [42]:
n
Out[42]:
3×3×3 Array{Float64,3}:
[:, :, 1] =
 0.0  1.0  0.0
 0.0  1.0  0.0
 0.0  1.0  0.0

[:, :, 2] =
 0.0  1.0  0.0
 0.0  1.0  0.0
 0.0  1.0  0.0

[:, :, 3] =
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0
In [38]:
(ones(3) .+ alpha)
Out[38]:
3×3 Array{Float64,2}:
 9.0  5.0  3.0
 5.0  3.0  2.0
 3.0  2.0  1.0
In [39]:
(ones(2) .+ alpha[2:3,2:3])
Out[39]:
2×2 Array{Float64,2}:
 3.0  2.0
 2.0  1.0

Parallel computing


So in addition to tasks there is parallel computing, which allows the user to run functions on seperate CPUs or even seperate machines entirely. The processes (procs) are called seperately and pre-set with their functions up on the scheduler.

new processes are started with addprocs(n), then they are given their new functions with remotecall() which returns a future value that can be evaluated and have data extraced via fetch()

There are also remote channels which are writable for more control over syncing processes.

In [43]:
addprocs(1)
In [44]:
fut = remotecall(sqrt, 2, 2)
Out[44]:
Future(2,1,3,Nullable{Any}())
In [45]:
fetch(fut)
Out[45]:
1.4142135623730951

This is the lowest level that a programmer could end up needing within Julia. As such the language tries to make it easier to use this if you needed to use it with the @spawn macro.

In [46]:
future = @spawn sqrt(2)
In [47]:
fetch(future)
Out[47]:
1.4142135623730951
In [48]:
addprocs(2)
Out[48]:
2-element Array{Int64,1}:
 3
 4
In [49]:
@everywhere function fib(n)
    if (n < 2)
        return n
    else 
        return fib(n-1) + fib(n-2)
    end
end
In [50]:
@everywhere function fib_parallel(n)
    if (n < 35)
        return fib(n)
    else
        x = @spawn fib_parallel(n-1)
        y = fib_parallel(n-2)
        return fetch(x) + y
    end
end
In [51]:
@time fib(42)
  2.271563 seconds (793 allocations: 40.718 KB)
Out[51]:
267914296
In [52]:
@time fib_parallel(42)
  3.483601 seconds (344.48 k allocations: 15.344 MB, 0.25% gc time)

After the function is run the process just waits for the next next call to process the function. The future value that has been returned is a single value. It doesn't re-calculate anything.

In [53]:
count1 = makecounter(3)
made counter
Out[53]:
(::#1) (generic function with 1 method)
In [54]:
futu = remotecall(count1, 2)
Out[54]:
Future(2,1,23,Nullable{Any}())
In [55]:
fetch(futu)
Out[55]:
4

Coroutines


In julia the main way that messages are passed between coroutines in Julia are produce() and consume(). This allows a two functions to pass a single bit of data back and forth to each other in a simple way.

Julia tasks have very little overhead but will always run on the same CPU. The coroutines are just a control structure to get the process to run between different threads for things like syncing.

----- pingpong.jl -----
In [56]:
function ping()
    println("ping")
    println(consume(pingreply))
end
Out[56]:
ping (generic function with 1 method)
In [57]:
function pong(message::String)
    while true
        produce(message)
    end
end
Out[57]:
pong (generic function with 1 method)
In [58]:
pingreply = Task(() -> pong("pong"))
Out[58]:
Task (runnable) @0x00007f3c3023ee60
In [59]:
ping()
ping
pong
In [60]:
for i in 1:4
    ping()
end
ping
pong
ping
pong
ping
pong
ping
pong

This is handling tasks manually, instead of letting the scheduler do it. At this stage the tasks are not even known to the scheduler so things like deadlock won't be caught at all.

To add things to be automatically scheduled using schedule(), or the @schedule and @sync macros


Meta Programming


This starts to get into how the language actually functions and features of that.

Our code starts off as a string. This code is then parsed into a format called an expression (expr)

interpolation

ability to replace things in strings with variables or equations. it happens within a string via using the key $ with parenthesis

In [61]:
println("1 plus 2 equals $(1+2)")
1 plus 2 equals 3
In [62]:
a = 1
b = 2
Out[62]:
2
In [63]:
println("a and b are $a and $b respectively so a + b = $(a+b)")
println("the sqrt of b (val: $b) is $(sqrt(b))")
a and b are 1 and 2 respectively so a + b = 3
the sqrt of b (val: 2) is 1.4142135623730951

introspection of code

Julia gives access to allow you to change code based on input. By being able to parse and evaluate code at run time were are able to build powerful meta programs that will be able to respond to the needs of the user at run time.

This start with how Julia intreprets code that it is given. It uses the Parse() and Eval() commands to be able to transform a string into runable code.

All code in julia starts as a string that is then parsed into expressions

In [64]:
tobeparsed = "1 + 1"
Out[64]:
"1 + 1"
In [65]:
tobeEval = parse(tobeparsed)
Out[65]:
:(1 + 1)

tobeEval uses the Expr data type which is the basic format for code to be computed in Julia.

In [66]:
eval(tobeEval)
Out[66]:
2

This is then evaluated into the final result

Having access to this middle step allows us to be able to modify our own functions.

In [78]:
function not_pre_set(op::String, num1::Real, num2::Real)
    parse("$op($num1, $num2)")
end
WARNING: Method definition not_pre_set(String, Real, Real) in module Main at In[67]:2 overwritten at In[78]:2.
Out[78]:
not_pre_set (generic function with 1 method)
In [79]:
not_pre_set("+", 1, 2)
Out[79]:
:(1 + 2)
In [80]:
eval(not_pre_set("+", 1, 2))
Out[80]:
3