coding 優雅指南:函數式編程

函數式編程是一種編程範式,主要思想是把程序用一系列計算的形式來表達,主要關心數據到數據之間的映射關係。同時在 react hook 中,其實存在了大量的函數式編程的思想。所以作爲一個前端,對於函數式編程的能力還是必須要有的。

what is it?

從兩種範式的區別講起

pure function(純函數) & 副作用

純函數的輸出可以不用和所有的輸入值有關,甚至可以和所有的輸入值都無關。但純函數的輸出不能和輸入值以外的任何資訊有關。純函數可以傳回多個輸出值,但上述的原則需針對所有輸出值都要成立。以下一個初中數學的圖,可以很好的說明這個道理。

再往下(從 lambda 演算開始):

計算機科學,尤其是編程語言,經常傾向於使用一種特定的演算:lambda 演算 [4] (Lambda Calculus)。這種演算也廣泛地被邏輯學家用於學習計算和離散數學的結構的本質。Lambda 演算偉大的的原因有很多,其中包括:

lambda 演算最重要的兩個規約:Alpha 規約和 Beta 規約,簡單來講,就是

初中數學知識,對吧。好的,我們再來引入一點知識:丘奇數 [6], 這樣我們能表示一定的計算了,再引入邏輯定義,和循環定義,這樣是不是就能表達所有計算了。這裏就不展開再繼續了,有興趣可以移步閱讀 https://cgnail.github.io/academic/lambda-1/[7]。函數式編程其實就是基於 lambda 演算衍生出來的。

why use it ?

從一個簡單的例子講起:

const add = (x,y)=>x+y; 
const multiply = (x,y)=>x*y; add(multiply(b, add(a, c)), multiply(a, b));
// add(b*(a+c),a*b); // add(add(ab+ac),a*b); // ab+ab+ac multiply(a,add(b,add(b+c)))

從上面代碼可以看出來,函數式的優點,可以任意組合,拆分。

特點:

優點:

// not pure
  const a = {x:5,y:10};
  const b = ()=>{
    console.log(a.x)
  }
  b();// =5
   // later ...
  a.x =10;
  // later ...
  b(); // =>10
// some thing are wrong  
// 誒 輸出爲什麼變了, b裏面怎麼計算的,b依賴的啥呀
// 誒 爺找到他怎麼計算的了
// 爲啥前後不一致呢
// 臥槽 這裏爲啥改了全局變量
// pure
const b = (obj)=>{
  console.log(obj?.a)
}
b({a:5});
// 這個東西出問題了.. 參數被改變了 over over

展開講講:

  1. 輸入輸出顯示,那麼我們可以得到這個函數的映射表,說明我們可以對這個函數計算結果進行緩存,如果有同樣輸入的調用,那麼我們可以直接返回計算後的值。
const memo = (fn)=>{
    const cache = new Map();
    return (...args)=>{
        const key = JSON.stringify(args);
        if(cache.has(key)){
            return cache.get(key);
        }
        const res = fn.call(fn,...args);
        cache.set(key,res);
        return res;
    }
}
const addOne = memo(x=>x+1);
addOne(5); // 計算
addOne(5); // 緩存
  1. 可以將一個不純的函數轉換成一個純函數
const pureHttpGet = memo((url,params)=>{
  return ()=>{
    return axios.get(url,params);
  }
})

這個函數之所以能稱爲純函數,他滿足純函數的特性,根據輸入,總是返回固定的輸出。

  1. 可移植性 一句名言:“面嚮對象語言的問題是,它們永遠都要隨身攜帶那些隱式的環境。你只需要一個香蕉,但卻得到一個拿着香蕉的大猩猩... 以及整個叢林”

  2. 可測試性與引用透明,對於一個純函數,我們可以很清晰的去斷言他的輸入輸出,同時因爲引用透明,可以很簡單的去推導出函數的內部的調用過程,從而去簡化 & 重構這個函數。

  3. 並行:回憶一下操作系統死鎖的原因,以及爲什麼有鎖這個機制的存在,就是因爲需要使用 / 更改外部的資源,但是純函數不需要訪問共享內存,所以也不會因爲副作用進入競爭態。

