函数式编程简介(函数式编程教程)

网友投稿 971 2022-08-20

函数式编程简介(函数式编程教程)

函数式编程简介(函数式编程教程)

函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。 – wiki

例子一 累加运算

// sum

List nums = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 10);

public static Integer sum(List nums){

int result = 0;

for (Integer num : nums) {

result += num;

}

return result;

}

sum(nums); // -> 46

同样的代码用 Java8 Stream 实现

Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 10).stream().reduce(0, Integer::sum);

同样的代码用 Clojure 实现

(apply + [0 1 2 3 4 5 6 7 8 10]) ; -> 46

#_(reduce + [0 1 2 3 4 5 6 7 8 10])

例子二 fabonacci数列

// java

public static int fibonacci(int number){

if (number == 1) {

return 1;

}

if(number == 2) {

return 2;

}

int a = 1;

int b = 2;

for(int cnt = 3; cnt <= number; cnt++) {

int c = a + b;

a = b;

b = c;

}

return b;

}

// java8

Stream.iterate(new int[]{1, 1}, s -> new int[]{s[1], s[0] + s[1]})

.limit(10)

.map(n -> n[1])

.collect(toList())

// -> [1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

// clojure

(->> (iterate (fn [[a b]] [b (+ a b)]) [1 1])

(map second)

(take 10))

; -> (1 2 3 5 8 13 21 34 55 89)

比起命令式的语言,函数式语言更加关注执行的结果,而非执行的过程。

函数式编程的历史

从Hilbert 23个数学难题谈起

1900年,Hilbert 提出了数学界悬而未决的10大问题,后续陆续添加成了23个问题,被称为著名的 Hilbert 23 Problem。针对其中第2个决定数学基础的问题——算术公理之相容性,年轻的哥德尔提出了哥德尔不完备定理,解决了这个问题形式化之后的前两点,即数学是完备的吗?数学是相容的吗?哥德尔用两条定理给出了否定的回答。所谓不完备,即系统中存在一个为真,但是无法在系统中推导出来的命题。比如:U说:“U在PM中不可证”。虽然和说谎者很类似,但其实有明显的差异。我们可以假设U为可证,那么可以推出PM是矛盾(不相容)的;但是假设U不可证,却推导不出PM是矛盾的。U的含义是在M中不可证,而事实上,它被证明不可证,所以U是PM中不可证的真命题。基于第一条不完备定理,又可以推导出第二条定理。如果一个(强度足以证明基本算术公理的)公理系统可以用来证明它自身的相容性,那么它是不相容的。

而最后一个问题,数学是确定的吗?也就是说,存在一个算法判定一个给定的命题是否是不确定的吗(Entscheidungsproblem 确定性问题)?这个问题引起了阿隆佐·邱奇和年轻的阿兰·图灵的兴趣。阿隆佐·邱奇的lambda calculus和图灵的图灵机构造出了可计算数,图灵的那篇论文 ON COMPUTABLE NUMBERS, WITH AN APPLICATION TO THE ENTSCHEIDUNGSPROBLEM 的意义不在于证明可计算数是否可数,而在于证明可判定性是否成立。在1936年他们对判定性问题分别独立给出了否定的答案。也就是现在被我们熟知的图灵停机问题:不存在这样一个程序(算法),它能够计算任何程序(算法)在给定输入上是否会结束(停机)。图灵借此发明了通用图灵机的概念,为后来的冯·诺依曼体系的计算机体系提供了理论基础。

Lambda Calculus

Lambda 表达式包含三个要素

变量

lambda 抽象

lambda 应用 据此我们可以用函数给出布尔值的定义

data BOOL = FALSE | TRUE

TRUE = λx.λy.x

FALSE = λx.λy.y

not = λb.b FALSE TRUE

and = λb1.λb2.b1 b2 FALSE

or = λb1.λb2.b1 TRUE b2

xor = λb1.λb2.b1 (not b2) b2

自然数的定义

data NAT = Z | S NAT

0 = λf.λs.s

1 = λf.λs.f s

2 = λf.λs.f f s

succ n = λf.λs.f (n f s)

zero? n = n (λb.FALSE) TRUE

add = succ n1 n2

函数式编程语言的发展

在这之后,随着通用计算机的产生,人们发觉使用机器码写程序太没有效率。所以1956年左右,John Buckus发明了Fortran(FORmula TRANslating 的缩写)语言,如果对编译原理有了解,那么对BNF范式就不陌生了。与此同时,John McCarthy 发明了Lisp语言,现代的Clojure就是Lisp的方言之一。1966年,Niklaus Wirth发明了Pascal。1969年,Ken Thompson和Dennis Ritchie发明了C语言,过程式语言由于其高效和可移植性迅速崛起。1973年,Robin Milner 发明了ML(Meta Language),后来演变成了OCaml和Stardard ML。1977年,John Buckus在其图灵奖的演讲中创造了 Functional Programming 这个词。1990年,惰性求值的函数式编程语言 Haskell 1.0 发布。

神奇的 Y Combinator

(def Y (fn [f]

((fn [x] (x x))

(fn [x]

(f (fn [y]

((x x) y)))))))

Lisp、ML以及Haskell的关系

Lisp是动态语言,使用S表达式

ML和Haskell都是静态强类型函数式语言

ML是第一个使用Hindley-Milner type inference algorithm的语言

Lisp和ML都是call-by-value,但是Haskell则是call-by-name

Lisp和ML都是不纯的编程语言,但是Haskell是side effect free的

函数是一等公民

函数是一等公民,指的是你可以将函数作为参数、返回值、数据结构存在,而且不仅可以用函数名引用,甚至可以匿名调用。

1. 作为参数

(map inc [1 2 3 4 5]) ;-> (2 3 4 5 6) ;; inc is an argument

2. 作为返回值

(defn add [num]

(fn [other-num] (+ num other-num))) ;; as return-value

(def add-one (add 1))

(add-one 2) ;-> 3

(defn flip [f] ;; as argument and return-value

(fn [x y]

(f y x)))

3. 数据结构

(def dictionary {:a "abandon"}) ;; map is also a function, data is code.

(dictionary :a) ;-> "abandon"

(:a dictionary) ;-> "abandon"

4. 匿名函数

((fn [x] (* x x))

2) ;-> 4

(map

(fn [num] (+ 1 num)) ;; anonymous function

[1 2 3 4 5]) ;-> (2 3 4 5 6)

5. 模块化

在面向对象中,对象是一等公民。所以我们处处要从对象的角度去考虑计算问题,然后产生一种共识——数据应该和它相关的操作放到一起,也就是我们所说的封装。确实没错,但是我们得知道封装的意义在哪里?功能内聚好理解(分块)和局部性影响(控制可变性)。函数式编程同样考虑这些,功能内聚不一定要用类的方式(考虑一下JS的prototype,也是一种面向对象),只要模块做得好,一样能达到效果。局部性影响,其本质是封装可变因素以避免其扩散到代码各处。函数式给出了自己的答案,消除可变因素。

高阶函数和惰性求值也非常有利于模块化。

纯函数和不可变性

纯函数是指执行过程中没有副作用的函数,所谓副作用是说超出函数控制的操作,比如在执行过程中操作文件系统、数据库等外部资源。纯函数还具有引用透明性的特点,也就是同样的输入导致同样的输出,以至于完全可以用函数的值代替对函数的调用。

引用透明

举个例子:

(inc 1) ; -> 2

(= (inc (inc 1)

(inc 2))) ; -> true

你们可能就会问,这种东西究竟有什么用呢?纯函数可以很方便地进行缓存。

(defn fibonacci [number]

(if (or (zero? number) (= 1 number)) 1

(+

(fibonacci (dec number))

(fibonacci (- number 2)))))

(fibonacci 30) ; -> "Elapsed time: 185.690208 msecs"

