关于添加新的函数式功能的建议

概要

在学习 Julia 之前,我通过 Mathematica(MMA) 学习的函数式编程。MMA 的一些函数式操作很方便,而 Julia 添加这些功能往往只需要一两行代码。所以想请问:

  • Julia 有没有添加这些特性的考虑
  • 或者说怎么向 Julia 建议这类想法
  • 此外,这类特性是否会带来什么损失?
  • 放在 Base 中的函数有什么要求,多了会带来什么影响?

以下是具体建议,包括两方面:

  • 给支持函数式编程的函数添加特性
  • 添加新函数

添加特性

Julia 中的函数是“一等公民”,也即函数与普通变量平等看待。但有一些函数式编程特性 Julia 没有使用,或者说没有完全使用。

逻辑运算

  1. Julia 的 !, >, in 等逻辑运算符支持函数式,比如
    f = i -> true
    !f # 等同于 (args...) -> !(f(args...))
    >=(2) # 等同于 i -> (i >= 2)
    
    可以考虑新增的运算 &&, ||
    f && g # 等同于 i -> f(i) && g(i)
    ## 使用场景
    filter(>=(3) && <=(6), 1:100) # 返回 [3, 4, 5, 6]
    findall(in([1,2,3]) || <(0), data)
    

柯里化

维基百科:柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数的函数的技术。

比如从 1:10 中筛选大于 3 的数,一般方式为

filter(>(3), 1:10)

当这个操作经常用到时,我们希望将其独立出来

f = filter(>(3))
f(1:10)
f(2:9)

也即,当数据省略时,也能正常输入,只是返回值是算子(函数)。利用多重派发,这个只需要一行代码

filter(f::Function) = data - > filter(f, data)

p.s. 这些只是简单的修改,但可以更好地发挥函数式的编程特性


添加函数

以下函数可以在 MMA 官网教程的这一页找到,其中只有函数 Fold 存在对应的 Julia 版本 foldl

  1. NestWhile函数,迭代直到判断为否

    function nest_while(f::Function, val::T, chk::Function)::T where T
        while chk(val)
            val = f(val)
        end
        val
    end
    
  2. NestWhileList 函数,将过程结果一起返回

    function nest_while_list(f::Function, val::T, chk::Function)::Vector{T} where T
        res = [val]
        while chk(val)
            val = f(val)
            push!(res, val)
        end
        res
    end
    
    • 例1:nest_while_list(i->i//2, 100, >=(1)) 返回值为 [100, 50, 25, 12, 6, 3, 1, 0]
    • 例2:一些 LeetCode 题,比如 172,代码可以简化到一行
      trailing_zeroes(n::Int) = sum(nest_while_list(i->i÷5, n÷5, >(1)))
      
  3. Nest 函数,比如 mma_nest(f, val, 3) => f(f(f(val)))

    function mma_nest(f::Function, val::T, times::Int)::T where T
        ## e.g. mma_nest(f, val, 3) | returns f(f(f(val)))
        for _ in 1:times
            val = f(val)
        end
        val
    end
    
  4. NestList 函数,将过程结果一起返回

     function mma_nest_list(f::Function, val::T, times::Int)::Vector{T} where T
         ## e.g. mma_nest(f, val, 2) | returns [val, f(val), f(f(val))]
         res = Vector{T}(undef, times + 1)
         res[1] = val
         for i in 2:(times + 1)
             res[i] = val = f(val)
         end
         res
     end
    

    比如 mma_nest_list(f, val, 2) => [val, f(val), f(f(val))]

还有一些使用频率相对不高的函数,比如 FoldList, FixPointList

一般的流程是在 Julialang/julia 搜索一下有没有相关的 issue/PR,然后如果没有的话,开一个 issue 看看其他人什么反馈,如果反馈比较积极,或者你非常想要添加这个功能,就可以自己去提交一个 PR。目前 Julialang/julia 的人手还是不太足,所以仅仅只是开 issue 的话这些可有可无的功能很容易被忽略掉(实际上即使提交 PR 也很容易被忽略掉)。第一次参与的话可以先看一看 Julia 贡献者须知。还有就是就正如 @henry2004y 所说的,一旦你试图加入到 Julia 社区的话,就要尽可能多地使用英文与开发者们在 Github,slack 以及英文 discourse 上保持交流,中文社区在这方面暂时没有人可以在 Julialang/julia 上下决策,我唯一知道的有权限还活跃在一线的华人是 @yuyichao.

关于 Curry 化,目前社区的一个共识应该是不着急推出,他们希望找到在语言层面的一个通用的解决方案而不是查漏补缺式地对特定函数给出特定的方法: RFC: curry underscore arguments to create anonymous functions, 当然其实也能看到一些这样或者那样的小 PR 被合并,比如说 Curried versions of endswith and startswith,但这些功能的合并只能算是偶然现象.

后面提及的函数也是类似的,需要去说服社区这个功能很常用并且 Julia 社区目前没有好的方案:“XX有所以Julia 也应该有”不是一个好的理由。在你举的这四个函数里面,NestNestList 是不必要的:

# nest
reduce(1:times; init=val) do x, _
    x = f(x)
end

# nest_list
reduce(1:times; init=[val]) do x, _
    push!(x, f(x[end]))
end

NestWhile 的话因为将 reduce 和 终止条件混合起来了,似乎没有现成的函数可以覆盖这个功能,我觉得可以试着在上游开一个 issue 提一下

3 个赞

我的理解:Julialang/julia 在接受新的函数名进入 Base 时相对保守,大量的功能函数都被放在第三方库里实现了。另外顺便提及一点就是 Julia 本身其实非常稳定了,所以在接受 PR 方面会接收到更多的debate:PSA: Julia is not at that stage of development anymore。也正是因为贡献到 Base 的流程非常耗时,所以大家伙都倾向于写一个独立的 package 来存放想要的功能,比如说 GitHub - JuliaFolds/Transducers.jl: Efficient transducers for Julia

2 个赞

非常感谢这个详细且耐心的解答。我刚意识到把问题细节单独搜索可以找到更多解答,比如柯里化的建议在这里 Improve currying in Julia Base 已有充分的讨论。

Currying 的包有 GitHub - Orbots/Curry.jl: Currying for Julia ,不过没有注册。但我觉得 Julia 比较难以做 Currying 的原因是你如何和 optional arguments 的函数作区分呢?还有如果一个函数可以接受2个参数,也可以接受3个参数,那么你给2个参数的时候,Julia 怎么知道你想返回的是那个2个参数的函数的结果,还是一个 Currying 的函数呢?

这个问题很好, Currying 特性并不适合所有函数,而是针对一类函数的建议,通常是为了让函数表义更具体,比如

  • filter 用于筛选,而指定参数的 filter(>(2)) 用于“筛选大于 2 的数”
  • in 只是空泛的包含判断,in(data) 用于 data 的元素判断

如果遇到上边提到的函数,应在外部设置 Currying ,将输出明确为函数而避免歧义,比如 - 运算,输入一参代表负数,二参代表减运算,用 Base.Fix2 指定第二参

f = Base.Fix2(-, 1)
f(3) # 3 - 1
f() # -1

返回值 f 为固定一个参数得到的新函数,需调用才会返回结果。

我的想法是定义一个 @currying macro, 让用户自己选择哪些函数可以 currying,而不是 Julia 直接开启所有函数都默认 currying。

1 个赞