haskell趣学指南_Matrix技术分享| Haskell与函数式编程简介

57cd99d789b362ef2fceea8f83fe9af9.png

本期分享会由庄天衢同学为我们带来

Haskell与函数式编程简介

一、从命令式到函数式

所有的程序代码,都是描述客观事物的模型。

不同的编程范式,反映了人类认识事物的不同方法。

  • 汇编指令编程

    其每条指令都与电路的某种过程相对应,它是基于硬件建模的。

2d3a6e15c1e853a65a79a9ada88126a7.png 36c6dc24839adbd58d257b55c13e23b8.png
  • 面向过程编程

    其每个函数都与流程图中的某个方框相对应,它是基于流程建模的。

9c63e36428af35f78edcfc4b0de2b9fc.png 540606134fa8b81a50477fd954945f73.png
  • 面向对象编程

    其每个实例都与现实中的某个实体相对应,其每个class与现实中的某种类别相对应,它是基于实体及其从属关系建模的。

d87d85a1de12c436ad9014e30ffab378.png ce6c60393a2a7fd3bc3e11dea07e532a.png

纵观人类主流编程范式的发展史,可见其发展规律是越来越抽象。面向对象编程发展到下一阶段就是泛型编程。然而,面向过程编程还孕育出了一个支流——函数式编程。它的理念过于超前,抽象程度甚至在泛型之上。

函数式编程有多抽象呢?这么说吧:它是基于数学建模的,从下图可见一斑:

cfde345309ed9f1fc35b72dddc8990ec.png

“得其大者可以兼其小”,函数式作为最抽象的编程范式,也兼具面向对象、泛型的一些特性(甚至支持得更好)。

目前主流的编程语言,都属于“命令式语言”,它们共同的特点是:用一段指令序列,要求计算机一步步完成,得出最终答案。而“函数式语言”则完全不同,它是用一段数学定义,告诉计算机答案是什么,而暂时不去求解。简单来说,前者告诉计算机“做什么”,后者告诉计算机“是什么”。

ad4da0a38a8675d892f16d43837a1368.png 99858712ef062ddb16d6c689bba1db22.png

为了说清楚二者的根本区别,笔者要引入“副作用”这个概念,它指的是代码对程序内部状态或外部状态造成的改变,请看例子:

d66ffeea21021cdff8e272a4b129c915.png

PrintHelloWorld_1是一个有外部副作用的函数,它引起了函数外部状态的变化。调用这种函数前后,程序的其他部分的表现可能会发生改变。

a49f06808606c101e7d02523da7a6d30.png

PrintHelloWorld_2是一个有内部副作用的函数,它引起了函数内部状态的变化。在不同的时间调用这种函数,它可能会有不同的表现。

9531f61e6f4f89b33301f439f056bd74.png

Hello是一个没有副作用的函数,它不会对函数的内外状态有任何的修改。调用这种函数,它的表现只与你的传入参数有关,无论你什么时候调用它、调用了多少次。

特别地,如果这种无副作用的函数无需传入任何参数,那么它的返回值就是固定的,那么它其实就是一个常量。在笔者看来,常量就是无参的无副作用函数。

讲清楚了副作用这个概念之后,笔者就可以给出一个结论:代码有没有副作用,是命令式语言与函数式语言的根本区别。在函数式语言中,所有的函数都是没有副作用的。

二、纯函数式编程语言Haskell

说起函数式编程语言,人们首先就会想到Haskell,因为它是最纯粹的函数式。Haskell的研发工作始于1987年,当时一群天才的数学精英齐聚一堂,商讨设计一种贴近他们数学语言的编程语言,直到1999年Haskell Report的发布,才标志着稳定版本的最终确定。

让我们来看看,Haskell是通过哪几招彻底消灭代码的副作用,实现纯函数式编程的。

第一招:程序即函数(数学意义上的)

2dc5208cdbacb6aea044a3991e566f4c.png

Haskell要求每一个函数都按照数学的风格来编写。每个函数应该指明从哪个集合映射到哪个集合(泛型除外),而且必须有唯一确定的返回值。最重要的是,函数的返回值只能与传入参数和字面常量有关。这种设定保证了所有函数都是无副作用的。