core:

  1. 高階函數
Array.prototype.fakemap = function (callback, thisArg) {
    if (!Array.isArray(this)) {
      throw new Error("Type Error");
    }
    if(typeof callback!=="function"){
        throw new Error(callback.toString()+'is not a function')
    }
    let resArr = [];
    let cb = callback.bind(thisArg);
    for (let i = 0; i < this.length; i++) {
      resArr.push(cb(this[i], i, this));
    }
    return resArr;
};
// 高階函數當然也可以組合使用 與純函數性質一致
[1,2,3,4].filter(item=>item>2).map(item=>item -1)
  1. 偏函數應用 partial function
// 創建偏函數,固定一些參數
const partial = (f, ...args) =>
  // 返回一個帶有剩餘參數的函數
  (...moreArgs) =>
    // 調用原始函數
    f(...args, ...moreArgs)

const add3 = (a, b, c) => a + b + c

// (...args) => add3(2, 3, ...args)
// (c) => 2 + 3 + c
const fivePlus = partial(add3, 2, 3)

fivePlus(4)  // 9

js 中最常見的 Function.prototype.bind(會改變 this 指向), 其實就可以實現

const foo = (a:number,b:number)=>{
  return a+b;
}
const bar = foo.bind(null,2);
bar(3);//5

簡單來說,偏函數就是固定部分參數,返回新函數做計算,如果需要完整實現的話,可以參考一下 lodash 的 partial.js 這個文件,大致意思簡單的將源碼思路寫一下

function partial(fn) {
  const args = Array.slice.call(arguments,1);
  return function(){
    const length = args.length;
    let position = 0;
    // 把用佔位符的參數替換掉
    for(let i =;i<length,i++){
      args[i] = args[i]=== _ ? arguments[position++]:args[i]
    }
    // 將剩下的參數懟進去
    while(position<arguments.length) args.push(arguments[postion++]);
    return fn.apply(this,args)
  };
}
  1. 柯里化
const curry = fn ={
  if (fn.length <= 1) {
    return fn;
  }
  const iter = args =>
    args.length === fn.length
      ? fn(...args)
      : arg => iter([...args, arg]);
  return iter([]);
};
  1. 閉包

閉包最初是來源於 lambda 演算中的一個概念,閉包(closure)或者叫完全綁定(complete binding)。在對一個 Lambda 演算表達式進行求值的時候,不能引用任何未綁定的標識符。如果一個標識符是一個閉合 Lambda 表達式的參數,我們則稱這個標識符是(被)綁定的;如果一個標識符在任何封閉上下文中都沒有綁定,那麼它被稱爲自由變量。

一個 Lambda 演算表達式只有在其所有變量都是綁定的時候才完全合法。js 中的閉包就不在這囉嗦了,爲什麼需要閉包,我們可以看一個最簡單的柯里化的例子。

const add = (w,x,y,z)=>w+x+y+z;
const curryAdd = curry(add);
// 根據上面 寫的curry函數, 我們來分解一下


const a = curryAdd(1); // 這個時候返回了一個包含1的函數 & add的函數
const b = a(2);
const c = b(3);
const d = c(4);
// 如果沒有閉包,之前傳入的東西參數都無跡可尋了。

從 webstorm 單步調試可以更清楚的知道這個函數的作用域 & 存在哪些閉包。

How do I use it ?

// Bad
const BlogController = {
  index(posts) { return Views.index(posts); },
  show(post) { return Views.show(post); },
  create(attrs) { return Db.create(attrs); },
  update(post, attrs) { return Db.update(post, attrs); },
  destroy(post) { return Db.destroy(post); },
};
// Good
const BlogController = {
  index: Views.index,
  show: Views.show,
  create: Db.create,
  update: Db.update,
  destroy: Db.destroy,
};

