Matrix技术分享| Haskell与函数式编程简介

Posted VMatrix

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Matrix技术分享| Haskell与函数式编程简介相关的知识,希望对你有一定的参考价值。


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

Haskell与函数式编程简介



一、从命令式到函数式


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

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


  • 汇编指令编程

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


Matrix技术分享| Haskell与函数式编程简介
Matrix技术分享| Haskell与函数式编程简介


  • 面向过程编程

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


Matrix技术分享| Haskell与函数式编程简介


Matrix技术分享| Haskell与函数式编程简介


  • 面向对象编程

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


Matrix技术分享| Haskell与函数式编程简介


Matrix技术分享| Haskell与函数式编程简介



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


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


Matrix技术分享| Haskell与函数式编程简介


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


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


Matrix技术分享| Haskell与函数式编程简介
Matrix技术分享| Haskell与函数式编程简介


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


Matrix技术分享| Haskell与函数式编程简介


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


Matrix技术分享| Haskell与函数式编程简介


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


Matrix技术分享| Haskell与函数式编程简介


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


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


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




二、纯函数式编程语言Haskell


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


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


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


Matrix技术分享| Haskell与函数式编程简介


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


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


Matrix技术分享| Haskell与函数式编程简介
Matrix技术分享| Haskell与函数式编程简介


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


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


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


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


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


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


Matrix技术分享| Haskell与函数式编程简介


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




三、把函数作为一等公民


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


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

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


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


Matrix技术分享| Haskell与函数式编程简介


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


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


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


Matrix技术分享| Haskell与函数式编程简介


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


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


Matrix技术分享| Haskell与函数式编程简介


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


Matrix技术分享| Haskell与函数式编程简介


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


Matrix技术分享| Haskell与函数式编程简介


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


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


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

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

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




四、函数式编程的威力


威力之一:引用透明


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


威力之二:惰性求值


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


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


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


Matrix技术分享| Haskell与函数式编程简介


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


Matrix技术分享| Haskell与函数式编程简介


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


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


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




五、如何学习Haskell


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


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


Matrix技术分享| Haskell与函数式编程简介


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




后记:不忘初心


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


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


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



本期排版:夹心


以上是关于Matrix技术分享| Haskell与函数式编程简介的主要内容,如果未能解决你的问题,请参考以下文章

技术分享 | Haskell 工程化的挑战和收获

专访《Haskell函数式编程入门》作者张淞:浅谈Haskell的优点与启发

函数式编程中的常用技巧

这些电子书新上架

将“为啥函数式编程很重要”翻译成 Haskell

Frege-基于JVM的类Haskell纯函数式编程语言