在Julia中,函数是一个将参数值元组映射到返回值的对象。从函数可以更改并受程序全局状态影响的意义上讲,Julia函数不是纯数学函数。在Julia中定义函数的基本语法为:
julia> function f(x,y)
x + y
end
f (generic function with 1 method)
在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函数参数遵循有时称为“传递共享”的约定,这意味着将值传递给函数时不会复制它们。函数参数本身充当新的变量绑定(可以引用值的新位置),但是它们引用的值与传递的值相同。Array
在函数内对可变值(例如s)进行的修改对调用者是可见的。这与Scheme,大多数Lisps,Python,Ruby和Perl以及其他动态语言中的行为相同。
return
关键字函数返回的值是最后一个表达式的值,默认情况下,它是函数定义主体中的最后一个表达式。在示例函数中f
,从上一节开始,这是expression的值x + y
。与C语言以及大多数其他命令式或函数式语言一样,该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中,大多数运算符只是支持特殊语法的函数。(例外是具有特殊评估语义的运算符,例如&&
和||
。这些运算符不能用作函数,因为“ 短路评估”要求在评估运算符之前不对它们的操作数进行评估。)因此,您也可以使用带括号的参数列表来应用它们,就像您将执行其他任何功能:
julia> 1 + 2 + 3
6
julia> +(1,2,3)
6
中缀形式与功能应用程序形式完全等效-实际上,前者被解析为内部产生函数调用。这也意味着您可以像使用其他函数值一样分配和传递诸如+()
和的运算符*()
:
julia> f = +;
julia> f(1,2,3)
6
但是f
,该函数不支持名称前缀。
一些特殊的表达式对应于具有非显而易见名称的函数的调用。这些是:
hcat()
Base.Operators
即使这些函数没有类似操作符的名称,它们也包含在模块中。
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 Array{Float64,1}:
1.0
4.0
2.0
如果已经实现一个想要实现转换的命名函数作为第一个参数传递给,这很好map()
。但是,通常不存在即用型命名功能。在这种情况下,匿名函数构造无需名称即可轻松创建一次性函数对象:
julia> map(x -> x^2 + 2x - 1, [1,3,-1])
3-element Array{Int64,1}:
2
14
-2
可以使用语法编写接受多个参数的匿名函数(x,y,z)->2x+y-z
。零参数匿名函数编写为()->3
。没有参数的函数的概念可能看起来很奇怪,但对于“延迟”计算很有用。在这种用法中,代码块包装在零参数函数中,该函数随后通过将其调用为来调用f()
。
在Julia中,返回一个元组值以模拟返回多个值。但是,可以在不需要括号的情况下创建和分解元组,从而产生一种幻想,即返回多个值而不是单个元组值。例如,以下函数返回一对值:
julia> function foo(a,b)
a+b, a*b
end
foo (generic function with 1 method)
如果在交互式会话中调用它而未在任何地方分配返回值,则将看到返回的元组:
julia> foo(2,3)
(5, 6)
但是,这种返回值对的典型用法是将每个值提取到变量中。Julia支持简单的元组“解构”,从而简化了此过程:
julia> x, y = foo(2,3)
(5, 6)
julia> x
5
julia> y
6
您还可以通过显式使用return
关键字来返回多个值:
function foo(a,b)
return a+b, a*b
end
与的先前定义完全相同foo
。
能够编写带有任意数量参数的函数通常很方便。此类函数在传统上称为“可变参数”函数,是“可变数量的参数”的缩写。您可以在最后一个参数后面加上省略号来定义varargs函数:
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
。
可以限制作为变量参数传递的值的数量。稍后将在参数约束Varargs方法中对此进行讨论。
另一方面,将可迭代集合中包含的值作为单独的参数“拼接”到函数调用中通常很方便。为此,还可以...
在函数调用中使用but:
julia> x = (3, 4)
(3, 4)
julia> bar(1,2,x...)
(1, 2, (3, 4))
在这种情况下,值的元组被精确地连接到varargs调用中,该变量位于可变参数数目所在的位置。但是,不必如此:
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 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))
同样,参数要加入的函数不必是varargs函数(尽管通常是这样):
julia> baz(a,b) = a + b;
julia> args = [1,2]
2-element Array{Int64,1}:
1
2
julia> baz(args...)
3
julia> args = [1,2,3]
3-element Array{Int64,1}:
1
2
3
julia> baz(args...)
ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)
Closest candidates are:
baz(::Any, ::Any) at none:1
如您所见,如果拼接容器中的元素数量错误,则函数调用将失败,就像显式给出太多参数一样。
在许多情况下,函数参数具有合理的默认值,因此可能不需要在每次调用中显式传递。例如,库函数parse(T, num, base)
将字符串解释为某个基数的数字。该base
参数默认为10
。此行为可以简明表示为:
function parse(type, num, base=10)
###
end
使用此定义,可以使用两个或三个参数调用该函数,并且10
在未指定第三个参数时会自动传递该函数:
julia> parse(Int,"12",10)
12
julia> parse(Int,"12",3)
5
julia> parse(Int,"12")
12
可选参数实际上只是用于编写具有不同数量参数的多个方法定义的便捷语法(请参阅有关可选参数和关键字Arguments的注释)。
一些函数需要大量的参数,或具有大量的行为。记住如何调用此类函数可能很困难。关键字参数可以通过名称而不是位置来标识,从而使这些复杂的界面更易于使用和扩展。
例如,考虑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::Int64=1)
###
end
可以使用来收集额外的关键字参数...
,如varargs函数中所示:
function f(x; y=0, kwargs...)
###
end
在中f
,kwargs
将是一个(key,value)
元组集合,其中每个元组key
都是一个符号。可以在调用中使用分号将此类集合作为关键字参数传递f(x, z=1; kwargs...)
。字典也可以用于此目的。
一个人还可以传递(key,value)
元组,或者=>
可以在分号后显式地分配可分配给该元组的任何可迭代表达式(例如,对)。例如,plot(x, y; (:width,2))
和plot(x, y; :width => 2)
等价于plot(x, y, width=2)
。在运行时计算关键字名称的情况下,这很有用。
关键字参数的性质使得可以多次指定同一参数。例如,在调用plot(x, y; options..., width=2)
中,options
结构也可能包含的值width
。在这种情况下,最右边的事件优先。在此示例中,width
肯定具有值2
。
可选参数和关键字参数在评估其默认值方面略有不同。评估可选参数默认表达式时,只有先前的参数在范围内。相反,当评估关键字参数默认表达式时,所有参数都在范围内。例如,给定以下定义:
function f(x, a=b, b=1)
###
end
将b
在a=b
指的是b
在一个外部范围,而不是随后的参数b
。但是,如果a
和b
是关键字参数,则两者都将在同一范围内创建,而b
in a=b
将引用后续参数b
(b
在外部范围内阴影),这将导致未定义的变量错误(因为默认表达式为从左到右评估,并且b
尚未分配)。
将函数作为参数传递给其他函数是一种强大的技术,但是其语法并不总是很方便。当function参数需要多行时,编写此类调用特别麻烦。例如,考虑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
将声明其后是形式为的匿名函数() -> ...
。
这些参数的初始化方式取决于“外部”功能。在这里,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
块语法,可以帮助检查文档或实现,以了解如何初始化用户函数的参数。
在技术计算语言中,通常会使用功能的“向量化”版本,该版本仅将给定功能f(x)
应用于数组的每个元素A
以通过产生新的数组f(A)
。这种语法对于数据处理很方便,但是在其他语言中,性能通常也需要向量化:如果循环很慢,则函数的“向量化”版本可以调用用低级语言编写的快速库代码。在Julia中,矢量化函数并不是提高性能所必需的,确实,编写自己的循环通常是有好处的(请参见Performance Tips),但是它们仍然很方便。因此,任何 Julia函数f
可以使用语法逐元素地应用于任何数组(或其他集合)f.(A)
。例如,sin
可以将其应用于vector中的所有元素A
,如下所示:
julia> A = [1.0, 2.0, 3.0]
3-element Array{Float64,1}:
1.0
2.0
3.0
julia> sin.(A)
3-element Array{Float64,1}:
0.841471
0.909297
0.14112
当然,如果您编写了专门的“向量”方法(f
例如通过)f(A::AbstractArray) = map(f, A)
,则可以省略点,这与一样有效f.(A)
。但是,这种方法要求您预先确定要向量化的功能。
更一般地说,f.(args...)
实际上等效于broadcast(f, args...)
,它允许您对多个数组(甚至具有不同形状)进行操作,或者对数组和标量进行混合操作(请参见Broadcasting)。例如,如果你有f(x,y) = 3x + 4y
,那么f.(pi,A)
将返回由一个新的数组f(pi,a)
的每个a
中A
,并且f.(vector1,vector2)
将返回由一个新的向量f(vector1[i],vector2[i])
为每个索引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 Array{Float64,1}:
13.4248
17.4248
21.4248
julia> f.(A, B)
3-element Array{Float64,1}:
19.0
26.0
33.0
而且,嵌套 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)))
所述sin
并cos
由于存在中间sort
功能,因此无法合并循环。
最后,当向量化操作的输出数组被预先分配时,通常可以实现最大效率,因此重复调用不会为结果一遍又一遍地分配新数组(预分配输出:)。方便的语法是X .= ...
,它等效于,broadcast!(identity, X, ...)
除了如上所述,broadcast!
循环与任何嵌套的“点”调用融合在一起。例如,X .= sin.(Y)
等效于broadcast!(sin, X, Y)
,X
用sin.(Y)
就地覆盖。如果左手侧是一个数组索引表达,例如X[2:end] .= sin.(Y)
,然后将其转换为broadcast!
一个view
,例如broadcast!(sin, view(X, 2:endof(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 Array{Float64,1}:
0.514395
-0.404239
-0.836022
-0.608083
类似的二进制(或一元)运算符.+
使用相同的机制处理:它们等效于broadcast
调用,并与其他嵌套的“点”调用融合。 X .+= Y
etcetera等同于X .= X .+ Y
并导致融合的就地分配;另请参阅点运算符。
我们应该在这里提到,这远不是定义函数的完整图景。Julia具有完善的类型系统,并允许对参数类型进行多次分派。此处给出的示例均未在其参数上提供任何类型注释,这意味着它们适用于所有类型的参数。在Types中描述了类型系统,在Methods中描述了通过对运行时参数类型进行多次调度而选择的方法来定义函数。