BlogController,雖說添加一些沒有實際用處的間接層實現起來很容易,但這樣做除了徒增代碼量,提高維護和檢索代碼的成本外,沒有任何用處。

另外,如果一個函數被不必要地包裹起來了,而且發生了改動,那麼包裹它的那個函數也要做相應的變更。

foo(a, b => bar(b));

如果 foo 增加回調中處理的函數,那麼不只是要改掉 foo 和 bar,所有涉及到的調用也會更改。

foo(a, (x, y) => bar(x, y));

寫成一等公民函數的形式,要做的改動將會少得多:

foo(a, bar);  // 只需要更改foo中執行bar的邏輯和 bar中執行的邏輯
// 只針對當前的博客
const validArticles = articles =>
 articles.filter(article => article !== null && article !== undefined),

// 對未來的項目更友好
const compact = xs => xs.filter(x => x !== null && x !== undefined);

使用 javascript api 過程中,不要使用含有副作用的 API,而選擇無副作用的 api。例如 slice 和 splice,肯定是選擇 slice,不要修改傳入的引用對象等。來看一點 case

// bad errInfo 永久的被改動了,如果有其他地方使用到的話,可能會出現問題
const valiateRepeatPhone = (phones: string[], errInfo: IErrorInfo[]) ={
  for (let i = 0; i < phones.length; i++) {
    if (errInfo[i] === ERROR_TYPE.PHONE_REPEAT) {
      errInfo[i] = ERROR_TYPE.NULL;
    }
    if (
      errInfo[i] === ERROR_TYPE.NULL &&
      phones[i] !== '' &&
      phones.indexOf(phones[i]) !== i
    ) {
      errInfo[i] = ERROR_TYPE.PHONE_REPEAT;
    }
  }
};
// 稍好一點的
const valiateRepeatPhone = (phones: string[], errInfo: IErrorInfo[]) ={
   return errInfo.map((item,index)=>{
       if(item === ERRORTYPE.PHONE_REPEAT || item === ERROR_TYPE.NULL){
           return (phones[i] !== '' && phones.indexOf(phones[i]) !== i)
                   ? ERROR_TYPE.PHONE_REPEAT
                   : ERROR_TYPE.NULL
       }
       return item;
   })
}
// 命令式
const makes = [];
for(let i =0;i<car.length;i++){
  makes.push(cars[i].make);
}
const makes = cars.map(item=>item.make)
const compose = (f,g) ={
  return x ={
    return f(g(x));
  }
}
class Functor {
    private val:any;
    private constructor (val: any) {
       this.val = val;
    }
    public static of(val){
        return new Functor(val);
    }
    public map(Fn){
        return Functor.of(Fn(this.val))
    }
    
}
Functor.of(5).map(add5).map(double)

好的,根據上面,其實我們已經實現了一個函數式編程中比較重要的概念,函子(functor)。functor 是實現了 map 函數並遵守一些特定規則的容器類型。

接下來我們進一步分析,可能會存在一種情況,如果傳入是空值,會導致報錯。所以需要引入一個函子,Maybe 函子, 只需要引入一個三則,這樣我們就能夠過濾空值,防止報錯。

class Maybe {
    private val:any;
    private constructor (val: any) {
       this.val = val;
    }
    public static of(val){
        return new Maybe(val);
    }
    public map(Fn){
        return this.val ? Maybe.of(Fn(this.val)) : Maybe.of(null);
    }
    
}
Maybe.of(5).map(add5).map(double)

好的,我們現在已經得到了一個可以過濾空值的函數,但是我們現在在執行完調用後,我們獲得的是一個什麼呢,是一個對象對吧,我們還需要把值取出來,所以需要添加一個取值的方法

