一篇有关函数式编程的形象生动教程

18-11-12 banq
                   

函数式编程(FP)与面向对象编程(OOP)的诞生的时间差不多,但它最近才最受欢迎,特别是在JavaScript社区中,为什么?

我在00年代早期就学麻省理工学院。计算机程序的体系结构和解释(SICP)是我的教科书。所以我的第一个正式学习的编程语言是函数性的,然后我在工业界工作了十多年,几乎没有多少时间考虑过使用FP。当得知大学的教科书被认为是“函数性编程圣经”时很震惊。

别误会我的意思,这是一本很好的教科书。我敢肯定它会让我成为一个更好的程序员,但这并不是我需要经常在Java / ActionScript / PHP / Python / Ruby / JavaScript等职业生涯中使用的东西。

然后我在Wyncode Academy任教四年,并发现自己试图向新手解释FP概念。这很难 - 比OOP更难。

为什么FP比OOP更难?

相关问题:为什么FP需要这么长时间才能流行起来?

我们编码社区需要解决为什么FP难以教授。像一个宗教说教一样传播FP重复了同样的错误,导致FP长期在这个行业中萎靡不振。

对FP的许多介绍都遗漏了一些东西。它不仅仅是一种替代编程风格。这是一种新的思维方式,它代表的类似的技巧也可以与来自OOP背景的更有经验的程序员一起使用。

我在Wyncode中使用的技术之一就是讲述一个难以理解的概念时,首先让学生了解概念的背景以及 这个概念的大致样子(big picture),之后就更容易解释技术细节了。

大致样子(big picture):历史背景

计算机是如何工作的?最常见的(流行的?易于理解的?)计算模型是图灵机。FP程序员抱怨的状态正在图灵机中正视着我们。用于操作该机器的算法表示不同状态之间的转换,例如从打开 / 关闭 (1或0)的一些盒子到打开 / 关闭的一些其他盒子。

如果我们试图想象两个图灵机同时在同一磁带上运行,我们就可以开始理解为什么“共享状态”和OOP中的并发性都是难题。

图灵机是一台通用机器。它可用于解决每个可有效计算的数学和逻辑问题。这是个简单的操作集: 向左移动,向右移动,写点,读点,擦除点(增删改查CRUD)。 足以(给予足够的时间和资源)来解决宇宙中的每个数学问题。

这就是艾伦·图灵在1936年所证明的。

在许多方面,图灵机是计算机“工作”的方式。但这也是计算机的工作原理。但是还有另外一种理论:

电路是不同的计算模型。每个逻辑门(AND,OR,NAND,NOR,XOR等)都是纯函数。他们接受输入并产生没有副作用的输出。如果我们只有能够创建和组合这些“函数”,我们还可以解决宇宙中每个可解决的数学问题。这也是阿隆佐·邱奇在1936年证明的。

所以我们有两种不同的计算模型:图灵机​​的0和1(对象)和阿隆佐·邱奇用逻辑门(函数)构建的lambda演算。哪一个是正确的?

有一段时间,关于抽象图灵机是否能解决与lambda演算相同的数学问题(反之亦然)的辩论。最终他们被证明是等同的。

等同意味着它们同样强大。任何可以为图灵机编写的算法也可以使用函数编写。因此,任何可以用图灵机软件编写的程序也可以用电路硬件表示。

“硬件编程”是什么意思?我们可以看到专用集成电路(ASIC)中体现的“硬件编程” 。可以创建“编程”的电路,以便快速完成一件事,比如我的比特币下棋

我们现在有两种编程选择。硬件更快,软件更慢。在软件中犯了错误?只需点击删除键,然后重试。硬件出错?是时候抓烙铁了。这是一个经典的工程设计权衡。

因此,假设我们有一个以OOP风格编写的算法,我们希望将其转换为ASIC硬件编程。以FP风格重写程序可能是一个很好的策略,因此它可以更好地映射到底层电路图。

面向FP的语言往往看起来像电路。特别是Unix,Elixir,F#,JavaScript和其他“管道操作员” 使代码看起来像电路图:输入进入左侧,流过许多“门”(管道)直到它们被转换进入右边的最终输出。某些语言(|>)使用的管道运算符看起来像逻辑门,这可能不是巧合。

大致样子(big picture):哲学

我拿到了我的CS学位的哲学辅修,所以我着迷的一件事是这两个研究领域的交集。我发现在教授新程序员时,特别是那些具有人文学科而不是STEM背景的程序员,有助于在这两个领域重叠处讨论思想。

FP中一个哲学上重要的概念是“函数相等 functionally equivalent”。

也许证明这种等价性的最好例子是汤姆斯图尔特的伟大文章“从无开始编程”。这里借用他对数字如何完全用函数表示的方式来解释编码:

首先将数字0的概念定义为接受函数参数但不对其执行任何操作的函数。

# Ruby
ZERO = -> (func) { 
  # does nothing
  func
}

类似地,我们可以将所有自然数定义为接受函数参数的函数,并将它们调用n次。

ONE = -> (func) {
  # calls it once
  # same as func.call()
  func[];
  func
}

TWO = -> (func) {
  # calls it twice
  func[]
  func[]
  func
}

要测试这些“函数数字”,请将它们传递给测试函数。

