編程任務之:打造斐波那契世界

本次我領到的任務以下:javascript

任務:java

你正在打造一個斐波那契世界,這是一個函數式的世界,
在這個世界中每一個生命都是一個函數

root是這個世界的祖先
root.value; // 1

在這樣的世界,生孩子特別容易:

const child = root(); // 建立下一代 
child.value // 1

const child_of_child = child(); // 孫子
child_of_child.value // 2

child_of_child().value // 3
child_of_child()().value // 5

const xxx = root()()()()()... // 子子孫孫無窮盡也
xxx.value // 已經不知道是多少了

請建立這個世界的祖先 root

任務
完成這個斐波那契世界代碼

這個任務的本意是探索原型(prototype based)編程的,這樣能夠領略一個更加精簡的javascript,不過在編寫示例代碼過程當中沒收住,使用了流和函數式編程去搞定了,實現過程當中偶爾的一些想法也值得記錄,因此此次先聊聊函數式編程,下次再專門探索原型編程。算法

關於斐波那契算法自己,及其在天然界中神奇的存在這裏就略過了,知乎中有很專業的回答,公式很專業,尤爲是裏面的圖片真不錯。編程

之前的編程任務多數是要求打印出序列前n項的值,接口每每像這樣數據結構

function fibs(n) {
 ...
}

而後咱們巴拉巴拉用一個循環搞定, 而此次重點在於接口,須要實現一個斐波那契序列發生器。dom

我快速實現了第一個版本:函數式編程

class Fibs {
  constructor() {
    this.prev = 0;
    this.cur = 1;
  }
  
  next() {
    const value = this.prev;
    [this.prev, this.cur] = [this.cur, this.prev + this.cur];
    return value;
  }
}

而後用一段平凡的for語句打印一下,看看有沒有弄對。函數

const fib = new Fibs();
for (let i = 0; i < 10; i++) {
  const value = fib.next();
  console.log(value);
}

還沒寫完時就想到了還可使用生成器函數來解決:工具

function* fibs() {
  let [prev, cur] = [0, 1];
  while (true) {
    yield prev;
    [prev, cur] = [cur, prev + cur];
  }
}

對於生成器,咱們可使用for of來迭代,爲了代碼更優雅,先提供兩個工具方法。學習

一個用於打印:

function p(...args) {
  console.log(...args);
}

再寫一個take,用於從迭代器中截取指定數量的元素。

function take(iter, n) {
  const list = [];
  for (const value of iter) {
    list.push(value);
    if (list.length === n) {
      break;
    }
  }
  return list;
}

而後就能夠輸出fib序列的前20個元素了

p(take(fibs(), 20));

不知不覺走遠了,回到題目才發現有點搞不定。

雖然題目中存在着迭代結構,但數據本質是immutable的,而上面兩個版本的實現,第一個是採用普通的面向對象來實現,每次調用方法獲得結果的同時,也修改了對象的狀態,爲下一次調用作好準備。
第二個是生成器函數,依靠它產生的迭代器不斷迭代獲得結果, 但迭代的同時也會修改其內部狀態。

這種依靠維護對象狀態變化來解決問題是面向對象編程的特色,學習面向對象編程就是探討如何更好地處理好狀態的變化,如何把狀態以一種更合理的方式劃分到不一樣的對象中,如何合理地處理好各對象之間的關係,使它們的鏈接更加清晰簡單,這是面向對象原則和模式所追求的。

堂堂面向對象就搞不定這活?

呃,不變(Immutable)也能夠啦:

class Fib {
  constructor(prev = 0, cur = 1) {
    this.prev = prev;
    this.cur = cur;
  }
  
  get value() {
    return this.prev;
  }
  
  next() {
    return new Fib(this.cur, this.prev + this.cur);
  }
}

而後看當作果:

const r0 = new Fib();
p(r0.value);

const r1 = r0.next();
p(r1.value);

const r5 = r1.next().next().next().next();
p(r5.value);

let r = new Fib();
for (let i = 0; i < 19; i++) {
  r = r.next();
}
p(r.value);   // r20

真是披着OO的皮,操着FP的心,算是接近題目的答案了。

再加點語法糖就搞定了:

function funlike(o) {
  const fn = () => funlike(o.next());
  fn.value = o.value;
  return fn;
}

結果在這裏:

const root = funlike(new Fib());
p('root', root.value);

const c1 = root();
p('c1', c1.value);

const c2 = c1();
p('c2', c2.value);

const c3 = c2();
p('c3', c3.value);

const c10 = c3()()()()()()();
p('c10', c10.value);
p('c3', c3.value);
p('root', root.value);

感受不是很簡潔呀,經過一個class兜了一大圈,
重構精簡一下不過5句話:

