First of all to install julia from homebrew:
$ brew cask install julia
Another way is using a tap from a guy in the JuliaLang community of github:
$  brew tap staticfloat/julia
$  brew install julia
To get Julia run with iPython/jupyter notebooks, simply open the julia prompt by
$ julia
Then use julia's package manager to get IJulia package.
$ julia > Pkg.add("IJulia")
To install Julia packages run the Pkg.add() function! 
For example
$ Pkg.add("PyPlot")   # to get the package to make plots as Matplotlib
This will download all the dependencies needed and include the PyPlot package in the current Julia directory.
In [1]:
    
2+3
    
    Out[1]:
In [2]:
    
4./2.
    
    Out[2]:
In [3]:
    
4/2
    
    Out[3]:
help?> fft
search: fft fft! FFTW fftshift rfft ifft bfft ifft! bfft! ifftshift irfft brfft plan_fft plan_fft!
  fft(A [, dims])
  Performs a multidimensional FFT of the array A. The optional dims argument specifies an iterable
  subset of dimensions (e.g. an integer, range, tuple, or array) to transform along. Most
  efficient if the size of A along the transformed dimensions is a product of small primes; see
  nextprod(). See also plan_fft() for even greater efficiency.
  A one-dimensional FFT computes the one-dimensional discrete Fourier transform (DFT) as defined
  by
\operatorname{DFT}(A)[k] =
  \sum_{n=1}^{\operatorname{length}(A)}
  \exp\left(-i\frac{2\pi
  (n-1)(k-1)}{\operatorname{length}(A)} \right) A[n].
  A multidimensional FFT simply performs this operation along each transformed dimension of A.
  Higher performance is usually possible with multi-threading. Use FFTW.set_num_threads(np) to use
  np threads, if you have np processors.
Numeric values are similar to Python
Variables are similar to Python, but any unicode string can be used as variable name! Even latex notation!
In [4]:
    
x=3
    
    Out[4]:
In [5]:
    
α = 4
    
    Out[5]:
In [231]:
    
ℵ = 5
    
    Out[231]:
In [7]:
    
println("x = ",x)
    
    
In [8]:
    
f(x) = 2x^2 + 3x +1
    
    Out[8]:
In [9]:
    
g(x) = f(x) - 2f(x-2)
    
    Out[9]:
In [10]:
    
g(2)
    
    Out[10]:
In [11]:
    
f(2)
    
    Out[11]:
The value of variables may be substituted inside strings with the $ operator.
In [12]:
    
name  = "Nikos"
    
    Out[12]:
In [13]:
    
my_greeting = "Hello $name"
    
    Out[13]:
In [14]:
    
println(my_greeting)
    
    
more complicated stuff can be used with wrapped parenthesis:
In [15]:
    
a = 3
    
    Out[15]:
In [16]:
    
println("The sine of $a is $(sin(a))")
    
    
In [17]:
    
a = Int8(1)
    
    Out[17]:
In [18]:
    
b = Int16(2)
    
    Out[18]:
In [19]:
    
a+b
    
    Out[19]:
 Arbitrary-precision integers and floiting points are available through the types BigInt() and BigFloat(). The function big() converts a number into the corresponding Big type.
In [20]:
    
big(10)
    
    Out[20]:
In [21]:
    
typeof(ans)
    
    Out[21]:
Unlike Python, integers are NOT automatically promoted to arbitrary-precision integers.
In [22]:
    
a=7
c = (1+3im)*a
    
    Out[22]:
To get the real and imaginary parts:
In [23]:
    
c.re
    
    Out[23]:
In [24]:
    
c.im
    
    Out[24]:
In [25]:
    
c.re, c.im ## this is a tuple, just as Python
    
    Out[25]:
The conjugate of a complex is given by the conj() function:
In [26]:
    
conj(a)
    
    Out[26]:
In [27]:
    
conj(c)
    
    Out[27]:
You can use rational numbers (fractions) with the built in \\ operator:
In [28]:
    
3//4
    
    Out[28]:
In [29]:
    
3//4 + 5//6
    
    Out[29]:
In [30]:
    
typeof(ans)
    
    Out[30]:
Operators are convenienet way of writing functions
In [31]:
    
//(3,4)  ## you could write +(3,4) -> ans = 7
    
    Out[31]:
In [32]:
    
//
    
    Out[32]:
In [33]:
    
