函数式编程简介

conn9062 2年前
   <p>函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。 – wiki</p>    <h3>例子一 累加运算</h3>    <pre>  <code class="language-java">// sum  List<Integer> nums = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 10);    public static Integer sum(List<Integer> nums){   int result = 0;   for (Integer num : nums) {    result += num;   }      return result;  }    sum(nums); // -> 46  </code></pre>    <p>同样的代码用 Java8 Stream 实现</p>    <pre>  <code class="language-java">Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 10).stream().reduce(0, Integer::sum);  </code></pre>    <p>同样的代码用 Clojure 实现</p>    <pre>  <code class="language-java">(apply + [0 1 2 3 4 5 6 7 8 10]) ; -> 46  #_(reduce + [0 1 2 3 4 5 6 7 8 10])  </code></pre>    <h3>例子二 fabonacci数列</h3>    <pre>  <code class="language-java">// 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;  }  </code></pre>    <pre>  <code class="language-java">// 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]  </code></pre>    <pre>  <code class="language-java">// clojure  (->> (iterate (fn [[a b]] [b (+ a b)]) [1 1])       (map second)       (take 10))  ; -> (1 2 3 5 8 13 21 34 55 89)  </code></pre>    <p>比起命令式的语言,函数式语言更加关注执行的结果,而非执行的过程。</p>    <h2>函数式编程的历史</h2>    <h3>从Hilbert 23个数学难题谈起</h3>    <p>1900年,Hilbert 提出了数学界悬而未决的10大问题,后续陆续添加成了23个问题,被称为著名的 Hilbert 23 Problem。针对其中第2个决定数学基础的问题——算术公理之相容性,年轻的哥德尔提出了哥德尔不完备定理,解决了这个问题形式化之后的前两点,即数学是完备的吗?数学是相容的吗?哥德尔用两条定理给出了否定的回答。所谓不完备,即系统中存在一个为真,但是无法在系统中推导出来的命题。比如:U说:“U在PM中不可证”。虽然和说谎者很类似,但其实有明显的差异。我们可以假设U为可证,那么可以推出PM是矛盾(不相容)的;但是假设U不可证,却推导不出PM是矛盾的。U的含义是在M中不可证,而事实上,它被证明不可证,所以U是PM中不可证的真命题。基于第一条不完备定理,又可以推导出第二条定理。如果一个(强度足以证明基本算术公理的)公理系统可以用来证明它自身的相容性,那么它是不相容的。</p>    <p>而最后一个问题,数学是确定的吗?也就是说,存在一个算法判定一个给定的命题是否是不确定的吗(Entscheidungsproblem 确定性问题)?这个问题引起了阿隆佐·邱奇和年轻的阿兰·图灵的兴趣。阿隆佐·邱奇的lambda calculus和图灵的图灵机构造出了可计算数,图灵的那篇论文 <em>ON COMPUTABLE NUMBERS, WITH AN APPLICATION TO THE ENTSCHEIDUNGSPROBLEM</em> 的意义不在于证明可计算数是否可数,而在于证明可判定性是否成立。在1936年他们对判定性问题分别独立给出了否定的答案。也就是现在被我们熟知的图灵停机问题:不存在这样一个程序(算法),它能够计算任何程序(算法)在给定输入上是否会结束(停机)。图灵借此发明了通用图灵机的概念,为后来的冯·诺依曼体系的计算机体系提供了理论基础。</p>    <h3>Lambda Calculus</h3>    <p><img src="https://simg.open-open.com/show/dd1d861afce3655cd7c1c3327d714b2c.png"></p>    <p>Lambda 表达式包含三个要素</p>    <ol>     <li>变量</li>     <li>lambda 抽象</li>     <li>lambda 应用<br> 据此我们可以用函数给出布尔值的定义 <pre>  <code class="language-java">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  </code></pre> </li>    </ol>    <p>自然数的定义</p>    <pre>  <code class="language-java">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  </code></pre>    <h3>函数式编程语言的发展</h3>    <p>在这之后,随着通用计算机的产生,人们发觉使用机器码写程序太没有效率。所以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 发布。</p>    <p><img src="https://simg.open-open.com/show/6c9127ebfc6ff6e1d458708f30fc3275.png"></p>    <h3>神奇的 Y Combinator</h3>    <pre>  <code class="language-java">(def Y (fn [f]           ((fn [x] (x x))            (fn [x]              (f (fn [y]                   ((x x) y)))))))  </code></pre>    <h3>Lisp、ML以及Haskell的关系</h3>    <p>Lisp是动态语言,使用S表达式</p>    <p>ML和Haskell都是静态强类型函数式语言</p>    <p>ML是第一个使用Hindley-Milner type inference algorithm的语言</p>    <p>Lisp和ML都是call-by-value,但是Haskell则是call-by-name</p>    <p>Lisp和ML都是不纯的编程语言,但是Haskell是side effect free的</p>    <h2>函数是一等公民</h2>    <p>函数是一等公民,指的是你可以将函数作为参数、返回值、数据结构存在,而且不仅可以用函数名引用,甚至可以匿名调用。</p>    <h3>1. 作为参数</h3>    <pre>  <code class="language-java">(map inc [1 2 3 4 5]) ;-> (2 3 4 5 6) ;; inc is an argument  </code></pre>    <h3>2. 作为返回值</h3>    <pre>  <code class="language-java">(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)))  </code></pre>    <h3>3. 数据结构</h3>    <pre>  <code class="language-java">(def dictionary {:a "abandon"}) ;; map is also a function, data is code.  (dictionary :a) ;-> "abandon"  (:a dictionary) ;-> "abandon"  </code></pre>    <h3>4. 匿名函数</h3>    <pre>  <code class="language-java">((fn [x] (* x x))          2) ;-> 4        (map       (fn [num] (+ 1 num)) ;; anonymous function      [1 2 3 4 5]) ;-> (2 3 4 5 6)  </code></pre>    <h3>5. 模块化</h3>    <p>在面向对象中,对象是一等公民。所以我们处处要从对象的角度去考虑计算问题,然后产生一种共识——数据应该和它相关的操作放到一起,也就是我们所说的封装。确实没错,但是我们得知道封装的意义在哪里?功能内聚好理解(分块)和局部性影响(控制可变性)。函数式编程同样考虑这些,功能内聚不一定要用类的方式(考虑一下JS的prototype,也是一种面向对象),只要模块做得好,一样能达到效果。局部性影响,其本质是封装可变因素以避免其扩散到代码各处。函数式给出了自己的答案,消除可变因素。</p>    <p>高阶函数和惰性求值也非常有利于模块化。</p>    <h2>纯函数和不可变性</h2>    <p>纯函数是指执行过程中没有副作用的函数,所谓副作用是说超出函数控制的操作,比如在执行过程中操作文件系统、数据库等外部资源。纯函数还具有引用透明性的特点,也就是同样的输入导致同样的输出,以至于完全可以用函数的值代替对函数的调用。</p>    <h3>引用透明</h3>    <p>举个例子:</p>    <pre>  <code class="language-java">(inc 1) ; -> 2    (= (inc (inc 1)     (inc 2))) ; -> true  </code></pre>    <p>你们可能就会问,这种东西究竟有什么用呢?纯函数可以很方便地进行缓存。</p>    <pre>  <code class="language-java">(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"  </code></pre>    <h3>不可变计算</h3>    <p>谈到不可变性,我们做个游戏。统计在座的一共有多少人数。我们都知道从某个人开始依次报数,最后得到的数字就是总人数,其实这就是一种不可变计算的游戏,为什么这么说呢?因为报数其实一个计算的过程,第一个人计算出1这个数,传递给第二个人。然后第二个人拿着前面的1进行加一操作,然后把结果2传递给后面的人做加法,以此类推。为了提高统计的效率,我也可以进行分组,然后每组自行报数,最后统计结果。但是如果我在白板上写个数字1,然后让大家来过来该这个数字,很大可能会出现错误,因为这个数字成为了竞态条件。在多并发的情况下,就得用读写锁来控制。所以不可变性特别利于并发。</p>    <p><img src="https://simg.open-open.com/show/f12957dc95a683ae86d8599763a99fe6.png"></p>    <h3>不可变的链式结构</h3>    <p>好了,现在我们有个新的需求,设计一个不可变列表收集大家的名字。每个节点存储一个姓名的字符串,并且有个指针指向下一个节点。但是这也打破了列表的不可变性。怎么办?我们可以把新的节点指向旧有的列表,然后返回一个新的列表。这就是不可变列表实现的机制。随便一提,这也是区块链不可变特征的由来。</p>    <p><img src="https://simg.open-open.com/show/f4b8917d1ae7d276b7b16d1622a67716.png"></p>    <p>Clojure的创造者Rich Hickey扩展了Ideal Hash Tree数据结构,实现了Persistent Vector。由于此处的叶子节点可以扩展成32个,所以可以大量存储数据。利用Ideal Hash Tree的特点可以快速索引出数据,与此同时,数据的“增删改”也能做到近常数化的时间,并且总是产生新的数据结构替换原有的数据结构,即一种不可变的链式存储结构。</p>    <p><img src="https://simg.open-open.com/show/a14f5d7249ec0cd5540d87883fac79ac.png"></p>    <h3>不可变的树状结构</h3>    <p>Zipper数据结构类似于文本编辑器中的 gap buffer,编辑文本时,光标左边和右边分别是独立的buffer,光标处也是单独的buffer,这样便可以方便地添加文字,也很方便删除左右buffer中的文字;移动光标会涉及buffer之间的拷贝。基本上能在常数时间内完成编辑。Zipper数据结构模仿了这种方式,能在常数时间内完成树的编辑工作,也能很快地重新构建一棵树。</p>    <p><img src="https://simg.open-open.com/show/87e39201b50d9c82e071b8e13f56a922.png"></p>    <h2>递归</h2>    <p>可计算很大问题就是得实现递归功能。</p>    <pre>  <code class="language-java">(defn reverse-seq [coll]    (when-let [elem (first coll)]      (concat (reverse-seq (rest coll)) [elem])))  (reverse-seq [1 2 3]) ; -> (3 2 1)  </code></pre>    <p>和循环无异的尾递归</p>    <pre>  <code class="language-java">(defn gcd [& nums]    (reduce #(if (zero? %2)                  %                  (recur %2 (mod % %2))) nums))  (gcd 8 16) ; -> 8  </code></pre>    <h2>生成式测试</h2>    <p>生成式测试会基于输入假设输出,并且生成许多可能的数据验证假设的正确性。</p>    <pre>  <code class="language-java">(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}  </code></pre>    <p>测试结果表明,刚才运行了100组测试,并且都通过了。理论上,程序可以生成无数的测试数据来验证add方法的正确性。即便不能穷尽,我们也获得一组统计上的数字,而不仅仅是几个纯手工挑选的用例。</p>    <h2>抽象是什么</h2>    <p>抽取共性,封装细节,忘记不重要的差异点。这样的好处是可以做到局部化影响和延迟决策。</p>    <p><img src="https://simg.open-open.com/show/454a5479e6fd6e989289ec92ff6daccb.png"></p>    <h3>命名</h3>    <p>命名就是一种抽象,重构中最重要的技法就是重命名和提取小函数</p>    <pre>  <code class="language-java">(* 3 3 3)  (* x x x)  (* y y y)  ->  (defn cube [x]    (* x x x))  </code></pre>    <h3>延迟决策</h3>    <p>例如:我们定义数对 pair</p>    <pre>  <code class="language-java">pair:: (cons x y)  firstpair -> x  secondpair -> y  </code></pre>    <p>那么它的具体实现会是这样的</p>    <pre>  <code class="language-java">(defn cons [x y]    (fn [m]      (cond (= m 0) x            (= m 1) y)))  (defn first [z]    (z 0))  (defn second [z]    (z 1))  </code></pre>    <p>也可以是这样的,还可以是其它各种各样的形式。</p>    <pre>  <code class="language-java">(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)))  </code></pre>    <h2>高阶函数</h2>    <p>高阶函数就是可以接收函数的函数,高阶函数提供了足够的抽象,屏蔽了很多底层的实现细节。比如Clojure中的 map 高阶函数,它接收 (fn [v] ...) ,把一组数据映射成另外一组数据。</p>    <h3>过程抽象</h3>    <pre>  <code class="language-java">(map inc [1 2 3 4 5]) ; -> (2 3 4 5 6)  </code></pre>    <p>这些函数抽象出映射这样语义,除了容易记忆,还能很方便地重新编写成高效的底层实现。也就是说,一旦出现了更高效的 map 实现算法,现有的代码都能立刻从中受益。</p>    <h2>函数的组合</h2>    <p>函数组合之后会产生巨大的能量</p>    <h3>神奇的加法</h3>    <pre>  <code class="language-java">(((comp (map inc) (filter odd?)) +) 1 2) ; -> 4  </code></pre>    <p>怎么去理解这个函数的组合?我们给它取个好听的名字</p>    <pre>  <code class="language-java">(def special+ ((comp (map inc) (filter odd?)) +))  (special+ 1 2) ; -> 4    ; <=> 等价于  (if (odd? (inc 2))      (+ 1 3))      1)  </code></pre>    <p>这个未必是个好的组合方式,但是不可否认的是,我们可以用这些随意地将这些函数组合到一起,得到我们想要的结果。</p>    <h3>transducer</h3>    <pre>  <code class="language-java">(def xf (comp (filter odd?) (take 10)))  (transduce xf conj (range))  ;; [1 3 5 7 9 11 13 15 17 19]  </code></pre>    <p>这里直接将求值延迟到了 transduce 计算的时候,换句话说, xf 定义了一种过程:filter出奇数并取出前10个元素。同等的代码,如果用表达式直接书写的话,如下:</p>    <pre>  <code class="language-java">(->> (range)       (filter odd?)       (take 10))  </code></pre>    <p>这里的问题就是我们没能使用高阶函数抽象出过程,如果把 conj 换成其他的reduce运算,现在的过程无法支撑,但是tranducers可以!</p>    <pre>  <code class="language-java">(transduce xf + (range)) ;-> 100  </code></pre>    <p>我们再看一个tranducer的神奇使用方式:</p>    <pre>  <code class="language-java">(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  </code></pre>    <p>之所以会出现上述的结果,是因为 interleave xforms 将 (map inc) 以及 (filter odd?) 和logs进行了交叉,得到的结果是 (comp (map inc) (log) (filter odd?) (log)) ,所以如果是偶数就会被filter清除,看不见log了。</p>    <p>首先一定得理解:每个tranducer函数都是同构的!</p>    <p>形式如下</p>    <pre>  <code class="language-java">(defn m [f]      (fn [rf]           (fn [result elem]              (rf result (f elem)))))  </code></pre>    <p>这意味着 (m f) 的函数都是可以组合的,组合的形式如下:</p>    <pre>  <code class="language-java">(comp (m f) (m1 f1) ...)  </code></pre>    <p>展开之后</p>    <pre>  <code class="language-java">((m f)       ((m1 f1)           ((m2 f2) ...)))  ->  (fn [result elem]      (((m1 f1)           ((m2 f2) ...)) result (f elem)))  </code></pre>    <p>所以可以看到第一个执行的一定是 comp 的首个 reducing function 参数。故:</p>    <ol>     <li>xform 作为组合的前提</li>     <li>执行顺序从左到右;</li>     <li>+ 作为 reducing function 最后执行;</li>    </ol>    <h3>Monad</h3>    <p>什么是 <a href="/misc/goto?guid=4959757151012333026" rel="nofollow,noindex">Monad</a> 呢?A monad is just a monoid in the category of endofunctors.</p>    <ol>     <li>Identity—For a monad m, m flatMap unit => m</li>     <li>Unit—For a monad m, unit(v) flatMap f => f(v)</li>     <li>Associativity—For a monad m, m flatMap g flatMap h => m flatMap {x => g(x) flatMap h} <pre>  <code class="language-java">// java8 实现的 9*9 乘法表  public class ListMonad<T>{      private List<T> elements;        private ListMonad(T elem) {          this.elements = singletonList(elem);      }        private ListMonad(List<T> elems) {          this.elements = elems;      }        public <U> ListMonad<U> flatmap(Function<T, ListMonad<U>> fn) {          List<U> newElements = new ArrayList<>();          this.elements.forEach(elem -> newElements.addAll(fn.apply(elem).elements));          return new ListMonad<>(newElements);      }        public <X> ListMonad<X> uint(X elem) {          return new ListMonad<>(elem);      }        public <U> ListMonad<U> apply(ListMonad<Function<T, U>> m) {          return m.flatmap(this::map);      }        public <U> ListMonad<U> map(Function<T, U> fn) {          return flatmap(t -> uint(fn.apply(t)));      }        public static void main(String[] args) {          ListMonad<Integer> m = new ListMonad<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));          ListMonad<Integer> m1 = new ListMonad<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));            ListMonad<Integer> list = m.apply(m1.map(x -> y -> x * y));          // [1...81]      }  }  </code></pre> </li>    </ol>    <h2>表达式优于语句</h2>    <h3>S表达式</h3>    <ol>     <li>原子,或者;</li>     <li>形式为 (x • y) 的表达式,其中x和y也是S表达式。</li>    </ol>    <p>举个例子,递增一组数据,过滤奇数,然后进行排序,最终取出第一个。如果取不到,返回 :not-found 。</p>    <pre>  <code class="language-java">(-> [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  </code></pre>    <p>当然你也可以写成</p>    <pre>  <code class="language-java">(if-let [r (first (sort (filter odd? (map inc [1 1 1]))))]       r       :not-found)  ; -> :not-found  </code></pre>    <p>其实两者都是S表达式,但是下面的写法更加偏向于语句。从串联起来读来讲,前者明显是由于后者的。这要是放在其他函数式语言上,效果更加显著。比如下面重构if-else控制语句到Optional类型。</p>    <h3>if-else -> Optional</h3>    <pre>  <code class="language-java">Optional<Rule> 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();  }  </code></pre>    <p>这是典型的语句可以重构到表达式的场景,关键是怎么重构呢?</p>    <p>第一步,调转 if 。</p>    <pre>  <code class="language-java">Optional rule = ruleOf(id);    if(!rule.isPresent()) {      throw new RuntimeException();  }        return transform(rule.get());  </code></pre>    <p>第二步, Optional.map 函数</p>    <pre>  <code class="language-java">...  return rule.map(r -> transform(r)).get();  </code></pre>    <p>第三步, inline transform 函数</p>    <pre>  <code class="language-java">...  rule.map(r -> Rule.builder()                      .withName("No." + r.getId())                      .build()).get();  </code></pre>    <p>第四步, Optional.orElseThrow 函数</p>    <pre>  <code class="language-java">...  rule.map(r-> Rule.builder()                      .withName("No." + r.getId())                      .build())      .orElseThrow(() ->new RuntimeException());  </code></pre>    <p>第五步,注 if 释语句中的 throw new RuntimeException()</p>    <pre>  <code class="language-java">if(!rule.isPresent()) {     // throw new RuntimeException();  }  </code></pre>    <p>这时候发现语句中为空,即可将整个语句删除。可以考虑 inline rule 。</p>    <pre>  <code class="language-java">ruleOf(id).map(r-> Rule.builder()                      .withName("No." + r.getId())                      .build())      .orElseThrow(() ->new RuntimeException());  </code></pre>    <p>完毕。</p>    <h2>我们认识事物的方式</h2>    <ol>     <li>把几个简单的想法合并成一个复合概念,从而创造出所有复杂的概念。</li>     <li>简单的或复杂的两种思想融合在一起,并立即把它们联系起来,不要把它们统一起来,从而得到它所有的关系思想。</li>     <li>把他们与其他所有陪伴他们的真实存在的想法分开:这就是所谓的抽象,因此所有的一般想法都是被提出来的。</li>    </ol>    <p> </p>    <p>来自:https://lambeta.com/2018/02/17/The-Simple-Summary-of-FP/</p>    <p> </p>