Julia 函數(shù)

2022-02-25 16:45 更新

Julia 中的函數(shù)是將一系列參數(shù)組成的元組映設(shè)到一個返回值的對象,Julia 的函數(shù)不是純的數(shù)學(xué)式函數(shù),有些函數(shù)可以改變或者影響程序的全局狀態(tài)。Julia 中定義函數(shù)的基本語法為:

function f(x,y)
  x + y
end

Julia 中可以精煉地定義函數(shù)。上述傳統(tǒng)的聲明語法,等價于下列緊湊的“賦值形式”:

f(x,y) = x + y

對于賦值形式,函數(shù)體通常是單表達(dá)式,但也可以為復(fù)合表達(dá)式(詳見復(fù)合表達(dá)式)。Julia 中常見這種短小簡單的函數(shù)定義。短函數(shù)語法相對而言更方便輸入和閱讀。

使用圓括號來調(diào)用函數(shù):

julia> f(2,3)
5

沒有圓括號時, f 表達(dá)式指向的是函數(shù)對象,這個函數(shù)對象可以像值一樣被傳遞:

julia> g = f;

julia> g(2,3)
5

調(diào)用函數(shù)有兩種方法:使用特定函數(shù)名的特殊運算符語法(詳見后面函數(shù)運算符),或者使用 apply 函數(shù):

julia> apply(f,2,3)
5

apply 函數(shù)把第一個參數(shù)當(dāng)做函數(shù)對象,應(yīng)用在后面的參數(shù)上。

和變量名稱一樣, 函數(shù)名稱也可以使用 Unicode 字符:

julia> ∑(x,y) = x + y
∑ (generic function with 1 method)

參數(shù)傳遞行為

Julia 函數(shù)的參數(shù)遵循 “pass-by-sharing” 的慣例,即不傳遞值,而是傳遞引用。函數(shù)參數(shù)本身,有點兒像新變量綁定(引用值的新位置),但它們引用的值與傳遞的值完全相同。對可變值(如數(shù)組)的修改,會影響其它函數(shù)。

return 關(guān)鍵字

函數(shù)返回值通常是函數(shù)體中最后一個表達(dá)式的值。上一節(jié)中 f 是表達(dá)式 x + y 的值。在 C 和大部分命令式語言或函數(shù)式語言中, return 關(guān)鍵字使得函數(shù)在計算完該表達(dá)式的值后立即返回:

function g(x,y)
  return x * y
  x + y
end

對比下列兩個函數(shù):

f(x,y) = x + y

function g(x,y)
  return x * y
  x + y
end

julia> f(2,3)
5

julia> g(2,3)
6

在純線性函數(shù)體,比如 g 中,不需要使用 return ,它不會計算表達(dá)式 x + y ??梢园?x * y 作為函數(shù)的最后一個表達(dá)式,并省略 return 。只有涉及其它控制流時, return 才有用。下例計算直角三角形的斜邊長度,其中直角邊為 x 和 y,為避免溢出:

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

最后一行的 return 可以省略。

函數(shù)運算符

Julia 中,大多數(shù)運算符都是支持特定語法的函數(shù)。 && 、 || 等短路運算是例外,它們不是函數(shù),因為短路求值先算前面的值,再算后面的值。對于函數(shù)運算符,可以像其它函數(shù)一樣,把參數(shù)列表用圓括號括起來,作為函數(shù)運算符的參數(shù):

julia> 1 + 2 + 3
6

julia> +(1,2,3)
6

中綴形式與函數(shù)形式完全等價,事實上,前者被內(nèi)部解析為函數(shù)調(diào)用的形式??梢韵駥ζ渌瘮?shù)一樣,對 +* 等運算符進行賦值、傳遞:

julia> f = +;

julia> f(1,2,3)
6

但是,這時 f 函數(shù)不支持中綴表達(dá)式。

特殊名字的運算符

有一些表達(dá)式調(diào)用特殊名字的運算符:

表達(dá)式 調(diào)用
[A B C ...] hcat
[A, B, C, ...] vcat
[A B; C D; ...] hvcat
A' ctranspose
A.' transpose
1:n colon
A[i] getindex
A[i]=x setindex!

這些函數(shù)都存在于 Base.Operators 模塊中。

匿名函數(shù)

Julia 中函數(shù)是第一類對象,可以被賦值給變量,可以通過賦值后的變量來調(diào)用函數(shù), 還可以當(dāng)做參數(shù)和返回值,甚至可以被匿名構(gòu)造:

