学会运用函数式编程的程序员(第1部份)

《学会运用函数式编程的程序员(第1部份)》

想浏览更多优良文章请猛戳GitHub博客,一年百来篇优良文章等着你!

在这篇由多部份构成的文章中,接下来将引见函数式编程的一些看法,这些看法对你进修函数式编程有所协助。假如你已懂了什么是函数式编程,这能够加深你的明白。

请不要焦急。从这一点最先,花点时刻浏览并明白代码示例。你以至能够想在每节课完毕后住手浏览,以便让你的看法深切明白,然后再回来完成。

最主要的是你要明白。

纯函数(Purity)

《学会运用函数式编程的程序员(第1部份)》

所谓纯函数,就是指如许一个函数,关于雷同的输入,永久获得雷同的输出,它不依靠外部环境,也不会转变外部环境。假如不满足以上几个前提那就黑白纯函数。

下面是Javascript中的一个纯函数示例:

var z = 10;
function add(x, y) {
    return x + y;
}

注重,add 函数不触及z变量。它不从z读取,也不从z写入,它只读取xy,然后返回它们相加的效果。这是一个纯函数。假如 add 函数确切接见了变量z,那末它就不再是纯函数了。

请思索一下下面这个函数:

function justTen() {
    return 10;
}

假如函数justTen是纯的,那末它只能返回一个常量, 为何?

因为我们没有给它任何参数。 而且,既然是纯函数的,除了本身的输入以外它不能接见任何东西,它唯一能够返回的就是常量。

因为不带参数的纯函数不起作用,所以它们不是很有用。所以justTen被定义为一个常数会更好。

大多数有用的纯函数必需最少带一个参数。

斟酌一下这个函数:

function addNoReturn(x, y) {
    var z = x + y
}

注重这个函数是不返回任何值。它只是把变量xy相加赋给变量z,但并没有返回。

这个也是一个纯函数,因为它只处置惩罚输入。它确切对输入的变量举行操纵,然则因为它不返回效果,所以它是无用的。

一切有用的纯函数都必需返回一些我们希冀的效果。

让我们再次斟酌第一个add函数:

《学会运用函数式编程的程序员(第1部份)》

注重 add(1, 2) 的返回效果老是 3。这不是新鲜的事变,只是因为 add 函数是纯的。假如 add 函数运用了一些外部值,那末你永久没法展望它的行动。

在给定雷同输入的状况下,纯函数老是返回雷同的效果。

因为纯函数不能转变任何外部变量,所以下面的函数都不是纯函数:

writeFile(fileName);
updateDatabaseTable(sqlCmd);            
sendAjaxRequest(ajaxRequest);
openSocket(ipAddress);

一切这些功用都有副作用。当你挪用它们时,它们会变动文件和数据库表、将数据发送到服务器或挪用操纵系统以猎取套接字。它们不仅对输入操纵同时也对输出举行操纵,因而,你永久没法展望这些函数将返回什么。

纯函数没有副作用。

在Javascript、Java 和 c# 等敕令式编程言语中,副作用无处不在。这使得调试异常难题,因为变量能够在顺序的任何地方变动。所以,当你有一个毛病,因为一个变量在毛病的时刻被变动为毛病的值,这不是很好。

此时,你能够会想,“我怎样能够只运用纯函数呢?”

函数式编程不能消弭副作用,只能限定副作用。因为顺序必需与实在环境相连接,所以每一个顺序的某些部份肯定是不纯的。函数式编程的目的是只管写更多的纯函数,并将其与顺序的其他部份断绝开来。

不可变性 (Immutability)

《学会运用函数式编程的程序员(第1部份)》

你还记得你第一次看到下面的代码是什么时刻吗?

var x = 1;
x = x + 1;

教你初中数学的先生看到以上代码,能够会问你,你遗忘我给你教的数学了吗? 因为在数学中,x 永久不能即是x + 1。

但在敕令式编程中,它的意义是,取x的当前值加1,然后把效果放回x中。

在函数式编程中,x = x + 1黑白法的。所以这里你能够用数学的逻辑还记得在数式编程中如许写是不对的!

函数式编程中没有变量。

因为汗青缘由,存储值的变量依然被称为变量,但它们是常量,也就是说,一旦x取值,这个常量就是x返回的值。别忧郁,x 一般是一个局部变量,所以它的生命周期一般很短。但只需它还没被烧毁,它的值就永久不会转变。

下面是Elm中的常量变量示例,Elm是一种用于Web开辟的纯函数式编程言语:

addOneToSum y z =
    let
        x = 1
    in
        x + y + z

假如你不熟习ml作风的语法,让我诠释一下。addOneToSum 是一个函数,有两个参数分别为yz

let块中,x被绑定到1的值上,也就是说,它在函数的生命周期内都即是1。当函数退出时,它的生命周期完毕,或许更正确地说,当let块被求值时,它的生命周期就完毕了。