class Maybe {
   private val:any;
   private constructor (val: any) {
      this.val = val;
   }
   public static of(val){
       return new Maybe(val);
   }
   public map(Fn){
       return this.val ? Maybe.of(Fn(this.val)) : Maybe.of(null);
   }
   public join(){
       return this.val;
   }
}
Maybe.of(5).map(add5).map(double).join()

好了,我們現在可以拿到值了,但是,如果 may 層次太高,我們是不是需要像洋蔥一樣去剝開他的心,那更簡單的是什麼呢,在我們需要的時候去剝開它。

class Maybe {
    private val:any;
    private constructor (val: any) {
       this.val = val;
    }
    public static of(val){
        return new Maybe(val);
    }
    public map(Fn){
        return this.val ? Maybe.of(Fn(this.val)) : Maybe.of(null);
    }
    public join(){
        return this.val;
    }
    public chain(Fn) {
        return this.map(Fn).join();
    }
}
Maybe.of(5).map(add5).chain(Maybe.of(double))

IO monad,一個例子

如果我們要寫一個對 dom 的讀寫操作,將一個文本轉換爲大寫,先定義一下以下方法

const $ = (id: string) => <HTMLElement>document.querySelector(`#${id}`);
const read = (id: string) =$(id).value;
const write = (id: string) =(text: string) =$(id).textContent = text;
// not pure,因爲函數中操作了dom,對外部進行了改變
function syncInputToOutput(idInput: string, idOutput: string) {
  const inputValue = read(idInput);
  const outputValue = inputValue.toUpperCase();
  write(idOutput, outputValue);
}
export default class IO<T> {
  private effectFn: () => T;

  constructor(effectFn: () => T) {
    this.effectFn = effectFn;
  }

  bind<U>(transform: (value: T) => IO<U>) {
    return new IO<U>(() => transform(this.effectFn()).run());
  }

  run(): T {
    return this.effectFn();
  }
}

const read = (id: string) => new IO<string>(() =$(id).value);
const write = (id: string) =(text: string) => new IO<string>(() =$(id).textContent = text);

function syncInputToOutput(idInput: string, idOutput: string) {
  read(idInput)
    .bind((value: string) => new IO<string>(() => value.toUpperCase()))
    .bind(write(idOutput)))
    .run();
}

延伸閱讀

References:

參考資料

[1]

維基: https://zh.wikipedia.org/wiki / 純函數

[2]

狀態: https://zh.wikipedia.org/w/index.php?title = 程式狀態 & action=edit&redlink=1

[3]

函數副作用: https://zh.wikipedia.org/wiki / 函數副作用

[4]

lambda 演算: https://zh.wikipedia.org/wiki/Λ演算

[5]

圖靈完備: https://zh.wikipedia.org/wiki / 圖靈完備性

[6]

丘奇數: https://zh.wikipedia.org/wiki / 邱奇數

[7]

https://cgnail.github.io/academic/lambda-1/: https://cgnail.github.io/academic/lambda-1/

[8]

The Beauty of Functional Languages in Deep Learning — Clojure and Haskell: https://www.welcometothejungle.co/fr/articles/btc-deep-learning-clojure-haskell

[9]

前端中的 Monad: https://zhuanlan.zhihu.com/p/47130217

[10]

FP jargon 英文: https://github.com/hemanth/functional-programming-jargon#partial-application

[11]

FP jargon 中文: https://github.com/shfshanyue/fp-jargon-zh

[12]

https://github.com/fantasyland/fantasy-land: https://github.com/fantasyland/fantasy-land

[13]

https://github.com/MostlyAdequate/mostly-adequate-guide: https://github.com/MostlyAdequate/mostly-adequate-guide

[14]

https://github.com/llh911001/mostly-adequate-guide-chinese: https://github.com/llh911001/mostly-adequate-guide-chinese

❤️ 謝謝支持

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pf2GSrNLnw6dOmJuHfAPrw