函數
在 Julia 中,函數是一個將參數值元組對應到回傳值的物件。Julia 函數並非純數學函數,因為它們可以改變並受到程式全局狀態的影響。在 Julia 中定義函數的基本語法為
julia> function f(x,y)
x + y
end
f (generic function with 1 method)
此函數接受兩個參數 x
和 y
,並回傳最後評估式 x + y
的值。
在 Julia 中,還有一個更簡潔的函數定義語法。上面展示的傳統函數宣告語法等同於以下簡潔的「指定形式」
julia> f(x,y) = x + y
f (generic function with 1 method)
在指定形式中,函數的主體必須是一個單一式,儘管它可以是一個複合式(請參閱 複合式)。簡短、簡單的函數定義在 Julia 中很常見。因此,簡短的函數語法相當符合慣例,大幅減少輸入和視覺雜訊。
函數使用傳統的括號語法呼叫
julia> f(2,3)
5
不使用括號時,式 f
指函數物件,且可以像任何其他值一樣傳遞
julia> g = f;
julia> g(2,3)
5
與變數一樣,Unicode 也可以用於函數名稱
julia> ∑(x,y) = x + y
∑ (generic function with 1 method)
julia> ∑(2, 3)
5
參數傳遞行為
Julia 函數參數遵循一個有時稱為「傳遞分享」的慣例,表示值在傳遞給函數時不會被複製。函數參數本身充當新的變數繫結(可以指稱值的新的「名稱」),很像指定argument_name = argument_value
,因此它們指稱的物件與傳遞的值相同。在函數中對可變值(例如 Array
)所做的修改,呼叫者將可見。(這種行為與 Scheme、大多數 Lisp、Python、Ruby 和 Perl 等動態語言相同。)
例如,在函數中
function f(x, y)
x[1] = 42 # mutates x
y = 7 + y # new binding for y, no mutation
return y
end
陳述式 x[1] = 42
變異物件 x
,因此這個變更會在呼叫者為這個參數傳遞的陣列中可見。另一方面,指定 y = 7 + y
會將繫結(「名稱」)y
變更為指稱新值 7 + y
,而不是變異 y
指稱的原始物件,因此不會變更呼叫者傳遞的對應參數。如果我們呼叫 f(x, y)
,就可以看到這一點
julia> a = [4,5,6]
3-element Vector{Int64}:
4
5
6
julia> b = 3
3
julia> f(a, b) # returns 7 + b == 10
10
julia> a # a[1] is changed to 42 by f
3-element Vector{Int64}:
42
5
6
julia> b # not changed
3
作為 Julia 中的慣例(不是語法需求),此類函數通常會命名為 f!(x, y)
,而不是 f(x, y)
,作為呼叫位置的視覺提醒,表示至少一個參數(通常是第一個)正在變異。
當變異的引數與另一個引數共用記憶體時,變異函數的行為可能會出乎意料,這種情況稱為別名 (例如,當一個是另一個的檢視時)。除非函數文件字串明確指出別名會產生預期的結果,否則呼叫者有責任確保此類輸入的適當行為。
引數類型宣告
您可以透過在引數名稱後加上 ::TypeName
來宣告函數引數的類型,這在 Julia 中是 類型宣告 的慣例。例如,下列函數遞迴計算 費氏數列
fib(n::Integer) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)
而 ::Integer
規格表示只有當 n
是 抽象 Integer
類型的子類型時,它才會被呼叫。
引數類型宣告通常不會影響效能:無論宣告了哪些引數類型 (如果有),Julia 都會針對呼叫者傳遞的實際引數類型編譯函數的專門版本。例如,呼叫 fib(1)
會觸發編譯專門針對 Int
引數最佳化的 fib
專門版本,然後在呼叫 fib(7)
或 fib(15)
時重複使用。 (有少數例外情況,引數類型宣告可能會觸發其他編譯器專門化;請參閱:了解 Julia 何時避免專門化。) 相反地,在 Julia 中宣告引數類型的最常見原因是
- 派遣:如 方法 中所述,您可以針對不同的引數類型擁有函式的不同版本(「方法」),這種情況下,引數類型用於決定針對哪些引數呼叫哪個實作。例如,您可以實作一個完全不同的演算法
fib(x::Number) = ...
,它透過使用 Binet 公式 將其延伸至非整數值,以適用於任何Number
類型。 - 正確性:如果函式僅針對特定引數類型傳回正確的結果,則類型宣告會很有用。例如,如果我們省略引數類型並撰寫
fib(n) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)
,則fib(1.5)
會默默地給我們無意義的答案1.0
。 - 清晰度:類型宣告可以作為預期引數的文件形式。
然而,過度限制引數類型是一個常見的錯誤,這可能會不必要地限制函式的適用性,並阻止它在您未預期的情況下重複使用。例如,上述 fib(n::Integer)
函式對於 Int
引數(機器整數)和 BigInt
任意精確度整數(請參閱 BigFloats 和 BigInts)同樣有效,這特別有用,因為費氏數列會快速地呈指數成長,並會迅速溢位任何固定精確度類型,例如 Int
(請參閱 溢位行為)。然而,如果我們將函式宣告為 fib(n::Int)
,則應用於 BigInt
會無緣無故地受到阻止。一般來說,您應該對引數使用最通用的適用抽象類型,而且如有疑問,請省略引數類型。您隨時可以在需要時新增引數類型規格,而且您不會因為省略它們而犧牲效能或功能。
return
關鍵字
函數傳回的值是最後一個運算式評估的值,預設情況下,就是函數定義主體中的最後一個運算式。在前一節的範例函數 f
中,這是運算式 x + y
的值。或者,就像許多其他語言一樣,return
關鍵字會導致函數立即傳回,提供一個其值會傳回的運算式
function g(x,y)
return x * y
x + y
end
由於函數定義可以輸入互動式工作階段,因此很容易比較這些定義
julia> f(x,y) = x + y
f (generic function with 1 method)
julia> function g(x,y)
return x * y
x + y
end
g (generic function with 1 method)
julia> f(2,3)
5
julia> g(2,3)
6
當然,在像 g
這樣的純線性函數主體中,使用 return
是沒有意義的,因為運算式 x + y
永遠不會評估,我們可以簡單地讓 x * y
成為函數中的最後一個運算式,並省略 return
。然而,與其他控制流程結合時,return
是真正有用的。例如,這裡有一個函數,它計算邊長為 x
和 y
的直角三角形的斜邊長度,避免溢位
julia> function hypot(x,y)
x = abs(x)
y = abs(y)
if x > y
r = y/x
return x*sqrt(1+r*r)
end
if y == 0
return zero(x)
end
r = x/y
return y*sqrt(1+r*r)
end
hypot (generic function with 1 method)
julia> hypot(3, 4)
5.0
這個函數有三個可能的傳回點,傳回三個不同運算式的值,具體取決於 x
和 y
的值。最後一行上的 return
可以省略,因為它是最後一個運算式。
傳回類型
可以使用 ::
算子在函數宣告中指定傳回類型。這會將傳回值轉換為指定的類型。
julia> function g(x, y)::Int8
return x * y
end;
julia> typeof(g(1, 2))
Int8
這個函數將永遠傳回一個 Int8
,無論 x
和 y
的類型為何。有關傳回類型的更多資訊,請參閱 類型宣告。
傳回類型宣告在 Julia 中很少使用:一般來說,您應該改寫「類型穩定的」函數,讓 Julia 的編譯器可以自動推斷傳回類型。有關更多資訊,請參閱 效能提示 章節。
傳回無值
對於不需要傳回值的函數(僅用於某些副作用的函數),Julia 慣例是傳回值 nothing
function printx(x)
println("x = $x")
return nothing
end
這是一個慣例,因為 nothing
不是 Julia 關鍵字,而僅是型別為 Nothing
的單例物件。此外,您可能會注意到上述 printx
函數範例是人為的,因為 println
已經傳回 nothing
,因此 return
行是多餘的。
return nothing
表達式有兩種可能的縮寫形式。一方面,return
關鍵字會隱含傳回 nothing
,因此可以單獨使用。另一方面,由於函數會隱含傳回其最後一個評估的表達式,因此當 nothing
是最後一個表達式時,可以單獨使用 nothing
。相較於單獨使用 return
或 nothing
,偏好使用表達式 return nothing
是編碼風格的問題。
運算子是函數
在 Julia 中,大多數運算子只是支援特殊語法的函數。(例外情況是具有特殊評估語意的運算子,例如 &&
和 ||
。這些運算子無法成為函數,因為 短路評估 要求在評估運算子之前不評估其運算元。)因此,您也可以使用括號的引數清單套用它們,就像您會對任何其他函數所做的那樣
julia> 1 + 2 + 3
6
julia> +(1,2,3)
6
中綴形式與函數應用形式完全等效 - 事實上,前者會被解析為在內部產生函數呼叫。這也表示您可以像對待其他函數值一樣,指定和傳遞運算子,例如 +
和 *
julia> f = +;
julia> f(1,2,3)
6
不過,在名稱 f
下,函數不支援中綴表示法。
具有特殊名稱的運算子
一些特殊表達式對應於呼叫具有非顯然名稱的函數。這些是
表達式 | 呼叫 |
---|---|
[A B C ...] | hcat |
[A; B; C; ...] | vcat |
[A B; C D; ...] | hvcat |
[A; B;; C; D;; ...] | hvncat |
A' | adjoint |
A[i] | getindex |
A[i] = x | setindex! |
A.n | getproperty |
A.n = x | setproperty! |
請注意,類似於 [A; B;; C; D;; ...]
但連續 ;
多於兩個的表達式也對應於 hvncat
呼叫。
匿名函數
Julia 中的函數是 一級物件:它們可以指定給變數,並使用已指定給它們的變數從標準函數呼叫語法呼叫。它們可以用作引數,也可以作為值傳回。它們也可以匿名建立,而不需要給予名稱,使用下列任一種語法
julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)
julia> function (x)
x^2 + 2x - 1
end
#3 (generic function with 1 method)
這會建立一個函數,接受一個引數 x
並傳回多項式 x^2 + 2x - 1
在該值上的值。請注意,結果是一個通用函數,但具有基於連續編號的編譯器產生的名稱。
匿名函數的主要用途是將它們傳遞給將其他函數作為引數的函數。一個經典範例是 map
,它將一個函數套用於陣列的每個值,並傳回包含結果值的陣列
julia> map(round, [1.2, 3.5, 1.7])
3-element Vector{Float64}:
1.0
4.0
2.0
如果已存在一個命名函式來執行轉換,並將其作為第一個參數傳遞給 map
,這是沒問題的。然而,通常並不存在一個現成的命名函式。在這些情況下,匿名函式結構允許輕鬆建立一個一次性使用的函式物件,而不需要名稱
julia> map(x -> x^2 + 2x - 1, [1, 3, -1])
3-element Vector{Int64}:
2
14
-2
可以使用語法 (x,y,z)->2x+y-z
來撰寫一個接受多個參數的匿名函式。零參數匿名函式寫成 ()->3
。一個沒有參數的函式這個概念可能看起來很奇怪,但對於「延遲」計算很有用。在此用法中,一段程式碼會包裝在一個零參數函式中,稍後再透過呼叫它作為 f
來呼叫它。
舉例來說,考慮呼叫 get
get(dict, key) do
# default value calculated here
time()
end
上面的程式碼等於呼叫 get
,並使用一個匿名函式,其中包含在 do
和 end
之間的程式碼,如下所示
get(()->time(), dict, key)
呼叫 time
會透過將它包裝在一個 0 參數匿名函式中來延遲,只有在 dict
中沒有所要求的鍵時才會呼叫它。
Tuples
Julia 有內建資料結構稱為 tuple,它與函式參數和傳回值密切相關。Tuple 是固定長度的容器,可以容納任何值,但不能修改(它是 不可變的)。Tuple 是用逗號和括號建構的,並且可以透過索引來存取
julia> (1, 1+1)
(1, 2)
julia> (1,)
(1,)
julia> x = (0.0, "hello", 6*7)
(0.0, "hello", 42)
julia> x[2]
"hello"
請注意,長度為 1 的 tuple 必須寫成一個逗號,(1,)
,因為 (1)
只會是一個括號值。()
代表空的(長度為 0)tuple。
Named Tuples
Tuple 的組成部分可以選擇命名,這種情況下會建構一個 命名 tuple
julia> x = (a=2, b=1+2)
(a = 2, b = 3)
julia> x[1]
2
julia> x.a
2
除了常規索引語法(x[1]
或 x[:a]
)之外,還可以透過點語法(x.a
)使用名稱來存取命名 tuple 的欄位。
解構賦值和多重回傳值
變數的逗號分隔清單(選擇性地用括號包住)可以出現在賦值的左側:右側的值會透過依序迭代和賦值給每個變數來解構
julia> (a,b,c) = 1:3
1:3
julia> b
2
右側的值應該是一個迭代器(請參閱迭代介面),至少與左側的變數數量一樣長(迭代器的任何多餘元素都會被忽略)。
這可以用於透過回傳一個元組或其他可迭代值來從函式回傳多個值。例如,以下函式回傳兩個值
julia> function foo(a,b)
a+b, a*b
end
foo (generic function with 1 method)
如果你在互動式會話中呼叫它,而沒有將回傳值賦值給任何地方,你會看到回傳的元組
julia> foo(2,3)
(5, 6)
解構賦值會將每個值萃取到一個變數中
julia> x, y = foo(2,3)
(5, 6)
julia> x
5
julia> y
6
另一個常見的用途是交換變數
julia> y, x = x, y
(5, 6)
julia> x
6
julia> y
5
如果只需要迭代器的部分元素,一個常見的慣例是將被忽略的元素賦值給只包含底線 _
的變數(這是一個否則無效的變數名稱,請參閱允許的變數名稱)
julia> _, _, _, d = 1:10
1:10
julia> d
4
其他有效的左側表達式可以用作賦值清單的元素,它會呼叫setindex!
或setproperty!
,或遞迴地解構迭代器的個別元素
julia> X = zeros(3);
julia> X[1], (a,b) = (1, (2, 3))
(1, (2, 3))
julia> X
3-element Vector{Float64}:
1.0
0.0
0.0
julia> a
2
julia> b
3
帶有賦值的...
需要 Julia 1.6
如果賦值清單中的最後一個符號後綴為...
(稱為slurping),那麼它將被賦予右側迭代器中剩餘元素的集合或惰性迭代器
julia> a, b... = "hello"
"hello"
julia> a
'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase)
julia> b
"ello"
julia> a, b... = Iterators.map(abs2, 1:4)
Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4)
julia> a
1
julia> b
Base.Iterators.Rest{Base.Generator{UnitRange{Int64}, typeof(abs2)}, Int64}(Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4), 1)
有關特定迭代器的精確處理和自訂,請參閱 Base.rest
。
...
在指定位置的非最後位置需要 Julia 1.9
指定位置的吸入也可能發生在任何其他位置。然而,與吸入集合的結尾不同,這將永遠是急切的。
julia> a, b..., c = 1:5
1:5
julia> a
1
julia> b
3-element Vector{Int64}:
2
3
4
julia> c
5
julia> front..., tail = "Hi!"
"Hi!"
julia> front
"Hi"
julia> tail
'!': ASCII/Unicode U+0021 (category Po: Punctuation, other)
這是根據函數 Base.split_rest
執行的。
請注意,對於可變參數函數定義,吸入仍然只允許在最後位置。不過,這不適用於 單一參數解構,因為這不會影響方法調用
julia> f(x..., y) = x
ERROR: syntax: invalid "..." on non-final argument
Stacktrace:
[...]
julia> f((x..., y)) = x
f (generic function with 1 method)
julia> f((1, 2, 3))
(1, 2)
屬性解構
除了根據迭代解構之外,指定位置的右側也可以使用屬性名稱解構。這遵循 NamedTuples 的語法,並透過使用 getproperty
將指定位置右側的每個屬性指定給左側的每個變數,其名稱相同
julia> (; b, a) = (a=1, b=2, c=3)
(a = 1, b = 2, c = 3)
julia> a
1
julia> b
2
參數解構
解構功能也可以在函數參數中使用。如果函數參數名稱寫成元組(例如 (x, y)
),而不是只寫成符號,則會為您插入指定 (x, y) = argument
julia> minmax(x, y) = (y < x) ? (y, x) : (x, y)
julia> gap((min, max)) = max - min
julia> gap(minmax(10, 2))
8
請注意 gap
定義中的額外一組括號。如果不使用這些括號,gap
將會是一個二元參數函數,而且此範例將無法執行。
類似地,屬性解構也可以用於函數參數
julia> foo((; x, y)) = x + y
foo (generic function with 1 method)
julia> foo((x=1, y=2))
3
julia> struct A
x
y
end
julia> foo(A(3, 4))
7
對於匿名函數,解構單一參數需要一個額外的逗號
julia> map(((x,y),) -> x + y, [(1,2), (3,4)])
2-element Array{Int64,1}:
3
7
可變參數函數
通常很方便能撰寫接受任意數目參數的函式。此類函式傳統上稱為「變數參數」函式,簡稱為「可變數目參數」。你可以透過在最後一個位置參數後加上省略號來定義變數參數函式
julia> bar(a,b,x...) = (a,b,x)
bar (generic function with 1 method)
變數 a
和 b
會像往常一樣繫結到前兩個參數值,而變數 x
則會繫結到一個可迭代集合,其中包含傳遞給 bar
的零個或多個值,在它的前兩個參數之後
julia> bar(1,2)
(1, 2, ())
julia> bar(1,2,3)
(1, 2, (3,))
julia> bar(1, 2, 3, 4)
(1, 2, (3, 4))
julia> bar(1,2,3,4,5,6)
(1, 2, (3, 4, 5, 6))
在所有這些情況中,x
都會繫結到傳遞給 bar
的尾隨值的元組。
可以限制傳遞為變數參數的值的數量;這將在 參數約束變數參數方法 中稍後討論。
另一方面,通常很方便將包含在可迭代集合中的值「展開」到函式呼叫中,作為個別參數。為此,也可以使用 ...
,但出現在函式呼叫中
julia> x = (3, 4)
(3, 4)
julia> bar(1,2,x...)
(1, 2, (3, 4))
在這種情況下,一個值元組會被拼接成一個變數參數呼叫,而可變數目參數會出現在精確的位置。然而,這並非必要
julia> x = (2, 3, 4)
(2, 3, 4)
julia> bar(1,x...)
(1, 2, (3, 4))
julia> x = (1, 2, 3, 4)
(1, 2, 3, 4)
julia> bar(x...)
(1, 2, (3, 4))
此外,展開到函式呼叫中的可迭代物件不必是元組
julia> x = [3,4]
2-element Vector{Int64}:
3
4
julia> bar(1,2,x...)
(1, 2, (3, 4))
julia> x = [1,2,3,4]
4-element Vector{Int64}:
1
2
3
4
julia> bar(x...)
(1, 2, (3, 4))
此外,展開參數的函式不必是變數參數函式(儘管通常是)
julia> baz(a,b) = a + b;
julia> args = [1,2]
2-element Vector{Int64}:
1
2
julia> baz(args...)
3
julia> args = [1,2,3]
3-element Vector{Int64}:
1
2
3
julia> baz(args...)
ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)
Closest candidates are:
baz(::Any, ::Any)
@ Main none:1
Stacktrace:
[...]
如你所見,如果展開容器中的元素數量錯誤,則函式呼叫將會失敗,就像明確給出太多參數一樣。
選用引數
通常可以為函數引數提供合理的預設值。這可以讓使用者不必在每次呼叫時傳遞每個引數。例如,Dates
模組中的函數 Date(y, [m, d])
會為指定的年分 y
、月份 m
和日期 d
建立 Date
型別。不過,m
和 d
引數是選用的,其預設值為 1
。這種行為可以簡潔地表示為
julia> using Dates
julia> function date(y::Int64, m::Int64=1, d::Int64=1)
err = Dates.validargs(Date, y, m, d)
err === nothing || throw(err)
return Date(Dates.UTD(Dates.totaldays(y, m, d)))
end
date (generic function with 3 methods)
請注意,此定義會呼叫 Date
函數的另一個方法,該方法會採用一個 UTInstant{Day}
型別的引數。
有了這個定義,函數可以使用一個、兩個或三個引數呼叫,而且當只指定一個或兩個引數時,會自動傳遞 1
。
julia> date(2000, 12, 12)
2000-12-12
julia> date(2000, 12)
2000-12-01
julia> date(2000)
2000-01-01
選用引數實際上只是撰寫具有不同引數數量的多個方法定義的便利語法(請參閱 選用和關鍵字引數注意事項)。這可以用呼叫 methods
函數來檢查我們的 date
函數範例
julia> methods(date)
# 3 methods for generic function "date":
[1] date(y::Int64) in Main at REPL[1]:1
[2] date(y::Int64, m::Int64) in Main at REPL[1]:1
[3] date(y::Int64, m::Int64, d::Int64) in Main at REPL[1]:1
關鍵字引數
有些函數需要大量的引數,或有大量的行為。記住如何呼叫此類函數可能會很困難。關鍵字引數可以讓這些複雜的介面更容易使用和擴充,方法是允許透過名稱(而非僅透過位置)來識別引數。
例如,考慮一個繪製線條的函數 plot
。此函數可能有許多選項,用於控制線條樣式、寬度、顏色等等。如果它接受關鍵字參數,一個可能的呼叫看起來像 plot(x, y, width=2)
,其中我們選擇僅指定線條寬度。請注意,這有兩個目的。呼叫更容易閱讀,因為我們可以用其含義標籤參數。它也可以按任何順序傳遞大量參數的任何子集。
使用關鍵字參數定義的函數在簽章中使用分號
function plot(x, y; style="solid", width=1, color="black")
###
end
呼叫函數時,分號是可選的:可以呼叫 plot(x, y, width=2)
或 plot(x, y; width=2)
,但前一種樣式更常見。僅在傳遞變數或計算關鍵字(如下所述)時才需要明確的分號。
關鍵字參數預設值僅在必要時評估(當未傳遞對應的關鍵字參數時),並按從左到右的順序評估。因此,預設表達式可能參考先前的關鍵字參數。
關鍵字參數的類型可以明確如下
function f(;x::Int=1)
###
end
關鍵字參數也可以用於變數函數
function plot(x...; style="solid")
###
end
可以使用 ...
蒐集額外的關鍵字參數,如同在變數函數中
function f(x; y=0, kwargs...)
###
end
在 f
內部,kwargs
將是對命名元組的不可變鍵值反覆運算。命名元組(以及鍵為 Symbol
的字典,以及其他產生以符號為第一個值的二值集合的反覆運算)可以使用呼叫中的分號作為關鍵字參數傳遞,例如 f(x, z=1; kwargs...)
。
如果在方法定義中未指定關鍵字參數的預設值,則該參數為必要:如果呼叫者未指定值,則會擲出 UndefKeywordError
例外
function f(x; y)
###
end
f(3, y=5) # ok, y is assigned
f(3) # throws UndefKeywordError(:y)
您也可以在分號後傳遞 key => value
表達式。例如,plot(x, y; :width => 2)
等同於 plot(x, y, width=2)
。這在關鍵字名稱在執行階段計算的情況下很有用。
當分號後出現空白識別字或點運算式時,關鍵字參數名稱由識別字或欄位名稱暗示。例如,plot(x, y; width)
等同於 plot(x, y; width=width)
,而 plot(x, y; options.width)
等同於 plot(x, y; width=options.width)
。
關鍵字參數的性質使得可以多次指定相同的參數。例如,在呼叫 plot(x, y; options..., width=2)
中,options
結構也可能包含 width
的值。在這種情況下,最右邊的出現優先;在此範例中,width
肯定具有值 2
。但是,明確多次指定相同的關鍵字參數(例如 plot(x, y, width=2, width=3)
)是不允許的,並會導致語法錯誤。
預設值的評估範圍
評估選用和關鍵字參數預設表達式時,只有先前的參數在範圍內。例如,針對此定義
function f(x, a=b, b=1)
###
end
a=b
中的 b
參照外部範圍中的 b
,而不是後續參數 b
。
函數參數的 Do 塊語法
將函數作為引數傳遞給其他函數是一種強大的技術,但其語法並不總是方便。當函數引數需要多行時,此類呼叫特別難寫。舉例來說,考慮對具有多個案例的函數呼叫 map
map(x->begin
if x < 0 && iseven(x)
return 0
elseif x == 0
return 1
else
return x
end
end,
[A, B, C])
Julia 提供保留字 do
以更清楚地改寫此程式碼
map([A, B, C]) do x
if x < 0 && iseven(x)
return 0
elseif x == 0
return 1
else
return x
end
end
do x
語法建立一個具有引數 x
的匿名函數,並將其作為第一個引數傳遞給 map
。類似地,do a,b
會建立一個二個引數的匿名函數。請注意,do (a,b)
會建立一個一個引數的匿名函數,其引數是一個要解構的元組。一個單純的 do
會宣告其後是一個形式為 () -> ...
的匿名函數。
這些引數如何初始化取決於「外部」函數;這裡,map
會依序將 x
設為 A
、B
、C
,並對每個呼叫匿名函數,就像在語法 map(func, [A, B, C])
中發生的一樣。
此語法讓使用函數有效地擴充語言變得更容易,因為呼叫看起來像一般的程式碼區塊。有許多可能的用途與 map
非常不同,例如管理系統狀態。舉例來說,有一個 open
版本會執行程式碼,確保已開啟的檔案最終會關閉
open("outfile", "w") do io
write(io, data)
end
這可透過下列定義達成
function open(f::Function, args...)
io = open(args...)
try
f(io)
finally
close(io)
end
end
在這裡,open
首先開啟檔案以進行寫入,然後將產生的輸出串流傳遞給你在 do ... end
區塊中定義的匿名函數。在你的函數結束後,open
會確保串流正確地關閉,不論你的函數是正常結束還是擲回例外狀況。(try/finally
建構會在 控制流程 中說明。)
使用 do
區塊語法時,有助於查看文件或實作,以了解使用者函式的引數如何初始化。
do
區塊,就像任何其他內部函式,可以「擷取」其封裝範圍中的變數。例如,在 open...do
的上述範例中,變數 data
會從外部範圍擷取。擷取的變數可能會造成效能挑戰,如 效能提示 中所討論。
函式組合與串接
Julia 中的函式可以透過組合或串接 (連接) 它們來結合。
函式組合是指將函式組合在一起,並將組合結果套用至引數。使用函式組合運算子 (∘
) 來組合函式,因此 (f ∘ g)(args...)
等於 f(g(args...))
。
可以在 REPL 和適當設定的編輯器中輸入組合運算子,方法為 \circ<tab>
。
例如,sqrt
和 +
函式可以像這樣組合
julia> (sqrt ∘ +)(3, 6)
3.0
這會先加總數字,然後找出結果的平方根。
以下範例組合了三個函式,並將結果對應到一個字串陣列
julia> map(first ∘ reverse ∘ uppercase, split("you can compose functions like this"))
6-element Vector{Char}:
'U': ASCII/Unicode U+0055 (category Lu: Letter, uppercase)
'N': ASCII/Unicode U+004E (category Lu: Letter, uppercase)
'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
函式連接 (有時稱為「串接」或「使用管線」將資料傳送至後續函式) 是將函式套用至前一個函式的輸出
julia> 1:10 |> sum |> sqrt
7.416198487095663
在此,sum
產生的總和會傳遞給 sqrt
函式。等效的組合會是
julia> (sqrt ∘ sum)(1:10)
7.416198487095663
管道運算子也可以用於廣播,例如 .|>
,以提供鏈接/管道和點向量化語法(如下所述)的有用組合。
julia> ["a", "list", "of", "strings"] .|> [uppercase, reverse, titlecase, length]
4-element Vector{Any}:
"A"
"tsil"
"Of"
7
當將管道與匿名函數結合時,如果後續管道不應作為匿名函數主體的一部分進行解析,則必須使用括號。比較
julia> 1:3 .|> (x -> x^2) |> sum |> sqrt
3.7416573867739413
julia> 1:3 .|> x -> x^2 |> sum |> sqrt
3-element Vector{Float64}:
1.0
2.0
3.0
用於向量化函數的點語法
在技術計算語言中,通常有函數的「向量化」版本,它只將給定的函數 f(x)
套用至陣列 A
的每個元素,以透過 f(A)
產生新的陣列。這種語法對於資料處理很方便,但在其他語言中,向量化通常也需要效能:如果迴圈很慢,函數的「向量化」版本可以呼叫以低階語言編寫的快速函式庫程式碼。在 Julia 中,向量化函數不需要效能,事實上,撰寫自己的迴圈通常是有益的(請參閱 效能提示),但它們仍然很方便。因此,任何 Julia 函數 f
都可以使用語法 f.(A)
對任何陣列(或其他集合)逐元素套用。例如,sin
可以套用至向量 A
中的所有元素,如下所示
julia> A = [1.0, 2.0, 3.0]
3-element Vector{Float64}:
1.0
2.0
3.0
julia> sin.(A)
3-element Vector{Float64}:
0.8414709848078965
0.9092974268256817
0.1411200080598672
當然,如果您撰寫 f
的特殊「向量」方法,例如透過 f(A::AbstractArray) = map(f, A)
,您可以省略該點,這與 f.(A)
一樣有效率。f.(A)
語法的優點在於函數是否可向量化不需由函式庫撰寫者事先決定。
更一般地說,f.(args...)
實際上等於 broadcast(f, args...)
,它允許您對多個陣列(即使形狀不同)或陣列和標量的混合進行操作(請參閱 廣播)。例如,如果您有 f(x,y) = 3x + 4y
,那麼 f.(pi,A)
將返回一個新陣列,其中包含 A
中每個 a
的 f(pi,a)
,而 f.(vector1,vector2)
將返回一個新向量,其中包含每個索引 i
的 f(vector1[i],vector2[i])
(如果向量的長度不同,則會引發異常)。
julia> f(x,y) = 3x + 4y;
julia> A = [1.0, 2.0, 3.0];
julia> B = [4.0, 5.0, 6.0];
julia> f.(pi, A)
3-element Vector{Float64}:
13.42477796076938
17.42477796076938
21.42477796076938
julia> f.(A, B)
3-element Vector{Float64}:
19.0
26.0
33.0
關鍵字參數不會廣播,而是簡單地傳遞給函數的每個呼叫。例如,round.(x, digits=3)
等於 broadcast(x -> round(x, digits=3), x)
。
此外,嵌套 f.(args...)
呼叫會融合到單一的 broadcast
迴圈中。例如,sin.(cos.(X))
等於 broadcast(x -> sin(cos(x)), X)
,類似於 [sin(cos(x)) for x in X]
:只有一個迴圈遍歷 X
,並且為結果分配一個陣列。[相比之下,典型「向量化」語言中的 sin(cos(X))
首先會為 tmp=cos(X)
分配一個臨時陣列,然後在一個單獨的迴圈中計算 sin(tmp)
,分配一個第二個陣列。] 這種迴圈融合不是可能會或可能不會發生的編譯器最佳化,而是在遇到嵌套 f.(args...)
呼叫時的一種語法保證。技術上來說,融合會在遇到「非點」函數呼叫時停止;例如,在 sin.(sort(cos.(X)))
中,由於中間的 sort
函數,sin
和 cos
迴圈無法合併。
最後,當向量化運算的輸出陣列是預先配置時,通常會達到最大效率,這樣一來,重複呼叫不會反覆為結果配置新的陣列(請參閱預先配置輸出)。一個方便的語法是 X .= ...
,它等同於 broadcast!(identity, X, ...)
,但與上述相同,broadcast!
迴圈會與任何巢狀「點」呼叫合併。例如,X .= sin.(Y)
等同於 broadcast!(sin, X, Y)
,以 sin.(Y)
原地覆寫 X
。如果左側是一個陣列索引表達式,例如 X[begin+1:end] .= sin.(Y)
,則它會轉換為 view
上的 broadcast!
,例如 broadcast!(sin, view(X, firstindex(X)+1:lastindex(X)), Y)
,這樣左側會原地更新。
由於在表達式中為許多運算和函式呼叫新增點可能會很繁瑣,而且會導致難以閱讀的程式碼,因此提供巨集 @.
,將表達式中的每個函式呼叫、運算和指定轉換成「點」版本。
julia> Y = [1.0, 2.0, 3.0, 4.0];
julia> X = similar(Y); # pre-allocate output array
julia> @. X = sin(cos(Y)) # equivalent to X .= sin.(cos.(Y))
4-element Vector{Float64}:
0.5143952585235492
-0.4042391538522658
-0.8360218615377305
-0.6080830096407656
二元(或一元)運算子(例如 .+
)使用相同的機制處理:它們等同於 broadcast
呼叫,並與其他巢狀「點」呼叫合併。X .+= Y
等同於 X .= X .+ Y
,並會產生合併的原地指定;另請參閱點運算子。
您也可以使用 |>
結合點運算和函式鏈結,如下例所示
julia> 1:5 .|> [x->x^2, inv, x->2*x, -, isodd]
5-element Vector{Real}:
1
0.5
6
-4
true
進一步閱讀
我們在此必須提到,這遠非定義函式的完整範例。Julia 有一個精密的類型系統,並允許對參數類型進行多重調用。這裡給出的範例都沒有在參數上提供任何類型註解,這表示它們適用於所有類型的參數。類型系統說明在 類型 中,而根據執行時間參數類型選擇的方法來定義函式的說明在 方法 中。