julia> x -> x^2 + 2x - 1
(anonymous function)

上例構(gòu)造了一個匿名函數(shù),輸入一個參數(shù) x ,返回多項式 x^2 + 2x - 1 的值。匿名函數(shù)的主要作用是把它傳遞給接受其它函數(shù)作為參數(shù)的函數(shù)。最經(jīng)典的例子是 map 函數(shù),它將函數(shù)應(yīng)用在數(shù)組的每個值上,返回結(jié)果數(shù)組:

julia> map(round, [1.2,3.5,1.7])
3-element Array{Float64,1}:
 1.0
 4.0
 2.0

map 的第一個參數(shù)可以是非匿名函數(shù)。但是大多數(shù)情況,不存在這樣的函數(shù)時,匿名函數(shù)就可以簡單地構(gòu)造單用途的函數(shù)對象,而不需要名字:

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

匿名函數(shù)可以通過類似 (x,y,z)->2x+y-z 的語法接收多個參數(shù)。無參匿名函數(shù)則類似于 ()->3 。無參匿名函數(shù)可以“延遲”計算,做這個用處時,代碼被封裝進無參函數(shù),以后可以通過把它命名為 f() 來引入。

多返回值

Julia 中可以通過返回多元組來模擬返回多值。但是,多元組并不需要圓括號來構(gòu)造和析構(gòu),因此造成了可以返回多值的假象。下例返回一對兒值:

julia> function foo(a,b)
         a+b, a*b
       end;

如果在交互式會話中調(diào)用這個函數(shù),但不將返回值賦值出去,會看到返回的是多元組:

julia> foo(2,3)
(5,6)

Julia 支持簡單的多元組“析構(gòu)”來給變量賦值:

julia> x, y = foo(2,3);

julia> x
5

julia> y
6

也可以通過 return 來返回:

function foo(a,b)
  return a+b, a*b
end

這與之前定義的 foo 結(jié)果相同。

變參函數(shù)

函數(shù)的參數(shù)列表如果可以為任意個數(shù),有時會非常方便。這種函數(shù)被稱為“變參”函數(shù),是“參數(shù)個數(shù)可變”的簡稱??梢栽谧詈笠粋€參數(shù)后緊跟省略號 ... 來定義變參函數(shù):

julia> bar(a,b,x...) = (a,b,x)
bar (generic function with 1 method)

變量 ab 是前兩個普通的參數(shù),變量 x 是尾隨的可迭代的參數(shù)集合,其參數(shù)個數(shù)為 0 或多個:

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 的尾隨的值多元組。

函數(shù)調(diào)用時,也可以使用 ... :

julia> x = (3,4)
(3,4)

julia> bar(1,2,x...)
(1,2,(3,4))

上例中,多元組的值完全按照變參函數(shù)的定義進行內(nèi)插,也可以不完全遵守其函數(shù)定義來調(diào)用:

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))

被內(nèi)插的對象也可以不是多元組:

julia> x = [3,4]
2-element Array{Int64,1}:
 3
 4

julia> bar(1,2,x...)
(1,2,(3,4))

julia> x = [1,2,3,4]
4-element Array{Int64,1}:
 1
 2
 3
 4

julia> bar(x...)
(1,2,(3,4))

原函數(shù)也可以不是變參函數(shù)(大多數(shù)情況下,應(yīng)該寫成變參函數(shù)):

baz(a,b) = a + b

julia> args = [1,2]
2-element Int64 Array:
 1
 2

julia> baz(args...)
3

julia> args = [1,2,3]
3-element Int64 Array:
 1
 2
 3

julia> baz(args...)
no method baz(Int64,Int64,Int64)

但如果輸入的參數(shù)個數(shù)不對,函數(shù)調(diào)用會失敗。

可選參數(shù)

很多時候,函數(shù)參數(shù)都有默認(rèn)值。例如,庫函數(shù) parseint(num,base) 把字符串解析為某個進制的數(shù)。base 參數(shù)默認(rèn)為 10。這種情形可以寫為:

function parseint(num, base=10)
    ###
end

這時,調(diào)用函數(shù)時,參數(shù)可以是一個或兩個。當(dāng)?shù)诙€參數(shù)未指明時,自動傳遞 10

julia> parseint("12",10)
12

julia> parseint("12",3)
5

julia> parseint("12")
12

可選參數(shù)很方便參數(shù)個數(shù)不同的多方法定義(詳見方法)。

關(guān)鍵字參數(shù)

