准备充分了嘛就想学函数式编程?(第一部分)

本文讲的是准备充分了嘛就想学函数式编程?(第一部分),


迈出理解函数式编程概念的第一步是最重要的,有时也是最难的一步。但是不一定特别难。只要选对了思考方法就不难。

学开车

第一次学车时,我们也曾挣扎过。看别人学开车时觉得真的很简单。但事实上学车比我们想象的难多了。

我们借父母的车子练习,在家周围街道上开熟练之前甚至都不敢冒险开到公路上去。

但是通过不断的练习,在经历过一些父母想忘掉的担心令人的经历之后,我们学会了开车,最终拿到了驾照。

拿到驾照之后我们一有机会就会把车开出去。每次出行都会让我们的技术越来越娴熟,信心也随之增长。终于有一天,我们必须开别人的车,或者自己的车最终报废了,只能买辆新的。

第一次开另一辆不同的车是什么感觉?和第一次开车的感觉相同吗?差得远呢。第一次开车时一切都是那么陌生。我们之前在车里待过,但只是个乘客。这一次我们可是在驾驶席上,掌控着一切。

但当我们开第二辆车时,我们只是简单问问自己几个问题就够了,比如钥匙怎么用,灯光在哪里,怎么用转向灯还有如何调整后视镜。

之后就顺利多了。但是为什么这次比起第一次要容易那么多?

这是因为这个新车和旧车基本上没什么区别。它们都有构成汽车的基本部件,而且都在差不多一样的地方。

新车有些部件的实现方式有些差异,也可能有了一些新功能,但是第一次,甚至第二次开的时候都不会用到这些功能。直到最后我们了解了所有的新功能。至少是那些我们觉得有用的功能。

其实学习编程语言的过程和学车有点类似。第一次是最难的。但是只要掌握了第一种语言,后面的都会轻松许多。

开始学第二门语言的时候,我们也只是问几个诸如「如何创建模块?如何搜索数组?子字符串的参数是什么?」之类的简单问题。

你有信心驾驭第二门语言,因为你觉得这就是旧语言加了一些新东西,学起来也就更轻松。

初次驾驶宇宙飞船

不管你这辈子开过一辆车还是十来量,想象一下你就要开宇宙飞船了。

要开宇宙飞船的话你就不能指望在路上开车的技术有什么用了,一切都要从零学起。(我们可是程序员啊,我们从 0 开始计数。)

开始练习之前我们要想到在太空中一切都不同了,要把这玩意开起来和在公路上开车可完全不同。

物理学倒是不会变。变的只是我们在这同一个宇宙中航行的方式。

学习函数式编程就是这样。一定要做好大部分东西都会改变的准备。而且已有的编程知识并不会有太大帮助。

编程就是思维方式,而函数式编程则是新的思维方式。习惯了这种新的思维方式之后你甚至可能没办法回到从前。

忘了知道的一切

(说到函数式编程时,)人们喜欢说这句话,其实也有几分道理。学习函数式编程就像从零学起。 虽然不准确,但是不无道理。虽然函数式编程有许多类似的概念,但是最好还是让自己把所有东西都重新学一遍。

有了正确的思考方法就会有正确的预期,而预期对了就不会在遇到困难时轻言放弃。

许多在之前编程时习惯了的东西在函数式编程中都用不了了。

这就比如在开车时你已经习惯了倒车出库,但是开飞船的时候发现没有倒挡。这时候你会想了,「什么?没有倒挡?!没法儿倒车了我到底该怎么开?!」

好吧,其实开飞船的时候不需要倒挡,因为它是在三维空间中移动的。一旦了解了这一点,你就再也不会想什么倒挡了。事实上,总有一天你会回想起以前开车时的限制可真多啊。

学习函数式编程没那么快。所以别着急。

那么我们就离开寒冷的指令式编程世界,沉浸在函数式编程的温泉中吧。

这篇文章分为好几节,之后还介绍了一些函数式编程的概念,你可以在深入研究第一门函数式编程语言之前看看,会有帮助的。如果你已经开始着手学习了,它们也会帮助你加深理解。

务必不要心急。从这里开始慢慢地读,慢慢地理解示例代码。你甚至会想在读完每一节之后停顿一会儿,让自己充分领会,然后接着读下去。

最重要的是理解。

纯粹

函数式编程语言中的纯粹指的就是纯函数。

纯函数就是极简单的函数,只对输入参数起作用。

下面是 Javascript 中纯函数的例子:

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