in块中,盘算能够包含在let块中定义的值,即 x,返回盘算效果 x + y + z,或许更正确地说,返回 1 + y + z,因为 x = 1。

你能够又会想 :“我怎样能在没有变量的状况下做任何事变呢?”

我们想一下什么时刻须要修转变量。一般会想到两种状况:多值变动(比方修正或纪录对象中的单个值)和单值变动(比方轮回计数器)。

函数式编程运用参数保留状况,最好的例子就是递归。是的,是没有轮回。“什么没有变量,如今又没有轮回? ”我憎恶你! ! !”

哈哈,这并不是说我们不能做轮回,只是没有特定的轮回构造,比方for, while, do, repeat等等。

函数式编程运用递归举行轮回。

这里有两种要领能够在Javascript中实行轮回:

《学会运用函数式编程的程序员(第1部份)》

注重,递归是一种函数式要领,它经由过程运用一个完毕前提 start (start + 1) 和挪用本身 accumulator (acc + start) 来完成与 for 轮回雷同的功用。它不会修正旧的值。相反,它运用从旧值盘算的新值。

不幸的是,这在 Javascript中 很难想懂,须要你花点时刻研讨它,缘由有二。第一,Javascript的语法相对别的高等言语比较乱,其次,你能够还不习气递归头脑。

在Elm,它更轻易浏览,以下:

sumRange start end acc =
    if start > end then
        acc
    else
        sumRange (start + 1) end (acc + start) 
    

它是如许运转的:

《学会运用函数式编程的程序员(第1部份)》

你能够以为 for 轮回更轻易明白。虽然这是有争议的,而且更多是一个熟习的题目,但非递归轮回须要可变性,这是不好的。

在这里,我还没有完整诠释不变性的优点,然则请检察全局可变状况部份,即为何顺序员须要限定来相识更多。

我还没有完整诠释不可变性(Immutability)在这里的优点,但请检察 为何顺序员须要限定的全局可变状况部份 以相识更多信息。

不可变性的优点是,你读取接见顺序中的某个值,但只要读权限的,这意味着不必畏惧其他人变动该值使本身读取到的值是毛病。

不可变性的另有一个优点是,假如你的顺序是多线程的,那末就没有其他线程能够变动你线程中的值,因为该值是不可变,所以另一个线程想要变动它,它只能从旧线程建立一个新值。

不变性能够建立更简朴、更平安的代码。

重构

让我们斟酌一下重构,下面是一些Javascript代码:

《学会运用函数式编程的程序员(第1部份)》

我们之前能够都写过如许的代码,跟着时刻的推移,最先意想到这两个函数实际上是雷同的,函数称号,打印效果不太一样罢了。

我们不应当复制 validateSsn 来建立 validatePhone,而是应当建立一个函数(配合的部份),经由过程参数情势完成我们想要的效果。

重构后的代码以下:

《学会运用函数式编程的程序员(第1部份)》

旧代码参数中 ssnphone 如今用 value 示意,正则表达式 /^\d{3}-\d{2}-\d{4}$/ and /^\(\d{3}\)\d{3}-\d{4}$/ 由变量 regex. 示意。末了,音讯“SSN”“电话号码” 由变量 type 示意。

这个有相似的函数都能够运用这个函数来完成,如许能够坚持代码的整齐和可维护性。

高阶函数

《学会运用函数式编程的程序员(第1部份)》

很多言语不支撑将函数作为参数通报,有些会支撑但并不轻易。

在函数式编程中,函数是一级国民。换句话说,函数一般是另一个函数的值。

因为函数只是值,我们能够将它们作为参数通报。纵然Javascript不是纯函数言语,也能够运用它举行一些功用性的操纵。 所以这里将上面的两个函数重构为单个函数,要领是将考证合法性的函数作为函数 parseFunc 的参数:

function validateValueWithFunc(value, parseFunc, type) {
  if (parseFunc(value))
    console.log('Invalid ' + type);
  else
    console.log('Valid ' + type);
}

像函数 parseFunc 吸收一个或多个函数作为输入的函数,称为 高阶函数

高阶函数要么接收函数作为参数,要么返回函数,要么二者兼而有之。

如今能够挪用高阶函数(这在Javascript中有用,因为Regex.exec在找到婚配时返回一个truthy值):

validateValueWithFunc('123-45-6789', /^\d{3}-\d{2}-\d{4}$/.exec, 'SSN');
validateValueWithFunc('(123)456-7890', /^\(\d{3}\)\d{3}-\d{4}$/.exec, 'Phone');
validateValueWithFunc('123 Main St.', parseAddress, 'Address');
validateValueWithFunc('Joe Mama', parseName, 'Name');

这比有四个险些雷同的函数要好很多。

