函数式编程心得
最近一年来,我在函数式编程上不停探索,想要构建一个属于自己的编程思维。然而在实际开发过程中,经常会因为各种原因导致思路混乱,难以继续开发。
所以这篇文章主要总结一下自己的开发思维模式,以便自己开发过程中能有经验所依。
当需要开发一个系统时,需要明确一件事情,即任何计算机程序,都可以理解为解释器。
eval和apply的矛盾统一
既然涉及到解释器,那么免不了谈论eval和apply两个函数。
eval函数作用是求值表达式,apply函数是使用指定参数去调用某个函数。如下所示
(eval '(+ 1 1)')
(apply + (list 1 1))
任何程序都是这两个函数的循环递归调用。就如SICP里面的求值器,eval求值一个表达式的时候,如果该表达式是程序,就将函数名和参数传递个apply,求出结果并返回。如下所示:
(define (eval exp env)
(cond ...
[(application? exp)
(apply (eval (operator exp) env)
(list-of-values (operands exp) env))]
[else
(error 'eval "Unknow expression type -- EVAL" exp)]))
(define (apply procedure arguments)
(cond [(primitive-procedure? procedure)
(apply-primitive-procedure procedure arguments)]
[(compound-procedure? procedure)
(eval-sequence
(procedure-body procedure)
(extend-environment
(procedure-parameters procedure)
arguments
(procedure-environment procedure)))]
[else
(error 'apply "Unknow procedure type -- APPLY" procedure)]))
eval需要调用apply,而apply也需要调用eval来求值子表达式。这就称为元循环。
而eval和apply从本质上看是同一个东西,它们都是在指定环境下对表达式进行求值,看看下面它们的入参:
(define (eval expr env))
(define (apply proc args))
对于eval来说,它的表达式类型很多,每个表达式都必须在环境下对其求值。而apply需要函数作为参数,其实这作为参数的函数也可以被认为是表达式,而该作为参数的函数所需的信息,也是可以被认为是环境。不同的地方仅仅在于eval的环境比较单一,而apply则丰富多样。对应地,eval的表达式非常丰富,而apply的表达式单一。
所以它们两个都是解释器,都是在特定环境下对表达式进行解释。
计算机程序是解释器
理解了这一个层面,就能快速理解复杂系统背后的精髓。比如,大家熟悉的MYSQL看似复杂精深,其实它对我们而言只是一个解释器,它解释我们编写的SQL语言,并把解释结果返回给我们。这个解释器的环境包含着我们所说的表,数据,索引等等。而对SQL解释的过程,就是不断的对环境中的各种数据进行查询和修改。
又比如,我们日常所写的Web系统,也不过是另一种解释器而已。该解释器的表达式就是用户发送过来的http请求,而求值环境就是数据库,缓存等等。对表达式的求值程序,被我们拆分成路由,控制器,具体业务逻辑等等层面而已。
把所有程序当作是解释器,可以扩宽我们的编程思维。而一旦我们想要运用这个编程思维去设计程序的时候,我们就必须深刻理解解释器背后设计思维和概念。
比如,分析一个模块时,我们可以思考该模块到底是什么样的解释器,这个解释器的表达式有哪些,分别有哪些作用,解释器的求值环境到底是什么?
举个具体例子,现在需要做一个http服务器,其功能是从socket中读取用户发送的字节流,并转换成json格式。
按照过去的思维去思考,功能无非就是写一个函数,其入参是socket字节流,返回是json格式。然后就开始编写这个函数的逻辑:不断的读取input的内容,对这些内容进行解析,而后把解析内容组合起来,形成json格式,并返回。
用解释器的设计思维,同样需要写这个函数,只不过这个函数的实现方式更加细致灵活。这种实现方式,不再简单地把socket字节流当成是参数,而是把字节流当做是求值环境,而我们需要做的是构建一些合理的表达式,在特定环境下,当用自己编写的解释器去求值表达式,从而将socket字节流转换成json格式。
表达式设计思维
运用解释器思维来设计系统的过程中,第一步要做的事情,也是最难的事情就是设计表达式,设计良好的表达式能够让上层程序员高效开发应用。这往往有三个重要问题需要思考:基本表达式是什么,如何组合基本表达式,如何构建它们的抽象等。
下面,我将试着阐述这三个问题。
首先要明确基本表达式的通用定义,我给它定义为用户操作环境的基本方式,它屏蔽环境的细节,使得用户可以更流程表达想法。换句话说只要是能够操作环境,并且能让用户无需关注环境的,都算是基本表达式。
然而基础表达式的功能是很单一的,我们需要对基础表达式进行各种形式的组合,才能完整的表达一件事物。因此,如何对基本元素进行组合,是一件很必要的事情。组合形式一般而已无关紧要,最关键的在于组合表达式的意义。有些组合表达式,将顺序求值子表达式,并将子表达式的结果汇总起来,有些组合表达式却是组成一条调用链,后表达式依赖前表达式的结果。
抽象应该如何理解呢?举例来说,掀开被子,坐立起来,穿好鞋子,这些动作我们可以汇总起来叫做起床,而起床就是这些动作的抽象。只要我对自己说起床,我的身体就做那些动作,就叫做使用抽象。在构建表达式的时候,我们为了更流畅地表达自己的想法,同样需要一些方式进行抽象和使用抽象。
举例说,define
, +
, lambda
, 数字,变量都是scheme的基本表达式,这些基础表达式的意义都依赖环境,甚至define
表达式还会改变环境,然而基础表达式很好屏蔽了环境,使得用户无需了解环境的任何细节。
(define a (lambda () (+ 1 1) (+ 2 2)))
这是一种组合方式,即通过list将基本表达式组合起来,形成功能更加强大的表达式。
(define a (lambda () (+ 1 1) (+ 2 2)))
这是一种组合方式,即通过list将基本表达式组合起来,形成功能更加强大的表达式。
而lambda表达式是一种抽象方式,它将多个表达式组装形成一个匿名函数。调用该匿名函数,即相当于调用lambda体里的组合表达式。由于define过程把 a
与一个lambda关联起来,也就是说 a
可以说是lambda的抽象,只要调用a,就相当于调用lambda里的过程。换句话说,抽象是通过lambda
来封装,define
建立关联。
由于我们要描述一些表达式,可以采用下面这种数学标准方法来描述需要构建的表达式。
image.png此外,使用编译器思维去设计程序,是要达成最终目的的,因此也需要以目标导向原则来设计表达式。例如要将字节流转换为json格式。转换过程中,需要从字节流转换成字符串,再提取里面的内容,组合成json。因此,这里涉及到转换表达式,提取表达式,组合表达式。
解释器设计心得
我没有写过复杂的编译器,所以在设计编译器的方面有一定欠缺,这里总结出来的心得或许不够成熟。
表达式可以采用面向对象思维进行封装,这是做模块化最基本的单元。而在scheme中,对象是可以使用算子来模拟的,而且算子还具备其他语言外更灵活的特性。
一个复杂的编译器,往往由多个子编译器组合而成。不要试图用一个编译器去囊括所有功能,而是应该拆分成职责明确的子编译器。对于功能单一的模块,能用简单函数取代就不要用编译器理念来复杂化模块。
复杂编译器有多个子编译器组成,而子编译器也有更细小更具体的编译器所组成。我们是否能将把编译器也看成是表达式,子编译器的排列组合,就像一个复合表达式包含多个子表达式。因此,是否可以试图够构造一个更高层次的编译器,让编译器去生成编译器?
网友评论