(def fibonacci

(memoize (fn [number] ;;

(if (or (zero? number) (= 1 number)) 1

(+

(fibonacci (dec number))

(fibonacci (- number 2)))))))

(fibonacci 30) ; -> "Elapsed time: 0.437114 msecs"

不可变计算

谈到不可变性,我们做个游戏。统计在座的一共有多少人数。我们都知道从某个人开始依次报数,最后得到的数字就是总人数,其实这就是一种不可变计算的游戏,为什么这么说呢?因为报数其实一个计算的过程,第一个人计算出1这个数,传递给第二个人。然后第二个人拿着前面的1进行加一操作,然后把结果2传递给后面的人做加法,以此类推。为了提高统计的效率,我也可以进行分组,然后每组自行报数,最后统计结果。但是如果我在白板上写个数字1,然后让大家来过来该这个数字,很大可能会出现错误,因为这个数字成为了竞态条件。在多并发的情况下,就得用读写锁来控制。所以不可变性特别利于并发。

不可变的链式结构

好了,现在我们有个新的需求,设计一个不可变列表收集大家的名字。每个节点存储一个姓名的字符串,并且有个指针指向下一个节点。但是这也打破了列表的不可变性。怎么办?我们可以把新的节点指向旧有的列表,然后返回一个新的列表。这就是不可变列表实现的机制。随便一提,这也是区块链不可变特征的由来。

Clojure的创造者Rich Hickey扩展了Ideal Hash Tree数据结构,实现了Persistent Vector。由于此处的叶子节点可以扩展成32个,所以可以大量存储数据。利用Ideal Hash Tree的特点可以快速索引出数据,与此同时,数据的“增删改”也能做到近常数化的时间,并且总是产生新的数据结构替换原有的数据结构,即一种不可变的链式存储结构。

不可变的树状结构

Zipper数据结构类似于文本编辑器中的 gap buffer,编辑文本时,光标左边和右边分别是独立的buffer,光标处也是单独的buffer,这样便可以方便地添加文字,也很方便删除左右buffer中的文字;移动光标会涉及buffer之间的拷贝。基本上能在常数时间内完成编辑。Zipper数据结构模仿了这种方式,能在常数时间内完成树的编辑工作,也能很快地重新构建一棵树。

递归

可计算很大问题就是得实现递归功能。

(defn reverse-seq [coll]

(when-let [elem (first coll)]

(concat (reverse-seq (rest coll)) [elem])))

(reverse-seq [1 2 3]) ; -> (3 2 1)

和循环无异的尾递归

(defn gcd [& nums]

(reduce #(if (zero? %2)

%

(recur %2 (mod % %2))) nums))

(gcd 8 16) ; -> 8

生成式测试

生成式测试会基于输入假设输出,并且生成许多可能的数据验证假设的正确性。

(defn add [a b]

(+ a b))

;; 任取两个整数,把a和b加起来的结果减去a总会得到b。

(def test-add

(prop/for-all [a (gen/int)

b (gen/int)]

(= (- (add a b) a) b)))

(tc/quick-check 100 test-add)

; -> {:result true, :num-tests 100, :seed 1515935038284}

测试结果表明,刚才运行了100组测试,并且都通过了。理论上,程序可以生成无数的测试数据来验证add方法的正确性。即便不能穷尽,我们也获得一组统计上的数字,而不仅仅是几个纯手工挑选的用例。

抽象是什么

抽取共性,封装细节,忘记不重要的差异点。这样的好处是可以做到局部化影响和延迟决策。

命名

命名就是一种抽象,重构中最重要的技法就是重命名和提取小函数

(* 3 3 3)

(* x x x)

(* y y y)

->

(defn cube [x]

(* x x x))

延迟决策

例如:我们定义数对 pair

pair:: (cons x y)

firstpair -> x

secondpair -> y

那么它的具体实现会是这样的

(defn cons [x y]

(fn [m]

(cond (= m 0) x

(= m 1) y)))

(defn first [z]

(z 0))

(defn second [z]

(z 1))

也可以是这样的,还可以是其它各种各样的形式。

(defn cons [x y]

(fn [b]

(b x y))

(defn first [z]

(z (fn [x y] x)))

(defn second [z]

(z (fn [x y] y)))

高阶函数

高阶函数就是可以接收函数的函数,高阶函数提供了足够的抽象,屏蔽了很多底层的实现细节。比如Clojure中的 map 高阶函数,它接收 (fn [v] ...) ,把一组数据映射成另外一组数据。

过程抽象

(map inc [1 2 3 4 5]) ; -> (2 3 4 5 6)

这些函数抽象出映射这样语义,除了容易记忆,还能很方便地重新编写成高效的底层实现。也就是说,一旦出现了更高效的 map 实现算法,现有的代码都能立刻从中受益。

函数的组合

函数组合之后会产生巨大的能量

神奇的加法

(((comp (map inc) (filter odd?)) +) 1 2) ; -> 4

怎么去理解这个函数的组合?我们给它取个好听的名字

(def special+ ((comp (map inc) (filter odd?)) +))

(special+ 1 2) ; -> 4

; <=> 等价于

(if (odd? (inc 2))

(+ 1 3))

1)

这个未必是个好的组合方式,但是不可否认的是,我们可以用这些随意地将这些函数组合到一起,得到我们想要的结果。

transducer

(def xf (comp (filter odd?) (take 10)))

(transduce xf conj (range))

;; [1 3 5 7 9 11 13 15 17 19]

这里直接将求值延迟到了 transduce 计算的时候,换句话说, xf 定义了一种过程:filter出奇数并取出前10个元素。同等的代码,如果用表达式直接书写的话,如下:

(->> (range)

(filter odd?)

(take 10))

这里的问题就是我们没能使用高阶函数抽象出过程,如果把 conj 换成其他的reduce运算,现在的过程无法支撑,但是tranducers可以!

(transduce xf + (range)) ;-> 100

我们再看一个tranducer的神奇使用方式:

(defn log [& [idx]]

(fn [rf]

(fn

([] (rf))

([result] (rf result))

([result el]

(let [n-step (if idx (str "Step: " idx ". ") "")]

(println (format "%sResult: %s, Item: %s" n-step result el)))

(rf result el)))))

(def ^:dynamic *dbg?* false)

(defn comp* [& xforms]

(apply comp

(if *dbg?*

(->> (range)

(map log)

(interleave xforms))

xforms)))

(binding [*dbg?* true]

(transduce

(comp*

(map inc)

(filter odd?))

+

(range 5))) ;; -> 9

Step: 0. Result: 0, Item: 1

Step: 1. Result: 0, Item: 1

Step: 0. Result: 1, Item: 2

Step: 0. Result: 1, Item: 3

Step: 1. Result: 1, Item: 3

Step: 0. Result: 4, Item: 4

Step: 0. Result: 4, Item: 5

Step: 1. Result: 4, Item: 5

之所以会出现上述的结果,是因为 interleave xforms 将 (map inc) 以及 (filter odd?) 和logs进行了交叉,得到的结果是 (comp (map inc) (log) (filter odd?) (log)) ,所以如果是偶数就会被filter清除,看不见log了。

首先一定得理解:每个tranducer函数都是同构的!

形式如下

(defn m [f]

(fn [rf]

(fn [result elem]

(rf result (f elem)))))

这意味着 (m f) 的函数都是可以组合的,组合的形式如下:

(comp (m f) (m1 f1) ...)

展开之后

((m f)

((m1 f1)

((m2 f2) ...)))

->

(fn [result elem]

(((m1 f1)

((m2 f2) ...)) result (f elem)))

所以可以看到第一个执行的一定是 comp 的首个 reducing function 参数。故:

xform 作为组合的前提

执行顺序从左到右;

+ 作为 reducing function 最后执行;

Monad

