函数

在 Julia 中,函数是一个对象,它将参数值的元组映射到返回值。Julia 函数不是纯数学函数,因为它们可以改变并受到程序全局状态的影响。在 Julia 中定义函数的基本语法是

julia> function f(x,y)
           x + y
       end
f (generic function with 1 method)

此函数接受两个参数 xy,并返回最后求值的表达式的值,即 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) = ...,它使用 比奈公式 将其扩展到非整数值,该算法适用于任何 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 确实有用。例如,以下是一个函数,它计算一个直角三角形的斜边长度,该三角形的边长分别为 xy,避免溢出。

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

此函数有三个可能的返回点,根据 xy 的值返回三个不同表达式的值。最后一行上的 return 可以省略,因为它是最末一个表达式。

返回值类型

可以使用 :: 运算符在函数声明中指定返回值类型。这将返回值转换为指定的类型。

julia> function g(x, y)::Int8
           return x * y
       end;

julia> typeof(g(1, 2))
Int8

无论 xy 的类型如何,此函数始终返回 Int8。有关返回值类型的更多信息,请参阅 类型声明

返回值类型声明在 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 是最后一个表达式时,它可以单独使用。对于 return nothing 表达式而不是单独使用 returnnothing,优先使用哪种表达式是一个编码风格问题。

运算符是函数

在 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] = xsetindex!
A.ngetproperty
A.n = xsetproperty!

请注意,类似于 [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

上面的代码等效于使用包含在 doend 之间的代码的匿名函数调用 get,如下所示

get(()->time(), dict, key)

time 的调用通过将其包装在一个仅在 dict 中不存在请求的键时才调用的 0 参数匿名函数中来延迟。

元组

Julia 具有一个称为元组的内置数据结构,它与函数参数和返回值密切相关。元组是一个固定长度的容器,可以容纳任何值,但不能修改(它是不可变的)。元组使用逗号和圆括号构建,可以通过索引访问。

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 的元组必须使用逗号 (1,) 编写,因为 (1) 只是一个带括号的值。() 表示空(长度为 0)元组。

命名元组

元组的组件可以选择命名,在这种情况下,将构建一个命名元组

julia> x = (a=2, b=1+2)
(a = 2, b = 3)

julia> x[1]
2

julia> x.a
2

命名元组的字段可以通过名称使用点语法(x.a)访问,除了常规索引语法(x[1]x[:a])之外。

解构赋值和多返回值

可以在赋值的左侧出现一个逗号分隔的变量列表(可选地包含在圆括号中):右侧的值将通过依次遍历每个变量并分配给每个变量来解构

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

... 带赋值需要 Julia 1.6

如果赋值列表中的最后一个符号后缀为 ...(称为吸取),那么它将被分配一个包含右侧迭代器剩余元素的集合或延迟迭代器。

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

属性解构

而不是基于迭代进行解构,赋值的右侧也可以使用属性名称进行解构。这遵循命名元组的语法,并通过使用 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)

变量 ab 如常绑定到前两个参数值,变量 x 绑定到一个可迭代集合,该集合包含在 bar 的前两个参数之后传递给 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:
[...]

如您所见,如果展开的容器中的元素数量不正确,则函数调用将失败,就像显式给出过多参数一样。

可选参数

通常可以为函数参数提供合理的默认值。这可以省去用户每次调用都必须传递每个参数的麻烦。例如,函数Date(y, [m, d]) 来自 Dates 模块,为给定的年份 y、月份 m 和日期 d 构造 Date 类型。但是,md 参数是可选的,它们的默认值为 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 设置为 ABC,并在每个上调用匿名函数,就像在语法 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)将返回一个新的数组,该数组包含每个aA中的f(pi,a),而f.(vector1,vector2)将返回一个新的向量,该向量包含每个索引if(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)))中,sincos循环不能合并,因为中间有sort函数。

最后,当向量化操作的输出数组被预先分配时,通常可以实现最大的效率,这样重复调用不会为结果反复分配新的数组(参见预分配输出)。一个方便的语法是X .= ...,它等同于broadcast!(identity, X, ...),除了如上所述,broadcast!循环与任何嵌套的“点”调用融合在一起。例如,X .= sin.(Y)等同于broadcast!(sin, X, Y),它会将X原地覆盖为sin.(Y)。如果左侧是一个数组索引表达式,例如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 具有复杂的类型系统,允许对参数类型进行多重分派。这里给出的示例都没有对其参数进行任何类型标注,这意味着它们适用于所有类型的参数。类型系统在类型中进行了描述,根据运行时参数类型进行多重分派的定义函数方法在方法中进行了描述。