Julia 的函数与 Python 很类似,最大的区别可能就是关键字 def 换成 function,除此以外都是一些细节差异。

基础

简单例子

最基本的例子如下

1
2
3
4
5
function f(x,y)
x + y
end

f(1,2) # 3

和 Python 一样,Julia 的函数不需要额外的类似函数句柄的机制,函数对象可以和普通的值一样被传递。

1
2
3
4
5
function f(x,y)
x + y;
end

g = f; g(1,2) # 3

函数的赋值形式

Julia 支持以单行的赋值形式定义函数

1
f(x, y) = x + y;

此时的函数体只能是一个单行表达式,这个表达式的结果自然就是函数的返回值。由于函数分派机制的存在,这种简单函数在 Julia 中实际上很常见。

return 语句

Julia 可以使用 return 语句返回值,但是和 Python 不同的是,Julia 在不使用 return 语句时,自动将最后一个表达式的结果作为函数返回值。为了代码可读性,还是不建议省略 return

例如

1
2
3
4
5
6
7
8
function update(x)
println("x = $x");
x + 1 # return x + 1
end

y = update(1)
# x = 1
# 2

如果希望函数不提供返回值,有以下几种等效写法(nothing 相当于 Python 的 None

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hello1(x)
println("Hello, $x");
return nothing
end

function hello2(x)
println("Hello, $x");
return
end

function hello3(x)
println("Hello, $x");
nothing
end

返回值类型

Julia 支持指定函数的参数和返回值的类型,关于参数的类型比较复杂,这里暂时不作讨论,我们关注返回值的类型

1
2
3
4
5
function h(x, y)::Int64
x + y
end

h(1, 2.0) # 3

如果函数体中的实际返回值类型与函数声明中的返回值类型不匹配,Julia 会对其进行类型转换。(但是对函数参数则不会进行自动的类型转换)

函数返回多值

与 Python 一样,Julia 可以通过返回元组的形式来返回多值,例如

1
2
3
4
5
function test(a)
return (a - 1, a + 1)
end

result = test(10) # (9, 11)

在接收返回值时也可以自动对元组解包,例如

1
2
3
r1, r2 = test(10);
# r1 = 9
# r2 = 11

函数参数

传参行为

与 Python 类似,Julia 在函数传参过程中通常不会发生实参到形参的拷贝,而是引用的传递,因此面临和 Python 一样的问题:直接修改函数参数时,如果是复合类型,修改可能会影响到外部。

例如

1
2
3
4
5
6
7
8
9
10
11
12
function test(a)
a[1] = 100
return
end

x = [1, 2, 3]
test(x)
x
# 3-element Vector{Int64}:
# 100
# 2
# 3

对于执行过程中会修改参数的函数,在命名上约定使用 ! 结尾(! 是函数名称的一部分),例如

1
2
3
4
function test!(a)
a[1] = 100
return
end

一个典型的例子是sortsort!,这是两个不同的函数,前者的排序不会改变参数,后者在排序的同时会就地改变参数。 很多名为abc的函数的实现直接依赖对应的abc!函数:先对参数进行显式拷贝,然后调用abc!,最后返回之前拷贝的副本。

参数默认值

Julia 支持给参数提供默认值,例如

1
2
3
4
5
6
function h(x, y = 1)
return x + y
end

h(1) # 2
h(1, 2) # 3

显然提供默认值的参数要放在靠后的位置,此后不能有任何无默认值的参数出现,否则语法上存在歧义,会报错。

Python 的参数默认值只会在定义时计算一次,而 Julia 则会延迟到每一次调用时,因此多个参数的默认值允许相互依赖,例如

1
2
3
4
5
function f(x, y = x+1, z = x+y)
println("x=", x)
println("y=", y)
println("z=", z)
end

运行效果如下

1
2
3
4
julia> f(3)
x=3
y=4
z=7

位置参数和关键字参数

Julia 支持位置参数和关键字参数,但是比 Python 更加严格:在函数声明时,出现在 ; 之前的视作位置参数,出现在 ; 之后的视作关键字参数

1
2
3
4
5
function h(x, y; z)
return x + y + z
end

h(1, 2; z = 3) # 6

即使完全使用关键字参数,在定义时形参列表中的 ; 也不允许省略。

1
2
3
function h2(; y, z)
return y + z
end

与 Python 不同,Julia 在函数调用时需要严格区分位置参数和关键字参数,不允许以键值对形式给位置参数赋值。

1
h(x = 1, y = 2; z = 3) # ERROR

在函数调用时的语法并没有那么严格,支持各种简写形式。

  • 关键字参数前的 ; 有时可以使用 , 替代,而且参数顺序相对自由,例如关键字参数不要求在所有位置参数之后才出现。下面这些写法都是可行的

    1
    2
    h(1, 2, z = 3) # 6
    h(1, z = 3, 2) # 6

  • 分号后的关键字参数也可使用 key => value 表达式,注意 key 要使用符号,例如

    1
    h(1 ; :y => 2, :z => 3) # 6

  • 分号后的关键字参数如果只是传递同名参数(例如 :z => z),可以直接简写为名称,例如

    1
    2
    y = 2; z = 3;
    h(1 ; y, z) # 6

如果关键字参数重复出现,会直接抛出异常,而不是用后者覆盖。

这部分的语法比较灵活,但是最好还是用最标准的写法,也就是和函数定义时的写法对应,先提供所有位置参数,然后提供所有键值对参数。

元组参数

Julia 支持在定义时把函数形参整体写成一个元组,在调用时同样也需要将参数打包为一个元组进行传递,例如

1
2
3
4
5
function func((a, b))
return a + b
end

func((1, 2)) # 3

这个机制可以极大简化函数复合使用时的写法,将上一个函数返回的元组直接提供给下一个函数,不需要额外的赋值过程,例如

1
2
3
4
5
6
7
8
9
function test(a)
return (a - 1, a + 1)
end

function func((a, b))
return a + b
end

func(test(3)) # 6

不定位置参数

与 Python 类似,Julia 提供了不定位置参数的语法,自动将多余的位置参数使用元组打包(类似Python的 *args ),例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f(x, y, args...)
println("x = $x")
println("y = $y")
println("args = $args")
end

f(1, 2)
# x = 1
# y = 2
# args = ()

f(1, 2, 3, 4)
# x = 1
# y = 2
# args = (3, 4)

位置参数解包

与不定位置参数的语法直接对应,我们也可以在调用函数时,将列表/元组等解包为多个位置参数,依次传递给函数。

1
2
3
4
5
6
7
8
9
function g(x, y, z)
return x + y + z
end

g([1, 2, 3]...) # 6
# = g(1, 2, 3)

g((2, 3, 4)...) # 9
# = g(2, 3, 4)

不定关键字参数

与不定位置参数类似,我们也可以使用具名元组来收集多余的关键字参数(Python的 **kwargs 则把多余的关键字参数收集为字典)

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn(; kwargs...)
println(typeof(kwargs))
println(kwargs)
end

fn(x = 1)
# Base.Pairs{Symbol, Int64, Tuple{Symbol}, @NamedTuple{x::Int64}}
# Base.Pairs(:x => 1)

fn(x = 1, y = 2)

# Base.Pairs{Symbol, Int64, Tuple{Symbol, Symbol}, @NamedTuple{x::Int64, y::Int64}}
# Base.Pairs(:x => 1, :y => 2)

关键字参数解包

同样地,我们也可以在调用函数时,将具名元组或键类型为Symbol的字典解包为多个关键字参数,依次传递给函数。

例如将具名元组解包

1
2
3
nt = (z = 3, y = 2, x = 1)
h(; nt...) # 6
# = h(; z = 3, y = 2, x = 1)

将字典解包(注意字典的键的类型要求必须是Symbol,不能是String

1
2
3
4
5
6
7
function h(; x, y, z)
return x + y + z
end

dic1 = Dict(:x => 1, :y => 2, :z => 3)
h(; dic1...) # 6
# = h(; x = 1, y = 2, z = 3)

注意这里的 ; 都不可以省略,否则 Julia 会尝试将其解包后作为位置参数传递。

匿名函数

基础

Julia 支持匿名函数,写法非常数学化,比其它各种语言中的 lambda 语句都更加自然,例如

1
2
3
4
f1 = x -> x^2 + 1;

f1(1) # 2
f1(2) # 5

涉及多个参数和无参数的匿名函数写法如下(也就是对参数列表必须使用小括号)

1
2
3
4
5
6
7
f2 = (x,y) -> x + y;

f2(1,2) # 3

f0 = () -> println("Hello World");

f0() # Hello World

不建议使用下面这种无意义的匿名函数封装,可以直接用函数本身

1
x -> f(x)

匿名函数的主要用法是作为参数传递给其他函数,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
map(x -> 2*x, [1, 2, 3])
# 3-element Vector{Int64}:
# 2
# 4
# 6

filter(x -> x % 2 == 0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
# 5-element Vector{Int64}:
# 2
# 4
# 6
# 8
# 10

do 语句

考虑到 map 等函数所接收的第一个参数可能是非常复杂的函数,此时使用匿名函数(即使加上begin ... end语句)的写法会非常难看

1
2
3
4
5
6
7
8
9
10
11
12
13
map(x -> begin
if x == 1
return 1
else
return x * 2
end
end,
[1, 2, 3])

# 3-element Vector{Int64}:
# 1
# 4
# 6

Julia 为此提供了一个专门的 do 语法,可以将函数需要的匿名函数参数的定义后置,实际运算中作为一个匿名函数传递给函数的第一个参数

1
2
3
4
5
6
7
8
9
10
11
12
map([1, 2, 3]) do x
if x == 1
return 1
else
return x * 2
end
end

# 3-element Vector{Int64}:
# 1
# 4
# 6

补充

函数复合

Julia 为函数的复合调用提供了一些语法糖。

第一个是完全遵循数学表示习惯的 \circ,例如

1
2
3
4
5
6
7
8
f(x) = x * x;
g(x) = x + 1;

f(g(10)) # 121
g(f(10)) # 101

(f ∘ g)(10) # 121
(g ∘ f)(10) # 101

第二种则是管道风格的调用语法 |>,例如

1
2
3
sqrt(sum(1:10)) # 7.416198487095663

1:10 |> sum |> sqrt # 7.416198487095663