HELLO = ->() { puts "hello" }

# same as: ZERO.call(HELLO)
ZERO[HELLO] # nothing displayed
ONE[HELLO]  # one "hello" displayed
TWO[HELLO]  # "hello" twice

这种函数数字符号表示法其实很难玩和调试。因此,为了更容易使用,我们可以定义一个函数,将这些函数数转换为我们习惯使用的对象数字。

# convert number function into number object
def to_integer(func)
  # count how many times counter is called
  n = 0
  counter = ->() { n += 1 }
  func[counter]
  n
end

p to_integer(ZERO) # 0
p to_integer(ONE)  # 1
p to_integer(TWO)  # 2

这个转换器创建计数功能并将其传递给数字函数。ZERO函数将调用它为零次,ONE函数将调用它一次,等等。我们跟踪计数器被调用多少次以获得结果。

对于这些函数数字定义,我们可以实现添加各种功能:ADD SUM统计等。

ADD = -> (func1, func2) {
  -> (f) { func1[func2[f]] }
}

sum = ADD[ZERO, ZERO]
p to_integer(sum) # 0

sum = ADD[ZERO, ONE]
p to_integer(sum) # 1

sum = ADD[ONE, ONE]
p to_integer(sum) # 2

如果TWO调用一个函数两次,那么ADD[TWO, TWO]将返回一个调用其参数四次的函数数字(函数数字FOUR)。

这是一个令人费解的方式。当我看完“从无开始编程”这本书时,我感觉这是一个巧妙应用基本计算机科学概念的有趣书籍,但不是我在日常工作中可以使用的东西。

而这正是我(我怀疑很多其他人)对FP一般的感觉 - 它很聪明,但似乎没有用。这是我们需要解决的问题。

所以,教FP更好的开始地方是“黑客帝国3:矩阵革命”。

什么是矩阵与FP呢? 这部电影告诉我们:我们没有证据证明我们周围的世界是真实的。也许世界上有实际的物体,或者我们只是在罐子里的大脑

因此,至少有两个相互矛盾的理论,例如,第一个是什么。我们可以与之交互(触摸和感觉)是一种(名词,对象)吗?或者它是一个动作(一个动词,一个函数),一个作用于世界的东西,但没有实在的呈现?

函数数字one是数字1的模拟,它是在函数等同 functionally equivalent于对象数字one,意味着它可以做对象数字one做的任何事情。

但是OOP中的对象“存在”并不是真的“存在”。这是一个矩阵模拟。它没有固有属性 - 它不是x,它只是像x。

打个比喻,你坐在真正的椅子上只是一种用力按压你的身体功能?“椅子”可以是存在于现实世界中的椅子这个对象,也可以是一种椅子功能:一种希望用舒适的力量推动你的功能,并没有潜在的客观基础。

想想颜色,红色美味的苹果真的是红色的(形容词、描述名词)还是扮演红色(动词)?颜色是真正的底层苹果对象的固有属性,还是仅仅是当光照在它上面时执行函数的动作结果呢?苹果真的还是模拟的呢?

解释这个哲学概念的难度是解释为什么FP难以教授的好隐喻。为了帮助学生理解,首先要开放他们的想法,让世界完全由“功能/函数”组成。从样子big picture概念开始,然后转向世界的FP模型:它们与OOP表示的区别以及它们如何具有相同的结果。(banq注:从概念开始转向有两个:一个是名词,一个是动词,FP是转向到动词,而我们日常是转向名词,但是很多人没有意识到这种转向,所以很难转到动词方向去接受函数编程)

 

                   

3
sinaID75194
2018-11-13 15:40

顶,深度好文。

sinaID75194
2018-11-13 16:09

顶,深度好文,受益匪浅。

我一直认为,软件世界是对现实世界的抽象。

而现实世界里具体的物体,对应的是语言里的名词,往往是具有一种或几种功能(Function?功能是不是也可以叫函数)或某几种功能的表述。

但函数足够抽象且复杂,描述世界还需要名词,所以在OOP领域,主张是,一切皆对象,对象的属性描述名词、对象的方法描述动词或行为。

世界完全由“功能/函数”组成,FP是把名词转成动词,用函数描述世界。

那这么说,FP和OOP岂不是有关联的地方?

还是有很多迷惑的地方。

banq
2018-11-13 16:33

楼上正解,名词和动词是两种不同视角,都是需要的,这其实是一种系统模型,名词+动词= 图灵机 + 函数式 =对象(状态) + process ,有一种系统模型语言OPM表达的就是这个思想,OPM是可以表达除了软件以外的系统,比如火箭宇航等复杂系统:

具体可见MIT的课程视频:https://www.youtube.com/watch?v=CTVFDb44ses&t=5490s

在事件驱动编程中,事件代表动词,事件建模就是首先找动词,区别于传统DDD首先找名词实体和结构,UML分结构性(类图和组件图)和行为性(用例和顺序图)两种,也是名词和动词之分,GoF设计模式中结构性的都是关于名词的,行为性模式是关于动词,数据表+SQL也是一种结构性名词+动词,业务+流程也是名词+动词。

sinaID75194
2018-11-19 11:57

楼主有没有微信群,或者其他兴趣讨论组啊,求拉。