[开场音乐] Astrian:你现在正在收听是 Echo.js, Echo.js是一档以编程和技术为主要话题的播客栏目,我是 Astrian。 白羊:我是白羊。 Astrian:大哥大嫂过年好! 白羊:过年好。 Astrian:过年好,我们终于更新了,具体为什么隔了那么久,其实没有什么特别理由,就单纯的就是懒。 白羊:你连解释也不想解释了。 Astrian:解释就是掩饰,不如不解释,对吧? 白羊:说得好,我们堂堂正正摆烂。 Astrian:不过我们还是有一点小小的变化的,大家刚才应该可以听到了,所以大家其实没有转错台,没有开错播客。我们找人新创作了一个片头和片尾的一段小音乐,待会章节之间也会有他帮我们制作的一个音乐,所以非常感谢这位 Meso 大佬。当时是这样的,当时我在微博上看到他那边可能会有一点点困难,是专门做这种类似于游戏音乐创作的音乐人,所以就找他约了一下我们 Echo.js 的一个开场音乐,所以在这里非常感谢这位大佬能来帮我们创作出这样子,大概就是这个。 白羊:所以是不要钱的吗? Astrian:还是花了点小钱的,不过还好,我觉得对于这种音乐创作来说,还是相对比较物美价廉。我会把他的一个 Discord 联系链接会放在 Show Notes 里面,如果大家感兴趣的话,可以去找他再约一约之类的。 [章节奏间音乐] Astrian:OK,这么快就进入正题了。 白羊:那么今天我们要聊一聊函数式编程,这个是我比较,也不能说擅长,只能说是我一直在研究的一些东西, Astrian 对此并不是很了解,所以他今天捧哏的,我今天是主讲。 Astrian:白羊今天会出现的多一点,如果对函数式编程还想听点什么,我们可以聊的话,也欢迎在评论区留言,内容上面可能会有一点点偏深,大家有什么意见的话也可以给我们留言,我们可以去做调整,如果是喜欢的话,我们也可以尝试再多做几期。 白羊:是的,这个东西其实是比较难的,我已经研究了可以说很久了。 Astrian:我记得你好像从大学开始就研究。 白羊:对,然后想录这个节目也是很久了。因为这个东西也是比较难,加上我本身可能以前也不是很懂,以前一直就想录一期,在我们还没有歌之前我就一直想录一期这个,但是就一直感觉比较难,我自己也讲不清楚,就一直在拖着。现在我感觉渐渐的熟练了,所以我才敢说这个。 白羊:但是这个东西确实还是比较复杂的,所以说里面可能会出现一些个人理解以及错误,以及可能以后会吃书的地方,所以各位需要自行鉴别正确性之类的。 Astrian:其实如果说是有哪些地方我们说的不太对的话,也欢迎大家能够给我们我们留言或者发邮件过来。我们先从哪开始呢?我们先从解释函数式编程到底是个什么东西这一点开始。毕竟这个东西我不敢说全部,至少绝大部分我们的听众应该还是,包括我自己,也都不是特别了解这个东西的。 白羊:函数式编程是一种所谓的编程范式,其实你也知道很多很多的编程语言它基本上就是个语法有点点小区别,有的类型写在前面,有的类型写在后面,但是它主要的想法是一样的,它都是一行一行运行,所以说基本上你学了一个语言,你就可以去用其他的语言,就几乎没有什么大的偏差,但是范式是不一样的,比如说我们常见的面向过程和面向对象,我们就可以说是完全不一样的,因为他们的思考方法,他们的组织代码的模式都是不一样的,这种东西称为范式。那么函数式编程也是一个范式,那么它相对的就跟过程式的和对象式的来对比,都是同样级别的东西,也是一种编程范式。 Astrian:函数式编程这个东西它到底跟传统的过程式和面向对象的这种有什么差别?过程式是面向过程,它就是一行一行执行下来这样子。面向对象的话,它就是说你去操作一个对象,由对象来完成你这个任务,那么函数式编程是不是可以理解成整一个程序本身就是由多多个函数组成的一个东西? 白羊:那么我们先要搞清楚不同范式到底本质的区别是什么,因为我们知道这个程序最终最终运行起来的时候,它肯定是一行一行运行的,或者说它最后编译成汇编的时候,它是有各种跳转之类的东西来做的。那么过程式和对象式到底有什么区别呢?那过程式的想法是我的程序就是一行一行执行的,有些东西比较通用的,我就可以把它抽出去作为一个子程序,执行到那个地方的时候,我就跳到那个子程序,执行完之后再跳回来,这是过程式,基本上的想法还是一行一行执行。 Astrian:就过程式编程就特别符合程序这两个字的一个定义,一个是程,程就一个过程,然后序就是按序列来完成。这很像这样子的一个。 白羊:对,是这样的。对象式就有一些不同,对象式它的思考方式是对象,它把程序的各种部分都划分成了所谓的对象,由对象的组合让程序来自己去运行起来。比如说我生成一个小明,生成一个小红,它们各自都有各自的行为,然后它们就可以自己运转了,就跟我们理解世界的模式是一样的,就是世界上的每一个东西都是一个独立的个体。每一个个体都会独立的思考或者发生作用之类的,这个世界就整体运行起来了。 Astrian:其实严格来说面向对象编程,只不过是将在整个过程里面的一些东西把它给抽象成一个对象,由对象去执行它自己的,因为原本是我创建一个函数,把我想要处理的东西丢进去,其实函数本身是个黑箱,就丢到这个黑箱里面去,等待黑箱出结果,我就可以继续运行。对象的话,实际上是把黑箱再分解成抽象成的一个一个的对象这样子,有点类似于这种思想。 白羊:是的,最后编译成底层语言的时候,当然还是一行一行执行的,和过程式其实是没有区别的。只是编写这个对象式代码的时候的想法是不同的,你不需要去写一行一行的代码,你写的是一个对象一个对象的代码,这就是组织代码方式的不同,这就是所谓的范式的不同,就是我们如何理解一个程序,如何理解代码,如何组织代码这方面,不同的范式有不同的答案。 Astrian:OK,我们简单说完了过程式和对象式,应该叫简单复习一遍,一些区别。那么我们再说说函数式编程,既然原来的代码范式里面就已经有函数这个概念了,为什么还要有一个函数式编程呢?这个函数又体现在哪些地方呢? 白羊:函数式和过程式主要的区别就在于,其实我也很难说清楚有哪些区别,但是我可以举一个例子,考虑一个比较简单的情况,比如说现在想要找出1到100之间的所有的偶数,如果是一个过程式的代码应该怎么做,我应该先声明一个变量,当然你也知道这个变量肯定是对应的一个内存里的,然后我先给变量一个初始值,比如说给它个1,然后去判断1能不能被2整除,如果可以的话,我就把1存到另外一个变量里,如果不能我就跳过这一点,不管能不能之后我会把变量加1,就把它对应的内存地址数字加1,再重重新执行这个逻辑,一直执行到变量值等于100的时候。 Astrian:这是过程式,就听上去跟我们平常使用的 if 和 else 判断也差不太多。 白羊:对的,过程本质就是循环条件判断之类的东西了,如果用函数来解决这个问题是怎样?可以先有一个函数,我定义一个函数,这个函数是要输入一个数字 n,如果这个 n 是偶数,就返回真,不然就返回假。这是第一个函数。第二个函数是我要输入一个数组和一个判断规则,只留下满足判断规则的数组的项,组成一个新的数组就是我们常见的函数过滤,听我们节目的应该都是前端的程序员,所以应该知道 JS 里面不是也有数组的过滤嘛,FI 什么的。你可以现在去查一查,filter。 Astrian:Filter。我以为是什么东西。 白羊:总之这个函数可以输入一个数组,然后再输入一个判断规则,所谓判断规则其实就是一个函数,结果就是仅仅留下满足判断规则的这些项组成一个新的数组,返回的就是新的数组,这是我们的第二个函数。第三个函数我需要输入 m 和 n,我就会生成一个 m 到 n 的整数数组,比如说我输入 0 和 100,我就可以得到一个 0 到 100 的数组。现在我们就有这三个函数,我们就可以把这三个函数组合起来,就可以达到我们的目标。 白羊:组合的方式是先生成一个 1~100 的数组,把这个数组丢给过滤函数,过滤函数不是要两个参数嘛,第一个参数数组就是我们生成的数组,第二个参数是一个函数,但这个函数就是我们第一个函数就是 n 如果是偶数就返回真不然就返回假的函数。这样一组合它就可以用了,你完全不需要去考虑什么变量,什么内存地址之类的东西,你甚至不需要考虑循环。 Astrian:函数式编程实际上是将判断的一个过程再抽象循环,再抽象成一个函数来进行运行判断和循环,再抽象成一个一个函数这样子。可以这么理解吗? 白羊:也可以这么说,但是当然也有一些更加细节的部分,函数式编程还有一些特性也是这个过程是不具备的。比如说函数式编程非常讲究一个叫做透明性,比如说我这里有一串代码,我把这套代码移动到任何地方,给他它一个名字,然后把这个名字代换到这串代码原来的地方,它绝对也是可以用的,不管这串代码到底是什么都无所谓,这就是所谓的透明性,就是可以任意的代换,对于任意层级的任意长度的代码都可以这样简单代换,这样的话就非常适合代码的重构。 Astrian:实际上你这个程序本身它的顺序是怎么样的事其实是没有所谓的。 白羊:因为在普通的尤其是过程式编程里面,它是依赖上下文的,我在第一行声明了一个变量,我在第十行使用这个变量,如果我把第十行的这个东西拿到其他的函数里面就不能用,因为第十行的东西用的是之前声明过的变量,函数这里根本就没有变量这个说法,它所有的东西都是一个映射,就是我有一个值,我把这个值变成另一个值,所谓的函数调用也是把一个值变成另一个值,没有变量。 白羊:像刚才我们举的例子里面也是,比如说过滤,把原来的数组变成一个新数组的表达式,这个表达式可以放在任何地方,它都不会有任何的影响。剩下还有一点跟过程是非常不同的地方,是函数是会所谓的隔离 IO,就是有一些操作是不确定的,比如说我要去读一个数据库的数据,如果这时候我的网断了,我肯定是读不出来的。 白羊:你们这种操作就叫做副作用,它可能失败,它要跟程序的外界交互,这样的操作在函数式里是有严格的限制的。一旦我在一个函数里使用了副作用,那么所有使用这个函数的函数都会被污染成为带有副作用的函数,这样的话我就可以很清楚的看到哪些函数是可能失败的,哪些函数是纯的,然后我可以把它做一个切分,把主要的业务逻辑都写成纯的,只有那些没有办法的时候我才会写副作用。 白羊:这样的话出错的话肯定是在这些副作用函数里出错的,这也是一个非常我觉得还是不错的策略,并且这个限制是非常严格的。它通过它的这些类型系统之类的,所以说虽然写代码觉得更加束手束脚了,但是。 Astrian:其实你 Trade off 之后,你换来的东西是普通的编程要更加方便。 白羊:是的,有一句口号叫做如果它能编译它就能运行。 Astrian:刚才讲完了有关于过程式和函数式之间有什么区别,函数式相比于过程式编程有的优势在哪里?那我们再来聊聊面向对象的编程方式跟函数式编程到底有什么区别和优势? 白羊:对于面向对象来说,它跟函数式的区别其实很明显,面向对象的组织代码的方式是类和对象,函数式里就没有这些复杂的东西。我说它复杂的原因是它会出现很多个进程,这些进程每一个类里边又有它自己的什么?有公有、私有、静态这一这一套的东西,那么这些东西不能说它有问题,但是当系统变得很庞大的时候,这些东西就会变得难以理解,或者说至少是更加复杂的,那在函数式里就没有这些东西,函数式里就是简单的函数和值,函数就是把值进行转换的一种工具,就仅此而已。 白羊:另外一个就是相比面向对象的区别是所谓的函数是一等公民,因为在面向对象里是不能够单独存在函数的,面向对象里只有类和对象。那么在类里面你可以定义一个方法,那么这个方法可以叫做函数,但是它不能够存在一个单独的函数,但是在函数式里可以你可以把函数作为一个变量,把这个变量传给其他的函数,甚至它还有所谓的柯里化,比如说我现在有一个函数,这个函数要接收两个参数,分别叫做 a 和 b。 白羊:函数的实现是把 a 和 b 加起来,正常情况下我应该是传给它两个参数,比如说传给他 1 和 2 都会得到 3,对吧?有一个操作叫做柯里化,它可以把函数变成只有一个参数的函数,比如说对于刚才这个加法来说,我可以只给它传一个 1,那么它会返回什么?它会返回一个需要再传入一个参数的函数。 Astrian:就是等于是函数套娃了,有点这个意思。 白羊:对,那么这个也是一个非常常见的函数式编程里面的手段,叫做柯里化,在很多时候可以让代码变得更加简单,也有一些更特别的用处。 白羊:另外有一个更总体的不同是函数式,里面没有什么对象,没有什么状态,包括变量这种东西,函数式里都没有。因为函数里非常讨厌,就是说这个东西我在这个时刻访问它是 1,我在下一个时刻访问它就是 2 之类的。这种不确定性是程序 bug 出现的根源,所以在函数这里就不存在变量,也不存在可变状态。 白羊:不存在变量的话,我要怎么实现一些需要有状态的东西呢?之后可能会说到了。这种不存在这种异变量或者说管理状态的这种机制,在我们前端常见的很多库里也有,比如说 Vuex,再比如说 Redux,那么他们都有各自的实现管理对象的方案。基本上就是,你要变这个对象,你就需要一些很复杂的操作才能变,变的过程会不会严格监控之类的。但是它跟函数的想法也不太一样。 白羊:总而言之,这里想说的就是状态的变化是出现 bug 的根源之一,这样的感觉。 [章节奏间音乐] 白羊:说到这里,你对函数式编程有什么了解吗? Astrian:大概了解了一下,函数编程跟普通的过程式和对象式的一个差异和优势是怎么样的?其实我了解这么多之后,下面是不是该了解一下,到底有什么样的函数式编程的语言了? 白羊:主要的函数式编程语言非常常见的叫做 Haskell,这个就是函数式里非常常见的语言,就算是跟函数是无关的一些地方,也基本上都在用 Haskell 的语法来介绍一些函数式的理论,所以是非常常见的大家公认的这样的一个入门。 Astrian:简单来说一下它的语法长什么样。 白羊:这让我非常喜欢,它的语法没有括号这种,不能说没有括号,我们一般的语言,比如说我在一个符号后面打一个括号,表示的是这个符号是一个函数,括号是指调用函数的一个标志,但是在 Haskell 里调用函数的标志是空格,这样的话我的整个程序都不会有那种各种各样的括号。其他语言比较常见的是那种大括号来弄一个函数的区域,Haskell 跟 Python 差不多,通过缩进来控制的。 Astrian:纯缩进吗?其实我挺讨厌纯缩进的,最近写 Python 的作业,非常讨厌。 白羊:这样的话就缺少了很多乱七八糟的括号,非常的漂亮,这也是我非常喜欢的一点。 Astrian:其实我个人非常讨厌写代码不用大括号的,非常讨厌。 白羊:你还不够熟练。 Astrian:不是不够熟练问题,非常多的 bug 是因为状态改变,对吧?我说非常多的 bug 就是因为缩进没有。 白羊:你可以配上什么自动化,我现在配的就是保存的时候,它就会自动的帮你格式化,如果缩进不对的话,我就能立刻看出来,推荐大家也使用一下。除了 Haskell 之外,还有其他的很多函数式语言,但是这些函数式就不是所谓的纯函数式,它就是采纳了一些函数式的想法做的语言,比如说最常见的我们的老古董叫做 LIST,还有比如说像 F#——你知道 C# 吗?它有一个另外的东西叫做 F#。还有一些什么 Java 的变种之类的,叫什么 SC 啥啥啥的。有很多很多的编程语言。包括我们的 JS,怎么说呢?其实也算是函数式,它也引入了一些函数式的理念,但是它也没有做得很纯。 Astrian:一说到 JavaScript,联想到你刚才说的叫什么?在函数式编程里面函数是一等公民,其实 JavaScript 里面也是,如果你有做过 JavaScript 相关的开发,你就知道在 JavaScript 里面本身函数是可以被存储到一个变量的,反正就可以在整个代码里面变成各种各样的形态。 白羊:对,它可以变成参数传给其他的函数,这其实就是所谓的回调函数,也是非常常见的套路。 Astrian:JS 的回调函数也是被吐槽最多的一个,因为会造成大量的回调地狱。 白羊:是的,当然也有很多解决方案,但是相比来说,为什么大家去 Python 学的更多一点,就是那种纯入门,学 Python 会更简单一点。 白羊:其实 Python 并不比 JS 简单多少,但是问题是 JS 这个回调对于新手来说很难理解,因为 JS 也引入了很多函数的特性,有很多人想要在 JS 里去引入更多的函数式的特性,有一个非常常见的这个就是函数式的工具库叫做 Ramda,它就引入了很多乱七八糟的函数,就是一个小的工具库的函数,这里面引入了很多。 白羊:比如说像我刚才说的柯里化,就我随便写一个函数,我把丢到 Ramda 的柯里化里,它就可以变成我刚才说的那种单参数函数,返回函数的那种形式。 白羊:当然还有很多其他的东西,只能说可以凑合用,除了 Ramda 还有很多其他的工具库,就有人出了一些规范,规范试图来协调各种工具库,让这些工具库可以混用,这个规范叫做幻想领地,那个里面就规定了很多所谓的类型类这样的东西。 Astrian:类似于框架吗?它本身是一个框架。 白羊:不,它本身是一个文档。 Astrian:它就是一个类似于叫什么,JS 里面,JavaScript 里面的一个函数式编程的规范文档,民间自己整理的那种是吧? 白羊:对,比如说我想实现一个库,我如果能按照它那个文档去把我的库实现,我的库就可以跟其他的这些λ之类的东西混合使用,就不会出现问题。虽然大家已经做很多这样的努力,当然还有很多其他的 JS 的库,比如说什么 S,我只记得翻译过的名字,大概就是有,一个叫做避难所,还有一个叫做民间故事。 Astrian:什么鬼。 白羊:总而言之就是有这样很多的这些工具库,这些工具库都实现了一些函数式的结构,并且好像大部分都遵循了规范,可以互相使用。但即使这样你用起来还是会有很多不爽的地方,因为 JS 看起来它挺函数式的,但是你用起来你就会发现有很多屎的地方。 Astrian:所以其实如果说是真的想去接触那个函数式编程,最好还是原汤化原食地去找 Haskell 去看一下会比较好是吧? 白羊:对,但不幸的是 Haskell 写网页会非常的不舒服,因为它本来就是一个后端语言,你不可能去用 JS 的生态,你就更不可能去用什么 React 这样的东西,你难道要全部从头开始,这样就太费劲了。 Astrian:我觉得在座的各位也不太想用 JavaScript 之外的语言来写网页了。我觉得,对,一般来说,这些编程语言大多都是处理后端请求会比较多,我觉得。 白羊:是的,怎么说,其实后端的复杂度其实我觉得没有那么高,后端有什么?不就是一个请求来了,我去查一下数据库。 Astrian:这样子吗?原来后端在你这里是如此的不堪。 白羊:那些花里胡哨的什么线程这些东西,基本上都是 bug 的来源。(笑)大家也基本上都是这样想的,比如说什么云函数之类的东西,其实把一个接口简单的实现,就丢到云上。 Astrian:没错,类似于 Server-less 那种概念,我只需要编写一个接口的程序,我就可以把丢到云端,直接执行了。 白羊:就可以用了,所以你看多么简单方便,没有那些 Java 那些屎,我的上家公司写 Java,他们就有人非要在那里面搞线程,写的又跟屎一样,就天天死锁,就天天重启,最后也查不出来问题是啥,每次出了问题就得重启。 Astrian:太惨了。 白羊:所以不会写复杂的代码就不要写,不要瞎搞。 Astrian:好的,我们继续。接下来到哪了? 白羊:现在编程主要的困难其实在前端,尤其是一些很复杂的网页,我们已经出现了什么 Vue、React 这样的东西,但是它还有很多复杂的东西,所以我们又出现了什么 Vuex 与 Redux 各种各样的前端的框架。 白羊:这些框架其实我觉得它的本质逻辑就是把前端这个工作,再细分成为逻辑的部分和显示页面的部分。其实前端要处理的逻辑挺多的,包括请求后端,然后把后端返回的数据怎么样能够展示到屏幕上,如果后端返回的数据不对的话,还需要自己处理一下之类的,所以主要的复杂度就在这里。而 JS 又不是那么的好用,所以我们的页面就写的跟屎一样。 白羊:那么幸运的是函数式这一套东西也可以完全的跟 JS 的生态对接,当然不是用 JS 语言,有一个东西叫做 Elm,它就是很参考函数式的一个前端框架,但是它并不是引入一个库那么简单,它是一套新的语言,它最后能编译成 JS,最后能编译成网页。 Astrian:听上去就跟 TypeScript 的是一样的一种类型。 白羊:对,它的语法就跟 Haskell 差不多的,但是它比 Haskell 来说要少一点类型类,一个比较复杂的东西的能力,因为这个东西过于复杂,就把它去掉了,那就不允许你写,你只能写简单的。 白羊:但即使你写简单的也会方便很多,比 JS 的表现力来说。大家可以去尝试一下,之后我们会把地址放上。还有一个就是我现在的主力叫做 PureScript,当然这个也是一个新的语言,它的语法跟 Haskell 几乎是一样的,并且它有刚才说的 Elm 里边的没有的很多特性,比如说类型类之类的。 白羊:但是它比起 Haskell 来说还是缺少着很多插件之类的东西。但是它的能力已经很强了,几乎是可以用的了这样的感觉。 Astrian:几乎可以用和可用这个差别还是挺大的,你到时候说一下,它可能缺一些什么东西吧。 白羊:我觉得最不爽的地方是它的一些插件之类的,就 VS Code 的插件,什么格式化的插件之类的。 Astrian:就生态相对没那么好。 白羊:对,代码提示姑且还是有的,然后它也有自己的包管理器之类的,但是因为没什么人用,所以有很多都是那种被淘汰的,很久以前的库,你还得自己手动的重搞一下你才能用之类的。 白羊:还有一些比如说代码提示没有 Haskell 做的那么好,还有他的格式化工具也是一个非常垃圾的东西,就会出现一些不说 bug,但是会出现一些我希望它是这样格式的,但是它格式出来的效果并不是我想要的这种。 Astrian:这样的话就说明 PureScript 是不是在语法上会有一些歧义,如果听你这么说的话。 白羊:我觉得只是它格式化工具的实现有问题罢了。 Astrian:如果是这样的话,我第一反应可能会想到 PureScript 它本身是不是有些东西是有歧义的,它只是说两种写法都可以,只不过你是更 prefer 你自己的写法,它改不了这样子是吧? 白羊:没有,比如说我写两个函数写在一起,我希望它们中间不要有空格,但是我一保存它空格就出现了,就会出现这样的情况。 白羊:就是因为格式化工具太烂了,我就非常依赖这个东西,我已经不太想自己整格式了,我就配保存的时候自动格式化,然后因为这个东西太恶心了,所以每次把我搞得很火大,我就自己改了一下。现在 VS Code 上还能看到我改过的插件。 白羊:那么 PureScript 还有一个优势是它可以和 JS 无缝的对接,我可以把一个函数用 JS 实现,再把它导入到 PureScript 里面去。 Astrian:也就是说我可以把我之前写的代码直接调用到 PureScript 里面去,然后无缝直接这样子使用,是吗? 白羊:对,几乎是没有问题的。但是也可以反过来,我把 PureScript 的代码编译成 JS,然后再用 JS 载入进来。 Astrian:它编译后的结果是不是把你改成…… 为了压缩代码,然后会改掉你那个(变量)。 白羊:你可以配置它要不要压缩代码,但是它编译出来的结果其实虽然也算是可读,但是没有你想象的那么可读,它是那种疯狂嵌套的那种。 Astrian:所以最好还是编译之后将它整作为一个黑箱使用会比较好。 白羊:对对对。 [章节奏间音乐] Astrian:OK 既然讲到这里的话,我们就稍微往深入那么一丢丢,来讲讲有关于函数式编程更深层次的一些东西。 白羊:我觉得它的优势首先它是简单的,它没有那么花里胡哨的东西,没有那些类之类的东西,你的思维负担不需要那么大,你只需要着重你眼前想要实现的东西就可以了。 白羊:实现完之后它就是个黑箱,就可以把它摆在旁边,你想用的时候拿过来用,没有什么类、对象这些你得去考虑一个很庞大的结构。另外它是所谓的透明的,可以随意的等量代换,刚才也说过,对于重构代码来说就非常的方便,这个真的非常的爽。你可以试一试。 Astrian:我觉得我不试我就听你说,我就觉得你很爽,爽到升天的那种。 白羊:这样基本上我随便胡写,只要东西能够实现就可以了,之后想要优化的时候,我就可以把它优化得非常漂亮。另外一点它很适合于做并发,它是尽可能的不互相依赖的,不会出现一大坨东西,我们没有办法把它分开的情况。 白羊:这个时候我就可以做一些比如说并行操作,如果两个运算没有任何关系的话,我就可以把它放在不同 CPU 来计算,这样所谓的多核的并发能力就会很自然的,不需要你去搞什么线程之类的,我在编译器的层面就可以做这件事情。 Astrian:其实这些东西都是你刚才大致讲过的,我们再深入一点,比如说我在函数式编程下面,还有什么比较常见的一些理论可以给大家讲一讲的。 白羊:好,说到这里就更加复杂了,函数式里有一套非常复杂的叫做类型系统,它跟我们常见的那种什么 TS 或者说 C 的类型系统有一些区别。 白羊:总的来说它可以通过两个东西,一个叫做类型,一个叫做类型类来定义一个概念。在逻辑学里边我们有一个描述的方法叫做一个概念它有所谓的内涵和外延,所谓的内涵就是指这个东西的特殊之处是什么,外延是指这个东西它属于什么类里面。 白羊:比如说三角形是什么?三角形可以描述成平面上的三个非共线的点,也可以描述成比如说一个角度和一个点什么两角一边两边一角高中学的那些。 Astrian:对,如何确定一个三角形的那些,大致就是说不在一条线上的三个点组合起来就可以是个三角形。 白羊:我们可以有很多种方法都可以描述一个三角形,为什么它被称为一个三角形?是因为它是这样的,是这三个不共线点组成这样的。外延是什么呢?外延是指它属于什么,比如说我们说三角形它属于一个图形,接下来我就可以再规定所有的图形都会有面积这样一个属性,显然三角形也就有面积,接下来我还可以再定义圆形,圆形的内涵是什么?可能是一个圆心和一个半径可以组成一个圆之类的。圆我也可以说它的外延也是一个图形。那么圆就也有面积。 白羊:(我们)就可以通过这样的一些描述,我们在函数这里有一些类型系统的规则,可以描述出这样的东西,你就可以发现你在所谓的建模就并不是去写电脑能够理解的一行一行执行的东西,而是去构建一个所谓的比较虚拟的世界,然后在这个虚拟世界里面解决问题,或者做你想做的事情这样的感觉。这就是所谓的建模,它有这样一套类型系统能够帮助你表达建模的逻辑。 Astrian:它跟那个函数式编程的关系在哪里呢?就是函数编程类型系统,就是使用了这样的一个概念是吧? 白羊:准确的说是 Haskell 这个类型系统使用了这一套类型系统来描述,那么这一套类型系统它叫做 System F。System F 有一些不好处理的地方,实际实现的是一个叫做 HM 的类型系统。 Astrian:这还挺复杂的。 白羊:那么另外它还有一点还是这个类型系统的特点,就是谓词的上下文多态,比如说把 1 和 1 加起来,你知道它等于 2,我说你把 a 和 b 加起来,你知道它等于 ab,我们都用了「加」这个词,但是它在不同的上下文里它的意思是不一样的。 白羊:在 1+1 这个例子里,它的意思是数学的加法;在 a 加 b 的这个例子,它的意思其实是字符串拼接,就是说在不同的上下文里,谓词它的意思是不一样的,那么在这个类型系统里也可以表达这一点。 Astrian:所以他就会有类似于一个二义性的问题,不过二义性的问题的话,那么它是怎么去处理?因为如果你处理不当的话就会造成歧义,一个数字不能加一个字符串的。 白羊:所以它有一套比较严格的类型系统,就是所谓的类型类来实现这一套逻辑,这个东西挺复杂的,不是一下子能够讲清的。 Astrian:如果大家感兴趣的话,我们会再专门找几期来专门来聊一聊了不如。 白羊:好,还有一个非常常见的模型叫做函子单子模型,比如说我现在说一个值它是一个 int 类型,那么你可以知道这个值就是一个数字,就是一个整数,但是我可以在前面加一个修饰词,比如说我可以在前面加一个 maybe,那么它就变成了一个可能是数字,也可能是空的一个东西。 Astrian:那就相当于是其他比如 Python 或者 Swift 里的一个 Optional 的关键词了。 白羊:你说的这个结构几乎就是从函数式的 maybe 翻译过去的。有了这种能力的话,把它当成一个 int 的值来什么加减乘除这都可以。但是刚才不是说它可能是空的吗?如果它是空的,那么我最后取答案的时候,我得到的就是一个 nothing,就是一个啥都没有的东西。如果它不是空的,我最后就能取到一个正确的计算结果,这样我就不用一点一点的去每次运算这些它是不是有问题的,我就可以不用管它,直接是用在到最后再来解决可能是空的这个问题,就会省很多的事儿。这就是所谓的上下文的力量,就是把一个类型放在一个上下文里,它就会有一个新的意思这样的感觉。 白羊:那么还有一个非常好用的上下文叫做非确定性,比如说我们说把两个 int 相加,它可以得到一个新的 int,那么我们可以有一个结构叫做非确定性的 int,常见的描述其实叫做数组,你考虑一个数组,这个数组里边的元素是 1、2、3 这三个元素,那么你当然可以把它理解为数组,你也可以把它理解为这是一个 int 值,它既可能是 1,又可能是 2,又可能是 3,就可以做这样的事情:比如说,把一个既可能是 1,又可能是 2 的值,加上一个既可能是 3,又可能是 4 的值,得到的结果就是他们所有的可能的总和,就是 1+3、1+4、2+3、2+4。 Astrian:所以就会输出有 4 个值的数组类似于。 白羊:对,但是你不要把它理解为数组,把它理解为非确定性的值,这样的话就会方便很多。(Astrian:它就是一个集合)这就是另一种上下文叫做非确定性的上下文,上下文这个理论所谓的外层的模型就叫做「函子—单子模型」,大概就是这样,当然还有很多更加复杂的东西也就不纠结了。 [章节奏间音乐] 白羊:最后我来在我再吟唱一些魔法。 Astrian:再吟唱一些魔法,还有什么魔法赶紧吟唱了。 白羊:这里我就不详细讲解了,想了解的同学……(Astrian:抛砖引玉一下)这里给一些关键词吧,你能够去搜索之类的。 Astrian:就抛砖引玉一下,我们后面会把这些相关资料再放到 Show Notes 里面去,大家有兴趣的话,也可以自己去研究研究,如果大家呼声比较高的话,我们会再抽一些概念出来讲。 白羊:首先函数式编程里有一个概念叫做 ADT,ADT 这个词其实是个歧义,在这里我想说的意思叫做代数数据类型。为什么叫代数数据类型呢?因为它引入了数学里边的加法和乘法的类型在这个概念里。举个例子,比如说现在说有一个类型,它可能是 int 也可能是字符串,我可以构造出这样的一个类型,叫做联合类型。 Astrian:它有可能是 A 类型,也有可能是 B 类型这样子。 白羊:对,现在一个问题来了,我联合类型里边有多少种可能的值?答案就是有 A 类型的所有的值加上 B 类型的所有的值。所谓的联合类型叫做一个加法的类型,它把两个类型加起来了,相对的叫做乘法类型,考虑一个对象,这个对象里面有 A 和 B 两个键,A 键对应的值的类型是 A 类型,B 键对应的值的类型是 B 类型。考虑 AB 组成的对象,它所有的值有多少种可能,就是 A 乘 B 中和,这就是所谓的乘法类型,在这个基础上可以再去推演函数的类型,泛型的类型之类的,都可以套到数学公式上,非常的神奇,这套理论叫做 ADT,就叫做代数数据类型。 白羊:如果你是用 TS 的话,这个也是非常明显的,但 Java 里边并没有这样的设计,TS 的类型就跟 ADT 有相当大的关系。刚才说的 Haskell 也是 ADT 作为主要来构建的。 白羊:再说一些常见的魔法,比如说函数式这一套东西,它有一个理论基础叫做 λ 演算,可能也有很多同学听说过,具体是啥我就不讲了,然后在下面会有一个叫做组合子逻辑,就有一个叫做 SK 组合子,这个组合子跟 λ 演算是等价的,它也是一套数学理论,挺有趣的。 白羊:在 λ 演算之后,人们引入了所谓的有类型的 λ 演算,在这个上面发展出了一个理论叫做柯里-克里霍华同构。在这一套架构里面的程序的类型,可以和逻辑学里边的逻辑定理联系在一起,它的核心逻辑是这样子的,就是我给定一个类型,然后我去问这个类型你能不能写出一个对应的 λ 演算里边的值?这个问题叫做类型居留问题。 白羊:最后的答案是如果这个类型里边有值的话,把类型翻译成一个逻辑定理,这个逻辑定理一定是成立的,就非常的神奇,所以就有人拿这个东西去做一些数学上的证明之类的,比较有名的东西叫做 COQ,这里边又有一些非常复杂的东西,刚才说的逻辑定理,它要放在一个框架里叫做直觉逻辑,这个直觉逻辑有一个非常常见的表述叫做希尔伯特演绎系统,不过这些都比较复杂,就不在这里深入讲解了。 白羊:刚才说 System F 是有类型 λ 演算的一个进阶版本,在这个之上还有一个东西叫做 λ 立方体,就有一些跟更加复杂的类型系统,这些就是比较前沿的部分了,这里我也不是特别的理解,给大家自己去查吧。 Astrian:我觉得光听你说就已经够复杂的了。 白羊:是,这些超级复杂的。 Astrian:这些东西,我们就暂且按下不表,我们如果以后有机会的话,也许可能会在节目里面。 白羊:对对,然后如果有什么问题,我们加群可以来跟我探讨,我可以手把手的带你入门函数式好吧? Astrian:那今天的节目就先到这了。 白羊:好好。 Astrian:你现在正在收听是 Echo.js,Echo.js 是一档以编程和技术为主要话题的播客栏目,我们推荐使用泛用型播客客户端收听 Echo.js,你也可以在 QQ 音乐、小宇宙、Spotify、蜻蜓 FM 等音频平台收听我们的节目,我们的官网地址是 echojspodcast.com,你也可以关注我们的 Telegram 频道,地址是 t.me/echojspodcast,你也可以通过留言、邮件和加入 Echo.js 听众 QQ 群的方式与我们联系,联系方式详情见官网。我是 Astrian。 白羊:我是白羊。 Astrian:那我们下期再见。 [结尾音乐]