function fibworld([prev, cur] = [0, 1]) {
  const fn = () => fibworld([cur, prev + cur]);
  fn.value = prev;
  return fn;
}

這樣使用:

const d0 = fibworld();
p('d0', d0.value);

const d1 = root();
p('d1', d1.value);

const d2 = d1();
p('d2', d2.value);

const d3 = d2();
p('d3', d3.value);

const d10 = d3()()()()()()();
p('d10', d10.value);
p('d3', d3.value);
p('d0', d0.value);

答案太簡單,下面嘗試把問題複雜化, 學習時咱們要把簡單問題複雜化,如此才能在工做中把複雜問題簡單化。

上面咱們實現了一個函數,使用這個函數能夠源源不斷地產生斐波那契數,咱們常常須要源源不斷地產生一些東西, 爲此咱們定義一個標準的對象來表示這種能夠源源不斷地產生東西的行爲,給它一個很酷的名字:無窮流

{
  value: {any}      // 值
  next: {function}  // 產生下一個對象
}

好比咱們寫一個一直輸出1的流

function ones() {
  return {
    value: 1,
    next: () => ones()
  };
}

這還用了遞歸呀,還好問題自己比較簡單,應該不會繞暈。

爲了能更好地觀察無窮流產生的元素,也須要一個take:

function take(stream, n) {
  return n > 0 ? [stream.value].concat(take(stream.next(), n - 1)) : [];
}

啊哦,這回的遞歸可真的繞暈了, 其實寫成迭代也能夠,主要是由於下面會不斷用到遞歸因此先習慣一下:

function take(stream, n) {
  const list = [];
  for (let i = 0; i < n; i++) {
    list.push(stream.value);
    stream = stream.next();
  }
  return list;
}

而後嘗試打印一下:

log(take(ones(), 10));
// [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

這有點無聊,咱們再來一個天然數:

function ints(n = 0) {
  return {
    value: n,
    next: () => ints(n + 1)
  };
}
log(take(ints(), 10));
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

重點來了,關鍵是咱們能夠像操做數據一下操做這個流。

好比把兩個流相加:

function add(a, b) {
  return {
    value: a.value + b.value,
    next: () => add(a.next(), b.next())
  };
}

而後咱們就能夠計算1+1=2

function twos() {
  return add(ones(), ones());
}

一個2到底的流:

log(take(twos(), 10));
// [2, 2, 2, 2, 2, 2, 2, 2, 2, 2]

天然數流也可使用add獲得:

function ints() {
  return {
    value: 0,
    next: () => add(ones(), ints())
  }
}
log(take(ints(), 10));
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

如今你以爲什麼是天然數呢?

真正的重點來了,咱們可使用相似的方法產生斐波那契流:

function fibs() {
  return {
    value: 0,        // 第1個元素是0
    next: () => ({
      value: 1,      // 第2個元素是1
      next: () => add(fibs(), fibs().next())   // 相加。。。
    })
  };
}

這真的能工做!

log(take(fibs(), 20));
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]

咱們又不知不覺接近題目的答案,只是此次換了一種方法, 一樣也要加點語法糖:

function funlike(stream) {
  const fn = () => funlike(stream.next());
  fn.value = stream.value;
  return fn;
}

結果就產生了另外一個斐波那契世界:

const root = funlike(fibs());
log('root', root.value);

const c1 = root();
log('c1', c1.value);

const c2 = c1();
log('c2', c2.value);

const c3 = c2();
log('c3', c3.value);

const c10 = c3()()()()()()();
log('c10', c10.value);
log('c3', c3.value);
log('root', root.value);

咱們能夠像操做數據同樣操做流,這意味着除了普通的add, 咱們還能夠filter, map, reduce,因而全部本來只對列表操做的美好東西均可以使用到流身上。

流同時還兼具過程式for循環語句節儉的特性,只進行必要的計算

除此以外,更重要的是它還能夠自由組合

假設如今實現一個需求:

從斐波那契序列出找出>1000的2個素數。

若是是過程式的方法,實現起來也不難,就是幾段實現細節的代碼會揉在一塊兒,要是再添點邏輯就會糊了。
而若是採用組合的方式,咱們能夠這樣:

  1. 斐波那契序列,咱們已搞定
  2. 查找素數,因此得實現一個filter用於過濾,接下來會作
  3. 查找>1000的數,使用第2步的filter便可。
  4. 前2項,使用已實現的take便可
  5. 素數值,這個小時候寫過不少次,應該也不難。

根據目前的分析,咱們只須要實現一個filter和一個isPrime便可。

先回憶小時候的isPrime:

function isPrime(n) {
  if (n < 2 || n % 2 === 0) {
    return false;
  }
  
  const len = Math.sqrt(n)
  for (let i = 2; i <= len; i++) {
    if (n % i === 0) {
      return false;
    }
  }
  return true;
}

