函数式编程学习总结

155 阅读36分钟

引言

函数式编程是一种的抽象程度很高编程范式,因其编程的特殊性,并不能完全替代更加通用的面向对象编程范式。但是作为一种补充,它也有很大存在、发展和学习的意义。当前广泛使用的Java、Go、Python、JavaScript等语言都在逐步支持函数式编程的思想和用法,比如闭包、lambda函数(匿名函数)、惰性求值、尾递归、流式处理等。包括进行大数据处理运算的MapReduce(MapReduce 的抽象受到LISP和其他函数式语言中的map和reduce原语的启发)。

本文结构: 首先介绍下函数式编程的理论基础、历史、现状。然后通过对Lisp的方言scheme的学习,简单探索下函数式编程的基本特性和高级特性-宏;最后介绍下函数式编程当前在Java语言中的使用。

一、什么是函数式编程?

函数式编程(functional programming 简称FP)是一种抽象程度很高编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。在函数式编程中一个程序会被看作是一个无状态的函数计算的序列。函数式编程的目的是使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用并减少对状态的改变[1]。它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用[2]。

函数式编程将计算视为对数学函数的评估,避免状态变化和可变数据的发生。 它是声明式编程范例,使用表达式或声明而不是语句来完成编程。 在函数式编程中,函数的输出值仅取决于其参数,用相同的参数调用函数始终会产生相同的结果。

也就是说,函数式编程是一种无状态的编程。

面向对象编程(OOP)通过封装变化使得代码更易理解。 函数式编程(FP)通过最小化变化使得代码更易理解。 -- Michacel Feathers(Twitter)

数学表达式实现举例[2]:(1 + 2) * 3 - 4

传统的过程式编程实现:

int a = 1 + 2;

int b = a * 3;

int c = b - 4;

FP要求使用函数,将运算过程定义为不同的函数,然后写成下面这样:

var result = subtract(multiply(add(1,2), 3), 4);

1.1 历史

函数式编程的理论基础是的Lambda演算(数学家阿隆佐·邱奇在1930年代引入)。于20世纪50年代后期,John McCarthy在麻省理工学院的一篇论文设计函数式语言LISP,运行在大型IBM主机(IBM700/7000系列)上。LISP的函数是使用邱奇的lambda表示法定义的,并扩展了标签构造来允许递归函数。最开始的LISP是多范型语言,并且随着新的范型的发展,越来越多的编程风格得到了支持。后来发展出来的方言比如Scheme、Clojure,和分支语言比如Dylan和Julia等,试图简化LISP,使它围绕一个函数式核心,而Common Lisp旨在保留并更新它所替代的各种更早先LISP方言的那些范型特征[1]。

函数式编程长期以来在学术界流行,在教育方面函数式编程一直受到了很大的重视(SICP),很多学校使用函数式编程来教授算法和几何的相关概念。但几乎没有工业应用。造成这种局面的主因是函数式编程常被认为严重耗费CPU和存储器资源,这是由于在实现早期的函数式编程语言时并没有考虑过效率问题,而且面向函数式编程特性(如保证参照透明性等)要求独特数据结构和算法。函数式编程因其编程的特殊性,仅在科学计算、数据处理、统计分析等领域,才能更好地发挥它的优势,所以它并不能完全替代更加通用的面向对象编程范式。但是作为一种补充,它也有很大存在、发展和学习的意义。

1.2 lambda演算

lambda演算,这是一套用于研究函数定义、应用和递归的形式系统。λ演算(lambda calculus,λ-calculus)是一套从数学逻辑中发展,以变量绑定和替换的规则,来研究函数如何抽象化定义、函数如何被应用以及递归的形式系统。它由数学家阿隆佐·邱奇在20世纪30年代首次发表。lambda演算作为一种广泛用途的计算模型,可以清晰地定义什么是一个可计算函数,而任何可计算函数都能以这种形式表达和求值,它能模拟单一磁带图灵机的计算过程;尽管如此,lambda演算强调的是变换规则的运用,而非实现它们的具体机器。[6]

语法名称描述
a变量表示参数或数学/逻辑值的字符或字符串
(λx.M)抽象化函数定义(M是一个lambda项)。变量x在表达式中已被绑定。
(M N)应用将函数应用于参数。 M 和 N 是 lambda 项。



产生了诸如:(λx.λy.(λz.(λx.zx)(λy.zy))(x y))的表达式。

简单来说lambda演算将计算过程看过一系列的函数代换;lambda演算就是反复将函数应用于实际值,并用实际值代替参数,最终得出结果。使用函数式语言scheme(Lisp的一个方言)来进行问题描述。scheme在本质上与lambda演算是等价的,只是看起来更好懂,例如不需要遵循lambda演算一个变态的规定:每个函数只允许有一个参数(虽然任何多参数函数式程序都可以通过Currying过程化归为等价的lambda演算)[7]。

#scheme 函数f

(define (f x y) (+ x y))

(f 2 7)

9

#scheme 函数f

(define (f x) (lambda (y) (+ x y)))

返回:(lambda (y) (+ x y))

((f 2)7)

9

1.2 现状

最近几种函数式编程语言已经在商业或工业系统中使用,例如:Erlang编程语言由瑞典公司爱立信在20世纪80年代后期开发,最初用于实现容错电信系统。此后,它已在Nortel,Facebook,ÉlectricitédeFrance和WhatsApp等公司作为创建一系列应用程序的语言。Lisp方言Scheme被用作早期Apple Macintosh计算机上的几个应用程序的基础,并且最近已应用于诸如训练模拟软件和望远镜控制等方向。OCaml于20世纪90年代中期推出,已经在金融分析,驱动程序验证,工业机器人编程和嵌入式软件静态分析等领域得到了商业应用。Haskell虽然最初是作为一种研究语言,也已被一系列公司应用于航空航天系统,硬件设计和网络编程等领域。其他在工业中使用的函数式编程语言包括Scala[19]、F#(两者都是函数式和面向对象编程的混合,支持纯函数式和指令式编程)、Wolfram语言、Lisp、Standard ML和Clojure。

使用广泛的Java、Go、Python、JavaScript等语言都在逐步支持函数式编程的思想和用法,比如闭包、lambda函数(匿名函数)、惰性求值、尾递归、流式处理等。比如MapReduce(MapReduce 的抽象受到LIsp和其他函数式语言你中的map和reduce原语的启发)、Scala与Spark

1.3 函数式编程优势

1.3.1 更高抽象

维基百科:在计算机科学中,抽象化(英语:Abstraction)是将资料与程序,以它的语义呈现外观,但是隐藏起它的实现细节。抽象化是用来减少程序的复杂度,使得程序员可以专注在处理少数重要的部分。一个电脑系统可以分割成几个抽象层(Abstraction layer),使得程序员可以将它们分开处理。举例:冯诺伊曼计算机体系结构、计算机网络7层模型等。在计算器处理器里,指令集架构提供了对实际处理硬件的抽象,使用这个抽象,机器代码程序表现得就好像运行在一个一次只执行一条指令的处理器上,但底层的硬件远比抽象描述的要复杂精细,它并行地执行多条指令,但又总是与那个简单有序的模型保持一致(为什么说 CPU 是人造物的巅峰?-知乎)。

抽象的重要性







1986年,IBM大型机之父佛瑞德·布鲁克斯发表一篇关于软件工程的经典论文《No Silver Bullet—Essence and Accidents of Software Engineering》,将软件开发的困难分为本质性(essence)和附属性(accident)困难两类,这篇论文的核心论述软件开发真正的困难,是在于这种概念构造的规格制定、设计和测试,而并非在概念、方案的呈现方式,以及测试该呈现方式的精确程度。附加性的困难会随着工具的改善而逐渐淡化,反而是本质性的困难最难以解决。

本质性(essence):软件本身在概念(conceptual)建构上存先天的困难;亦即如何从抽象性问题,发展出具体概念上的解决方案。

附属性(accident):将概念上的构思施行于计算机上,所遭遇到的困难。

云原生时代领域驱动设计(DDD)的价值——从《没有银弹》说起

DDD 是不是“银弹”?



计算(computing)是唯一可将某一种思想用1比特到几百兆比特来度量的领域,其所用比特数比可达1到109,这样庞大的数字是令人惊讶的。Edsger Dijkstra这样表达:“和这样级别的数相比,一般的数学方法是无能为力的。通过激起对更深的概念等级的需求,计算机向我们提出了以前从没遇到的挑战。”计算机科学的核心是减少复杂性,尽管谁希望成为一个英雄并能自如地解决各种计算机问题,但没有人真正有处理109级数的能力(微软windows 10有多少行代码?), 计算机科学和软件工程已经开发了许多工具以处理这样的复杂问题。



函数式编程是一种抽象程度很高编程范式。

1.3.1.1 Formulating Abstractions with Higher-Order Procedures



Procedures as General Methods-不动点迭代









原始转换后
x^2=y->x=y/x->x=(x+y/x)/2
x^3=y->x=y/x^2->x=(x+y/x^2)/2
x^2-8x+1=y->8x=x^2+1-y->x=(x^2+1-y)/8
->->

#|

(define (fixed-point f first-guess)

(define (close-enough? v1 v2)

(< (abs (- v1 v2))

tolerance))

(define (try guess)

(let ((next (f guess)))

(if (close-enough? guess next)

next

(try next))))

(try first-guess))

(define (average x y)

(/ (+ x y) 2))

(define (sqrt x)

(fixed-point (lambda (y) (average y (/ x y)))

1.0))

;the root of three

(define (rootOfThree x)

(fixed-point (lambda (y) (average y (/ x (* y y))))

1.0))

(sqrt 0.01)

;Value: .10000000000139897

(sqrt 2)

;Value: 1.414213562373095

1 ]=> (sqrt 3)

;Value: 1.7320508075688772

1 ]=> (sqrt 5)

;Value: 2.236067977499978

1 ]=> (sqrt 9)

;Value: 3.

1 ]=> (sqrt 90000)

;Value: 300.

1 ]=> (cubeRoot 8)

;Value: 2.0000002271906077

1 ]=> (cubeRoot 9)

;Value: 2.0800840609348006

1 ]=> (cubeRoot 27)

;Value: 2.9999998270063113

1 ]=> (cubeRoot 1331)

;Value: 11.000000206034244

;y=x^2-8x+1

(define (exe1 x)

(fixed-point (lambda (y)(/ (- (+ (* y y) 1) x) 8))

1.0))

;Value: exe1

1 ]=> (exe1 0)

;Value: .12701665982441213

1.3.2 提升代码可读性

可读性很重要

对于程序而言,正确性是排第一的,可读性是很重要的,因为任何人都能写出计算机读懂的程序,但是只有高手才能写出人读懂的程序。

编程中所做的工作仅有一小部分是为使计算机读懂程序,而做大部分工作则为能使人读懂程序。

现在的大部分编程方法,都是在降低程序的复杂度,提高程序的可读性。因为程序给人看的次数,远远大于编写程序的次数。

//www.ioccc.org/1990/baruch…

/*This program, written by Doron Osovlanski and Baruch Nissenbaum,

prints all solutions to the Eight Queens problem (the problem

of placing eight queens on a chessboard in such a way that no queen attacks any other queen).

In fact, it works for any number of queens between 4 and 99.

*/

main()

{

for(scanf("%d",&s);a-s;v=a[j=v]-a[i],k=i<s,j+=(v=j<s&&(!k&&!!printf(2+"\n\n%c"-(!l<=s*k&&++a[--i])

;

}



[3]

声明式编程&流式处理

将计算过程转化为一系列嵌套函数的调用

函数式编程属于声明式编程范式:这种范式会描述一系列的操作,但并不会暴露它们是如何实现的或是数据流如何传过它们。

//命令式编程方式--很具体的告诉计算机如何执行某个任务。

var array = [0, 1, 2, 3]

for(let i = 0; i < array.length; i++) {

array[i] = Math.pow(array[i], 2)

}

array; // [0, 1, 4, 9]

//声明式编程方式--将程序的描述与求值分离开。关注如何用各种表达式来描述程序逻辑,而不一定要指明其控制流或状态关系的变化。

[0, 1, 2, 3].map(num => Math.pow(num, 2))

而声明式是去掉变量代码循环,循环是一种重要的命令控制结构,但很难重用,并且很难插入其他操作中。而函数式编程旨在尽可能的提高代码的无状态性和不变性--使用纯函数(无副作用的函数)。

Point-free

A popular style of coding in the FP world aims to reduce some of the visual clutter by removing unnecessary parameter-argument mapping. This style is formally called tacit programming, or more commonly: point-free style.Pointfree:不使用所要处理的值,只合成运算过程。中文可以译作"无值"风格,函数式编程中的一种流行编码风格。Point-free :定义的东西(如函数)是自由的(可以独立存在且具有完整的语义),是避免重复的有效手段。
Currying

计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。柯里化的好处就是我们可以最大程度的重用我们的函数。

柯里化可以让我们给一个函数传入较少的参数 得到一个已经记住了一部分参数的新函数。

让函数变得更灵活,颗粒度更小。

可以把多元函数编程一元函数,可以组合使用函数产生强大的功能。

var foo = function(a, b) {

return a * a + b * b;

}

var foo = function(a) {

return function(b) {

return a * a + b * b;

}

}

举例

此处为语雀内容卡片,点击链接查看:yuque.antfin-inc.com/chenyun.zc/…

此处为语雀内容卡片,点击链接查看:yuque.antfin-inc.com/chenyun.zc/…



1.3.3 纯函数

定义:在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数:

此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。

该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

更通俗的讲法:具备直接的输入输出,没有副作用的函数,相同的输入始终得到相同的输出。

特征:

幂等性,给定输入,输出是确定的;

引用透明(referential transparency),所谓引用透明指的是,函数调用能被函数输出值直接替代;

var z = random();

// 函数一 addTwo

function addTwo(x,y){

return x + y

}

// 函数二 adAnother

function addAnother(x,y){

return addTwo(x,y) + z;

}

提高纯净度的两种方式:

使用闭包

如果一个函数返回另一个函数,而被返回函数又需要外层函数的变量时,不会立即释放这个变量,而是允许被返回的函数引用这些变量。支持这种机制的语言称为支持闭包机制,而这个内部函数连同其自由变量就形成了一个闭包(函数 + 引用环境)。

延长变量生命周期。

immutability

变量的不可变性,比如Java的String、纯函数式语言如Haskell,号称“变量的不可变性”(immutable),即只绑定一次,完全无副作用(这种理念影响了Scala、Rust等现代语言,如Scala中val和var的区别,标准库分别提供immutable和mutable collections)。

优点:

易于"并发编程"

强调无变量/状态,更容易开发无锁代码

可测试性(Testable)

相同的输入值时,需产生相同的输出;在语义上没有可观察的函数副作用。

可移植性/自文档化(Portable / Self-Documenting)

自给自足,依赖少,易复用、易模块化、热升级(Erlang语言是瑞典爱立信公司为了管理电话系统而开发的,支持不停机升级[2])。

...

1.3.4 函数式编程思维

函数式编程关心数据的映射,命令式编程关心解决问题的步骤。[5]

#二叉树反转命令式编程实现--首先判断节点是否为空;然后翻转左树;然后翻转右树;最后左右互换。

def invertTree(root):

if root is None:

return None

root.left, root.right = invertTree(root.right), invertTree(root.left)

return root

#二叉树反转函数式编程实现--描述一个 旧树->新树 的映射,而不是描述「从旧树得到新树应该怎样做」来达到目的。

def invert(node):

if node is None:

return None

else

return Tree(node.value, invert(node.right), invert(node.left))

[5]

二、函数式编程探索-Scheme

Scheme是一种函数式编程语言,是Lisp的两种主要方言之一(另一种为Common Lisp)。Scheme遵循极简主义哲学,以一个小型语言核心作为标准,加上各种强力语言工具(语法糖)来扩展语言本身[1]

麻省理工学院与其他院校曾采用Scheme教授计算机科学入门课程。著名的入门教材《计算机程序的构造和解释》(SICP)利用Scheme来解释程序设计[2]

Scheme最早由麻省理工学院盖伊·史提尔二世杰拉德·杰伊·萨斯曼在1970年代发展出来。 Scheme语言与λ演算关系十分密切。小写字母“λ”是Scheme语言的标志

Scheme的哲学是:设计计算机语言不应该进行功能的堆砌,而应该尽可能减少弱点和限制,使剩下的功能显得必要[4]。Scheme是第一个使用静态作用域的Lisp方言,也是第一个引入“干净宏”和第一类续延的编程语言。

保罗·格雷厄姆和罗布特·莫里斯(莫里斯病毒之父)等在1995年开始使用Lisp创造最早的Web应用Viaweb(1988年被雅虎1998年5千万美元收购,成为Yahoo! Store)。

John McCarthy本来没打算把Lisp设计成编程语言,至少不是我们现在意义上的编程语言。他的原意只是想做一种理论演算,用更简洁的方式定义图灵机。

Lisp 9大特性

1.条件结构(即"if-then-else"结构)。现在大家都觉得这是理所当然的,但是Fortran I就没有这个结构,它只有基于底层机器指令的goto结构。

2.函数也是一种数据类型。在Lisp语言中,函数与整数或字符串一样,也属于数据类型的一种。它有自己的字面表示形式(literal representation),能够储存在变量中,也能当作参数传递。一种数据类型应该有的功能,它都有。

3.递归。Lisp是第一种支持递归函数的高级语言。

4.变量的动态类型。在Lisp语言中,所有变量实际上都是指针,所指向的值有类型之分,而变量本身没有。复制变量就相当于复制指针,而不是复制它们指向的数据。

5.垃圾回收机制。

6.程序由表达式(expression)组成。Lisp程序是一些表达式区块的集合,每个表达式都返回一个值。这与Fortran和大多数后来的语言都截然不同,它们的程序由表达式和语句(statement)组成。

区分表达式和语句,在Fortran I中是很自然的,因为它不支持语句嵌套。所以,如果你需要用数学式子计算一个值,那就只有用表达式返回这个值,没有其他语法结构可用,因为否则就无法处理这个值。

后来,新的编程语言支持区块结构(block),这种限制当然也就不存在了。但是为时已晚,表达式和语句的区分已经根深蒂固。它从Fortran扩散到Algol语言,接着又扩散到它们两者的后继语言。

7.符号(symbol)类型。符号实际上是一种指针,指向储存在哈希表中的字符串。所以,比较两个符号是否相等,只要看它们的指针是否一样就行了,不用逐个字符地比较。

8.代码使用符号和常量组成的树形表示法(notation)。

9.无论什么时候,整个语言都是可用的。Lisp并不真正区分读取期、编译期和运行期。你可以在读取期编译或运行代码;也可以在编译期读取或运行代码;还可以在运行期读取或者编译代码。

思想8可能是最有意思的一点。它与思想9只是由于偶然原因,才成为Lisp语言的一部分,因为它们不属于John McCarthy的原始构想,是由他的学生Steve Russell自行添加的。

用一门语言自己的数据结构来表达该语言,这被证明是非常强大的功能。思想8和思想9,意味着你可以写出一种能够自己编程的程序。这可能听起来很怪异,但是对于Lisp语言却是再普通不过。最常用的做法就是使用宏。

术语"宏"在Lisp语言中,与其他语言中的意思不一样。Lisp宏无所不包,它既可能是某样表达式的缩略形式,也可能是一种新语言的编译器。如果你想真正地理解Lisp语言,或者想拓宽你的编程视野,那么你必须学习宏。

就我所知,宏(采用Lisp语言的定义)目前仍然是Lisp独有的。一个原因是为了使用宏,你大概不得不让你的语言看上去像Lisp一样古怪。另一个可能的原因是,如果你想为自己的语言添上这种终极武器,你从此就不能声称自己发明了新语言,只能说发明了一种Lisp的新方言。

如果你创造了一种新语言,其中有car、cdr、cons、quote、cond、atom、eq这样的功能,还有一种把函数写成列表的表示方法,那么在它们的基础上,你完全可以推导出Lisp语言的所有其他部分。事实上,Lisp语言就是这样定义的,John McCarthy把语言设计成这个样子,就是为了让这种推导成为可能。

2.1 Scheme基础

2.1.1 The Elements of Programming

2.1.1.1 Expressions

3.1415926

(+ 137 349)

486

(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))

57

2.1.1.2 Naming and the Environment

(define size 2)



2.1.1.3 Evaluating Combinations

前缀表达式

(* (+ 2 (* 4 6)) (+ 3 5 7))



2.1.1.4 Compound Procedures

格式:

(define (⟨name⟩ ⟨formal parameters⟩) ⟨body⟩)

例子:求平方

(define (square x) (* x x))

(square (square 3))

81

例子:求2数平方和

(define (sum-of-squares x y) (+ (square x) (square y)))

(sum-of-squares 3 4)

25



2.1.1.6 Conditional Expressions and Predicates

格式(cond):

(cond (⟨p1⟩ ⟨e1⟩)

(⟨p2⟩ ⟨e2⟩)

. . .

(⟨pn⟩ ⟨en⟩))

格式(if):

(if ⟨predicate⟩ ⟨consequent⟩ ⟨alternative⟩)

语或非格式:

(and ⟨e1⟩ . . . ⟨en⟩)

(or ⟨e1⟩ . . . ⟨en⟩)

(not ⟨e⟩)



例子:绝对值函数(cond实现)

(define (abs x)

(cond ((> x 0) x)

        ((= x 0) 0)

((< x 0) (- x))))



例子:绝对值函数(if实现)

(define (abs x)

(if (< x 0)

(- x)

x))



例子:X大于等于Y

(define (>= x y) (or (> x y) (= x y)))

(define (>= x y) (not (< x y)))



2.1.2 Formulating Abstractions

2.1.2.1 Procedures as Black-Box Abstractions

例子:牛顿二分法求平发根(通俗易懂地讲解牛顿迭代法求开方)



;Guess Quotient Average cur X -> nextCur = (cur + X / cur) / 2

;1 (2/1) = 2 ((2 + 1)/2) = 1.5

;1.5 (2/1.5) = 1.3333 ((1.3333 + 1.5)/2) = 1.4167

;1.4167 (2/1.4167) = 1.4118 ((1.4167 + 1.4118)/2) = 1.4142

;1.4142 ... ...

;伪代码 sqrt

;(define (sqrt x)

; (the y (and (>= y 0)

; (= (square y) x))))

(define (sqrt-iter guess x)

(if (good-enough? guess x)

guess

(sqrt-iter (improve guess x) x)))

(define (improve guess x)

(average guess (/ x guess)))

(define (average x y)

(/ (+ x y) 2))

(define (good-enough? guess x)

(< (abs (- (square guess) x)) 0.0000001))

(define (sqrt x)

(sqrt-iter 1.0 x))

1 ]=> (sqrt 0.01)

;Value: .10000000000139897

1 ]=> (sqrt 2)

;Value: 1.4142135623746899

1 ]=> (sqrt 9)

;Value: 3.000000001396984

1 ]=> (sqrt (+ 100 37))

;Value: 11.704699910719626

1 ]=> (sqrt (+ (sqrt 2) (sqrt 3)))

;Value: 1.7737712336472033

1 ]=> (square (sqrt 1000))

;Value: 1000.0000000000343

#正常sqrt procedure的定义

(define (sqrt x)

(sqrt-iter 1.0 x))

(define (sqrt-iter guess x)

(if (good-enough? guess x)

guess

(sqrt-iter (improve guess x) x)))

(define (good-enough? guess x)

(< (abs (- (square guess) x)) 0.001))

(define (improve guess x)

(average guess (/ x guess)))

#sqrt procedure 定义中定义依赖的函数

(define (sqrt x)

(define (good-enough? guess x)

(< (abs (- (square guess) x)) 0.001))

(define (improve guess x) (average guess (/ x guess)))

(define (sqrt-iter guess x)

(if (good-enough? guess x)

guess

(sqrt-iter (improve guess x) x)))

(sqrt-iter 1.0 x))

#x的声明周期贯穿整个sqrt procedure的定义,可以不用额外传递

(define (sqrt x)

(define (good-enough? guess)

(< (abs (- (square guess) x)) 0.001))

(define (improve guess)

(average guess (/ x guess)))

(define (sqrt-iter guess)

(if (good-enough? guess)

guess

(sqrt-iter (improve guess))))

(sqrt-iter 1.0))



2.1.2.2 higher-order procedures – procedures as types

Procedures that manipulate procedures are called higher-order procedures. higher-order procedures can serve as powerful abstraction

mechanisms, vastly increasing the expressive power of our language.







2.1.2.3 lambda

格式:

(lambda (⟨formal-parameters⟩) ⟨body⟩)

例子:质能方程E=mc²

(lambda (m) (* m (* C C)))

2.1.2.4 Using let to create local variables

格式:

(let ((⟨var1⟩ ⟨exp1⟩)

(⟨var2⟩ ⟨exp2⟩)

: : :

(⟨varn⟩ ⟨expn⟩))

⟨body⟩)

例子:

(let ((x 3))

(+ x (* x 10)))

33

2.1.2.5 Pairs

语法含义例子
Pairscons( )car(p)cdr(p)(define x (cons 1 2))(car x)1(cdr x)2
stream-pair;stream-cons stream-car stream-cdr ;(stream-car (cons-stream x y)) = x ;(stream-cdr (cons-stream x y)) = y (stream-car (cons-stream 1 2));Value: 11 ]=> (stream-cdr (cons-stream 1 2));Value: 2

2.1.3 求兀

数据公式1:



数学公式2:





Nilakantha 级数

π = 3 + 4/(234) - 4/(456) + 4/(678) - 4/(8910) + 4/(101112) - (4/(121314) ...





割圆术-刘徽/祖冲之



分割到12288边形,又用刘徽多边形面积公式,求得24576边形的面积-》3.1415926<pi <3.1415927(小数点后7位)









;Pair : cons car 第一个 cdr 第二个

; such that (partial-sums integers) gives 1, 3, 6, 10, 15

(define (partial-sums stream)

(let ((first (stream-car stream)))

(cons-stream

first

(stream-map (lambda (x) (+ x first)) (partial-sums (stream-cdr stream))))))

(define (stream-map proc s)

(if (stream-null? s)

the-empty-stream

(cons-stream (proc (stream-car s))

(stream-map proc (stream-cdr s)))))

(define (scale-stream stream factor)

(stream-map (lambda (x) (* x factor))

stream))

(define (stream-ref s n)

(if (= n 0)

(stream-car s)

(stream-ref (stream-cdr s) (- n 1))))

(define (stream-for-each proc s)

(if (stream-null? s)

'done

(begin (proc (stream-car s))

(stream-for-each proc (stream-cdr s)))))

(define (display-line x) (newline) (display x))

(define (display-stream s)

(stream-for-each display-line s))

(define (pi-summands n)

(cons-stream (/ 1.0 n)

(stream-map - (pi-summands (+ n 2)))))

(define pi-stream

(scale-stream (partial-sums (pi-summands 1)) 4))

(define (euler-transform s)

(let ((s0 (stream-ref s 0)) ; Sn  1

(s1 (stream-ref s 1)) ; Sn

(s2 (stream-ref s 2))) ; Sn+1

(cons-stream (- s2 (/ (square (- s2 s1))

(+ s0 (* -2 s1) s2)))

(euler-transform (stream-cdr s)))))

(define (make-tableau transform s)

(cons-stream s (make-tableau transform (transform s))))

(define (accelerated-sequence transform s)

(stream-map stream-car (make-tableau transform s)))

(display-stream

(accelerated-sequence euler-transform pi-stream))

3.166666666666667

3.142105263157895

3.1415993573190044

3.141592714033778

3.1415926539752923

3.141592653591176

3.141592653589777

3.141592653589794

3.1415926535897936

2.2 进阶探索

2.2.1 宏

宏(英语:Macro),是一种批量处理的称谓。计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式。解释器编译器在遇到宏时会自动进行这一模式替换。对于编译语言,宏展开在编译时发生,进行宏展开的工具常被称为宏展开器。宏这一术语也常常被用于许多类似的环境中,它们是源自宏展开的概念,这包括键盘宏和宏语言。绝大多数情况下,“宏”这个词的使用暗示着将小命令或动作转化为一系列指令。

宏的用途在于自动化频繁使用的序列或者是获得一种更强大的抽象能力。计算机语言如C语言汇编语言有简单的宏系统,由编译器汇编器的预处理器实现。C语言宏预处理器的工作只是进行简单的文本搜索和替换,使用附加的文本处理语言如M4,C程序员可以获得更精巧的宏。

Lisp类语言如Common LispScheme有更精巧的宏系统:宏的行为如同是函数对自身程序文本的变形,并且可以应用全部语言来表达这种变形。一个C宏可以定义一段语法的替换,然而一个Lisp的宏却可以控制一节代码的计算。获得了控制代码的执行顺序(见惰性计算非限制函数)的能力,使得新创建的语法结构与语言内建的语法结构不可区分。例如,一种Lisp方言有cond而没有if,就可以使用宏由前者定义后者。Lisp语法的去部主要扩展,比如面向对象的CLOS系统,可以由宏来定义。



Lisp宏

底层支撑:

S表达式(symbolic-expression)是点对表示法的形式定义,是二叉树的一种线性编码。

| 原子 -> 数字 | 符号S表达式 -> 原子 | (S表达式 . S表达式) | | ----------------------------------------- |

Lisp语言定义了一套S表达式的化简规则。

(1)如果一个点号右邻一个左括号,那么就可以将这个点号,左括号以及匹配的右括号,一起去掉。

例如:(a . (b . c)) <=> (a b . c)



(2)如果一个点号右邻原子nil,那么就可以把这个点号和原子nil,一起去掉。

例如:(a . (b . nil)) <=> (a b . nil) <=> (a b)

 code as data:表达式与数据的转换是如此浑然天成。

程序 = 表达式+数据 -》 表达式=数据

Lisp宏定义:

从技术上讲,宏是一个函数,它接受一个s-expression作为参数,并返回一个LISP的形式,然后进行评估计算。    第一步,像函数那样,宏的body对你传入宏的参数进行操作,进行处理,进行加工;注意,传入宏的参数,是不会被求值的。    第二步,第一步处理的结果,会被LISP eval,也就是会被LISP执行;而在函数里,整个函数body的执行结果是不会被再次执行的。可以做到:●Lisp 可以写出可自己写程序的程序,可以跨越表达式与代码的界线●自动生成运行时的代码,可以在不改动语言本身的基础上,增加新的程序构造体



用处:

以扩展标准LISP的语法

//from Common Lisp 的宏(Macro)到底神奇在哪? - 李国栋的回答 - 知乎 www.zhihu.com/question/19…

//python在1.5版本后加入列表解析

divisibleByTwo= [x for x in range(10) if x % 2 == 0]

//1.5版本之前是不能使用的。但是我们通过其他方式可以达到目的。

divisibleByTwo = []

for x in range(10):

if x%2 == 0:

divisibleByTwo.append(x)

//lisp中可以这样做,我们定义一个叫做lcomp的宏。我们期望能达到这种效果

[x for x in range(10) if x % 2 == 0] ==> (lcomp x for x in (range 10) if (= (% x 2) 0))

(defmacro lcomp (expression for var in list conditional conditional-test)

;; create a unique variable name for the result

(let ((result (gensym)))

;; the arguments are really code so we can substitute them

;; store nil in the unique variable name generated above

`(let ((,result nil))

;; var is a variable name

;; list is the list literal we are suppose to iterate over

(loop for ,var in ,list

;; conditional is if or unless

;; conditioanl-test is (= (mod x 2) 0) in our examples

,conditional ,conditional-test

;; and this is the action from the earlier lisp example

;; result = result + [x] in python

do (setq ,result (append ,result (list ,expression))))

;; return the result

,result)))'

//在lisp解释器中就可以使用 (lcomp x for x in (range 10) if (= (mod x 2) 0))

//同样在lisp中也可以实现在python2.5中新加入的with语句。

//而不必等语言的实现者发布新版本后才可以使用。etc



支持DSL(domain-specific language 领域特定语言)编程

//www.infoq.cn/article/201…

//Clojure 是一个基于 JVM 的 Lisp 实现。ClojureScript 是 Clojure 的一个子集,它可以将 Clojure 编译成 JavaScript。

//core.async 很好地呈现了 Lisp 所必须提供的宏机制的能力:很多其他的语言要实现 core.async 所做的事情必须要对语言做出改变,但是在一个 Lisp 中则可以通过使用宏的库来实现。

//目的:简化异步编程

//et 引入了一个新的局部变量 ch,它是一个新通道

(let [ch (chan)]

//let 域中定义了两个 go 语句块,第一个是一个永久循环,它从通道 ch 中读取(<!)一个新的值赋给变量 v。然后它会把“Read:”和读取到的值打印到标准输出。

(go (while true

(let [v (<! ch)]

(println "Read: " v))))

//第二个 go 语句块向通道 ch:”hi”中写入(>!)两个值,然后它会等待 5 秒钟再向通道中写入“there”。等待 5 秒钟是通过一个 timeout 通道实现的,该通道会在设置的超时时间过后关闭自己(返回 nil)。

(go (>! ch "hi")

(<! (timeout 5000))

(>! ch "there")))

更多代码:github.com/clojure/cor…



Lisp程序(打印HelloWorld+打印水仙花数)———强大的操作系统宏处理———》操作系统Lisp程序





With great power comes great responsibility.



举例

ITA Software-wikipedia

Viaweb-wikipedia

三、Java如何支持FP

JDK VersionFP支持
JDK8Stream类Lambda表达式函数接口
JDK9-JDK16Java Platform, Standard Edition Documentation...

JDK16-Add Stream.toList() Method

内容:

stream.collect(Collectors.toList())  -> stream.toList()

理由

It makes the coding just easier, saves some time and place.

without extra allocation and copying.

相关:

JDK-8256441 : Add Stream.toList() method—— CSR(compatibility and specification review)

JDK-8180352 : Add Stream.toList() method—— Enhancement



JDK14-Expose JDK Flight Recorder data for continuous monitoring

背景:

Java Flight Recorder简称JFR,OpenJDK从11版本开始支持。它是一个低开销的数据收集框架,可用于在生产环境中分析Java应用和JVM运行状况及性能问题。

a.

AJDK8.5.10新功能——JFR

b.

JFR监控方案-JFR Streaming实现

目的:Expose JDK Flight Recorder data for continuous monitoring.

内容:EventStream

JDK Flight Recorder data is now available as a data stream allowing for continuous monitoring.

Provide an API for the continuous consumption of JFR data on disk, both for in-process and out-of-process applications.

Record the same set of events as in the non-streaming case, with overhead less than 1% if possible.

Event streaming must be able to co-exist with non-streaming recordings, both disk and memory based.

使用:

//prints the overall CPU usage and locks contended for more than 10 ms.

try (var rs = new RecordingStream()) {

rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));

rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));

rs.onEvent("jdk.CPULoad", event -> {

System.out.println(event.getFloat("machineTotal"));

});

rs.onEvent("jdk.JavaMonitorEnter", event -> {

System.out.println(event.getClass("monitorClass"));

});

rs.start();

}

/**

The RecordingStream class implements the interface jdk.jfr.consumer.

EventStream that provides a uniform way to filter and consume events regardless if the source is a live stream or a file on disk.

*/

public interface EventStream extends AutoCloseable {

public static EventStream openRepository();

public static EventStream openRepository(Path directory);

public static EventStream openFile(Path file);

void setStartTime(Instant startTime);

void setEndTime(Instant endTime);

void setOrdered(boolean ordered);

void setReuse(boolean reuse);

void onEvent(Consumer handler);

void onEvent(String eventName, Consumer<RecordedEvent handler);

void onFlush(Runnable handler);

void onClose(Runnable handler);

void onError(Runnable handler);

void remove(Object handler);

void start();

void startAsync();

void awaitTermination();

void awaitTermination(Duration duration);

void close();

}

相关:

JEP(JDK Enhancement Proposal) 349: JFR Event Streaming.

CSR for JFR: Remote Recording Stream

remote-recording-stream

3.1 Stream类

定义:The streams API was added in Java 8 to ease the task of performing bulk operations, sequentially or in parallel. This API provides two key abstractions: the stream, which represents a finite or infinite sequence of data elements, and the stream pipeline, which represents a multistage computation on these elements. The elements in a stream can come from anywhere.Common sources include collections, arrays, files, regular expression pattern matchers, pseudorandom number generators, and other streams. The data elements in a stream can be object references or primitive values. Three primitive types aresupported: int, long, and double.[14]



Java8





使用:A stream pipeline consists of a source stream followed by zero or more intermediate operations and one terminal operation.

创建流流的中间操作流的最终操作
// 使用List创建流`` ``list``.stream``()`` ``// 使用一个或多个元素创建流`` Stream. ``of``(T value) Stream.``of`` (T... values) ``// 使用数组创建流`` Arrays.stream(T ``[]`` ``array`` ) ``// 创建一个空流`` Stream.empty ``()`` ``// 两个流合并`` Stream.concat(Stream<? extends T> a, Stream<? extends T> b) ``// 无序无限流`` Stream.generate(Supplier<T> s) ``// 通过迭代产生无限流`` Stream.iterate(final T seed, final UnaryOperator<T> f) filter()map()mapToInt()mapToLong()mapToDouble()flatMap()flatMapToInt()flatMapToLong() flatMapToDouble()...peek()distinct()sorted()sorted()limit()skip()anyMatch()allMatch()noneMatch()findFirst()findAny()...count()collect()forEach()



特性:[14]

无存储

a.

不是数据结构,其本身并不存储任何元素(或其地址值),Stream是一个来自数据源的元素队列。它是有关算法和计算,是一个集合元素的函数模型。

惰性求值

a.

evaluation doesn’t start until the terminal operation is invoked, and data elements that aren’t required in order to complete the terminal operation are never computed.

b.

This lazy evaluation is what makes it possible to work with infinite streams.

c.

A stream pipeline without a terminal operation is a silent no-op.

Stream API 支持函数式编程和链式操作



好处:

提高编程效率、简洁性和程序可读性

多核友好、易开发并发处理程序

public class Demo {

.map(s -> s.length()) // map返回Stream对象

.filter(l -> l <= 3) // filter返回Stream对象

.max((o1, o2) -> o1-o2); // max终止操作:返回Optional

System.out.println(result.get()); // 输出2

}

}

/**

  • 并行执行

  • -Djava.util.concurrent.ForkJoinPool.common.parallelism=5

  • ForkJoinPool commonPool = ForkJoinPool.commonPool();

  • System.out.println(commonPool.getParallelism()); //11

**/

System.out.println("ForkJoinPool Parallelism : " + ForkJoinPool.commonPool().getParallelism());

Arrays.asList("大娃", "二娃", "三娃", "四娃", "五娃", "六娃", "七娃")

.parallelStream()

.filter(s -> {

System.out.format("Tread[%s] filter: %s\n",

Thread.currentThread().getName(), s);

return true;

})

.map(s -> {

System.out.format("Tread[%s] map: %s\n",

Thread.currentThread().getName(), s);

return s.toUpperCase();

})

.forEach(s -> System.out.format("Tread[%s] forEach: %s\n",

Thread.currentThread().getName(),s));

--------------output----------------

ForkJoinPool Parallelism : 11

Tread[main] filter: 五娃

Tread[ForkJoinPool.commonPool-worker-11] filter: 大娃

Tread[ForkJoinPool.commonPool-worker-2] filter: 七娃

Tread[ForkJoinPool.commonPool-worker-2] map: 七娃

Tread[ForkJoinPool.commonPool-worker-6] filter: 四娃

Tread[ForkJoinPool.commonPool-worker-6] map: 四娃

Tread[ForkJoinPool.commonPool-worker-9] filter: 二娃

Tread[ForkJoinPool.commonPool-worker-9] map: 二娃

Tread[ForkJoinPool.commonPool-worker-13] filter: 三娃

Tread[ForkJoinPool.commonPool-worker-13] map: 三娃

Tread[ForkJoinPool.commonPool-worker-11] map: 大娃

Tread[ForkJoinPool.commonPool-worker-4] filter: 六娃

Tread[ForkJoinPool.commonPool-worker-4] map: 六娃

Tread[main] map: 五娃

Tread[main] forEach: 五娃

Tread[ForkJoinPool.commonPool-worker-4] forEach: 六娃

Tread[ForkJoinPool.commonPool-worker-11] forEach: 大娃

Tread[ForkJoinPool.commonPool-worker-2] forEach: 七娃

Tread[ForkJoinPool.commonPool-worker-9] forEach: 二娃

Tread[ForkJoinPool.commonPool-worker-6] forEach: 四娃

Tread[ForkJoinPool.commonPool-worker-13] forEach: 三娃



3.2 Lambda表达式

1

(a, b) -> { 语句1;语句2;...; return 输出; }

好处:简化代码编写

Lambda表达式与匿名类的异同点:

Lambda就是为了优化匿名内部类而生,Lambda要比匿名类简洁的多得多。

Lambda仅适用于函数式接口,匿名类不受限。

即匿名类中的this是“匿名类对象”本身;Lambda表达式中的this是指“调用Lambda表达式的对象”。

3.3 函数接口

定义:有且仅有一个抽象方法的接口,一般用 @FunctionalInterface 标记。

函数式接口也是Java interface的一种,需要满足:

一个函数式接口只有一个抽象方法(single abstract method);

Object类中的public abstract method不会被视为单一的抽象方法;

函数式接口可以有默认方法和静态方法;

函数式接口可以用@FunctionalInterface注解进行修饰。



好处:用极简的lambda表达式实例化接口。

/**

  • If a type is annotated with this annotation type, compilers are

  • required to generate an error message unless:

    • The type is an interface type and not an annotation type, enum, or class.
    • The annotated type satisfies the requirements of a functional interface.
  • However, the compiler will treat any interface meeting the

  • definition of a functional interface as a functional interface

  • regardless of whether or not a {@code FunctionalInterface}

  • annotation is present on the interface declaration.

  • @jls 4.3.2. The Class Object

  • @jls 9.8 Functional Interfaces

  • @jls 9.4.3 Interface Method Body

  • @since 1.8

*/

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

public @interface FunctionalInterface {}

@FunctionalInterface

public interface Comparator {

/**

  • single abstract method

  • @since 1.8

*/

int compare(T o1, T o2);

/**

  • Object类中的public abstract method

  • @since 1.8

*/

boolean equals(Object obj);

/**

  • 默认方法

  • @since 1.8

*/

default Comparator reversed() {

return Collections.reverseOrder(this);

}

/**

  • 静态方法

  • @since 1.8

*/

public static <T extends Comparable<? super T>> Comparator reverseOrder() {

return Collections.reverseOrder();

}

//省略...

}

List persons = new ArrayList();

Collections.sort(persons,

new Comparator(){

@Override

public int compare(Person o1, Person o2) {

return Integer.compareTo(o1.getAge(), o2.getAge());

}

});

Comparator comparator = (p1, p2) -> Integer.compareTo(p1.getAge(), p2.getAge());



Runnable Callable

四、相关链接

函数式编程-中文维基百科

函数式编程初探-阮一峰-2012

函数式编程浅析

函数式编程,真香

什么是函数式编程思维? - nameoverflow的回答 - 知乎

λ演算-维基百科

闭包漫谈(从抽象代数及函数式编程角度)

Scheme-维基百科

Structure and Interpretation of Computer Programs - 2nd Edition (MIT)

黑客与画家

王垠的「40 行代码」真如他说的那么厉害吗?

Java如何支持函数式编程?-阿里云

Alan_Perlis-维基百科

《Effective Java》 Third Edition. ITEM 45-USE STREAMS JUDICIOUSLY

CSR FAQs--OpenJDK Wiki(explain CSR JEP)

Java Flight Recorder初探-知乎

Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I

Common Lisp 的宏(Macro)到底神奇在哪?

ANSI Common Lisp 第十章:宏

易百教程-LISP-宏

由浅入深学习 Lisp 宏(理论篇)

github.com/clojure/cor…

core.async: 另一种 Clojure 和 ClojureScript 异步编程方式