Julia 鼓励程序员把尽可能多的代码放进可被 JIT 编译的函数里,而且按照设计,创建大量小函数本就是高性能的做法。
这样一来,往往会留下许多简单的小函数,要完成不那么简单的任务,就得把它们组合起来。
一个显而易见的做法是嵌套函数调用。下面这个例子十分刻意,但足以说明问题。
julia> first.(titlecase.(reverse.(["my", "test", "strings"])))
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
这种做法的缺点是,嵌套越深,可读性下降得越快。
我们需要一种更简单、也更灵活的做法。
这是数学家钟爱的技巧,Julia 也照搬了数学的写法。
可以用∘运算符(先输入\circ,再按 Tab)把任意数量的函数composed起来。结果可以当作单个函数使用。
julia> compfunc = first ∘ titlecase ∘ reverse
first ∘ titlecase ∘ reverse
julia> compfunc.(["my", "test", "strings"])
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
# alternative syntax, giving the same result
julia> (first ∘ titlecase ∘ reverse).(["my", "test", "strings"])
有几点需要注意:
另一种做法或许可以看作_程序员_的思路,而不是_数学家_的。
Pipelines在 Unix shell 脚本里已经使用了很久,近年来又在主流编程语言中流行起来(有时人们把管道被广泛采用归功于 F# 的开创)。
基本思路是:从一些数据出发,让它依次经过一串函数,得到结果。
管道运算符是|>(与 F# 和较新版本的 R 一样),不过 Julia 还有一个广播版本.|>。
julia> ["my", "test", "strings"] .|> reverse .|> titlecase .|> first
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
现在执行顺序严格从左到右,每个函数的输出都顺着箭头方向流动,成为下一个函数的输入。
用来演示组合和管道的那些函数都只接受_一个_实参,这绝非巧合。
有些纯函数式语言会把_第一个_实参送进管道传给函数,同时允许一并传入其他实参。
相比之下,Julia 的管道里只期望出现函数的_名字_(或等价物),不能附加其他实参。
这背后有重要的技术原因(与currying并非该语言设计中的标准组成部分这一事实有关)。对柯里化毫无了解的_许多_人,只需接受一点:这个限制并非疏忽大意,未来版本的 Julia 也不大可能改变。
我们需要只接受一个实参、又能完成所需工作的函数。好在在 Julia 中定义新函数很容易。
最简单的做法是使用anonymous function。例如,我们有一个输入字符串,想按下划线把它拆分:
julia> "my_test_strings" |> (s -> split(s, '_'))
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
然后就可以像之前一样,把这个向量通过管道传给其他函数。
在这个例子里,给匿名函数加不加括号都行,但一般来说,加括号有助于减少歧义。
同样,我们也可以在程序靠前的位置定义一个具名函数,然后按需复用。
Closures超出了本概念的范围,但从其他语言熟悉闭包的人会看出,它们提供了更灵活的方式来创建单实参函数。
julia> function makesplit(sep)
fs(str) = split(str, sep)
fs
end
makesplit (generic function with 1 method)
julia> f_us = makesplit('_')
(::var"#fs#32"{Char}) (generic function with 1 method)
julia> "my_test_strings" |> f_us
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
# alternatively:
julia> "my_test_strings" |> makesplit('_')
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
一旦定义好makesplit(),就可以用它处理任意分隔符。注意makesplit('_')是一次_函数调用_,它的求值结果是另一个函数,而这个函数又会从管道接收输入。
如果这让你觉得困惑,一开始这很正常(多练习就会越来越清楚)。
关于如何在 Julia 基础库中让管道更灵活,已经讨论了很久,但各种建议彼此不兼容,始终没有达成一致。
与此同时,用户们采用了惯常的做法:编写各种可安装的包来满足特定需求。这些包在 Exercism 里都用不了,不过如果你感兴趣,可以看看: