javascript 函数式编程(1)—柯里化(currying)

765 阅读6分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

之前分享过一篇关于函数式编程感觉还不到位,有许多函数编程中的知识点还有没有涉及到,还有就是学习函数式编程是一种思想,我们不是模仿或者生搬硬套,而需要我们通过学习和思考将这个思想或者说方法论融入我们的 code 中,成为有助于我们解决自己身边实际问题。

008.png

什么是函数式编程

  • 并不是新生事物,早在 50-60s 就有人提出函数式编程
  • 不是一门语言,当然也不是一门语言,只是一种编程风格
  • 不是命令式也不是面向对象编程,对于 javascript 也可以看成一门支持函数式编程语言
  • 不是解决所有问题的完美解决方案
  • 不算难

函数式编程有哪些特点

  • 不变性
  • 高阶函数,也就是可以将函数作为变量和参数传递给函数,也可以将函数作为另一个函数返回值来使用
  • 函数中不包含状态,也就是函数内部不会状态,因为不同的状态导致相同输入得到不同输出
  • 引用透明性,也就是函数输出确定性,有了确定性也就是说明这个函数便于测试
  • 递归
  • 模式识别
  • 无状态
  • Mondas

在哪些语言对函数式编程是有好的

  • 函数式编程语言(Haskell, Erlang, Elm, 等)
  • .NET - c# F# 等
  • Javascript

什么情况下适合函数式编程

  • 数据处理
  • 并行系统
  • serverless

什么场景下不适合采用函数式编程

  • UI
  • 外部交互
  • IO
  • 高性能的系统

为什么是函数式编程

  • 简介
  • 可读性
  • 易于测试
  • 支持并行
  • 鲁棒性
  • 有趣

002.png

currying (柯里化)

Currying 是函数式编程中的一个过程,将有多个参数的函数转化为一系列嵌套的仅接受一个参数的函数,每返回一个函数,这个函数接受参数就是下一个参数。

例如 currying, 这个单词翻译过来柯里化,根据发音直译的单词。查字典有咖喱的意思,感觉不贴边。还有字典给出了加脂操作意思,看这个还比较贴边,最后找到currying 还有对食品进行腌制的意思,这个感觉就更贴近了 currying 含义

返回函数会持有执行该函数时所接受参数,然后就像剥洋葱似的,知道所有参数都使用到了,在没有传入最后一个参数前,其他参数都"live"接受这个参数的函数所形成的闭包中,当返回最后一个函数并执行时,所有参数就都会被使用,所以说 Currying 是函数式编程中一个过程。对于一些抽象概念用简介语言来解释清楚,的确需要有一定语言功底,这里就不多说了,我们直接上代码。估计通过 code 实例,大家就更好理解了。

function fn(a,b){}

function multiply(a,b,c){
    return a * b * c;
}

multiply(1,2,3);

上面的函数 multiply 接受 3 个参数,将这 3 个参数相乘后得到结果作为函数返回值。接下解释如何用 currying 函数将参数分配到一系列调整函数,这些函数每一个都只接受一个参数。

function curried_multiply(a){
    return (b) => {
        return (c) =>{
            return a * b * c;
        }
    }
}

curried_multiply(1)(2)(3);

现在 multiply(1,2,3)函数调用变成了multiply(1)(2)(3) 多个函数调用。也就是将一个的函数已经变成了一系列的函数。为了得到 1、2、3 三个数字的乘法结果,参数值一个接一个地传递,每个参数值都可以被随后函数所使用。

const mul1 = curried_multiply(1);
const mul2 = mul1(2);
const result = mul2(3);
console.log(result); // 6

现在开始详细地解释一下,如果觉得无聊可以跳过这部分内容,给 curried_multiply 函数传递参数为 1

return (b) => {
        return (c) => {
            return a * b * c
        }
    }

偏函数与 currying

function add(a){
    return (b)=> a + b
}

const add2 = add(2)

res = add2(3);
console.log(res)
const githubFavicon = buildUri('https', 'github.com', 'favicon.ico')
const githubFavicon =  buildHttpsUri('github.com', 'favicon.ico')

因为大多数情况下,网站都是 https 开头,那么对于 buildUri 函数每次都输入 ·https 这样看起来进行一些重复性的劳动。不过如果硬编码如 buildHttpsUri 开起来也不那么优雅。这时候我们可以使用偏函数来解决这个实际问题。

function buildUri(baseUrl){
  return function(website,path){
    //
  }
}

currying 的应用

随着Redux JavaScript 库、Reason 语法扩展和工具链以及 Cycle JavaScript 框架这些新兴框架无疑都用到函数式编程,所以在 JavaScript 这门语言中,函数式编程正变得越来越重要。有两个源于函数式思想的重要思想是 currying。接下里看一看在实际如何使用 currying。

编写小的代码模块,可以轻松地重复使用和配置

可以用函数来保存状态或者策略,例如销售策略,对多种商品使用相同销售策略

function discount(price, discount) {
    return price * discount
}
const price = discount(500,0.10);
const price = discount(50,0.10);
const price = discount(300,0.10);

从上面代码可以看出虽然商品价格不同,但是这些商品在促销活动中具有相同折扣率。

function discount(discount) {
    return (price) => {
        return price * discount;
    }
}
const tenPercentDiscount = discount(0.1);

避免频繁使用相同值参数来调用函数

还是通过代码给大家解释一下,定义函数 volume 接受分别表示长、宽和高的 3 个参数 l, w, h 然后计算出其体积

function volume(l, w, h) {
    return l * w * h;
}

这里虽然这里要计算体积时,对于高度取值都是 100,也就是上面说问题使用同一参数来频繁调用函数

volume(200,30,100) // 2003000l
volume(32,45,100); //144000l
volume(2322,232,100) // 53870400l
function volume(h) {
    return (w) => {
        return (l) => {
            return l * w * h
        }
    }
}

定义一个特定函数 hCylinderHeight 用于缓存高度,这样就可以避免频繁使用相同参数来调用一个函数。

const hCylinderHeight = volume(100);
hCylinderHeight(200)(30); // 600,000l
hCylinderHeight(2322)(232); // 53,870,400l

Currying 一般的形式

function curry(fn, ...args) {
    return (..._arg) => {
        return fn(...args, ..._arg);
    }
}
function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}
function curry(func) {

    return function curried(...args) {
      if (args.length >= func.length) {
        return func.apply(this, args);
      } else {
        return function(...args2) {
          return curried.apply(this, args.concat(args2));
        }
      }
    };
  
  }

  function sum(a, b, c) {
    return a + b + c;
  }
  
  let curriedSum = curry(sum);
  
  console.log( curriedSum(1, 2, 3) ); // 6, 每 currying 化的普通函数形式
  console.log( curriedSum(1)(2,3) ); // 6, 部分 currying 化
  console.log( curriedSum(1)(2)(3) ); // 6, 完全 curring 化

003.jpeg

总结

在 JavaScript 中 currying 是基于闭包实现的,通过 currying 能够保留已经执行的函数的状态,使我们有能力创建工厂函数,能够为其参数添加特定值的函数。

对于初学者理解 currying 可能比较绕,只要理解了闭包,currying 其实并不难理解,只要稍加练习随后你变可以掌握这门编程思想,而且也会发现 currying 值得拥有。

参考文献

参考 Understand currying in Javascript blog

参考 curring blog