学会使用函数式编程的程序员(第2部分)

栏目: 编程语言 · 发布时间: 7年前

内容简介:本系列的第一篇:

学会使用函数式编程的程序员(第2部分)

本系列的第一篇:

  1. 学会使用函数式编程的程序员(第1部分)

组合函数 (Function Composition)

学会使用函数式编程的程序员(第2部分)

作为程序员,我们是懒惰的。我们不想构建、测试和部署我们编写的一遍又一遍的代码。我们总是试图找出一次性完成工作的方法,以及如何重用它来做其他事情。

代码重用听起来很棒,但是实现起来很难。如果代码业务性过于具体,就很难重用它。如时代码太过通用简单,又很少人使用。所以我们需要平衡两者,一种制作更小的、可重用的部件的方法,我们可以将其作为构建块来构建更复杂的功能。

在函数式编程中,函数是我们的构建块。每个函数都有各自的功能,然后我们把需要的功能(函数)组合起来完成我们的需求,这种方式有点像乐高的积木,在编程中我们称为 组合函数

看下以下两个函数:

var add10 = function(value) {
    return value + 10;
};
var mult5 = function(value) {
    return value * 5;
};

上面写法有点冗长了,我们用箭头函数改写一下:

var add10 = value => value + 10;
var mult5 = value => value * 5;

现在我们需要有个函数将传入的参数先加上 10 ,然后在乘以 5 , 如下:

var mult5AfterAdd10 = value => 5 * (value + 10)

尽管这是一个非常简单的例子,但仍然不想从头编写这个函数。首先,这里可能会犯一个错误,比如忘记括号。第二,我们已经有了一个加 10 的函数 add10 和一个乘以 5 的函数 mult5 ,所以这里我们就在写已经重复的代码了。

使用函数 add10 mult5 来重构 mult5AfterAdd10

var mult5AfterAdd10 = value => mult5(add10(value));

我们只是使用现有的函数来创建 mult5AfterAdd10 ,但是还有更好的方法。

在数学中, f ∘ g 是函数组合,叫作“f 由 g 组合”,或者更常见的是 “ f after g ”。 因此 (f ∘ g)(x) 等效于f(g(x)) 表示调用 g 之后调用 f

在我们的例子中,我们有 mult5 ∘ add10 或 “ add10 after mult5 ”,因此我们的函数的名称叫做 mult5AfterAdd10 。由于Javascript本身不做函数组合,看看 Elm 是怎么写的:

add10 value =
    value + 10
mult5 value =
    value * 5
mult5AfterAdd10 value =
    (mult5 << add10) value

Elm 中 << 表示使用组合函数,在上例中 value 传给函数 add10 然后将其结果传递给 mult5 。还可以这样组合任意多个函数:

f x =
   (g << h << s << r << t) x

这里 x 传递给函数 t ,函数 t 的结果传递给 r ,函数 t 的结果传递给 s ,以此类推。在Javascript中做类似的事情,它看起来会像 g(h(s(r(t(x))))) ,一个括号噩梦。

Point-Free Notation

学会使用函数式编程的程序员(第2部分)

Point-Free Notation 就是在编写函数时不需要指定参数的编程风格。一开始,这风格看起来有点奇怪,但是随着不断深入,你会逐渐喜欢这种简洁的方式。

multi5AfterAdd10 中,你会注意到 value 被指定了两次。一次在参数列表,另一次是在它被使用时。

// 这个函数需要一个参数

mult5AfterAdd10 value =
    (mult5 << add10) value

但是这个参数不是必须的,因为该函数组合的最右边一个函数也就是 add10 期望相同的参数。下面的 point-free 版本是等效的:

// 这也是一个需要1个参数的函数

mult5AfterAdd10 =
    (mult5 << add10)

使用 point-free 版本有很多好处。

  1. 首先,我们不需要指定冗余的参数。由于不必指定参数,所以也就不必考虑为它们命名。
  2. 由于更简短使得更容易阅读。本例比较简单,想象一下如果一个函数有多个参数的情况。

天堂里的烦恼

学会使用函数式编程的程序员(第2部分)

