本系列的第一篇:

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

组合函数 (Function Composition)

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

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

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

看下以下两个函数:

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 ,所以这里我们就在写已经重复的代码了。

使用函数 add10mult5 来重构 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

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

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


// 这个函数需要一个参数mult5AfterAdd10 value =(mult5 << add10) value

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

// 这也是一个需要1个参数的函数mult5AfterAdd10 =(mult5 << add10)

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

  1. 首先,我们不需要指定冗余的参数。由于不必指定参数,所以也就不必考虑为它们命名。
  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)

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

上例我们在组合函数 mult5add(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 有两个参数 fg,然后返回一个函数,该函数有一个参数 x,并传给函数 f,当函数被调用时,先调用函数 g,返回的结果作为函数 f的参数。

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

柯里化与重构(Curring and Refactoring)

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

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

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

调用方式如下:

var bracketedJoe =  bracketed('小智')var doubleBracketedJoe =  doubleBracketed('小智')

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

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

如下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循环这种重复的方式,对数组做一些常见的操作。但在函数式语言中只有递归没有循环,这三个函数就更有用了。附带提一句,在函数式语言中,递归函数不仅非常有用,还必不可少。

原文:

https://medium.com/@cscalfani...
https://medium.com/@cscalfani...

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

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

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

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

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

  1. java静态方法mult_学会使用函数式编程的程序员(第3部分)

    想优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 本系列的其它篇: 引用透明 (Referential Transparency) 引用透明是一个富有想象力的优秀术语,它是用来描述纯函数可 ...

  2. 学会JavaScript函数式编程(第1部分)

    2019独角兽企业重金招聘Python工程师标准>>> 摘要: JS函数式编程入门. 原文:学会使用函数式编程的程序员(第1部分) 作者:前端小智 Fundebug经授权转载,版权归 ...

  3. 学会JavaScript函数式编程(第3部分)

    摘要: JS函数式编程入门. 原文:学会使用函数式编程的程序员(第3部分) 作者:前端小智 Fundebug经授权转载,版权归原作者所有. 本系列的其它篇: 学会使用函数式编程的程序员(第1部分) 学 ...

  4. 新手编程:程序员 5 种编程入门方法,快速学会一门编程语言!

    程序员要精通一门语言,还是要掌握多门语言? 对于这个问题,公说公有理,婆说婆有理. 技术更新太快,精通一门编程语言已经实属不易,又怎么好意思说精通多门语言呢? 不过呢,当你精通一门语言同时,涉猎一下其 ...

  5. 函数式编程语言python-10分钟学会python函数式编程

    原标题:10分钟学会python函数式编程 在这篇文章里,你将学会什么是函数范式以及如何使用Python进行函数式编程.你也将了解列表推导和其它形式的推导. 函数范式 在命令式范式中,通过为计算机提供 ...

  6. 【看动漫学编程】程序员在异世界生个娃 第2篇:外挂已准备就绪

    前言 作者文笔比较水,还请见谅. 以下内容还将使用视频动态漫画表现,剪辑完将会贴出链接. 小说剧情为剧情需要,过渡到知识点,部分篇幅可能没有技术知识点还望谅解. 由于没有经费支持,所以画出来的东西是我 ...

  7. 【看动漫学编程】程序员在异世界生个娃 第1篇:太极村

    前言 作者文笔比较水,还请见谅. 以下内容还将使用视频动态漫画表现,剪辑完将会贴出链接. 小说剧情为剧情需要,过渡到知识点,部分篇幅可能没有技术知识点还望谅解. 由于没有经费支持,所以画出来的东西是我 ...

  8. java开发 职业技能_java编程开发程序员需要具备哪些职业技能

    随着互联网的不断发展,java编程开发可以说是目前学习人数和应用范围非常多的一种编程语言了,而今天我们就一起来了解一下,java编程开发程序员需要具备哪些职业技能. 1.数据结构和算法分析 数据结构和 ...

  9. java弱签名_14条经典的编程签名(程序员个性签名)

    之前收集过<22条经典的编程签名(程序员个性签名)>,发现还有一些未收录的,下面这些引言也很有意思的,希望你喜欢. "The first 90% of the code acco ...

最新文章

  1. MathType的公式Latex到Katex转换程序
  2. 【深度学习】像素级分割网络新思路之DeepLabv3+
  3. java设计模式之简单工厂模式
  4. C#获取摄像头拍照显示图像
  5. matlab 写excel 慢_吐槽一下MATLAB的workspace
  6. deleted 表和 inserted 表
  7. leetcode89 (2022.1.8)
  8. tshark 操作后保存为pcap数据包
  9. 软件测试用例设计方法-等价类划分法
  10. Redis Cluster 伪集群的搭建
  11. 团队文化中的害群之马
  12. python生成条形码和二维码
  13. 模块已加载但找不到入口点dllregisterserver
  14. 网络安全架构与域划分(企业安全)
  15. 设计制作了一套easyui皮肤组件
  16. 【Python网络编程】爬取百度贴吧、小说内容、豆瓣小说、Ajax爬微博、多线程爬淘宝
  17. MFC 对话框添加背景图
  18. unity应用实例——从头撸一个全新的FPS游戏(5)
  19. 0415学习笔记:3决策树
  20. MIC灵敏度, MIC动态范围下限值估计, -3dB, dB加减

热门文章

  1. lol12月25服务器维护,lol12月10日维护到几点 英雄联盟12月10日10.25版本更新维护时间...
  2. 张小明教授+计算机,香港浸会大学、实验室兼职导师张晓明教授访问实验室并做学术报告...
  3. 李开复对谈张亚勤:科学家创业需要企业家伙伴,开放心态看待元宇宙 | MEET2022...
  4. 微软获GPT-3独家授权,可访问底层代码,Open AI:API用户可继续使用
  5. 高通5G开始挤牙膏?骁龙865+发布,性能小幅提升,一加听了要笑,魅族看了会流泪...
  6. 中国首份AI落地白皮书发布!地方政府规模大,金融领域最积极,北京供给超上海深圳总和...
  7. 这项X射线技术让芯片无秘密可言:纳米级还原内部构造,现已破解16nm芯片
  8. 效率达CPU一万倍的神经形态芯片发布后,英特尔引发了多方吐槽
  9. metrics-server最新版本有坑,慎用
  10. Istio所有模块、Service、Pod的功能介绍