methods(//) # it will list all the things you can done with this
            # function or operator
    
    Out[33]:
The expression n::Integer is a type annotation that specifies that the method applies when its first argument is of type Integer
In [34]:
    
2.4//1.4 ## cannot do this with floats
    
    
In [35]:
    
(3//4)^20
    
    Out[35]:
In [36]:
    
(3//4)^50 # when numbers getting big it will overflow
    
    
but using bigInts...
In [37]:
    
(big(3)//4)^50
    
    Out[37]:
In [38]:
    
typeof(ans)
    
    Out[38]:
To store several variables in one, use a "list" as in Python:
In [39]:
    
l = [3,4,5]
    
    Out[39]:
In [40]:
    
typeof(l)
    
    Out[40]:
In Julia, these are called Arrays.
The string Array{Int64,1} gives you the type of the array and the dimension of it.
All elements of the array, it must be of the same type!
In [41]:
    
k = [1,3, 9.2] # converts them all to float to be of the same type
    
    Out[41]:
In [42]:
    
m = [1, 4, "w00t"]  # that is of `Any` type
    
    Out[42]:
In [43]:
    
n = [1.4, 'a']
    
    Out[43]:
In [44]:
    
o = {3., 4, "hi", [2, 1]}
    
    
    Out[44]:
So you can make lists of mixed types, but if used for numerical calculations you lose the efficiency of "numpy".
In [45]:
    
k[1]
    
    Out[45]:
In [46]:
    
k[0]
    
    
For range:
In [47]:
    
k[1:3] # this will give you k[1], k[2] AND k[3]
    
    Out[47]:
Unlike Python the limits (start, end) must be mentioned explicitly:
In [48]:
    
k[1:end]
    
    Out[48]:
In [49]:
    
k[1:end-1]
    
    Out[49]:
In [50]:
    
k[-1] # does not work
    
    
In Julia Arrays, like Python lists, are dynamic, which is not the case for numpy arrays.
But the syntax is a bit different.
To add an element at the end of the list:
In [51]:
    
k
    
    Out[51]:
In [52]:
    
k+k
    
    Out[52]:
In [53]:
    
push!(k,7)  # the ! is a convention of Julia, which means that it will modify the argument
    
    Out[53]:
In [54]:
    
methods(push!)
    
    Out[54]:
In [55]:
    
methodswith(Array) #all the things I can do with the type Array
    
    Out[55]:
In [56]:
    
append!(k,[2.,3.,4.]) # similar to push! but for multiple variables at once
    
    Out[56]:
Arrays work like mathematical vectors .
In [57]:
    
a = [1.1, 2.2, 3.3]
    
    Out[57]:
In [58]:
    
b = [4.4, 5.5, 6.6]
    
    Out[58]:
In [59]:
    
a+b
    
    Out[59]:
In [60]:
    
b-a
    
    Out[60]:
In [61]:
    
(2.5 * a )
    
    Out[61]:
However, operations between arrays are not permitted because of their loose definition. (What does it mean to you a*b? Inner vector? Product of each element?)
In [62]:
    
a*b
    
    
HOWEVER for element-wise operations you can use the Matlab-syntax .
In [63]:
    
a.*b
    
    Out[63]:
But there are also so many usefull built-in functions:
In [64]:
    
dot(a,b) # dot product
    
    Out[64]:
In [65]:
    
a ⋅ b # using \cdot<TAB>
    
    Out[65]:
In [66]:
    
cross(a,b) # cross product of vectors
    
    Out[66]:
In [67]:
    
a×b # \times<TAB>
    
    Out[67]:
In [68]:
    
norm(a) # the norm of the vector a
    
    Out[68]:
Use the help() command for more info
In [69]:
    
help(dot)
    
    
In [70]:
    
?dot
    
    
    Out[70]:
In [71]:
    
transpose(a) # this is a row vector
    
    Out[71]:
In [72]:
    
a' # take the conjugate and then transpose
    
    Out[72]:
In [73]:
    
a.' 
# this just takes the transpose without the conjugate
    
    Out[73]:
In [74]:
    
j = [5+9im, 4+2im, 8+3im]
    
    Out[74]:
In [75]:
    
j'
    
    Out[75]:
In [76]:
    
j.'
    
    Out[76]:
To define a matrix use Matlab notation (with shape)
In [77]:
    
M = [2 1; 1 1]
    
    Out[77]:
or the numpy one by defining a list and reshaping:
In [78]:
    
N = reshape([1,3,4,4], (2,2))
    
    Out[78]:
In [79]:
    
O = reshape(1:8, (2,2,2))
    
    Out[79]:
In [80]:
    
transpose(M)
    
    Out[80]:
In [81]:
    
inv(M) # inverse
    
    Out[81]:
In [82]:
    
det(M) # determinant
    
    Out[82]:
Indentation is not significant. The syntax is the same as in Python, but drop the colon, and add an end !
In [83]:
    
i = 0
while i < 5 print("$i\t")
    i+=1
end
    
    
In [84]:
    
total = 0
for i = 1:10
    total +=i
end
println("Sum is $total")
    
    
In [85]:
    
typeof(1:10)
    
    Out[85]:
The notation 1:10 is a range object that can be iterated over!
You can construct an array by enclosing it in square brackets:
In [86]:
    
[1:2:10] # 1 to 10 by a step of 2
    
    
    Out[86]:
In [87]:
    
[1:2:10, 19]
    
    Out[87]:
In [88]:
    
a = 3
a < 5 && println("Small") # evaluate the second argument if the first is true
    
    
In [89]:
    
a>10 && println("Small")
    
    Out[89]:
In [90]:
    
a>10 || println("Small")  # this is semantics of the if_not-then
    
    
Similary the terciary operator:
In [91]:
    
a == 3 ? println("Hello") : println("Not true")
    
    
Just like in Python you can do array comprehensions.
In [92]:
    
squares = [i^2 for i in [1:2:10,7]]
    
    
    Out[92]:
In [93]:
    
sums = [i+j for i=1:5, j=1:5]
    
    Out[93]:
The notation with commas is a 1d vector (column vector).
In [94]:
    
v = [1,2,3]
    
    Out[94]:
For a row vector, or matrices use the Matlab-style notation.
If we omit the commas we obtain a 1 x n matrix
In [95]:
    
row_vec = [3 4 5]
    
    Out[95]:
Hermitian transpose : (Conjugate then transpose) by using '
 Simple transpose : Transpose by using .'
In [96]:
    
row_vec = [3im 4 1+5im]
    
    Out[96]:
In [97]:
    
row_vec'
    
    Out[97]:
In [98]:
    
row_vec.'
    
    Out[98]:
In [99]:
    
M = [1 2 ; 3 4]
    
    Out[99]:
NB: There is a difference in the way Python and Julia treat slices of matrices
In Python: a one-dimensional slice in either direction returns a 1-dimensional vector.
In Julia: a vertical one dimensional slice gives a 1-dimensional vector ("column vector"), but a horizontal one-dimensional slice produces a 1xn matrix!
In [100]:
    
M[:,1]
    
    Out[100]:
In [101]:
    
M[1,:]
    
    Out[101]:
In [102]:
    
v = [1, 2]
    
    Out[102]:
In [103]:
    
v*v
    
    
In [104]:
    
dot(v,v)
    
    Out[104]:
In [105]:
    
M = [1 2 ; 3 4]
    
    Out[105]:
In [106]:
    
M*v
    
    Out[106]:
In [107]:
    
methods(*)
    
    Out[107]:
In [108]:
    
N=[5 6 ; 9 8]
    
    Out[108]:
In [109]:
    
M*N
    
    Out[109]:
In [110]:
    
@which M*v  # this tells me which exact function this uses
    
    Out[110]:
This @which is a macro. It takes an expression and generates an expression.
In [111]:
    
rand()  # Uniform in [0,1]
    
    Out[111]:
In [112]:
    
x = rand(5)
    
    Out[112]:
In [113]:
    
y = rand(5,5)
    
    Out[113]:
For more distributions look at the Distributions.jl
In [114]:
    
M = rand(100,100)
    
    Out[114]:
In [115]:
    
eig(M) # eigenvalues and eigenvectors
    
    Out[115]:
In [116]:
    
typeof(ans)
    
    Out[116]:
In [117]:
    
M2 = map(big, M)  # `map` uses the function `big` over all elements of M
    
    Out[117]:
In [118]:
    
lu(M2)
    
    Out[118]:
In [119]:
    
methods(lu)
    
    Out[119]:
In [120]:
    
@edit lu(M)  # this is to open a text editor with the file
    
    
A julia script similar to Python is a sequence of commands placed on file with the suffix .jl.
From command-line, a script script.jl is ran as:
$ julia script.jl arg1 arg2
where arg1, arg2 are the command line arguments.
The command line arguments are placed in the variable ARGS as an array of strings.
In [121]:
    
outfile = open("test.txt", "w")
    
    Out[121]:
In [122]:
    
for i in 1:10
    println(outfile, "The value of i is $i") #send printl to file stream
end
close(outfile)
    
In [123]:
    
;cat test.txt # this is a shell command
    
    
In [124]:
    
infile = open("test.txt","r")
    
    Out[124]:
In [125]:
    
lines = readlines(infile)
    
    Out[125]:
In [126]:
    
map(split, lines)
    
    Out[126]:
In [127]:
    
[float(line[6]) for line in map(split, lines)]
    
    Out[127]:
In [128]:
    
x = rand(5)
    
    Out[128]:
In [129]:
    
writedlm("rand5.txt", x)
    
In [130]:
    
;cat rand5.txt
    
    
In [131]:
    
y = rand(5,5)
    
    Out[131]:
In [132]:
    
writedlm("rand55.txt",y)
    
In [133]:
    
;cat rand55.txt
    
    
Use readdlm to read a matrix from the source where each line (separated by eol) gives one row, with elements separated by the given delimeter. The source can be a text file, stream or byte array. Memory mapped files can be used by passing the byte array representation of the mapped segment as source.
In [134]:
    
z = readdlm("rand55.txt")
    
    Out[134]:
In [135]:
    
inv(z)
    
    Out[135]:
In [136]:
    
;ls
    
    
or use the `command` and then run()
In [137]:
    
cmd=`ls -lt`
    
    Out[137]:
In [138]:
    
run(cmd)
    
    
In [139]:
    
quad2(x) = x^2
    
    Out[139]:
In [140]:
    
quad2(2)
    
    Out[140]:
In [141]:
    
function quad(x)
    x^2 # no explicit return needed
end
    
    Out[141]:
The last value that is computed within the function is automatically returned, so no explicit return statement is needed.
In [142]:
    
quad(2)
    
    Out[142]:
Since every operator in Julia is a function and functions are implemented by specifying their action on different types something like this can work:
In [143]:
    
quad(2), quad(2.5), quad(1+3im), quad("hello")
    
    Out[143]:
NB: String concat in Julia is done with the * operator and not the + operator. Repeating a string is thus done by raising it to an integer power.
So multiplying a string with a number will not work, but rainsing it to an integer power (i.e. multiplying it p times) will..
In [144]:
    
2*"hello"
    
    
In [145]:
    
"hello"^2
    
    Out[145]:
In [146]:
    
s1 = "hello"
s2 = "Nikos"
    
    Out[146]:
In [147]:
    
s1+s2
    
    
In [148]:
    
s1*s2
    
    Out[148]:
But we can define the + operator to work with string concat:
In [149]:
    
+(s1::String, s2::String) = string(s1, s2)
    
    
    Out[149]:
In [150]:
    
"First"+" second"
    
    Out[150]:
In [151]:
    
"Value is " + 3 # this is not yet defined for my +
    
    
A user-defined "composite type" is a colleciton of data. Unlike Python types do not "own" methods (functions internal to the type).
Methods are defined separately and are characterized by the types of all their arguments : multiple dispatch (dispatch is the process of choosing which "version" of a function to use).
For example, let's define a 2D vector type.
In [152]:
    
immutable Vector2D  # we could also use type instead of immutable
    x::Float64
    y::Float64
end
    
We use the immutable instead of type for efficiency: the object s stored in an efficient packed form.
In [153]:
    
Vector2D(3.0, 2.0)
    
    Out[153]:
We then to define the operators ... For example for vector addition:
In [154]:
    
+(v::Vector2D, w::Vector2D) = Vector2D(v.x+w.x, v.y+w.y)
    
    
    Out[154]:
In [155]:
    
v = Vector2D(3, 2)
    
    Out[155]:
In [156]:
    
u = Vector2D(1,3)
    
    Out[156]:
In [157]:
    
u+v
    
    Out[157]:
In [158]:
    
import Base.show
    
In [159]:
    
show(io::IO, v::Vector2D) = print(io, "[$(v.x), $(v.y)]")
    
    Out[159]:
In [160]:
    
v
    
    Out[160]:
Types may have a parameter, for exampel:
In [161]:
    
immutable VectorND{T <: Real}
    x::T
    y::T
end
    
T is a type parameter.
T <: Real means that T must be a subtype of the abstract type Real.
We can investigate the hierarchy with the super() function
In [162]:
    
super(Real)
    
    Out[162]:
In [163]:
    
v = VectorND(3,4)
    
    Out[163]:
In [164]:
    
n = VectorND(3+1im, 2) # this does not work since VectorND works only
                       # with Real values
    
    
In [165]:
    
type Particle
    position::Vector2D
    momentum::Vector2D
end
    
In [166]:
    
move(p::Particle, dt::Real) = p.position += p.momentum*dt
    
    Out[166]:
In [167]:
    
show(io::IO, p::Particle) = print(io, "pos:$(p.position); vel: $(p.momentum)")
    
    Out[167]:
In [168]:
    
+(v1::Vector2D, v2::Vector2D) = Vector2D(v1.x+v2.x, v1.y+v2.y)
    
    
    Out[168]:
In [169]:
    
*(v1::Vector2D, lamb::Number) = Vector2D(lamb*v.x, lamb*v.y)
    
    
    Out[169]:
In [170]:
    
p = Particle(Vector2D(0,0), Vector2D(1,1))
    
    Out[170]:
In [171]:
    
move(p, 0.1)
    
    Out[171]:
Now we can define a gas as a collection of particles...
In [172]:
    
type Gas
    particles::Vector{Particle} # Array{Particle, 1}
    
    function Gas(N::Int) # constructor
        parts = [Particle(Vector2D(rand(2)...), Vector2D(rand(2)...)) for i in 1:N]
        new(parts)
    end
end
    
In [173]:
    
myg=Gas(10)
    
    Out[173]:
In [174]:
    
function move(g::Gas, dt::Number)
    for particle in g.particles
        move(particle, dt)
    end
end
    
    Out[174]:
In [175]:
    
move(myg, 2)
    
In [176]:
    
myg
    
    Out[176]:
Coming from Python the most accessible (and full-featured) package is PyPlot, the Julia wrapper for pyplot submodule of Matplotlib.
To get it:
In [177]:
    
Pkg.add("PyPlot")
    
    
Pkg is the package manager in Julia. It uses git repositories that are each cloned into their own subdirectory under the ~/.julia directory.
The list of packages is available at Julia Packages
In [178]:
    
using PyPlot
    
This is similar to the Python syntax:
from <package> import *
and it makes all names in the packages available.
Instead of using PyPlot one could use:
   import PyPlot
in this case the names of the package will be available as PyPlot.plot() etc
In [179]:
    
x = rand(10); y = rand(10);
    
In [180]:
    
p = plot(x,y, "ro-")
    
    
    Out[180]:
In [181]:
    
p
    
    Out[181]:
In [182]:
    
figure(figsize=(4,2))
    
    Out[182]:
In [183]:
    
plot(rand(10),rand(10))
    
    
    Out[183]:
In [184]:
    
L = 1000
    
    Out[184]:
In [185]:
    
diffs = []
    
    Out[185]:
In [186]:
    
for i in 1:10
    M = randn(L, L)
    M = Symmetric(M)
    
    lamb = eigvals(M)
    diffs = [diffs, diff(lamb)]
end
    
    
In [187]:
    
diffs
    
    Out[187]:
In [188]:
    
h = plt[:hist](diffs, 300)   #weird syntax
    
    
    Out[188]:
For more examples of plots using PyPlot check:
In [200]:
    
using Gadfly
    
    
In [201]:
    
xs =  1:10
    
    Out[201]:
In [202]:
    
ys = rand(10)
    
    Out[202]:
In [203]:
    
Gadfly.plot(x=xs, y=ys)
    
    Out[203]:
The plot can be panned and moved interactively.
In [204]:
    
Gadfly.plot(x=xs, y=ys, Geom.line)  # adding a Geometry
    
    Out[204]:
In [205]:
    
Gadfly.plot(x=xs, y=ys, Geom.line, Geom.point)
    
    Out[205]:
In [206]:
    
Gadfly.plot(x=rand(10), y=rand(10), Geom.line)
    
    Out[206]:
... is not really random at all.
Thus to preserve the order we can use the argument preserve_order=true.
In [207]:
    
Gadfly.plot(x=rand(10), y=rand(10), Geom.point, Geom.line(preserve_order=true))
    
    Out[207]:
In [208]:
    
p=Gadfly.plot(layer(x=rand(10), y=rand(10), Geom.point, Geom.line(preserve_order=true)), 
layer(x=rand(10), y=rand(10), Geom.point, Geom.line(preserve_order=true)), Guide.XLabel("first"), Guide.YLabel("second"))
    
    Out[208]:
Guide is the driver for the information about the plot. Guide.XLabel, Guide.YLabel put the labels into axes.
We can also write directly to PDF:
In [209]:
    
#Gadfly.draw(PDF("stuff.pdf", 10cm, 5cm), p)  # this is fucked up atm
    
In [210]:
    
#;open stuff.pdf
    
In [211]:
    
using RDatasets
    
In [212]:
    
irises = dataset("datasets", "iris")
    
    Out[212]:
In [213]:
    
head(irises)
    
    Out[213]:
In [214]:
    
Gadfly.plot(irises, x="SepalLength",y="SepalWidth", Geom.point)
    
    Out[214]:
In [216]:
    
Gadfly.plot(irises, x="SepalLength",y="SepalWidth", color="Species", Geom.point)
    
    Out[216]:
To be added (look at SciPy 2014)
When profiling run each function once with the correct argument types before timing it, since the first time is run the compilation time will play a large role!
In [222]:
    
@time cos(10) # run it 2 times ;)
    
    
    Out[222]:
Detailed profile can be done with the @profile macro
In [245]:
    
@profile cos(10)
    
    Out[245]:
A package named ProfileView.jl gives you a graphical profiling.
In [224]:
    
function sum1(N::Int)
    total = 0 # total is defined as Integer
    for i in 1:N
        total +=i/2
    end
    total
end
function sum2(N::Int)
    total = 0.0 # here total is defined as float from the start
    for i in 1:N
        total +=i/2
    end
    total
end
    
    Out[224]:
In [226]:
    
sum1(10), sum2(10)
    
    Out[226]:
In [227]:
    
N=1000000
    
    Out[227]:
In [229]:
    
@time sum1(N)
    
    
    Out[229]:
In [230]:
    
@time sum2(N)
    
    
    Out[230]:
They produce the same result, but the time is different.
There are 4 functions that can access every step of the compilation process
In [235]:
    
code_lowered(sum1, (Int,)) # name of func, tuple of tupes of arguments
    
    Out[235]:
In [234]:
    
code_lowered(sum2, (Int,))
    
    Out[234]:
Due to the type stability the two code_lowered are different
In [238]:
    
code_typed(sum1, (Int,)) # check the Union in the result
    
    Out[238]:
In [239]:
    
code_typed(sum2, (Int,))
    
    Out[239]:
In [240]:
    
code_llvm(sum1, (Int,))
    
    
In [242]:
    
code_native(sum1, (Int,))  # assembly
    
    
In [246]:
    
using PyCall
    
PyCall has a high-level intereface that transports between Julia and Python trasparently from the users point of view.
For example let's call Python's math module!
In [247]:
    
@pyimport math
    
In [248]:
    
math.sin(math.pi)
    
    Out[248]:
Array objects are automatically converted.. So for numpy
In [249]:
    
@pyimport numpy.random as nprandom
    
In [251]:
    
nr=nprandom.rand(3,4) # this returns a ndarray... but in juliaaa...
    
    Out[251]:
In [253]:
    
typeof(nr)  # but within Julia this is an array
    
    Out[253]:
Defining a Julia function
In [254]:
    
objective = x -> cos(x) - x  # this is a Julia anonymous function
    
    Out[254]:
In [255]:
    
objective(3)
    
    Out[255]:
We can also pass this Julia (anonymous) function to a Python module:
In [256]:
    
@pyimport scipy.optimize as so
    
In [257]:
    
so.newton(objective,1)
    
    Out[257]:
Julia has ODE solvers in ODE.jl and Sundials.jl, but we can also use Python solvers.
In [259]:
    
@pyimport scipy.integrate as integrate
f(x,t)= -x
    
    Out[259]:
In [261]:
    
t=[0:0.1:10];
    
In [262]:
    
soln = integrate.odeint(f,1,t)
    
    Out[262]:
Note that PyPlot package provides a higher-level wrapper than PyCall around matplotlib
In [264]:
    
plot(t, soln)
    
    
    Out[264]:
In Python accessing fields and properties of objects is done using the
obj.a  and obj.b()
syntax. However, the syntax obj.b in Julia is restricted to accessing fields of composite types.
Thus to access fields and methods of Python objects via PyCall, it is necessary to use the syntax:
For Fields:   obj[:a]
For Methods:  obj[:a]()
Where here, we use the Julia syntax :a to mean the Julia symbol a.
The PyCall interface is built ontop of the interface which transports objects between Python and Julia: PyObject, which wraps the PyObject* in C and represents a reference to a Python object
In [265]:
    
PyObject(3)
    
    Out[265]:
In [268]:
    
xx = PyObject(rand(5,5))
    
    Out[268]:
In [269]:
    
typeof(xx)
    
    Out[269]:
In [270]:
    
names(xx)
    
    Out[270]:
In [271]:
    
xx.o
    
    Out[271]:
In [272]:
    
#xx.shape in python becomes
xx[:shape]
    
    Out[272]:
NB: Julia arrays passed in Python without a copy, but by default Python array is copied when a result is requested in Julia. This can be avoided at a lower level using pycall and PyArray.
Within Julia you can call C and Fortran functions in shared libraries, via the ccall function.
To use it we need to specify :
and the name of the shared library; given as an ordered pair (tuple)
the return type of the function
An example using the clock function:
In [274]:
    
t = ccall( (:clock, "libc"), Int32, ())
    
    Out[274]:
In [275]:
    
typeof(t)
    
    Out[275]:
In [277]:
    
path = ccall( (:getenv, "libc"), Ptr{UInt8}, (Ptr{UInt8},), "PATH")
# (name of function, library), return type, (argument type,), argument)
# Ptr{UInt8} is a pointer to the Uint8 type
    
    Out[277]:
In [278]:
    
path
    
    Out[278]:
In [279]:
    
bytestring(path) # create a string from the address of a C string
    
    Out[279]:
In [289]:
    
:s
    
    Out[289]:
In [290]:
    
typeof(:s)
    
    Out[290]:
:s refers to the symbol s and we can evaluate with the eval function
In [291]:
    
eval(:s) # not yet defined..
    
    
In [292]:
    
s=3; eval(:s)
    
    Out[292]:
The eval function takes an expression and evaluates it, generating the corresponding code.
Everything is a symbol:
In [293]:
    
:+, :sin
    
    Out[293]:
and symbols can be combined into expressions, that are the basic objects which represent pieces of Julia code.
In [295]:
    
ex = :(a+b) # this is the expression a+b
    
    Out[295]:
In [296]:
    
typeof(ex)
    
    Out[296]:
Expressions are Julia's objects, so we can find more info about it
In [297]:
    
names(ex)
    
    Out[297]:
In [299]:
    
ex.args  # ex.<TAB>
    
    Out[299]:
In [302]:
    
ex.args[2]
    
    Out[302]:
But expressions can be arbitrary Julia code, that when evaluated will have side effects.
For longer blocks of code quote ... end may be used instead of :(...)
In [303]:
    
ex2 = 
quote
    y=3
    z=sin(y+1)
end
    
    Out[303]:
In [304]:
    
eval(ex2)
    
    Out[304]:
In [306]:
    
Meta.show_sexpr(ex2) # this is to show the full info
    
    
In [307]:
    
dump(ex2) # to dump the internal of an expression
    
    
Using this idea we can write Julia code on the fly from within Julia : programming a program!
The macro name is given to a kind of 'super-function' that takes a piece of code as an argument and returns an altered piece of code.
Macros map a tuple of argument expressions to a returned expression.
Macros are useful to
Macros are invoked using the @ sign. 
For example:
In [308]:
    
@time sin(10)
    
    
    Out[308]:
We can defin a macro as follows. The $ sign is used to interpolate the valie of the expression (as in string interpolation)
In [309]:
    
macro duplicate(ex)
    quote
        $ex
        $ex
    end
end
    
In [310]:
    
@duplicate println(sin(10))
    
    
In [315]:
    
ex = :(@duplicate println(sin(10))) #to understand what it does...
    
    Out[315]:
In [313]:
    
eval(ex)
    
    
In [314]:
    
macroexpand(ex)
    
    Out[314]:
In [316]:
    
macroexpand(:(@time sine(10)))
    
    Out[316]:
In [ ]: