写点什么

从历史讲起,JavaScript 基因里写着函数式编程

作者:掘金安东尼
  • 2022-11-02
    广东
  • 本文字数:3604 字

    阅读完需:约 12 分钟

本篇序言

本瓜很喜欢看历史,读史可知兴替、使人明智,作为程序员看“技术的演替历史”同样如此。过程是越看越有味,仿佛先贤智慧的光照亮了我原本封闭的心,每每只能感叹一个“服”字。所以,专栏第一篇打算先从技术历史讲起,从函数式编程的渊源讲起。


看完本篇:


你会知道为什么有人会说 “计算机是数学家一次失败思考的产物”


你会知道为什么 “ lambda 演算定义函数有效计算”


你会知道编程概念中 “闭包最初是如何形成的”


你还会知道为什么标题要说 “JavaScript 基因里写着函数式编程”


话不多说,开冲了~ 🏃‍🏃‍🏃‍

一、数学之美

函数式编程的历史最早可以追溯到 1930 年,一个叫 丘奇(Church)的人,提出了 λ (lambda)演算,这是所有函数式编程语言的基础。


那这人为啥要提出这个演算?1930 年这个时间比世界上第一台计算机诞生的时间都还要早 16 年。提出这个肯定不是因为计算机编程。


没错,他是为了解决一个数学问题。


这个数学问题是:


著名的希尔伯特第十问题—— 判定问题 (1900 年提出)


我们不妨来“浅看”一下这个数学问题,可以说这个问题促使了计算机的形成。


什么是希尔伯特第十问题之判定问题?


本瓜尝试用通俗的表达解释一下:


很简单,有下列这样一个方程:



其中所有的数(a<sub>j</sub>、b<sub>j</sub>、c)都是整数,求:能否找到一组 x<sub>j</sub> (全部为整数)的解?


乍一看这个公式有点费解。。。



其实我们可以构建一个大家都熟悉的实例,保证一看就明白了~


请问:x<sup>2</sup>+y<sup>2</sup>=z<sup>2</sup> // 这个方程,有整数解吗?


我敲,这不就是勾股定理吗?勾三股四弦五,老祖宗在西周时就发现了。


符合判定问题的实例方程还有很多,比如:裴蜀等式、佩尔方程、四平方和定理、以及著名的【费马大猜想】等等。



噢!希尔伯特提出判定问题,旨在“一劳永逸”,如果这个问题被解决了,那么它的子问题也都能被同样解决。所以,在 1900 年到 1930 年之间,以希尔伯特为代表的数学家们 试图构建一个自动化定理证明的系统,让公理系统内的所有命题都能用一套既定的规则得以证明或证伪。


说白了,就是这群数学家也想偷懒,证明各类数学公式已经累了,想搞点自动化的通用流程,能够用通用流程去证明或证伪数学难题。


大家都在前赴后继的尝试解决这个问题,直到 1930 年后,出现了 哥德尔、图灵、丘奇 这些人,他们几乎在同一时间,但又在不同角度对这个问题作出了解释。


更为神奇的是,最终证明他们的结论竟然是等效的。


哥德尔不完备性定理中递归函数 == 图灵完备 == lambda 演算


他们彻底解决了希尔伯特第十问题吗?


很遗憾,并没有。


不过在这个过程中,他们搞清楚了一个很重要的问题,一个对计算机科学至关重要的元核心问题:


什么样的函数是可以有效计算的?!


在这之前,数学家们对于这个问题并没有一个普遍结论,只知道一些最简单的函数,以及通过简单规则将简单函数组合起来的函数(比如加法),是可以有效计算的。


这种感觉像是无心插柳,本来大家是冲着解决数学公式论证问题去的,最后不约而同的得出了“函数可有效计算”的定义。这个定义由此发展,成为了 21 世纪最具颠覆力量的学科 —— 计算机。


所以才有人说:计算机是数学家一次失败思考的产物。


数学的局限也会造成计算机的局限。


不过依然无法掩盖数学之美,美在它足够基础,但又隐藏着巨大的能量,影响着万事万物。

二、lambda 演算核心

各位,你有想过,什么样的函数是可以有效计算的?


如果由你定义,你会从怎样的角度去思考?


  • 由于本篇重点是讲函数式编程起源的 lambda 演算,所以哥德尔和图灵的解释不作展开,在文尾有相关文章推荐,可自行了解。(尤其是图灵机,一定多看看、体会体会)


丘奇给出了它的观点:


有效计算的函数指的是:函数每一步都可被事先确定,而且该函数可在有限的步数之内生成结果。


通俗来理解,“有效”即要在有限步骤内产生确定的结果。


天才的丘奇给出了 lambda 演算表达式:


lambda x . body


其中 x 是输入的参数,body 是运算过程,意思是 x 经过 body 的运算,然后返回结果。


lambda 演算的伟大之处在于它非常简洁,揭示了计算的本质。



细看 lambda 表达式,你会发现函数只能接受一个参数,如果我们需要传两个参数呢?


其实也是能实现的,如下:


lambda x. ( lambda y. plus x y )
复制代码


这就是沿用至今的函数式编程 柯里化 思想,传入参数 x,经过运算体 body:lambda y. plus x y 的运算,body 又是一个 lambda 运算表达式,入参是 y,新的运算体是 plus x y


这种简化的设计,让我们无需过多的语法,便能实现接收多个参数。


lambda 演算核心还有两条重要的规则:转换 和 规约


  • 转换


转换的意思是:变量的名称并不重要,比如以下两种写法是等效的,相当于变量名只是形参而已。


lambda x. ( lambda y. plus x y )
lambda y. ( lambda x. plus x y )
复制代码


  • 规约


规约的意思是:我们可以对这个函数体中和对应函数标识符相关的部分做替换,替换方法是把标识符用参数值替换。


举个例子:


(lambda x . x + 1) 3
// 规约后
3 + 1
复制代码


这样写意味着 3 是形参 x 实际的值,数值“3”可直接取代引用的参数“x”,规约后即为 3 + 1


(lambda y . (lambda x . x + y)) q
// 规约后lambda x . x + q
复制代码


首先 q 是形参 y 实际的值,规约后,实际上就是求 lambda x . x + q


规约远能做的很多变化,正是由于规约的存在,让 lambda 演算可以实现递归,才让它可以等效于图灵完备。


我们再来概括一下:


  1. lambda 核心表达式:lambda x . body,简洁而优雅;

  2. 入参只有一个,可以通过柯里化来实现接受多个参数;

  3. lambda 演算的“规约”规则是它实现复杂运算的重要机制,由繁化简;

  4. 多问一句:把函数作为 body 返回,不正是 JavaScript 高阶函数的意思吗?


可见,现在很多我们觉得稀松平常的一些用法,其实早在近 100 年前就被提出来了,不可谓不震撼。


三、JavaScript 的基因

说了半天,终于来到了我们的 JavaScript,相信大家接触 JavaScript 之初都会被“闭包”这个概念搞得有点蒙,为什么要这样设计?我平常又确实用不上,好不容易学了个防抖、节流函数,你就不要再继续追问“什么是闭包了”。


兄弟,有福了,这次带你见识最初的闭包是如何产生的!


闭包的概念,在计算机诞生之前就被设计出来了,没错,还是来源于我们的 lambda 演算。


lambda 演算规定:


如果一个标识符是一个闭合 lambda 表达式的参数,我们则称这个标识符是被绑定的;如果一个标识符在任何封闭的上下文中都没有绑定,那么它被称为自由变量。


比如:


lambda x . plus x y
复制代码


在这个表达式中,x 是被绑定的,因为它是函数定义的闭合表达式 plus x y 的参数。而 y 是自由变量;


再比如:


lambda y . (lambda x . plus x y)
复制代码


在内层演算 lambda x . plus x y 中,x 是被绑定的,y 是自由的;而在完整表达中,x 和 y 是都是被绑定的:x 受内层绑定,而 y 由剩下的外层演算绑定。


这正是 JavaScript 闭包最初的雏形, 内部函数保持着对函数外部变量的引用。这里“被绑定的”意思就是变量不能被清理的,是以后会被用到的。


神奇吗?闭包早于计算机诞生,仿佛就像打火机早于火柴发明一样,让人有点意外~


<hr>


好了,最后说一说:为什么 JavaScript 基因里写着函数式编程 ?


这一段历史,应该很多工友早烂熟于心,网景公司想给 HTML 加一个脚本语言用于改善交互,于是招来了 布兰登·艾克,这老哥 10 天就把这门语言的框架设计好了。它思想上基于 Self 语言和 Scheme 语言,语法上和 C 语言相似。


我们再看这里的 Scheme 语言,它其实就是一门堂堂正正的函数式编程语言,它是第一大函数式编程语言 Lisp (1958 年)两种方言的其中一种。而 Lisp 则来源于 lambda 演算,来源于 丘奇,来源于那个解决 1900 年数学问题的意外收获!


时间线是这样的:


  • => 1900 年希尔伯特数学问题

  • => 1930 年丘奇 lambda 演算

  • => 1958 年 Lisp 语言

  • => 1975 年 Scheme 语言

  • => 1995 年 JavaScript 语言


知道从哪里来,才能知道往哪里去。


所以,朋友们,我们现在所用的 JavaScript,基因里有一个重要的组成部分是函数式,把函数放在第一位、关注输入输出、参数柯里化、高级函数等等,在近百年里逐渐演进。前段时间,看到一篇文章,JSON 之父吐槽说:现在我们更关注于把 JavaScript 的使用规模扩大,而不是关注怎样使这门语言变得更好。然后导致他建议退役 JavaScript,我大受震撼。或许,如果某一天,ES 版本迭代关注点只有:又新增了几个语法糖,而忽略了这门语言最初的设计思想,忽略去完善它,那真有点可惜。


OK,以上便是本篇分享,专栏第 1 篇,希望各位工友喜欢~ 欢迎点赞、收藏、评论 🤟



关注专栏 # JavaScript 函数式编程精要 —— 签约作者安东尼


我是掘金安东尼 🤠 100 万人气前端技术博主 💥 INFP 写作人格坚持 1000 日更文 ✍ 关注我,安东尼陪你一起度过漫长编程岁月 🌏

推荐阅读

发布于: 刚刚阅读数: 3
用户头像

安东尼陪你度过漫长编程岁月~ 2022-07-14 加入

真正的大师,永远怀着一颗学徒的心(易)

评论

发布
暂无评论
从历史讲起,JavaScript 基因里写着函数式编程_前端_掘金安东尼_InfoQ写作社区