我作了點優化:

  1. 偶數就不檢測了
  2. 只整除到平方根以前的數,由於更大的數不必除。

下面是咱們關心的filter:

function filter(stream, fn) {
  const {value} = stream;
  if (fn(value)) {
    return {value, next: () => filter(stream.next(), fn)};
  }
  return filter(stream.next(), fn);
}

接下來就能夠直接搞定了:

log(take(filter(filter(fibs(), n => n > 1000), isPrime), 2))
// [1597, 28657]

這裏有兩個問題,第一個是組合的語句是倒裝句形式,惋惜js中沒有管道操做符,只能依靠鏈式操做優化一些,第二個是素數的計算有點慢,卡了1s鍾。

實現一個函數,用於支持鏈式操做。

function chainable(fns) {
  return init => {
    const ret = {value: init};
    for (const k in fns) {
      ret[k] = (...args) => {
        args.unshift(ret.value);
        ret.value = fns[k](...args);
        return ret;
      };
    } 
    return ret;
  };
}
const $ = chainable({ log, take, filter, fibs, isPrime });

而後上面的語句就能夠改寫成:

$()
.fibs()
.filter(n => n > 1000)
.filter(isPrime)
.take(2)
.log();

至於素數檢測慢的問題,能夠利用費馬小定理來解決。

定理指出,對於任意一個素數p,知足如下等式:

Math.pow(base, p - 1) % p === 1

反過來也基本成立,因此咱們能夠隨機選一些base,檢測等式是否成立來判斷是否爲素數,
須要說明的是,這是個機率算法,只能保證在大機率上是素數,知足此定理但不是素數的數被稱爲僞素數,好比 341 = 11 * 31

這裏主要的邏輯是乘法除模運算,須要點技巧,由於正常算數字太大了會越界。

  1. 使用邊取模邊乘的方式來解決越界問題,由於: a * b % c === ((a % c) * (b % c)) % c
  2. 對於偶數 pow(base, exp) --> square(pow(base, exp / 2))
  3. 對於奇數 pow(base, exp) --> base * pow(base, exp - 1) --> base * 偶數狀況

這就把計算複雜度降到對數級。

function expmod(base, exp, m) {
  if (exp === 0) {
    return 1;
  }
  if (exp % 2 === 0) {
    return square(expmod(base, exp / 2, m) % m;
  }
  return expmod(base, exp - 1, m) * base % m;
}

function square(x) {
  return x * x;
}

接下來的實現就比較直接

function quickCheck(p) {
  if (p === 2) {
    return true;
  }
  if (p % 2 === 0) {
    return false;
  }
  if (p > 2) {
    // 隨機選擇10個數做爲底,使用以上公式進行驗證,全都經過則斷定爲素數
    return Array(10).fill(1).every(() => {
      let base = rand(p);
      base = base > 1 ? base : 2;
      return expmod(base, p - 1, p) === 1;
    });
  }
  return false;
}

function rand(n) {
  Math.floor(Math.random() * n);
}

簡單寫個函數比較一下二者的執行速度差別:

function timing(fn) {
  return (...args) => {
    const now = Date.now();
    fn(...args);
    const cost = Date.now() - now;
    log(`${fn.name} cost ${cost}ms`);
  }
}

選兩個比較大的素數測試下

log(timing(isPrime)(100001651));
log(timing(quickCheck)(100001651));

在個人機子上輸出:

isPrime cost 6ms
quickCheck cost 1ms

最後總結一下:

在面向對象編程中,咱們經過構建一個個具備狀態的對象來描述問題域,這些對象的狀態會隨着系統的運行而變化,這些狀態被封裝在對象內部,原則上對外界不可見。對象和對象之間會創建各類鏈接(包含、引用、繼承等),而後經過消息(方法調用)互動和協做。
因此在面向對象編程中,咱們須要關注對象的劃分是否合理,對象和對象之間的鏈接方式是否經得起折騰。

在函數式編程中,咱們讓數據暴露在陽光下,而不是隱藏在對象內部;咱們讓這些數據流過一個個簡潔的轉換器最終獲得咱們須要的樣子,而不是直接修改它。即:

1. Explicit state instead of implicit state
2. transformation instead of mutation

經過探索流這種數據結構,咱們知道數據不只能夠表明一時,並且能夠表明一世。在面向對象領域,對象的狀態隨着時間的變化而變化,任何某一時刻只表明當時的狀態,而流這種結構可以讓咱們同時擁有全部狀態,由於它描述的是產生狀態的規則。就像三維生命只能擁有當下,而更高維的生命能夠去往任什麼時候刻。

相關文章
相關標籤/搜索