In [1]:
x=1+2im
Out[1]:
In [2]:
typeof(x)
Out[2]:
A complex number consists of two fields: a real part (denoted re
) and an imaginary part (denoted im
). Fields of a type can be accessed using the .
notation:
In [3]:
x.re
Out[3]:
In [4]:
x.im
Out[4]:
We can also make our own types. Let's make a type to represent complex numbers in the format
$$z=rexp(i\theta)$$
That is, we want to create a type with two fields: r
and θ
. This is done using the type
syntax, followed by a list of names for the fields, and finally the keyword end
In [5]:
type MyComplex
r
θ
end
In [6]:
z=MyComplex(1,0.1)
Out[6]:
We can access fields for our new type using .r
and .θ
:
In [7]:
z.θ
Out[7]:
In [8]:
z.r, z.θ
Out[8]:
In [9]:
typeof(ans)
Out[9]:
In [10]:
function sq(x)
x^2
end
Out[10]:
In [11]:
sq(2),sq(3)
Out[11]:
Multiple arguments to the function can be included with ,
. Here's a function that takes in 3 arguments and returns the average. (We write it on 3 lines only to show that functions can take multiple lines.)
In [12]:
function av(x,y,z)
ret=x+y
ret=ret+z
ret/3
end
Out[12]:
In [13]:
av(1,2,3)
Out[13]:
Variables live in different scopes. In the previous example, x
, y
, z
and ret
are local variables: they only exist inside of av
. So this means x
and z
are not the same as our complex number x
and z
defined above.
Note: This is only true for x, y and z because they are bit types. If they are arrays, a function will change 'surrounding' x, y and z values.
Warning: if you reference variables not defined inside the function, they will use the outer scope definition. The following example shows that if we mistype the first argument as xx
, then it takes on the outer scope definition x
, which is a complex number
In [14]:
function av2(xx,y,z)
(x+y+z)/3
end
Out[14]:
In [15]:
av2(5,6,7)
Out[15]:
You should almost never use this feature!! We should ideally be able to predict the output of a function from knowing just the inputs.
In [16]:
v=[1; 2; 3; 4; 5] # Note the use of ';' to create a vector
function suminlastentry(v::Vector{Int64})
v[end]=sum(v)
end
suminlastentry(v)
v
Out[16]:
In [17]:
v=[1 2 3 4 5]
Out[17]:
In [18]:
v=[1, 2, 3, 4, 5]
Out[18]:
In Julia 0.5 this also works, but:
In [21]:
m=[1 2 3 4 5;]
Out[21]:
In [22]:
m=[1 2 3;4 5 6] # Replacing the ';' by a ',' will give an error
Out[22]:
In [116]:
v=rand(Int,5)
w=rand(Int,10)
length(v),length(w)
Out[116]:
To implement the function, we need to use a for loop, using the for
keyword. The following syntax evaluates the body of the for loop (between the lines after the for
and before the end
) one by one for k
equal to every number in the range 1:10
.
In [118]:
for k=1:5
k2=k^2
println(k2)
end
This is exactly the same as the following block of text, but without having to write it out explicitely
In [119]:
k=1
k2=k^2
println(k2)
k=2
k2=k^2
println(k2)
k=3
k2=k^2
println(k2)
k=4
k2=k^2
println(k2)
k=5
k2=k^2
println(k2)
We can use a for loop to step k
through every index of a vector. The following calculates the sum of the entries of the vector, printing out the current value for each value of k
In [120]:
v=[1,5,6,3]
ret=0
for k=1:length(v)
ret=ret+v[k]
println("At step $k, the current sum is $ret")
end
ret
Out[120]:
We are now ready to write a function that calculates the average of the entries of a vector:
In [39]:
function vecav(v)
ret=0
for k=1:length(v)
ret=ret+v[k]
end
ret/length(v)
end
Out[39]:
In [122]:
vecav([1,5,2,3,8,2])
Out[122]:
julia has an inbuilt sum
command that we can use to check our code:
In [123]:
sum([1,5,2,3,8,2])/6
Out[123]:
functions can be defined only for specific types using ::
after the variable name. The same function name can be used with different type signatures.
The following defines a function mydot
that calculates the dot product, with a definition changing depending on whether it is an Integer
or a Vector
. Note that Integer
means any kind of integer: mydot
is defined for pairs of Int64's, Int32's, etc.
In [124]:
function mydot(a::Integer,b::Integer)
a*b
end
function mydot(a::Vector,b::Vector)
# we assume length(a) == length(b)
ret=0
for k=1:length(a)
ret=ret+a[k]*b[k]
end
ret
end
Out[124]:
In [125]:
mydot(5,6) # calls the first definition
Out[125]:
In [126]:
mydot(Int8(5),Int8(6)) # also calls the first definition
Out[126]:
In [127]:
mydot([1,2,3],[4,5,6]) # calls the second definition
Out[127]:
In [128]:
mydot([1,2,3,4],[4,5,6]) # an error is thrown because length(a) > length(b)
We should actually check that the lengths of a
and b
match. Let's rewrite mydot
using an if
, else
statement. The following code only does the for loop if the length of a is equal to the length of b, otherwise, it throws an error.
Note that ==
checks if two quantities are equal. This is not the same as =
, which assigns the value of one quantity to the other
If we name something with the exact same signature (name, and argument types), previous definitions get overriden.
In [129]:
function mydot(a::Vector,b::Vector)
ret=0
if length(a) == length(b)
for k=1:length(a)
ret=ret+a[k]*b[k]
end
else
error("arguments have different lengths")
end
ret
end
Out[129]:
In [130]:
mydot([1,2,3,4],[5,6,7,8])
Out[130]:
In [131]:
mydot([1,2,3,4],[5,6,7])
In [57]:
r=Ref(1)
r.x
Out[57]:
Ref
is just a composite type with a single field called x
. We can make our own version of Ref
called MyRef
:
In [132]:
type MyRef
x
end
The function call MyRef(52)
creates a new MyRef
, with x
initialized as 52:
In [133]:
myref=MyRef(52)
Out[133]:
In [134]:
myref.x
Out[134]:
we can create another variable n
that is equal to myref
.
In [135]:
n=myref
Out[135]:
Unlike bittypes, the fields of composite types point to locations in memory that store the values. In this example, myref.x
lives somewhere in memory, let's say at address 1543. But setting n=myref
has the property that all the fields of n
also point to the same location in memory. This means n.x
also points to address 1543.
So if we change the value of myref.x
to 6, this changes the value living in address 1543 to 6, and so n.x
is also automatically 6:
In [136]:
myref.x=6
Out[136]:
In [137]:
n.x
Out[137]:
This is very different from bittypes. Here, myrefx
and nx
are in two different locations in memory, let's say 1765 and 1987, and the =
copies the value 52 from myrefx
's address 1765 to nx
's address 1987. Then calling myrefx=6
actually creates a new address in memory, let's say 2076, with the value of 6. But nx
still corresponds to 1987, and is still 52.
In [138]:
myrefx=52
nx=myrefx
myrefx=6
nx
Out[138]:
Here is another example. Let's return to the composite type set-up:
In [139]:
myref=MyRef(52)
myref.x
n=myref
Out[139]:
If instead of calling myref.x=6
we call myref=MyRef(6)
, this creates a brand new MyRef
,
with the new myref.x
pointing to a new address in memory (let's say 6543) initialized with
the value 6. Whereas n.x
still points to the same address in memory as the old myref.x
,
so is still 52:
In [67]:
myref=MyRef(6)
Out[67]:
In [68]:
n.x
Out[68]:
In [141]:
v=[1,2,3,4]
w=v
Out[141]:
You can change values of a vector using brackets and =:
In [142]:
v[2]=52
Out[142]:
This has changed v:
In [72]:
v
Out[72]:
But it's also changed w, since w points to the same location in memory as v:
In [143]:
w
Out[143]:
If we assign v to a new vector, w still points to the old location in memory, so is unchanged.
Makes sure it is clear the difference between v=
, which reassigns the variable v to a new value, and v[1]=
, which leaves v the same, but modifies a value in memory.
In [74]:
v=[6,75]
Out[74]:
In [75]:
w
Out[75]:
If you actually want to copy the entries of a vector, without pointing to a new vector, use copy
:
In [147]:
v=[6,75]
w=v
w2=copy(v)
Out[147]:
In [148]:
v[1]=2
Out[148]:
In [150]:
w
Out[150]:
In [149]:
w2
Out[149]:
In [153]:
x=1/2
typeof(x)
Out[153]:
In [154]:
y=1/3
typeof(y)
bits(y)
Out[154]:
We can create floats by adding .0 to the end. The following creates a Float64 to represent the integer 1:
In [155]:
x=1.0
bits(x)
Out[155]: