跳至內容

Introducing Julia/Functions

維基教科書,自由的教學讀本


« Introducing Julia
Functions
»
Controlling the flow Dictionaries and sets

函數[編輯]

函數是 Julia 代碼的構建塊(building blocks),充當其他程式語言中的子例程、過程、塊和類似的結構概念。

函數的工作是將一組值作為參數列表並返回值。如果參數包含可變的值(如數組),則可以在函數中修改數組。按照慣例,感嘆號(!)函數結尾處的名稱表示該函數可以修改其參數。

定義函數有多種語法:

  • 函數包含單個表達式
  • 函數包含多個表達式
  • 函數不需要名字

單表達式函數[編輯]

要定義一個簡單的函數,您所需要做的就是在左邊提供函數名和參數,在等於號的右邊提供一個表達式。這些就像數學函數:

julia> f(x) = x * x
f (generic function with 1 method)

julia> f(2)
4
julia> g(x, y) = sqrt(x^2 + y^2)
g (generic function with 1 method)

julia> g(3,4)
5.0

具有多個表達式的函數[編輯]

用多個表達式定義函數的語法如下:

function functionname(arg1, arg2)
   expression
   expression
   expression
   ...
   expression
end

下面是一個典型的函數,它調用另外兩個函數,然後結束。

function breakfast()
   maketoast()
   brewcoffee()
end

breakfast (generic function with 1 method)

不管最後一個表達式 brewcoffee() 返回的值是什麼, breakfast()函數返回值就是這個。

你可以使用 return 關鍵字來指定一個特定的值來返回。

julia> function paybills(bankbalance)
    if bankbalance < 0
       return false
    else
       return true
    end
end
paybills (generic function with 1 method)
julia> payBills(20)
true
 
julia> payBills(-10)
false

Some consider it good style to always use a return statement, even if it's not strictly necessary. Later we'll see how to make sure that the function doesn't go adrift if you call it with the wrong type of argument.

多值返回[編輯]

To return more than one value from a function, use a tuple.

function doublesix()
    return (6, 6)
end
doublesix (generic function with 1 method)
julia> doublesix()
(6, 6)

Here you could write 6, 6 without parentheses.

可選參數和可變數量的參數[編輯]

You can define functions with optional arguments, so that the function can use sensible defaults if specific values aren't supplied. You provide a default symbol and value in the argument list:

function xyzpos(x, y, z=0)
    println("$x, $y, $z")
end
xyzpos (generic function with 2 methods)

And when you call this function, if you don't provide a third value, the variable z defaults to 0 and uses that value inside the function.

julia> xyzpos(1,2)
1, 2, 0
julia> xyzpos(1,2,3)
1, 2, 3

關鍵字參數與位置參數[編輯]

When you write a function with a long list of arguments like this:

function f(p, q, r, s, t, u)
...
end

早晚你會可能忘記他們的順序。 sooner or later, you will forget the order in which you have to supply the arguments. For instance, it can be:

f("42", -2.123, atan2, "obliquity", 42, 'x')

or

f(-2.123, 42, 'x', "42", "obliquity", atan2)

You can avoid this problem by using keywords to label arguments. Use a semicolon after the function's unlabelled arguments, and follow it with one or more keyword=value pairs:

function f(p, q ; r = 4, s = "hello")
  println("p is $p")
  println("q is $q")
  return "r => $r, s => $s"
end
f (generic function with 1 method)

When called, this function expects two arguments, and also accepts a number and a string, labelled r and s. If you don't supply the keyword arguments, their default values are used:

julia> f(1,2)
p is 1
q is 2
"r => 4, s => hello"

julia> f("a", "b", r=pi, s=22//7)
p is a
q is b
"r => π = 3.1415926535897..., s => 22//7"

If you supply a keyword argument, it can be anywhere in the argument list, not just at the end or in the matching place.

julia> f(r=999, 1, 2)
p is 1
q is 2
"r => 999, s => hello"

julia> f(s="hello world", r=999, 1, 2)
p is 1
q is 2
"r => 999, s => hello world"
julia>

When defining a function with keyword arguments, remember to insert a semicolon before the keyword/value pairs.

Here's another example from the Julia manual. The rtol keyword can appear anywhere in the list of arguments or it can be omitted:

julia> isapprox(3.0, 3.01, rtol=0.1)
true

julia> isapprox(rtol=0.1, 3.0, 3.01)
true

julia> isapprox(3.0, 3.00001)
true

A function definition can combine all the different kinds of arguments. Here's one with normal, optional, and keyword arguments:

function f(a1, opta2=2; key="foo")
   println("normal argument: $a1")
   println("optional argument: $opta2")
   println("keyword argument: $key")
end
f (generic function with 2 methods)
julia> f(1)
normal argument: 1
optional argument: 2
keyword argument: foo

julia> f(key=3, 1)
normal argument: 1
optional argument: 2
keyword argument: 3

julia> f(key=3, 2, 1)
normal argument: 2
optional argument: 1
keyword argument: 3

Functions with variable number of arguments[編輯]

Functions can be defined so that they can accept any number of arguments:

function fvar(args...)
    println("you supplied $(length(args)) arguments")
    for arg in args
       println(" argument ", arg)
    end
end

The three dots indicate the famous splat. Here it means 'any', including 'none'. You can call this function with any number of arguments:

julia> fvar()
you supplied 0 arguments

julia> fvar(64)
you supplied 1 arguments
argument 64

julia> fvar(64,65)
you supplied 2 arguments
argument 64
argument 65

julia> fvar(64,65,66)
you supplied 3 arguments
argument 64
argument 65
argument 66

and so on.

Here's another example. Suppose you define a function that accepts two arguments:

function test(x, y)
   println("x $x y $y")
end

You can call this in the usual way:

julia> test(12, 34)
x 12 y 34

If you have the two numbers, but in a tuple, then how can you supply a single tuple of numbers to this two argument function? Again, the answer is to use the ellipsis (splat).

julia> test((12, 34) ...)
x 12 y 34

The use of the ellipsis or 'splat' is also referred to as 'splicing' the arguments:

julia> test([3,4]...)
x 3 y 4

You can also do this:

julia> map(test, [3, 4]...)
x 3 y 4

局部變量與改變參數的值[編輯]

Any variable you define inside a function will be forgotten when the function finishes.

function test(a,b,c)
    subtotal = a + b + c
end
julia> test(1,2,3)
6
julia> subtotal
LoadError: UndefVarError: subtotal not defined

If you want to keep values around across function calls, then you can think about using global variables.

A function can't modify an existing variable passed to it as an argument, but it can change the contents of a container passed to it. For example, here is a function that changes its argument to 5:

function set_to_5(x)
    x = 5
end
julia> x = 3
3

julia> set_to_5(x)
5

julia> x
3

Although the x inside the function is changed, the x outside the function isn't. Variable names in functions are local to the function.

But a function can modify the contents of a container, such as an array. This function uses the [:] syntax to access the contents of the container x, rather than change the value of the variable x:

function fill_with_5(x)
    x[:] .= 5
end
julia> x = collect(1:10);

julia> fill_with_5(x)
5

julia> x
10-element Array{Int64,1}:
5
5
5
5
5
5
5
5
5
5

You can change elements of the array, but you can't change the variable so that it points to a different array. In other words, your function isn't allowed to change the binding of the argument.

匿名函數[編輯]

Sometimes you don't want to worry about thinking up a cool name for a function. Anonymous functions — functions with no name — can be used in a number of places in Julia, such as with map(), and in list comprehensions.

The syntax uses ->, like this:

x -> x^2 + 2x - 1

which defines a nameless function that takes a argument, calls it x, and returns x^2 + 2x - 1.

For example, the first argument of the map() function is a function, and you can define an one-off function that exists just for one particular map() operation:

julia> map(x -> x^2 + 2x - 1, [1,3,-1])
3-element Array{Int64,1}:
 2
14
-2

After the map() finishes, both the function and the argument x have disappeared:

julia> x
ERROR: x not defined

If you want an anonymous function that accepts more than one argument, provide the arguments as a tuple:

julia> map((x,y,z) -> x + y + z, [1,2,3], [4, 5, 6], [7, 8, 9])
3-element Array{Int64,1}:
 12
 15
 18

Notice that the results are 12, 15, 18, rather than 6, 15, and 24. The anonymous function takes the first value of each of the three arrays and adds them, followed by the second, then the third.

In addition, anonymous functions can have zero arguments, if you use an 'empty' tuple():

julia> random = () -> rand(0:10)
#3 (generic function with 1 method)

julia> random()
3
julia> random()
1

Map[編輯]

如果已有一個函數和數組,你可以通過使用 map() 對數組中的每個元素來調用函數。這將會依次對每個元素進行調用並收集結果,返回一個數組。這個過程稱之為 映射 (mapping):

julia> a=1:10;

julia> map(sin, a)
10-element Array{Float64,1}:
 0.841471
 0.909297
 0.14112
-0.756802
-0.958924
-0.279415
 0.656987
 0.989358
 0.412118
-0.544021

map() 返回一個新的數組,但調用 map!() 則會修改原始數組的內容。

通常,不必用 map() 來對數組的每個成員都調用 sin() 函數,因為很多函數本身就自動支持 "元素級別" 的操作, 兩種版本的耗時是相當的( 用 sin.() 可能還有優勢,取決於數據規模):

julia> @time map(sin, 1:10000);
 0.149156 seconds (568.96 k allocations: 29.084 MiB, 2.01% gc time)
   
julia> @time sin.(1:10000);
 0.074661 seconds (258.76 k allocations: 13.086 MiB, 5.86% gc time)

map() 會收集結果然後返回一個數組,優勢你只是想要 '映射' 操作但並不需要返回一個數組的結果。這樣的話,用 foreach():

julia> foreach(println, 1:20)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

此時 ans 是 nothing (ans == nothing 返回 true).

多個數組映射[編輯]

你可以對不止一個數組進行 map() 操作. The function is applied to the first element of each of the arrays, then to the second, and so on. The arrays must be of the same length (unlike the zip() function, which is more tolerant).

Here's an example which generates an array of imperial (non-metric) spanner/socket sizes. The second array is just a bunch of repeated 32s to match the integers from 5 to 24 in the first array. Julia simplifies the rationals for us:

julia> map(//, 5:24, fill(32,20))
20-element Array{Rational{Int64},1}:
 5//32
 3//16
 7//32
 1//4 
 9//32
 5//16
11//32
 3//8 
13//32
 7//16
15//32
 1//2 
17//32
 9//16
19//32
 5//8 
21//32
11//16
23//32
 3//4 

(In reality, an imperial spanner set won't contain some of these strange sizes - I've never seen an old 17/32" spanner, but you can buy them online.)

使用 . 語法來調用函數[編輯]

除了 map()以外, it's possible to apply functions directly to arguments that are arrays. See the section on Broadcasting: dot syntax for vectorizing functions.

Reduce 和 folding[編輯]

The map() function collects the results of some function working on each and every element of an iterable object, such as an array of numbers. The reduce() function does a similar job, but after every element has been seen and processed by the function, only one is left. The function should take two arguments and return one. The array is reduced by continual application, so that just one is left.

A simple example is the use of reduce() to sum the numbers in an iterable object (which works like the built-in function sum()):

julia> reduce(+, 1:10)
55

Internally, this does something similar to this:

((((((((1 + 2) + 3) + 4) + 6) + 7) + 8) + 9) + 10)

After each operation adding two numbers, a single number is carried over to the next iteration. This process reduces all the numbers to a single final result.

A more useful example is when you want to apply a function to work on each consecutive pair in an iterable object. For example, here's a function that compares the length of two strings and returns the longer one:

julia> l(a, b) = length(a) > length(b) ? a : b
l (generic function with 1 method)

This can be used to find the longest word in a sentence by working through the string, pair by pair:

julia> reduce(l, split("This is a sentence containing some very long strings"))
"containing" 

"This" lasts a few rounds, and is then beaten by "sentence", but finally "containing" takes the lead, and there are no other challengers after that. If you want to see the magic happen, redefine l like this:

julia> l(a, b) = (println("comparing \"$a\" and \"$b\""); length(a) > length(b) ? a : b)
l (generic function with 1 method)
 
julia> reduce(l, split("This is a sentence containing some very long strings"))
comparing "This" and "is"
comparing "This" and "a"
comparing "This" and "sentence"
comparing "sentence" and "containing"
comparing "containing" and "some"
comparing "containing" and "very"
comparing "containing" and "long"
comparing "containing" and "strings"
"containing"

You can use an anonymous function to process an array pairwise. The trick is to make the function leave behind a value that will be used for the next iteration. This code takes an array such as [1, 2, 3, 4, 5, 6...] and returns [1 * 2, 2 * 3, 3 * 4, 4 * 5...], multiplying adjacent elements.

store = Int[];
reduce((x,y) -> (push!(store, x * y); y), 1:10)
julia> store
9-element Array{Int64,1}:
 2
 6
12
20
30
42
56
72
90

Folding[編輯]

Julia also offers two related functions, foldl() and foldr(). These offer the same basic functionality as reduce(). The differences are concerned with the direction in which the traversal occurs. In the simple summation example above, our best guess at what happened inside the reduce() operation assumed that the first pair of elements were added first, followed by the second pair, and so on. However, it's also possible that reduce() started at the end and worked towards the front. If it's important, use foldl() for left to right, and foldr() for right to left. In many cases, the results are the same, but here's an example where you'll get different results depending on which version you'll use:

julia> reduce(-, 1:10)
-53
 
julia> foldl(-, 1:10)
-53

julia> foldr(-, 1:10)
-5

Julia offers other functions in this group: check out mapreduce(), mapfoldl(), and mapfoldr().

If you want to use reduce() and the fold-() functions for functions that take only one argument, use a dummy second argument:

julia> reduce((x, y) -> sqrt(x), 1:4, init=256)
1.4142135623730951

which is equivalent to calling the sqrt() function four times:

julia> sqrt(sqrt(sqrt(sqrt(256))))
1.4142135623730951

返回函數(柯里化)[編輯]

You can treat Julia functions in the same way as any other Julia object, particularly when it comes to returning them as the result of other functions.

For example, let's create a function-making function. Inside this function, a function called newfunction is created, and this will raise its argument (y) to the number that was originally passed in as the argument x. This new function is returned as the value of the create_exponent_function() function.

function create_exponent_function(x)
    newfunction = function (y) return y^x end
    return newfunction
end

Now we can construct lots of exponent-making functions. First, let's build a squarer() function:

julia> squarer = create_exponent_function(2)
#8 (generic function with 1 method)

and a cuber() function:

julia> cuber = create_exponent_function(3)
#9 (generic function with 1 method)

While we're at it, let's do a "raise to the power of 4" function (called quader, although I'm starting to struggle with the Latin and Greek naming):

julia> quader = create_exponent_function(4)
#10 (generic function with 1 method)

These are ordinary Julia functions:

julia> squarer(4)
16
 
julia> cuber(5)
125
 
julia> quader(6)
1296

The definition of the create_exponent_function() above is perfectly valid Julia code, but it's not idiomatic. For one thing, the return value doesn't always need to be provided explicitly — the final evaluation is returned if return isn't used. Also, in this case, the full form of the function definition can be replaced with the shorter one-line version. This gives the concise version:

function create_exponent_function(x)
   y -> y^x
end

which acts in the same way.

make_counter = function()
     so_far = 0
     function()
       so_far += 1
     end
end
julia> a = make_counter();

julia> b = make_counter();

julia> a()
1

julia> a()
2

julia> a()
3

julia> a()
4

julia> b()
1

julia> b()
2

Here's another example of making functions. To make it easier to see what the code is doing, here is the make_counter() function written in a slightly different manner:

function make_counter()
     so_far = 0
     counter = function()
                 so_far += 1
                 return so_far
               end
     return counter
end
julia> a = make_counter()
#15 (generic function with 1 method)

julia> a()
1

julia> a()
2

julia> a()
3

julia> for i in 1:10
           a()
       end

julia> a()
14

函數鏈與組合[編輯]

在 Julia 中函數可以互相組合。

函數組合是指將兩個或多個函數應用於參數。你可以使用組合運算符 () 來組合函數。(在 REPL 中使用\circ來打出運算符)。舉個例子,sqrt()+ 函數組合在一起:

julia> (sqrt ∘ +)(3, 5)
2.8284271247461903

先將數字相加,然後計算平方根。


下面這個例子複合了三個函數。

julia> map(first ∘ reverse ∘ uppercase, split("you can compose functions like this"))
6-element Array{Char,1}:
'U'
'N'
'E'
'S'
'E'
'S'

函數鏈(有時又稱為「管道」或者說「用管道來將數據送到下一個函數」)是指將前一個函數的輸出應用到後一個函數:

julia> 1:10 |> sum |> sqrt
7.416198487095663

其中 sum() 的值 遞給 sqrt() 函數。等價於:

julia> (sqrt ∘ sum)(1:10)
7.416198487095663

管道能夠給一個接受單參數的函數發送數據。如果函數需要多個參數,可以用匿名函數:

julia> collect(1:9) |> n -> filter(isodd, n)
5-element Array{Int64,1}:
 1
 3
 5
 7
 9

Methods[編輯]

A function can have one or more different methods of doing a similar job. Each method usually concentrates on doing the job for a particular type.

Here is a function to check a longitude when you type in a location:

function check_longitude_1(loc)
    if -180 < loc < 180
        println("longitude $loc is a valid longitude")
    else
        println("longitude $loc should be between -180 and 180 degrees")
    end
end
 check_longitude_1 (generic function with 1 method)

The message ("generic function with 1 method") you see if you define this in the REPL tells you that there is currently one way you can call the check_longitude_1() function. If you call this function and supply a number, it works fine.

julia> check_longitude_1(-182)
longitude -182 should be between -180 and 180 degrees

julia> check_longitude_1(22)
longitude 22 is a valid longitude

But what happens when you type in a longitude in, say, the format seen on Google Maps:

julia> check_longitude_1("1°24'54.6\"W")
ERROR: MethodError: `isless` has no method matching isless(::Int64, ::UTF8String)

The error tells us that the function has stopped because the concept of less than (<), which we are using inside our function, makes no sense if one argument is a string and the other a number. Strings are not less than or greater than integers because they are two different things, so the function fails at that point.

Notice that the check_longitude_1() function did start executing, though. The argument loc could have been anything - a string, a floating point number, an integer, a symbol, or even an array. There are many ways for this function to fail. This is not the best way to write code!

To fix this problem, we might be tempted to add code that tests the incoming value, so that strings are handled differently. But Julia proposes a better alternative: methods and multiple dispatch.

In the case where the longitude is supplied as a numeric value, the loc argument is defined as 'being of type Real'. Let's start again, define a new function, and do it properly:

function check_longitude(loc::Real)
    if -180 < loc < 180
        println("longitude $loc is a valid longitude")
    else
        println("longitude $loc should be between -180 and 180 degrees")
    end
end

Now this check_longitude function doesn't even run if the value in loc isn't a real number. The problems of what to do when the value is a string is avoided. With a type Real, this particular method can be called with any argument provided that it is some kind of number.

We can use the applicable() function to test this. applicable() lets you know whether you can apply a function to an argument — i.e. whether there is an available method for the function for arguments with that type:

julia> applicable(check_longitude, -30)
true 

julia> applicable(check_longitude, pi)
true

julia> applicable(check_longitude, 22/7)
true

julia> applicable(check_longitude, 22//7)
true

julia> applicable(check_longitude, "1°24'54.6\"W")
false

The false indicates that you can't pass a string value to the check_longitude() function because there is no method for this function that accepts a string:

julia> check_longitude("1°24'54.6\"W")
ERROR: MethodError: `check_longitude` has no method matching check_longitude(::UTF8String)

Now the body of the function isn't even looked at — Julia doesn't know a method for calling check_longitude() function with a string argument.

The obvious next step is to add another method for the check_longitude() function, only this time one that accepts a string argument. In this way, a function can be given a number of alternative methods: one for numeric arguments, one for string arguments, and so on. Julia selects and runs one of the available methods depending on the types of arguments you provide to a function.

This is multiple dispatch.

function check_longitude(loc::String)
  # not real code, obviously!
    if endswith(loc, "W")
       println("longitude $loc is West of Greenwich")
    else
       println("longitude $loc is East of Greenwich")
    end
end
check_longitude (generic function with 2 methods)

Now the check_longitude() function has two methods. The code to run depends on the types of the arguments you provide to the function. And you can avoid testing the types of arguments at the start of this function, because Julia only dispatches the flow to the string-handling method if loc is a string.

You can use the built-in methods() function to find out how many methods you have defined for a particular function.

julia> methods(check_longitude)
# 2 methods for generic function "check_longitude":
check_longitude(loc::Real) at none:2
check_longitude(loc::String) at none:3

An instructive example is to see how many different methods the + function has:

julia> methods(+)
# 176 methods for generic function "+":
[1] +(x::Bool, z::Complex{Bool}) in Base at complex.jl:276
[2] +(x::Bool, y::Bool) in Base at bool.jl:104
...
[174] +(J::LinearAlgebra.UniformScaling, B::BitArray{2}) in LinearAlgebra at  /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v0.7/LinearAlgebra/src/uniformscaling.jl:90
[175] +(J::LinearAlgebra.UniformScaling, A::AbstractArray{T,2} where T) in LinearAlgebra at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v0.7/LinearAlgebra/src/uniformscaling.jl:91
[176] +(a, b, c, xs...) in Base at operators.jl:466

This is a long list of every method currently defined for the + function; there are many different types of thing that you can add together, including arrays, matrices, and dates. If you design your own types, you might well want to write a function that adds two of them together.

Julia chooses "the most specific method" to handle the types of arguments. In the case of check_longitude(), we have two specific methods, but we could define a more general method:

function check_longitude(loc::Any)
    println("longitude $loc should be a string or a number")
end
check_longitude (generic function with 3 methods)

This method of check_longitude() is called when the loc argument is neither a Real number or a String. It is the most general method, and won't be called at all if a more specific method is available.

方法定義中的類型參數[編輯]

在方法定義中可以用到類型信息。以此為例:

julia>function test(a::T) where T <: Real
    println("$a is a $T")
end
test (generic function with 1 methods)
julia> test(2.3)
2.3 is a Float64

julia> test(2)
2 is a Int64

julia> test(.02)
0.02 is a Float64

julia> test(pi)
π = 3.1415926535897... is a Irrational{:π}
julia> test(22//7)
22//7 is a Rational{Int64}
julia> test(0xff)
255 is a UInt8

The test() method automatically extracts the type of the single argument a passed to it and stores it in the 'variable' T. For this function, the definition of T was where T is a subtype of Real, so the type of T must be a subtype of the Real type (it can be any real number, but not a complex number). 'T' can be used like any other variable — in this method it's just printed out using string interpolation. (It doesn't have to be T, but it nearly always is!)

This mechanism is useful when you want to constrain the arguments of a particular method definition to be of a particular type. For example, the type of argument a must belong to the Real number supertype, so this test() method doesn't apply when a isn't a number, because then the type of the argument isn't a subtype of Real:

julia> test("str")
ERROR: MethodError: no method matching test(::ASCIIString)

julia> test(1:3)
ERROR: MethodError: no method matching test(::UnitRange{Int64})

Here's an example where you might want to write a method definition that applies to all one-dimensional integer arrays. It finds all the odd numbers in an array:

function findodds(a::Array{T,1}) where T <: Integer
              filter(isodd, a)
           end
findodds (generic function with 1 method)
julia> findodds(collect(1:20))
10-element Array{Int64,1}:
 1
 3
 5
 7
 9
11
13
15
17
19

but can't be used for arrays of real numbers:

julia> findodds([1, 2, 3, 4, 5, 6, 7, 8, 9, 10.0])
ERROR: MethodError: no method matching findodds(::Array{Float64,1})
Closest candidates are:
  findodds(::Array{T<:Integer,1}) where T<:Integer at REPL[13]:2

Note that, in this simple example, because you're not using the type information inside the method definition, you might be better off sticking to the simpler way of defining methods, by adding type information to the arguments:

function findodds(a::Array{Int64,1})
   findall(isodd, a)
end

But if you wanted to do things inside the method that depended on the types of the arguments, then the type parameters approach will be useful.

« Introducing Julia
Functions
»
Controlling the flow Dictionaries and sets