第二招:绝不修改数据,而是给你新的数据

981019627c62e75c199819fa454665d4.png 9c2c9dc886049780e3fc871c947461b3.png

在命令式语言中,你要交换两个变量的值,就必然引起对变量的修改,也就必然伴随着程序状态的改变,也就必然伴随着副作用。因此交换变量的副作用是无法避免的。

但Haskell就不这样。当你要交换两个变量的值时,你需要用元组的形式把它们打包起来,Haskell的交换函数会接收这个元组,然后还你一个新的元组,后者的元素顺序与前者相反,而前者并没有被修改。这种“交换即映射”的设定消灭了修改变量引起的副作用。

在Haskell中,变量一经赋值就不可以再被修改了。如果你对它不满意,请构造一个新的。说白了,在Haskell中并没有变量这个概念,只有映射和常量(无参映射)。这种设定保证了处理数据的代码都是无副作用的。

第三招:IO操作与非IO操作的分离

IO操作有副作用吗?答案是肯定的。想象一下,当计算机执行PrintHelloWorld函数时,你的屏幕发射出光子,撞击在你的视网膜上,引起了你大脑中的一系列变化……IO操作的副作用其实是很严重的——它所改变的不仅是计算机的状态,而且是外部真实物理世界的状态。

Haskell有办法消灭IO操作的副作用吗?很遗憾答案是否定的。因为一旦消灭这种副作用,我们编写程序将变得毫无意义。Haskell不会做这种自毁于奥卡姆剃刀的事情,它选择了让IO操作与非IO操作相分离。

7cf1246e0c3c39ac626db04ec48dea14.png

图中所示的程序让用户输入一个字符串,然后打印这个字符串两次。main函数具有IO功能,因此它是一个“不纯净的”有副作用的函数,但它所调用的twice是一个“纯净的”无副作用的函数。Haskell让这两类函数严格分离,从而保证了IO操作不会污染你的代码。

三、把函数作为一等公民

前文说到在Haskell中不存在变量这个概念,那么函数就成为了“一等公民”。什么是“一等公民”呢?就是可以赋值给同类、可以作为参数传递、可以作为子程序返回值的东西。

前两种特性较为常见,C++的函数也可以通过函数指针来实现。这里重点讲讲第三种“作为子程序返回值”,Haskell的函数是通过“柯里化”来实现这一点的。

柯里化是数学家柯里提出的一种思想:把多元函数视为一元函数

这个概念比较深奥,我们来看一个例子:

f9d57c71dd1932e9d2128abc02f7c8c4.png

图中定义了add函数,它是一个二元函数。你传入两个常量1和2,它就会返回一个常量3。如果你只传入一个常量1,它返回的是什么?这种返回好像没有意义耶。

数学家柯里认为,这时候它返回的是一个函数,只不过是个一元函数。你再传入一个常量2给这个一元函数,它就会返回2加1之后的结果。这正是柯里化的含义:所有的多元函数都是一元函数,它们接收一个常量,返回一个(相对于自身更小的)函数。

让我们换一种函数头的写法,让这个事实更加清晰:

eb3f67b0dccfd364f307f4cb2a6dafce.png

这个事实意味着:当我们调用一个函数时,不一定要把所有的参数都确定下来。我们可以先传入一部分参数,得到一个“返回值”(这个“返回值”是有柯里化意义的,它是一个更小的函数),当最后要展示结果时,才把参数补充完整。

这种柯里化特性使得程序员可以专注于函数的传递、复用、组合,而不被无关紧要的数据传递干扰心神。如下图所示,程序员在编写addOne函数时,他只关心本函数与已有的add函数之间有怎样的关系,而毫不考虑传入的数据x与本函数的返回值有怎样的关系(甚至他都不必把x写出来):

63785204fbdc98ef51e539c3ceea7779.png

函数的组合也变得毫不费劲,只需要用点号把若干函数拼接起来即可:

99555d6bd730460fc0474c4e74a036ef.png

