Julia 函数
函数是一组一起执行一个任务的语句。
在 Julia 里,函数是将参数值组成的元组映射到返回值的一个对象。
定义函数
Julia 中使用 function 定义函数,基本语法是:
function functionname(args) expression ... end
默认情况下,函数返回的值是最后计算的表达式的值,所以上面的写法没有 return 语句。当然,如果使用 return 关键字,函数就会立即返回:
实例
x + y
end
f (generic function with 1 method)
julia> f(2, 3)
5
# 使用 return 提前返回
julia> function bills(money)
if money < 0
return false
else
return true
end
end
bills (generic function with 1 method)
julia> bills(50)
true
julia> bills(-50)
false
简写形式
当函数体只有一个表达式时,可以省略 function 关键字,用赋值形式定义函数,这是 Julia 中非常常用的风格:
实例
f (generic function with 1 method)
julia> f(2, 3)
5
julia> func(x, y) = sqrt(x^2 + y^2)
func (generic function with 1 method)
julia> func(5, 4)
6.4031242374328485
没有括号时,表达式 f 指的是函数对象,可以像任何值一样被传递:
实例
julia> g(2, 3)
5
和变量名一样,Unicode 字符也可以用作函数名:
实例
∑ (generic function with 1 method)
julia> ∑(2, 3)
5
参数类型声明
可以在参数名后用 :: 声明参数类型,用来限定函数适用的类型范围:
实例
julia> fib(n::Integer) = n <= 2 ? 1 : fib(n-1) + fib(n-2)
fib (generic function with 1 method)
julia> fib(10)
55
julia> fib(1.5)
ERROR: MethodError: no method matching fib(::Float64)
建议:给参数声明类型不会提高性能(Julia 会为实际传入的类型自动编译特化版本),它的作用主要是限制派发、保证正确性和增强可读性。不确定时可以不加类型声明,或使用更宽泛的抽象类型如 Integer、Real。
返回类型与多返回值
我们可以使用 :: 运算符在函数签名后指定返回类型:
实例
return x * y
end;
julia> typeof(g(1, 2))
Int8
以上函数实例会把返回值转换为 Int8 类型。需要注意返回类型声明在 Julia 中较少使用,更好的做法是写出类型稳定的函数。
如果函数要返回多个值,可以使用元组,调用时用解构接收:
实例
x + y, x * y
end
mul (generic function with 1 method)
julia> mul(5, 10)
(15, 50)
# 解构赋值接收多个返回值
julia> s, p = mul(5, 10)
(15, 50)
julia> s
15
约定:按惯例,只产生副作用不返回有意义结果的函数应显式返回
nothing。
可选参数
在函数中我们可以设置参数默认值,这样在没有提供该参数的时候,就可以使用默认值来计算:
实例
println("$ax, $by, $cz")
end
pos (generic function with 2 methods)
julia> pos(10, 30)
10, 30, 0
julia> pos(10, 30, 50)
10, 30, 50
关键字参数
Julia 关键字参数允许通过名称而不是仅通过位置来识别参数,使得这些复杂函数易于使用和扩展。
使用关键字参数,需要在函数的位置参数之后使用分号 ;,并在其后跟一个或多个键值对 key=value:
实例
println("a is $a")
println("b is $b")
return "c => $c, d => $d"
end
foo (generic function with 1 method)
julia> foo(100, 20)
a is 100
b is 20
"c => 10, d => hi"
julia> foo("Hello", "Runoob", c=pi, d=22//7)
a is Hello
b is Runoob
"c => π, d => 22//7"
重要:调用时位置参数必须写在关键字参数之前,关键字参数之间可以任意排列顺序,例如
foo("Hello", "Runoob", d=22//7, c=pi)也是正确的。但foo(c=pi, "Hello", "Runoob")这种关键字在前、位置在后的写法是错误的,会直接报 MethodError。
没有被匹配到的关键字参数可以用 kwargs... 收集起来,它是一个具名元组:
实例
println("x = $x, y = $y")
println("其他关键字: $kwargs")
end
f (generic function with 1 method)
julia> f(1; a=2, b=3)
x = 1, y = 0
其他关键字: (a = 2, b = 3)
可变参数函数(Varargs)
在最后一个位置参数后面加上 ...,可以定义接受任意数量参数的函数,多余的参数会被收集成一个元组:
实例
bar (generic function with 1 method)
julia> bar(1, 2)
(1, 2, ())
julia> bar(1, 2, 3, 4)
(1, 2, (3, 4))
反过来,在调用函数时用 ... 可以把一个集合"拆开"(splatting)作为单独的参数传入:
实例
(3, 4)
julia> bar(1, 2, x...)
(1, 2, (3, 4))
julia> max([1, 5, 3]...)
5
匿名函数
匿名函数是一个没有函数名的函数,语法使用符号 ->:
实例
#1 (generic function with 1 method)
julia> function (x)
x^2 + 2x - 1
end
#3 (generic function with 1 method)
匿名函数最主要的用法是传递给接收函数作为参数的函数。一个经典的例子是 map,为数组的每个元素应用一次函数,然后返回一个包含结果值的新数组:
实例
3-element Vector{Float64}:
1.0
4.0
2.0
julia> map(x -> x^2 + 2x - 1, [1, 3, -1])
3-element Vector{Int64}:
2
14
-2
接受多个参数的匿名函数写法可以使用语法 (x,y,z) -> 2x+y-z,而无参匿名函数写作 ()->3。
do 语句块
当需要向函数传入多行的匿名函数时,可以使用 do 语句块,让代码更像普通的代码块:
实例
julia> map(x -> begin
if x < 0
return 0
elseif x == 0
return 1
else
return x
end
end, [-2, 0, 3])
3-element Vector{Int64}:
0
1
3
# 使用 do 块更清晰
julia> map([-2, 0, 3]) do x
if x < 0
return 0
elseif x == 0
return 1
else
return x
end
end
3-element Vector{Int64}:
0
1
3
do x 语法会创建一个以 x 为参数的匿名函数,并把它作为第一个参数传给外层函数。
do 语句块最常见的用途是资源管理,例如 open 函数确保文件最终一定会被关闭:
实例
write(io, "RUNOOB")
end
它的实现原理大致如下:open 先打开文件,用 try/finally 保证无论函数是否抛出异常,文件都会被 close:
function open(f::Function, args...)
io = open(args...)
try
f(io)
finally
close(io)
end
end
函数组合与管道
Julia 提供了两个方便的运算符来组合函数。
∘(输入 \circ 后按 Tab)是函数组合运算符,(f ∘ g)(x) 等价于 f(g(x)):
实例
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)
|> 是管道运算符,把左边的值作为参数传给右边的函数,让数据处理的流向更加直观:
实例
点语法向量化
任何函数 f 都可以通过 f.(A) 的语法逐元素地应用到数组上,等价于 broadcast(f, A):
实例
julia> sin.(A)
3-element Vector{Float64}:
0.8414709848078965
0.9092974268256817
0.1411200080598672
# 多参数函数也可以广播
julia> f(x, y) = 3x + 4y
f (generic function with 1 method)
julia> f.(A, [10, 20, 30])
3-element Vector{Float64}:
43.0
86.0
129.0
嵌套的点调用会自动融合成单次循环,X .= f.(Y) 还可以把结果原地写入 X,详细内容见数组章节的广播部分。
Map 与 Filter
map(func, coll) 把函数依次应用于集合 coll 的每个元素,返回一个新的集合:
实例
3-element Vector{Int64}:
1027
33
-17
filter(function, collection) 返回集合的副本,只保留让函数返回 true 的元素:
实例
julia> filter(x -> x % 2 == 0, array)
3-element Vector{Int64}:
2
4
6
函数嵌套与递归
在 Julia 中,函数可以嵌套使用。内层函数可以访问外层函数的变量:
实例
y = x * 2
function add1(y)
return y + 1
end
return add1(y)
end
add (generic function with 1 method)
julia> add(10)
21
同样,Julia 中的函数也可以是递归的。递归指的是在函数的定义中使用函数自身的方法。
以下我们使用三元运算符来写递归,三元运算符 expr ? a : b,如果 expr 为 true,值为 a 的计算结果,否则为 b 的计算结果:
实例
mysum (generic function with 1 method)
julia> mysum(10)
55
递归最著名的例子是计算第 n 个斐波那契数:
实例
fib (generic function with 1 method)
julia> fib(10)
55
julia> fib(20)
6765
提示:直接递归的 fib 计算到 fib(50) 左右就会非常慢(指数级复杂度)。实际使用中可以用字典缓存中间结果,或改用迭代实现。
