函数式编程之 point free

函数式编程之 point free

在函数式编程中有一个比较重要的概念为 point free。这个名词我目前在网络上还没有找到比较信达雅的翻译,因此这篇文章中就直接使用未翻译的表达方式。个人理解,point free 像是与 args free 或者 temp var free。即在使用的过程中基本不显示声明形式参数以及中间变量。本文以 js 为例,通过几个简单的例子来说明如何实现 point free 的编程风格。

如何实现

pipline

pipline 在函数式编程中用于将多个函数组合起来,形成一个新的函数。例如实现这样一个功能,先对字符串进行进行 trim,然后大写然后添加一个前缀。我们使用面向对象的方式可能会这样编写:

function addPrefix(prefix, x){
    return prefix + x
}
const tmpString = "hello world".trim().toUpperCase()
cosnt res = addPrefix('*', tmpString) // *HELLO WORLD

而为了更贴近函数式编程,我们先定义 trim 以及 uppper 这两个函数,并进行同样的操作:

function addPrefix(prefix, x){
    return prefix + x
}
function trim(x){
    return x.trim()
}
function uppper(x){
    return x.toUpperCase()
}
const trimed = trim(" hello world ")
const upperd = uppper(trimed)
const res = addPrefix("*", upperd) // *HELLO WORLD

可以看出这段代码方式存在一个问题,即多出了两个中间变量。当然我们也可以写成函数嵌套的方式:

addPrefix('*', uppper(trim(" hello world ")))

但是这样同样存在问题,不利于维护以及阅读。例如如果需要增加一个处理步骤,或者当嵌套的层数过多时会变得非常不方便。因此在函数式编程中通常会使用 pipeline 来解决这样的问题。我们先实现一个简单的 pipline 函数,并用它实现上面的功能:

function pipeline(...funs){
    return funs.reduce((x, y) => (...args) => y(x(...args)))
}
pipeline(
    trim,
    uppper,
    str => addPrefix('*', str)
)(" hello world ") // *HELLO WORLD

pipline 的功能非常简单,即将多个功能简单的模块组合 为一个复杂的模块。也非常符合 pipline 的中文解释即管道。把数据想象为水流,水流从管道的一侧流向另一侧,而数据也同样可以从一侧通过转换到达另一侧。

在上面的例子中,由于 addPrefix 的输入参数为前缀以及需要添加前缀的字符串,因此我们不得不使用一个 lambda 表达式使其满足 pipeline 使用条件。但是这和我们谈到的 point free 冲突,即不声明形式参数以及中间变量。此时我们可以引入一个新的概念: 部分应用(partitial)。

部分应用(partitial)

部分应用概念非常简单,即接受一个函数以及一个或多个参数并返回一个新的函数,该函数是输入函数参数部分应用的一个新函数。听起来比较麻烦,但是看例子就比较简单:

function addPrefix(prefix, x){
    return prefix + x
}
function partial(func, ...args){
    return (...otherArgs) => func(...args, ...otherArgs)
}
const addStar = partial(addPrefix, '*')
addStar(" hello world ") // *hello world

可以看出在调用 parial 后返回了一个新的 addStart,它表示 addPrefix 的第一个参数 prefix 已经被应用了,但是第二个参数还需要稍后传递。部分应用可以用来记住一些公共的上下文。例如本例子中使用 parial 确定了第一个参数为 * 后续多次调用 addStar 就无需重复传递该参数。使用 parial 改写刚才 pipeline 的调用:

const res = pipeline(
    trim,
    uppper,
    partial(addPrefix, '*')
)(" hello world ") // *HELLO WORLD

可以看出经过改写后我们已经几乎实现了 point free,我们在调用已有的函数的过程中没使用任何中间变量以及形参。但是如果每遇到 addPrefix 这种情况都需要增加一个 partital 也会增加重复代码和心智负担。那么有没有一种方法能够去掉 partial 呢?此时可以引入一个新的概念柯里化(currying)

柯里化

柯里化表示这样一个概念,当函数只传递了部分参数时,将自动对其进行部分应用。例如有这样一个函数,如果其支持柯里化,我们可以这样调用:

function add(x, y, z)
add(1, 2, 3) //6
add(1)(2)(3) // 6
add(1, 2)(3) // 6

如果能使得函数支持柯里化,我们自然无需在使用 partial。在很多函数式编程语言如 haskell 函数都默认支持 currying,但是在 js 中我们可以这样实现一个工具函数来帮助其他函数支持柯里化:

function currying(func, restLen=func.length){
    return function() {
        if(arguments.length === restLen){
            return func(...arguments)
        }else{
            return currying((...args) => func(...arguments, ...args), restLen - arguments.length)
        }
    }
}
const addPrefix = currying((prefix, x) => {
    return prefix + x
})
console.info(addPrefix('*')('hello world')) // *hello world
console.info(addPrefix('*', 'hello world')) // *hello world

使用柯里化后的 addPrefix ,pipeline 可以进一步优化:

pipeline(
    trim,
    uppper,
    addPrefix('*')
)(" hello world ") // *HELLO WORLD

这样我们已经完美的实现了 point free,借用 currying 的力量,既可以不使用 lambda 表达式也无需使用paritial。

为什么要使用 point free

我也是函数式编程的新手,无法说出很多理论来说明使用 point free 编程的优点。但是考虑上面的例子:

const tmpString = "hello world".trim().toUpperCase()
cosnt res = addPrefix('*', tmpString) // *HELLO WORLD

如果此时我需要打印 trim 之后的结果,那么必须这样改写代码:

const trimed = "hello world".trim()
console.info(trimed)
const uppered = trimed.toUpperCase()
cosnt res = addPrefix('*', uppered) // *HELLO WORLD

可以看出有非常大的改动,增加了中间变量,还修改了 addPrefix 传入的参数。但是如果是 point free 风格的实现,那实际上只需要:

function debug(x){
    console.info(x)
    retun x
}
pipeline(
    trim,
    debug,
    uppper,
    addPrefix('*')
)(" hello world ") // *HELLO WORLD

可以看出仅仅需要新增一个 debug 工具方法,并插入到对应的位置即可。明显 point free 风格的代码更遵循开放封闭原则

猜你喜欢

转载自juejin.im/post/7113822879082348551
今日推荐