注意,add 函数没有碰 z 变量。它即不读 z 也不写 z。它只是读了 x 和 y 参数,然后返回了两者的和。

这就是纯函数。如果 add 动了 z 变量,它就不再纯粹了。

下面是另一个例子:

function justTen() {
    return 10;
}

如果 justTen 是纯函数,它就只能返回一个常量。为什么呢?

因为我们还没有给它任何输入参数。如果是纯函数,它就不能存取自身的输入参数之外的任何东西,所以它只能返回一个常量。

既然无参数的纯函数什么都做不了,没什么用,还不如直接把 justTen 定义为一个常量。

大多数 有用的 纯函数至少要有一个参数。

思考一下这个函数:

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

注意,这个函数什么都没有返回。它把 x y 之和赋给了变量 z,但并没有返回 z。

这也是个纯函数,它只是处理了自身的参数。虽然有加法,但是没返回结果,所以也没用。

有用的纯函数总要返回一些东西。

我们再回过头看看第一个 add 函数:

function add(x, y) {
    return x + y;
}
console.log(add(1, 2)); // prints 3
console.log(add(1, 2)); // still prints 3
console.log(add(1, 2)); // WILL ALWAYS print 3

add(1, 2) 总是返回 3。结果不出所料,这毕竟是个纯函数。如果 add 函数用了其他外部值,就没法预测它的行为了。

纯函数对相同的输入总能产生相同的输出。

因为纯函数不能修改任何外部变量,下面这些函数都是不纯粹的。

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

这些函数都有副作用。调用它们会修改文件和数据库的表,向服务器发送数据或者调用系统获取 socket。它们不仅处理输入参数并返回值,还做了其他事情。所以永远无法预测这些函数会返回什么。

纯函数没有副作用。

诸如 JavaScript, Java 和 C# 之类的指令式编程语言充斥着副作用。所以调试起来比较困难,毕竟变量有可能在任何地方遭到修改。如果遇到了因为变量被错误修改而导致的 bug,要从何看起呢?这一点都不好。

读到这里,你可能在想,「到底怎样才能用纯函数做任何事情呢?!」

函数式编程并不是只写纯函数。

函数式编程语言并不能彻底去除副作用,只能限制它们。因为函数总是要和现实世界打交道的,每个程序总有不纯粹的函数。而目标就是减少不纯粹代码的数量,并将它们和纯粹的代码隔离开来。

不可变性

还记得是在啥时候第一次看到下面这些代码吗?

var x = 1;
x = x + 1;

还记得是谁叫你忘了在数学课上学到的东西吗?数学中的 x 永远不会等于 x + 1。

但是在指令式编程中,这段代码表示将 x 的当前值加 1 再赋值回 x。

但是在函数式编程中 x = x + 1 是错误的。所以你得记得在数学课上忘掉了的东西…… 大致如此。

函数式编程中没有变量。

存储的值还是叫做变量,不过这是历史原因,它们其实是常量。比如一旦 x 有了一个值,这个变量就一直是这个值。

不要担心,x 通常是个局部变量,生命周期很短。但是只要它还在,值就不会变。

下面是个 Elm 中常变量的例子(Elm 是用于 Web 开发的纯函数编程语言):

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

如果不熟悉 ML 风格的语法,我在这里解释一下。 addOneToSum 是个有两个参数(y 和 z)的函数。

在 let 块中,x 绑定到的值是 1,所以它之后永远等于 1。函数退出,更准确地说是 let 块求值完之时,x 的生命周期才结束。

在 in 块中,计算过程中可以使用 let 块定义的值,即 x。x + y + z 的计算结果,更准确地说是 1 + y + z 的计算结果会被返回,因为 x = 1。

好吧,我听到你又问了一遍「到底怎样才能用纯函数做任何事情呢?!」

让我们想想何时需要修改变量。通常有两种情况:修改多个值(比如修改对象或 Record 中的值)和修改单一值(比如循环的计数器)。

函数式编程通过创建修改了值的 Record 副本来实现对 Record 中的值的修改。不过利用数据结构可以不必复制 Record 中的全部属性,十分高效。

函数式编程修改单一值也是通过创建副本实现的。

哦,对了。这样也没有用到循环。

「为什么没有变量和循环?!我讨厌你!!!」

等一下。这并不是说我们不能使用循环(我没有玩游戏文字),只是函数式编程语言没有像 for,while,do 以及 repeat 这样明确的循环结构而已。

函数式编程的循环用递归实现。

下面的代码展示了 Javascript 创建循环的两种方式:

// simple loop construct
var acc = 0;
for (var i = 1; i <= 10; ++i)
    acc += i;
console.log(acc); // prints 55

// without loop construct or variables (recursion)
function sumRange(start, end, acc) {
    if (start > end)
        return acc;
    return sumRange(start + 1, end, acc + start)
}
console.log(sumRange(1, 10, 0)); // prints 55

看看函数式编程中的递归是如何实现 for 循环的:就是用新起始值(start + 1)和新累积值(acc + start)不断地调用自身。它没有修改旧值,而是使用由旧值计算出来的新值。

不幸的是,即使学过一段时间 JavaScript 你也很难用到这种写法,原因有二。第一,JavaScript 语法很繁琐,第二是你可能不习惯递归式思考。

而 Elm 的写法更易读,也更易懂:

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

执行过程如下:

sumRange 1 10 0 =      -- sumRange (1 + 1)  10 (0 + 1)
sumRange 2 10 1 =      -- sumRange (2 + 1)  10 (1 + 2)
sumRange 3 10 3 =      -- sumRange (3 + 1)  10 (3 + 3)
sumRange 4 10 6 =      -- sumRange (4 + 1)  10 (6 + 4)
sumRange 5 10 10 =     -- sumRange (5 + 1)  10 (10 + 5)
sumRange 6 10 15 =     -- sumRange (6 + 1)  10 (15 + 6)
sumRange 7 10 21 =     -- sumRange (7 + 1)  10 (21 + 7)
sumRange 8 10 28 =     -- sumRange (8 + 1)  10 (28 + 8)
sumRange 9 10 36 =     -- sumRange (9 + 1)  10 (36 + 9)
sumRange 10 10 45 =    -- sumRange (10 + 1) 10 (45 + 10)
sumRange 11 10 55 =    -- 11 > 10 => 55
55

你可能认为 for 循环更好理解。这是有争议的,而且更多是因为我们更熟悉 for 循环。这种非递归实现的循环需要可变变量,更糟糕。

我在这里没有讲完不可变性的全部好处,更多内容请参阅为什么程序员需要限制一文中的全局可变状态一节。

不过一个显而易见的好处就是,如果对程序中的变量有访问权限,也只是只读权限,这也就是说,再也没人能改变这个值,哪怕是你自己。所以省去了很多意想不到的麻烦。

而且,如果是多线程程序,其他线程就无法干扰到当前线程。如果当前线程有一个常量而且其它线程试图改变它,其他线程只能用旧值复制一个新值出来。

在 90 世纪中期时,我写了生化危机游戏引擎,最大的 bug 源头就是各种多线程问题。我真希望自己那时候就知道不可变性。不过我那时候更应该担心 2 倍速和 4 倍速 CD-ROM 驱动在游戏渲染上的差异。

不可变性使代码更简单,安全。

我的脑子!!!!

本部分到此结束。

在下一部分中我会继续介绍高阶函数,复合函数和柯里化等内容。





原文发布时间为:2016年11月17日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-10-02 22:09:17

准备充分了嘛就想学函数式编程?(第一部分)的相关文章

准备充分了嘛就想学函数式编程?(第五部分)

本文讲的是准备充分了嘛就想学函数式编程?(第五部分), 迈出理解函数式编程概念的第一步是最重要的,有时也是最难的一步.但是不一定特别难.只要选对了思考方法就不难. 前几部分: 第一部分, 第二部分, 第三部分, 第四部分 引用透明 引用透明 是一个很酷炫的术语,它指的是一个纯函数能够安全地被它的表达式所替代.下面用一个例子来解释这个术语. 在代数中当你有以下这个公式时: y = x + 10 并且已知: x = 3 你可以将 x 代入方程来得到: y = 3 + 10 此时这个方程依旧成立.我们

准备充分了嘛就想学函数式编程?(第四部分)

本文讲的是准备充分了嘛就想学函数式编程?(第四部分), 想要理解函数式编程,第一步总是最重要,也是最困难的.但是只要有了正确的思维,其实也不是太难. 之前的部分: 第一部分, 第二部分, 第三部分 柯里化 如果你还记得第三部分内容的话,就会知道我们在组合 mult5 和 add 这两个函数时遇到问题的原因是:mult5 接收一个参数而 add 却接收两个. 其实只需要通过限制所有函数都只接收一个参数,就可以轻易地解决这个问题. 相信我,这并没有听起来那么糟糕. 我们只需要来写一个使用两个参数,但

准备充分了嘛就想学函数式编程?(Part 2)

本文讲的是准备充分了嘛就想学函数式编程?(Part 2), 想要理解函数式编程,第一步总是最重要,也是最困难的.但是只要有了正确的思维,其实也不是太难. 之前的部分: 第一部分 友情提示 请读仔细读代码,确保继续之前你已经理解.每一代码段落都基于它之前的代码. 如果你太急,可能会遗漏一些重要的细节. 重构 让我们先来重构一段 JavaScript 代码: function validateSsn(ssn) { if (/^\d{3}-\d{2}-\d{4}$/.exec(ssn)) consol

准备充分了嘛就想学函数式编程?(Part 6)

本文讲的是准备充分了嘛就想学函数式编程?(Part 6), 第一步,理解函数式编程概念是最重要的一步,同时也是最难的一步.如果你从正确的角度或方法来理解的话,它也未必会有那么难. 回顾之前的部分: Part 1, Part 2, Part 3, Part 4, Part 5 现在该做什么? 现在你已经学会了所有这些新东西了,你可能在想,"现在该干什么?我如何在日常编程中使用它?" 这得看情况.如果你会使用纯函数式语言(如 Elm 或 Haskell)编程,那么你可以尝试所有这些想法.这

想学jsp web编程,各位大虾有好意见么

问题描述 想学jspweb编程,各位大虾有好意见么 解决方案 解决方案二:多动手敲代码,多做项目解决方案三:做两个jsp+servlet+JavaBean的网站.熟悉mvc框架的结构.看下jsp,servlet相关书籍,struts,spring,hibernate相关书籍解决方案四:从jspservletjdbc整起解决方案五:我是学java方面的,jspweb我觉得是需要发一点时间的,现在学的话都是关于SSH编程的.建议下点视频看看.解决方案六:LZ基础是主要...先抓起基础.然后在学习框架

《JavaScript函数式编程》读后感_javascript技巧

本文章记录本人在学习 函数式 中理解到的一些东西,加深记忆和并且整理记录下来,方便之后的复习. 在近期看到了<JavaScript函数式编程>这本书预售的时候就定了下来.主要目的是个人目前还是不理解什么是函数式编程.在自己学习的过程中一直听到身边的人说面向过程编程和面向对象编程,而函数式就非常少.为了自己不要落后于其他同学的脚步,故想以写笔记的方式去分享和记录自己阅读中所汲取的知识. js 和函数式编程 书中用了一句简单的话来回答了什么是函数式编程: 函数式编程通过使用函数来将值转换为抽象单元

我想学计算机-想学计算机!从根本学起

问题描述 想学计算机!从根本学起 谁知道有什么书介绍了计算机的起源及发展和原理,还有汇编语言,c语言的原理 解决方案 要想学计算机,关键是要有一个系统的过程.大家都知道,计算机是美国人发明的,所以要学计算机需要看原版的高质量的书籍.看了不对的书,就要走冤枉路.像楼下的书,难度就比较大,不适合lz.姐姐有一些很好的入门的书,介绍各种原理的.都是金针度人的好书.lz如果采纳了姐姐的回答(方法是点击姐姐回答右边的采纳按钮),姐姐发给你.祝你好运. 解决方案二: 要想学计算机,关键是要系统的学习.1.硬

学做界面#-想学做界面的信息安全专业的会敲代码的色影丝小学渣

问题描述 想学做界面的信息安全专业的会敲代码的色影丝小学渣 自身具备的艺术素养对做出优质的界面有助推作用吗?我对别人做的界面的构图位置美观吧啦吧啦很敏感,脑中会形成一个自己感觉更舒服的界面版式,这对做出优质的界面有助推作用吗,还是小学生胡思乱想了,第一次提问,求指示,轻喷. 解决方案 有艺术细胞是好事.但是如果过于强调这个,而忽略了系统的学习.理性和理论,那么是没什么好处的. 一个靠直觉和自发得到的经验而进行界面设计的人,可能你能设计用户群体和你本人有着相同背景的简单的小软件. 但是一个前端交互

jsp-下边的代码谁懂啊,大神求解释。顺道告诉我下想学关于这个看什么,谢谢啦

问题描述 下边的代码谁懂啊,大神求解释.顺道告诉我下想学关于这个看什么,谢谢啦 <head> <jsp:include page='/res/inc/inc.jsp' flush='true'/> <style> .FixedTitleRow { position: relative; top: expression( this.offsetParent.scrollTop ); z-index: 10; background-color: #ffffff; } .Fi