딥스탯 2018. 9. 27. 13:46
6_Functions

Functions

Reference

https://github.com/JuliaComputing/JuliaBoxTutorials/tree/master/introductory-tutorials/intro-to-julia (github : JuliaComputing/JuliaBoxTutorials/introductory-tutorials/intro-to-julia/)

Topics:

  1. How to declare a function
  2. Duck-typing in Julia
  3. Mutating vs. non-mutating functions
  4. Some higher order functions
  5. Exercises

Series

How to declare a function

Julia gives us a few different ways to write a function. The first requires the function and end keywords

In [1]:
function sayhi(name)
    println("Hi $name, it's great to see you!")
end
Out[1]:
sayhi (generic function with 1 method)
In [2]:
function f(x)
    x^2
end
Out[2]:
f (generic function with 1 method)

We can call either of these functions like this:

In [3]:
sayhi("C-3PO")
Hi C-3PO, it's great to see you!
In [4]:
f(42)
Out[4]:
1764

Alternatively, we could have declared either of these functions in a single line

In [5]:
sayhi2(name) = println("Hi $name, it's great to see you!")
Out[5]:
sayhi2 (generic function with 1 method)
In [6]:
f2(x) = x^2
Out[6]:
f2 (generic function with 1 method)
In [7]:
sayhi2("R2D2")
Hi R2D2, it's great to see you!
In [8]:
f2(42)
Out[8]:
1764

Finally, we could have declared these as "anonymous" functions

In [9]:
sayhi3 = name -> println("Hi $name, it's great to see you!")
Out[9]:
#3 (generic function with 1 method)
In [10]:
f3 = x -> x^2
Out[10]:
#5 (generic function with 1 method)
In [11]:
sayhi3("Chewbacca")
Hi Chewbacca, it's great to see you!
In [12]:
f3(42)
Out[12]:
1764

Duck-typing in Julia

"If it quacks like a duck, it's a duck."

Julia functions will just work on whatever inputs make sense.

For example, sayhi works on the name of this minor tv character, written as an integer...

In [13]:
sayhi(55595472)
Hi 55595472, it's great to see you!

And f will work on a matrix.

In [14]:
A = rand(3, 3)
Out[14]:
3×3 Array{Float64,2}:
 0.452035  0.918894  0.0433122
 0.830987  0.45563   0.727485 
 0.295647  0.116701  0.389248 
In [15]:
f(A)
Out[15]:
3×3 Array{Float64,2}:
 0.980731  0.839104  0.70492 
 0.969337  1.05609   0.650628
 0.3457    0.370266  0.249217

f will also work on a string like "hi" because * is defined for string inputs as string concatenation.

In [16]:
f("hi")
Out[16]:
"hihi"

On the other hand, f will not work on a vector. Unlike A^2, which is well-defined, the meaning of v^2 for a vector, v, is not a well-defined algebraic operation.

In [17]:
v = rand(3)
Out[17]:
3-element Array{Float64,1}:
 0.1495218093491093
 0.261956305722296 
 0.47635710209852  
In [18]:
f(v)
MethodError: no method matching ^(::Array{Float64,1}, ::Int64)
Closest candidates are:
  ^(!Matched::Float16, ::Integer) at math.jl:782
  ^(!Matched::Missing, ::Integer) at missing.jl:120
  ^(!Matched::Missing, ::Number) at missing.jl:93
  ...

Stacktrace:
 [1] macro expansion at ./none:0 [inlined]
 [2] literal_pow at ./none:0 [inlined]
 [3] f(::Array{Float64,1}) at ./In[2]:2
 [4] top-level scope at In[18]:1

Mutating vs. non-mutating functions

By convention, functions followed by ! alter their contents and functions lacking ! do not.

For example, let's look at the difference between sort and sort!.

In [19]:
v = [3, 5, 2]
Out[19]:
3-element Array{Int64,1}:
 3
 5
 2
In [20]:
sort(v)
Out[20]:
3-element Array{Int64,1}:
 2
 3
 5
In [21]:
v
Out[21]:
3-element Array{Int64,1}:
 3
 5
 2

sort(v) returns a sorted array that contains the same elements as v , but v is left unchanged.

On the other hand, when we run sort!(v), the contents of v are sorted within the array v.

In [22]:
sort!(v)
Out[22]:
3-element Array{Int64,1}:
 2
 3
 5
In [23]:
v
Out[23]:
3-element Array{Int64,1}:
 2
 3
 5