什么是 Monad 呢?A monad is just a monoid in the category of endofunctors.

Identity—For a monad m, m flatMap unit => m

Unit—For a monad m, unit(v) flatMap f => f(v)

Associativity—For a monad m, m flatMap g flatMap h => m flatMap {x => g(x) flatMap h}

// java8 实现的 9*9 乘法表

public class ListMonad{

private List elements;

private ListMonad(T elem) {

this.elements = singletonList(elem);

}

private ListMonad(List elems) {

this.elements = elems;

}

public ListMonad flatmap(Function> fn) {

List newElements = new ArrayList<>();

this.elements.forEach(elem -> newElements.addAll(fn.apply(elem).elements));

return new ListMonad<>(newElements);

}

public ListMonad uint(X elem) {

return new ListMonad<>(elem);

}

public ListMonad apply(ListMonad> m) {

return m.flatmap(this::map);

}

public ListMonad map(Function fn) {

return flatmap(t -> uint(fn.apply(t)));

}

public static void main(String[] args) {

ListMonad m = new ListMonad<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));

ListMonad m1 = new ListMonad<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));

ListMonad list = m.apply(m1.map(x -> y -> x * y));

// [1...81]

}

}

表达式优于语句

S表达式

原子,或者;

形式为 (x • y) 的表达式,其中x和y也是S表达式。

举个例子,递增一组数据,过滤奇数,然后进行排序,最终取出第一个。如果取不到,返回 :not-found 。

(-> [1 2 3]

(->> (map inc)

(filter odd?)

(sort)

(first))

(or :not-found))

; -> 3

(-> [1 1 3]

(->> (map inc)

(filter odd?)

(sort)

(first))

(or :not-found)

; -> :not-found

当然你也可以写成

(if-let [r (first (sort (filter odd? (map inc [1 1 1]))))]

r

:not-found)

; -> :not-found

其实两者都是S表达式,但是下面的写法更加偏向于语句。从串联起来读来讲,前者明显是由于后者的。这要是放在其他函数式语言上,效果更加显著。比如下面重构if-else控制语句到Optional类型。

if-else -> Optional

Optional rule = ruleOf(id);

if(rule.isPresent()) {

return transform(rule.get());

} else {

throw new RuntimeException();

}

public Rule transform(Rule rule){

return Rule.builder()

.withName("No." + rule.getId())

.build();

}

这是典型的语句可以重构到表达式的场景,关键是怎么重构呢?

第一步,调转 if 。

Optional rule = ruleOf(id);

if(!rule.isPresent()) {

throw new RuntimeException();

}

return transform(rule.get());

第二步, Optional.map 函数

...

return rule.map(r -> transform(r)).get();

第三步, inline transform 函数

...

rule.map(r -> Rule.builder()

.withName("No." + r.getId())

.build()).get();

第四步, Optional.orElseThrow 函数

...

rule.map(r-> Rule.builder()

.withName("No." + r.getId())

.build())

.orElseThrow(() ->new RuntimeException());

第五步,注 if 释语句中的 throw new RuntimeException()

if(!rule.isPresent()) {

// throw new RuntimeException();

}

这时候发现语句中为空,即可将整个语句删除。可以考虑 inline rule 。

ruleOf(id).map(r-> Rule.builder()

.withName("No." + r.getId())

.build())

.orElseThrow(() ->new RuntimeException());

完毕。

我们认识事物的方式

把几个简单的想法合并成一个复合概念,从而创造出所有复杂的概念。

简单的或复杂的两种思想融合在一起,并立即把它们联系起来,不要把它们统一起来,从而得到它所有的关系思想。

把他们与其他所有陪伴他们的真实存在的想法分开:这就是所谓的抽象,因此所有的一般想法都是被提出来的。

来自:https://lambeta.com/2018/02/17/The-Simple-Summary-of-FP/

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Go 1.10中值得关注的几个变化(google)
下一篇:2018,如何成为一名React Native开发者?(如何快速成为一名)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~