有些函數(shù)的參數(shù)個數(shù)很多,或者有很多行為。很難記住如何調(diào)用這種函數(shù)。關(guān)鍵字參數(shù),允許通過參數(shù)名來區(qū)分參數(shù),便于使用、擴展這些復(fù)雜接口。

例如,函數(shù) plot 用于畫出一條線。此函數(shù)有許多可選項,控制線的類型、寬度、顏色等。如果它接收關(guān)鍵字參數(shù),當(dāng)我們要指明線的寬度時,可以調(diào)用 plot(x, y, width=2)之類的形式。這樣的調(diào)用方法給參數(shù)添加了標(biāo)簽,便于閱讀;也可以按任何順序傳遞部分參數(shù)。

使用關(guān)鍵字參數(shù)的函數(shù),在函數(shù)簽名中使用分號來定義:

function plot(x, y; style="solid", width=1, color="black")
    ###
end

額外的關(guān)鍵字參數(shù),可以像變參函數(shù)中一樣,使用 ... 來匹配:

function f(x; y=0, args...)
    ###
end

在函數(shù) f 內(nèi)部,args 可以是 (key,value) 多元組的集合,其中 key 是符號。可以在函數(shù)調(diào)用時使用分號來傳遞這個集合, 如 f(x, z=1; args...). 這種情況下也可以使用字典。

關(guān)鍵字參數(shù)的默認(rèn)值僅在必要的時候從左至右地被求值(當(dāng)對應(yīng)的關(guān)鍵字參數(shù)沒有被傳遞),所以默認(rèn)的(關(guān)鍵字參數(shù)的)表達(dá)式可以調(diào)用在它之前的關(guān)鍵字參數(shù)。

默認(rèn)值的求值作用域

可選參數(shù)和關(guān)鍵字參數(shù)的區(qū)別在于它們的默認(rèn)值是怎樣被求值的。當(dāng)可選的參數(shù)被求值時,只有在它之前的的參數(shù)在作用域之內(nèi); 與之相對的, 當(dāng)關(guān)鍵字參數(shù)的默認(rèn)值被計算時, 所有的參數(shù)都是在作用域之內(nèi)。比如,定義函數(shù):

function f(x, a=b, b=1)
    ###
end

a=b 中的 b 指的是該函數(shù)的作用域之外的 b ,而不是接下來 的參數(shù) b。然而,如果 ab 都是關(guān)鍵字參數(shù),那么它們都將在 生成在同一個作用域上,a=b 中的 b 指向的是接下來的參數(shù) b (遮蔽了任何外層空間的 b), 并且 a=b 會得到未定義變量的錯誤 (因為默認(rèn) 參數(shù)的表達(dá)式是自左而右的求值的,b 并沒有被賦值)。

函數(shù)參數(shù)的塊語法

將函數(shù)作為參數(shù)傳遞給其它函數(shù),當(dāng)行數(shù)較多時,有時不太方便。下例在多行函數(shù)中調(diào)用 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 的語法創(chuàng)建一個含有參數(shù) x 的匿名函數(shù),并將其傳給 map 作為第一個參數(shù)。類似地,do a,b 將創(chuàng)建一個含有兩個參數(shù)的匿名函數(shù),和一個樸素的 do 的聲明以 () -> .... 方式說明如下是一個匿名函數(shù)。

如何將這些參數(shù)初始化取決于“外部”函數(shù);在這里,map 將依次設(shè)置 xA, B, C,每個都將調(diào)用匿名函數(shù),就像在語法 map(func, [A, B, C]) 中做的一樣。

因為語法的調(diào)用看起來像正常的代碼塊,所以這種語法使它更容易使用函數(shù)來有效地擴展語言。這里有許多可能完全不同于 map 的用途,如管理系統(tǒng)狀態(tài)。例如,有一個版本的 open,運行代碼來確保打開的文件最終關(guān)閉:

open("outfile", "w") do io
    write(io, data)
end

它可以通過以下定義來實現(xiàn):

function open(f::Function, args...)
    io = open(args...)
    try
        f(io)
    finally
        close(io)
    end
end

對比的 map 的例子,這里的 IO 是通過 open("outfile", "w")來實現(xiàn)初始化的。字符流之后會傳遞給您的執(zhí)行寫入的匿名函數(shù);最后,open 的功能確保流在您的函數(shù)結(jié)束后是關(guān)閉狀態(tài)的。 try/finally 的構(gòu)造將在 控制流 中被描述。

do 塊語法的使用有助于檢查文檔或?qū)崿F(xiàn)了解用戶函數(shù)的參數(shù)是如何被初始化的。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號