哪怕没有addOne函数作为辅助,采用“多元函数+部分元+括号”的形式,也可以把若干多元函数拼接起来:

fee8bd52d6f8dbd033b7830c9bf3435f.png

上述f函数的功能是把传入的浮点数与50比较取较小值,然后取正切,然后向上取整,然后求与24的最大公因数。如此复杂的过程,用Haskell表达却非常简洁自然,由此可见柯里化的意义。

柯里化的意义远不止这些,由于篇幅所限,笔者只能介绍到此。以下是笔者对柯里化的意义的完整概括:

  1. 把函数作为函数的返回值,确立了函数的一等公民地位。

  2. 让函数之间的传递、复用、组合变得极其简单,强化了函数的一等公民地位。

  3. 让程序员专注于函数之间的关系,而不必考虑数据在其中的传递,贯彻了“函数为主数据为次”的先进理念。

四、函数式编程的威力

威力之一:引用透明

消灭了副作用之后,函数能做的唯一事情就是求值并返回结果,而且返回值只与你的传入参数有关,与你什么时候调用它、调用了多少次无关。换句话说,每个函数都是完全独立的,绝无任何的相互依赖。这使得函数式代码是完全并行化的(当其他语言的程序员正在追求代码的低耦合度时,你可以直接做到零耦合度)。在多核芯片取代单核芯片成为时代主流的今天,函数式编程语言的重要性与日俱增。

威力之二:惰性求值

得益于引用透明,既然函数的返回值只与传入参数有关,那么函数在什么时候真正执行计算,就显得无关紧要了。函数式语言总是尽可能地推迟计算时间,只要当你需要它展示结果时,它才会进行最少量的计算。这种特性不仅大大节约了计算资源,而且带来了下面这个“黑科技”。

威力之三:可定义无限长度数据结构

你有尝试定义一个数组,让它的元素是全体质数吗?这听上去不可思议,因为质数的个数是无限多的,描述它们也很困难。但Haskell真的可以做到:

63f657f457d30d27caa06b0370f5d290.png

得益于惰性求值,你可以在Haskell中定义无限长度数据结构,而不用担心你的时空资源消耗殆尽,因为它并没有真正被算出来。只有当你需要用到它的一部分时,Haskell才会执行最少量的计算,刚好满足你所需要的那一部分:

71faf4a44df64989d31aa046a3f1dd35.png

威力之四:易于形式化推理和程序验证

在前文的讲解中,读者已经可以体会到Haskell语言是高度数学化的。事实上,在Haskell中定义函数、数据类型、数据结构的方式与在数学中是完全一致的,这使得很多成熟的数学理论(如逻辑学、抽象代数、范畴论等)可以在Haskell中发挥威力。

数学家已经可以运用形式化推理和程序验证方法,去证明某些Haskell代码是正确无误的,从而把这些代码升级为数学中的“定理”。人们每天使用的许多安全无误的系统是用Haskell编写的,如Linux的窗口密码管理系统(Xmonad)。

五、如何学习Haskell

如果你在中山大学东校区,很幸运,你只需要报一门乔海燕老师的公选课《Haskell函数程序设计基础》,就可以最快最好地学会Haskell。

除此之外,你还可以阅读Simon Thompson著的,乔海燕、张迎周译的《Haskell函数式编程基础》。这本书比较厚实,兼具理论深度和实用价值。

c9c6629fc7ba839565fd3eb1ca3e183e.png

如果你已经有一定的命令式编程经验,推荐这本《Haskell趣学指南》,它以实用为导向,可以帮你快速上手。

226967261eb5287d4882cd16e4945f0a.png

后记:不忘初心

函数式语言为什么值得学习?答曰:开拓眼界、有趣。

请回顾一下最初学习编程的那种乐趣,是不是仿佛进入了新世界一般?支持我们踏出去每一步的原动力,归根到底都是最单纯的“开拓眼界”和“有趣”。

函数式语言可以助你找回那种久违的耳目一新的感觉,哪怕你已经写了许多年的代码。

46c7e31ba41de72f64995ce29a9522d6.png

本期排版:夹心

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值