然则请注重正则表达式,这里有点冗长了。简化一下:

var parseSsn = /^\d{3}-\d{2}-\d{4}$/.exec;
var parsePhone = /^\(\d{3}\)\d{3}-\d{4}$/.exec;
validateValueWithFunc('123-45-6789', parseSsn, 'SSN');
validateValueWithFunc('(123)456-7890', parsePhone, 'Phone');
validateValueWithFunc('123 Main St.', parseAddress, 'Address');
validateValueWithFunc('Joe Mama', parseName, 'Name');

如今看起来好多了。如今,当要考证一个电话号码时,不须要复制和粘贴正则表达式了。

然则假定我们有更多的正则表达式须要剖析,而不仅仅是 parseSsnparsePhone。每次建立正则表达式剖析器时,我们都必需记住在末端增加 .exec,这很轻易被遗忘。

能够经由过程建立一个返回exec 的高阶函数来防备这类状况:

function makeRegexParser(regex) {
    return regex.exec;
}
var parseSsn = makeRegexParser(/^\d{3}-\d{2}-\d{4}$/);
var parsePhone = makeRegexParser(/^\(\d{3}\)\d{3}-\d{4}$/);
validateValueWithFunc('123-45-6789', parseSsn, 'SSN');
validateValueWithFunc('(123)456-7890', parsePhone, 'Phone');
validateValueWithFunc('123 Main St.', parseAddress, 'Address');
validateValueWithFunc('Joe Mama', parseName, 'Name');

这里,makeRegexParser采纳正则表达式并返回exec函数,该函数接收一个字符串。validateValueWithFunc 将字符串 value 通报给 parse 函数,即exec。

parseSsnparsePhone 实际上与之前一样,是正则表达式的 exec 函数。

固然,这是一个细小的革新,然则这里给出了一个返回函数的高阶函数示例。然则,假如makeRegexParser 要庞杂很多,这类变动的优点是很大的。

下面是另一个返回函数的高阶函数示例:

function makeAdder(constantValue) {
    return function adder(value) {
        return constantValue + value;
    };
}

函数 makeAdder,接收参数 constantValue 并返回函数 adder,这个函数返回 constantValue 与它传入参数相加效果。

下面是它的用法:

var add10 = makeAdder(10);
console.log(add10(20)); // 打印 30
console.log(add10(30)); // 打印 40
console.log(add10(40)); // 打印 50

我们经由过程将常量10通报给 makeAdder 来建立一个函数 add10, makeAdder 返回一个函数,该函数将向返回的效果都加 10。

注重,纵然在 makeAddr 返回以后,函数 adder 也能够接见变量 constantValue。 这里能接见到 constantValue 是因为存在闭包。

闭包机制异常主要,因为假如没有它 ,返回函数的函数就不会有很大作用。所以必需相识它们是怎样事情。

闭包

《学会运用函数式编程的程序员(第1部份)》

下面是一个运用闭包的函数的示例:

function grandParent(g1, g2) {
    var g3 = 3;
    return function parent(p1, p2) {
        var p3 = 33;
        return function child(c1, c2) {
            var c3 = 333;
            return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3;
        };
    };
}

在这个例子中,child 函数能够接见它本身的变量,函数 parent 函数能够接见它的本身变量和函数 grandParent 的变量。而函数 grandParent 只能接见本身的变量。

下面是它的一个运用例子:

var parentFunc = grandParent(1, 2); // returns parent()
var childFunc = parentFunc(11, 22); // returns child()
console.log(childFunc(111, 222)); // prints 738
// 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738

在这里,parentFunc 保留了 parent 的作用域,因为 grandParent 返回 parent

相似地,childFunc 保留了 child 的作用域,因为 parentFunc 保留了 parent 的作用域,而 parent 的作用域 保留了 child 的作用域。

当一个函数被建立时,它在建立时作用域中的一切变量在函数的生命周期内都是可接见的。一个函数只需另有对它的援用就存在。比方,只需childFunc 还援用 child 的作用域,child 的作用域就存在。

闭包详细还看看之前整顿的一篇文章:我历来不明白JavaScript闭包,直到有人如许向我诠释它…

原文:
1、https://medium.com/@cscalfani…
2、https://medium.com/@cscalfani…

编辑中能够存在的bug没法及时晓得,预先为相识决这些bug,花了大批的时刻举行log 调试,这边顺便给人人引荐一个好用的BUG监控东西Fundebug

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

交换

干货系列文章汇总以下,以为不错点个Star,迎接 加群 互相进修。

https://github.com/qq44924588…

我是小智,民众号「大迁天下」作者,对前端手艺坚持进修爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!

关注民众号,背景复兴福利,即可看到福利,你懂的。

《学会运用函数式编程的程序员(第1部份)》

    原文作者:前端小智
    原文地址: https://segmentfault.com/a/1190000017511211
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