到目前为止,我们已经了解了组合函数如何工作以及如何通过 point-free 风格使函数简洁、清晰、灵活。

现在,我们尝试将这些知识应用到一个稍微不同的场景。想象一下我使用 add 来替换 add10

add x y =
    x + y
mult5 value =
    value * 5

现在如何使用这两个函数来组合函数 mult5After10 呢?

我们可能会这样写:

-- 这是错误的!!!

mult5AfterAdd10 =
    (mult5 << add) 10

但这行不通。为什么? 因为 add 需要两个参数。

这在 Elm 中并不明显,请尝试用Javascript编写:

var mult5AfterAdd10 = mult5(add(10)); // 这个行不通

这段代码是错误的,但是为什么?

因为这里 add 函数只能获取到两个参数(它的函数定义中指定了两个参数)中的一个(实际只传递了一个参数),所以它会将一个错误的结果传递给 mult5 。这最终会产生一个错误的结果。

事实上,在 Elm 中,编译器甚至不允许你编写这种格式错误的代码(这是 Elm 的优点之一)。

我们再试一次:

var mult5AfterAdd10 = y => mult5(add(10, y)); // not point-free

这个不是point-free风格但是我觉得还行。但是现在我不再仅仅组合函数。我在写一个新函数。同样如果这个函数更复杂,例如,我想使用一些其他的东西来组合 mult5AfterAdd10 ,我真的会遇到麻烦。

由于我们不能将这个两个函数对接将会出现函数组合的作用受限。这太糟糕了,因为函数组合是如此强大。

如果我们能提前给add函数一个参数然后在调用 mult5AfterAdd10 时得到第二个参数那就更好了。这种转化我们叫做 柯里化

柯里化 (Currying)

学会使用函数式编程的程序员(第2部分)

Currying 又称部分求值。一个 Currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值

上例我们在组合函数 mult5 add(in) 时遇到问题的是, mult5 使用一个参数, add 使用两个参数。我们可以通过限制所有函数只取一个参数来轻松地解决这个问题。我只需编写一个使用两个参数但每次只接受一个参数的add函数,函数柯里化就是帮我们这种工作的。

柯里化函数一次只接受一个参数。

我们先赋值 add 的第1个参数,然后再组合上 mult5 ,得到 mult5AfterAdd10 函数。当 mult5AfterAdd10 函数被调用的时候, add 得到了它的第 2 个参数。

JavaScript 实现方式如下:

var add = x => y => x + y

此时的 add 函数先后分两次得到第 1 个和第 2 个参数。具体地说, add 函数接受单参 x ,返回一个也接受单参 y 的函数,这个函数最终返回 x+y 的结果。

现在可以利用这个 add 函数来实现一个可行的 mult5AfterAdd10* :

var compose = (f, g) => x => f(g(x));
var mult5AfterAdd10 = compose(mult5, add(10));

compose 有两个参数 f g ,然后返回一个函数,该函数有一个参数 x ,并传给函数 f ,当函数被调用时,先调用函数 g ,返回的结果作为函数 f 的参数。

总结一下,我们到底做了什么?我们就是将简单常见的 add 函数转化成了柯里化函数,这样add函数就变得更加自由灵活了。我们先将第1个参数10输入,而当mult5AfterAdd10函数被调用的时候,最后1个参数才有了确定的值。

柯里化与重构(Curring and Refactoring)

学会使用函数式编程的程序员(第2部分)

函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。

例如,我们有以下两个函数,它们分别将输入字符串用单花括号和双花括号包裹起来:

bracketed = function (str) {
  retrun "{" + str + "}"
}
    
doubleBracketed = function (str) {
  retrun "{{" + str + "}}"
}

调用方式如下:

var bracketedJoe =  bracketed('小智')

var doubleBracketedJoe =  doubleBracketed('小智')

可以将 bracket doubleBracket 转化为更变通的函数:

generalBracket = function( prefix , str ,suffix ) {
  retrun  prefix ++ str ++ suffix
}

但每次我们调用 generalBracket 函数的时候,都得这么传参:

var bracketedJoe = generalBracket("{", "小智", "}")

var doubleBracketedJoe = generalBracket("{{", "小智", "}}")

之前参数只需要输入1个,但定义了2个独立的函数;现在函数统一了,每次却需要传入3个参数,这个不是我们想要的,我们真正想要的是两全其美。

因为生成小括号双括号功能但一,重新调整一下 我们将 generalBracket 三个参数中的 prefix,str 各柯里化成一个函数,如下:

generalBracket = function( prefix ) {
  return  function( suffix ){
      return function(str){
          return prefix + str + suffix
      }
  }
}

这样,如果我们要打印单括号或者双括号,如下:

// 生成单括号
var bracketedJoe = generalBracket('{')('}')
bracketedJoe('小智') // {小智}

// 生成双括号
var bracketedJoe = generalBracket('{{')('}}')
bracketedJoe('小智') // {{小智}}

常见的函数式函数(Functional Function)

函数式语言中3个常见的函数: Map , Filter , Reduce

学会使用函数式编程的程序员(第2部分)

如下JavaScript代码:

for (var i = 0; i < something.length; ++i) {
      // do stuff
 }

这段代码存在一个很大的问题,但不是bug。问题在于它有很多重复代码(boilerplate code)。如果你用命令式语言来编程,比如Java,C#,JavaScript,PHP,Python等等,你会发现这样的代码你写地最多。 这就是问题所在

现在让我们一步一步的解决问题,最后封装成一个看不见 for 语法函数:

先用名为 things 的数组来修改上述代码:

var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
    things[i] = things[i] * 10; // 警告:值被改变!
}
console.log(things); // [10, 20, 30, 40]

这样做法很不对,数值被改变了!

在重新修改一次:

var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
    newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]

这里没有修改 things 数值,但却却修改了 newThings 。暂时先不管这个,毕竟我们现在用的是 JavaScript。一旦使用函数式语言,任何东西都是不可变的。

现在将代码封装成一个函数,我们将其命名为 map ,因为这个函数的功能就是将一个数组的每个值映射(map)到新数组的一个新值。

var map = (f, array) => {

var newArray = [];
for (var i = 0; i < array.length; ++i) {
    newArray[i] = f(array[i]);
}
return newArray;

};

函数 f 作为参数传入,那么函数 map 可以对 array 数组的每项进行任意的操作。

现在使用 map 重写之前的代码:

var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);

这里没有 for 循环!而且代码更具可读性,也更易分析。

现在让我们写另一个常见的函数来过滤数组中的元素:

var filter = (pred, array) => {
    var newArray = [];
for (var i = 0; i < array.length; ++i) {
        if (pred(array[i]))
            newArray[newArray.length] = array[i];
    }
    return newArray;
};

当某些项需要被保留的时候,断言函数 pred 返回TRUE,否则返回FALSE。

使用过滤器过滤奇数:

var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]

比起用 for 循环的手动编程, filter 函数简单多了。最后一个常见函数叫reduce。通常这个函数用来将一个数列归约(reduce)成一个数值,但事实上它能做很多事情。

在函数式语言中,这个函数称为 fold

var reduce = (f, start, array) => {

var acc = start;
for (var i = 0; i < array.length; ++i)
    acc = f(array[i], acc); // f() 有2个参数
return acc;

});

reduce函数接受一个归约函数 f ,一个初始值 start ,以及一个数组 array

这三个函数,map,filter,reduce能让我们绕过for循环这种重复的方式,对数组做一些常见的操作。但在函数式语言中只有递归没有循环,这三个函数就更有用了。附带提一句,在函数式语言中,递归函数不仅非常有用,还必不可少。

原文:

编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

一个笨笨的码农,我的世界只能终身学习!

更多内容请关注公众号《大迁世界》!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Powerful

Powerful

Patty McCord / Missionday / 2018-1-25

Named by The Washington Post as one of the 11 Leadership Books to Read in 2018 When it comes to recruiting, motivating, and creating great teams, Patty McCord says most companies have it all wrong. Mc......一起来看看 《Powerful》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具