Some higher order functions

map

map is a "higher-order" function in Julia that takes a function as one of its input arguments. map then applies that function to every element of the data structure you pass it. For example, executing

map(f, [1, 2, 3])

will give you an output array where the function f has been applied to all elements of [1, 2, 3]

[f(1), f(2), f(3)]
In [24]:
map(f, [1, 2, 3])
Out[24]:
3-element Array{Int64,1}:
 1
 4
 9

Here we've squared all the elements of the vector [1, 2, 3], rather than squaring the vector [1, 2, 3].

To do this, we could have passed to map an anonymous function rather than a named function, such as

In [25]:
x -> x^3
Out[25]:
#7 (generic function with 1 method)

via

In [26]:
map(x -> x^3, [1, 2, 3])
Out[26]:
3-element Array{Int64,1}:
  1
  8
 27

and now we've cubed all the elements of [1, 2, 3]!

broadcast

broadcast is another higher-order function like map. broadcast is a generalization of map, so it can do every thing map can do and more. The syntax for calling broadcast is the same as for calling map

In [27]:
broadcast(f, [1, 2, 3])
Out[27]:
3-element Array{Int64,1}:
 1
 4
 9

and again, we've applied f (squared) to all the elements of [1, 2, 3] - this time by "broadcasting" f!

Some syntactic sugar for calling broadcast is to place a . between the name of the function you want to broadcast and its input arguments. For example,

broadcast(f, [1, 2, 3])

is the same as

f.([1, 2, 3])
In [28]:
f.([1, 2, 3])
Out[28]:
3-element Array{Int64,1}:
 1
 4
 9

Notice again how different this is from calling

f([1, 2, 3])

We can square every element of a vector, but we can't square a vector!

To drive home the point, let's look at the difference between

f(A)

and

f.(A)

for a matrix A:

In [29]:
A = [i + 3*j for j in 0:2, i in 1:3]
Out[29]:
3×3 Array{Int64,2}:
 1  2  3
 4  5  6
 7  8  9
In [30]:
f(A)
Out[30]:
3×3 Array{Int64,2}:
  30   36   42
  66   81   96
 102  126  150

As before we see that for a matrix, A,

f(A) = A^2 = A * A

On the other hand,

In [31]:
B = f.(A)
Out[31]:
3×3 Array{Int64,2}:
  1   4   9
 16  25  36
 49  64  81

contains the squares of all the entries of A.

This dot syntax for broadcasting allows us to write relatively complex compound elementwise expressions in a way that looks natural/closer to mathematical notation. For example, we can write

In [32]:
A .+ 2 .* f.(A) ./ A
Out[32]:
3×3 Array{Float64,2}:
  3.0   6.0   9.0
 12.0  15.0  18.0
 21.0  24.0  27.0

instead of

In [33]:
broadcast(x -> x + 2 * f(x) / x, A)
Out[33]:
3×3 Array{Float64,2}:
  3.0   6.0   9.0
 12.0  15.0  18.0
 21.0  24.0  27.0

and the two will perform exactly the same.

Exercises

6.1

Write a function add_one that adds 1 to its input.

In [34]:
function add_one1(n)
    n+1
end
Out[34]:
add_one1 (generic function with 1 method)
In [35]:
add_one1(1)
Out[35]:
2
In [36]:
add_one2(n) = n+1
Out[36]:
add_one2 (generic function with 1 method)
In [37]:
add_one2(1)
Out[37]:
2
In [38]:
add_one3 = n -> n+1
Out[38]:
#15 (generic function with 1 method)
In [39]:
add_one3(1)
Out[39]:
2

6.2

Use map or broadcast to increment every element of matrix A by 1 and assign it to a variable A1.

In [40]:
A1 = map(add_one1, A)
Out[40]:
3×3 Array{Int64,2}:
 2  3   4
 5  6   7
 8  9  10
In [41]:
A1 = broadcast(add_one2, A)
Out[41]:
3×3 Array{Int64,2}:
 2  3   4
 5  6   7
 8  9  10

6.3

Use the broadcast dot syntax to increment every element of matrix A1 by 1 and store it in variable A2

In [42]:
A2 = add_one3.(A1)
Out[42]:
3×3 Array{Int64,2}:
 3   4   5
 6   7   8
 9  10  11
In [43]:
A2 = A1 .+ 1
Out[43]:
3×3 Array{Int64,2}:
 3   4   5
 6   7   8
 9  10  11