函数

函数复合 属于 Julia

1 个练习

关于 函数复合

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 里都用不了,不过如果你感兴趣,可以看看:

通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 函数复合