JavaScript Function Programming - 函數式編程


編程思路的概念[補充]

//1.面向過程的 想到哪寫到哪
//2.面向對象的 共有的屬性和方法封裝到一個類裏 封裝
//3.面向切面編程 統計一個函數執行的時間
//4.函數式編程 提純無關於業務的純函數  函數套函數產生神奇的效果
//5.函數式編程不是用函數來編程 函數套函數讓函數更強大 OOP

函數式編程思維

範疇論

  • 範疇論是數學分支的一門學科, 世界上所有的體系都可以抽象出一個個範疇;
  • 彼此之間存在某種關係,概念,事物,對象等等,都構成範疇. 只要找出他們之間的關係就能定義;
  • 箭頭函數表示範疇成員之間的關係,正式名稱叫做態射,範疇論認爲,同一個範疇的所有成員,就是不同狀態的”變形”,通過 態射 一個成員可以變形成另一個成員.

所有成員是一個集合; 變形關係是函數

基本理論

  • javascript函數稱爲一等公民,指的是函數與其他數據類型一樣.處於平等地位,可以賦值給其他變量,也可以作爲參數,轉入另一個函數,或者作爲別的函數的返回值;
  • 不可改變量.在函數編程中, 我們通常理解的變量在函數式編程中也被函數代替了:在函數式編程中變量僅僅代表某個表達式, 這裏所說的變量是不可以被修改的. 所有的變量只能被賦值一次初值;
  • map & reduce 是最常用的函數編程的方法;

基本概念

純函數

  • 對於相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用,也不依賴外部環境的狀態;
var xs=[1,2,3,4,5];
//Array.slice是純函數,因爲他沒有副作用,對於固定的輸入和輸出總是固定的;
xs.slice(0,3);
xs.slice(0,3);
xs.splice(0,3);
xs.splice(0,3);
  • 優缺點:

//example 1
import _ from 'lodash';
var sin=_.mimorize(x=>Math.sin(x));
//第一次計算的時候,會稍微慢一點,
var a=sin(7);
//第二次有了緩存,速度極快;
var b=sin(7);

優 : 純函數不僅可以有效的降低系統的複雜度;還有很多很棒的特性,比如可緩存性;

//example 2
//不純的
var min=18;
var checeage=age=>age>18;
//純的
var checkage=age=>age>18;
//在不純的版本中,checkage不僅取決於age 還有 外部的依賴的變量min;而純的版本中,checkage 把關鍵字 18 硬編碼在函數內部,擴展性比較差,推薦使用 函數柯里化優雅的解決;

函數的柯里化

  • 傳遞給函數的一部分參數來調用它, 讓他返回一個函數去處理剩下的參數.
  • 我們一起用柯里化來改上面的那個代碼
//example 3 
var checkage=age=>age>min;
var checkage18=checkage(18)(20);
//example 4  我們再來看一組函數柯里化的code
//未柯里化之間
function add(x,y){return x+y};
add(1,2)//3
//柯里化之後
function add(x){
    return function(y){
        return x+y;
    }
}
add(2)(1)
//example 5 優缺點
import {curry} from 'lodash';
var match = curry((reg,str)=>str.match(reg));
var filter=curry((f,arr)=>arr.filter(f));
var haveSpace = match(/\s+/g);//清楚空格
filter(haveSpace)(["asdffgh","hello world"])

事實上柯里化 是一種預加載 函數的方法, 通過傳遞較少的參數,得到一個已經記住了這些參數的新函數,某種意義上講,這是一種對參數的”緩存”,是一種非常高效的編寫函數的的方法;

函數的組合

  • 純函數以及如何把柯里化寫出的洋蔥代碼h(g(f(x))),爲了解決函數嵌套問題,我們需要用到”函數”組合”;
  • 我們一起用柯里化開改他,讓多個函數像拼積木一樣
//example 6
const compose=(a,g)=>(x=>a(g(x)));
var first = arr => console.log(arr[0]);
var reverse=arr=>arr.sort();
var last = compose(first, reverse);
last([6,2,3,4,5,1])

函數組合圖解

Point Free

  • 把一些對象自帶的方法轉化成純函數,不要命名轉瞬即逝的中間變量.(一般這種思路用於封裝之間的API)
//example 7 
//一般我們是這樣使用api的,
const f=str=>str.toUpperCase().split(' ');
//Point Free 改寫之後呢.
var toUpperCase=word=>word.toUpperCase();
var split=x=>(str=>str.split(x));
var f=compose(split(' '),toUpperCase);//example 6 的compose

f("abcd efgh");

這種風格能夠幫我們減少不必要的命名,讓代碼保持簡潔和通用

聲明式與命令式代碼

//example 8
//先看段代碼
//命名式:需要我們一條一條的指令去執行代碼
let ceo=[];
for(var i=0;i>arr.length;i++){
    ceo.push(arr[i])
}
//聲明式:通過表達式的方式來聲明我們想幹的事,而不是一步一步指示;
let CEO=arr.map(c=>c)

優 : 這種聲明式的代碼,對於無副作用的純函數,可以不考慮函數內部的實現,專注編寫業務代碼;相反對於不純的函數,代碼會出現副作用或者依賴外部系統的環境,這對於程序員來說式極大的負擔.

惰性求值,惰性函數

  • 定義 由於每個函數都有可能改動或者依賴於其外部狀態,因此必須順序執行;

函數式編程常用的核心概念

高階函數

  • 函數當參數,把傳入的函數做一個封裝,然後返回這個封裝函數,達到更高程度的抽象
//example 9
//命令式
var add=function(a,b){
    return a+b;
};
function math(f,array){
    return f(array[0],array[1]);
}
math(add,[1,2]);//3

尾調用優化

  • 函數內部最後的動作是函數調用,該調用的返回值, 直接返回給函數;函數調用自身,稱爲遞歸;如果尾調用自身,就稱爲尾遞歸。遞歸需要保存大量的調用記錄,很容易發生棧溢出 錯誤,如果調用了尾遞歸優化,將遞歸變成循環,如果尾調用自身,就稱爲尾遞歸;
//example 10
 // 不是尾遞歸,無法優化function factorial(n) {
        if (n === 1) return 1;
        return n * factorial(n - 1);
    }

    function factorial(n, total) {
        if (n === 1) return total;
        return factorial(n - 1, n * total);
    } //ES6強制使用尾遞歸,一定要調用自身

閉包

//example 11
function makePowerFn(power) {
    function powerFn(base) {
        return Math.pow(base, power); //Math.pow(x,y)返回 x 的 y 次冪的值。
    }
    return powerFn;
}

var square = makePowerFn(2);
square(3)//3

雖然外層的makePowerFn函數執行完畢了,棧上的幀也被釋放了,但是堆上的作用域並沒有被釋放,因此powerFn依舊可以被

容器 \ Functor

  • 我們可以發”範疇”想象成一個容器,裏面包含兩樣東西,值(value),值的變形關係,也就是函數
  • 範疇論使用函數,表達範疇之間的關係.
  • 伴隨着範疇論的發展,就發展出一整套函數的運算方法.這套方法起初只是用於數學元算,後來有人將它在計算機上實現了. 就變成了今天的”函數式編程”.
  • 函數不僅可以用於同一個範疇之中值的轉換,還可以用於將一個範疇轉成另一個範疇,這就涉及到了函子(Functor)
  • 函子是函數式編程裏面最重要的數據類型,也是基本的運算單位的功能單位.它首先是一種範疇,也就是說,是一種容器,包含了值和變形關係; 比較特殊的是,它的變形關係可以依次作用於每一個值,將當前的容器變形成一個容器.
//example 12
//先設定一個容器
var Container=function (x) {
    this.__value=x;
}
//按照約定, 函子會有一個of方法
Container.of = x => new Container(x);

// 一般約定,函子的標誌就是容器具有map方法. 該方法將容器裏面的值映射到另一個容器
Container.prototype.map=function (f) {
    return Container.of(f(this.__value))
}
Container.of(3).map(x=>x+1).map(x=>console.log('Result is ' + x));

example 12中,Functor是一個函子,它的map方法接受函數f作爲 參數,然後返回一個新的函子,裏面包含的值是被f處理過的 (f(this.val))。 一般約定,函子的標誌就是容器具有map方法。該方法將容器裏 面的每一個值,映射到另一個容器。 上面的例子說明,函數式編程裏面的運算,都是通過函子完成, 即運算不直接針對值,而是針對這個值的容器—-函子。函子本 身具有對外接口(map方法),各種函數就是運算符,通過接口 接入容器,引發容器裏面的值的變形。 因此,學習函數式編程,實際上就是學習函子的各種運算。由 於可以把運算方法封裝在函子裏面,所以又衍生出各種不同類 型的函子,有多少種運算,就有多少種函子。函數式編程就變 成了運用不同的函子,解決實際問題

.

//example 13
class Functor{
    constructor(val){
        this.val=val;
    }
    map(f){
        return new Functor(f(this.val))
    }
}

(new Functor(2)).map(two=>two+2)

你可能注意到了,上面生成新的函子的時候,用了 new命令。這實在太不像函數式編程了,因爲new命令是 面向對象編程的標誌。 函數式編程一般約定,函子有一個of方法,用來生成新 的容器

.

//example 14 
var Maybe = function (x) {
    this.__value = x;
}
Maybe.of = function (x) {
    return new Maybe(x);
}
Maybe.prototype.map = function (f) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}
Maybe.prototype.isNothing = function () {
    return (this.__value === null || this.__value === undefined);
}
Maybe(null)

函子接受各種函數,處理容器內部的值。這裏就有一個問題,容器內部的值可能是一個 空值(比如null),而外部函數未必有處理空值的機制,如果傳入空值,很可能就會出錯 上面的 example 14 使用三目運算來判斷值, 產生新的容器稱之爲Maybe 函子

錯誤處理 \ Either \ AP

  • 我們的容器能做的事太少了,try/catch/throw 並不是”純”的,因爲它從外部接管了我們的函數,並且在這個函數出錯時拋棄了它的返回值;
  • Promise 是可以調用catch採集處理錯誤的
  • 事實上Either [譯:兩者之一] 並不是用來做錯誤處理的,它表示了邏輯”或”
class Functor{
    constructor(val){
        this.val=val;
    }
    map(f){
        return new Functor(f(this.val))
    }
}
class Either extends Functor {
    constructor(left, right) {
        super();
        this.left = left;
        this.right = right;
    }

    map(f) {
        return this.right ? Either.of(this.left, f(this.right)) : Either.of(f(this.left), this.right);
    }
}

Either.of = function (left, right) {
    return new Either(left, right);
};
var addOne=function (x) {
    return x+1;
};
Either.of(5,6).map(addOne);
// Either.of(1,null).map(addOne);
// Either.of({address: "xxxx"}, currentUser.address).map(updataField);//真實場景

IO

Monad

    //做一些準備工作
    localStorage.test = ["a", "b"];
    //函數組合代碼
    const compose = (f, g) => (x => f(g(x)));

    /**
     * 基礎函子
     * 1,擁有map對象的容器變成函子
     * 2,map對象的作用是可以通過變形關係 f 函數 作用到每一個函子的值
     */
    class Functor {
        constructor(val) {
            this.val = val;
        }

        map(f) {
            return new Functor(f(this.val));
        }
    }

    /**
     * Monad 函子
     * 1, 核心作用是總是返回一個單層的函子 '
     * 2, 通過拆解成互相鏈接的多個步驟,只要提供下一一步運行所需的函數,整個運算就會自動進行下去;
     * 3, 可以讓我們避開嵌套地獄,可以輕鬆的進行深度嵌套的函數, 比如IO和其他異步任務
     */
    class Monad extends Functor {
        join() {
            // 實現返回單層函子
            return this.val();
        }

        flatMap(f) {
            return this.map(f).join();
        }
    }

    /**
     * IO函子
     * 1,現實開發中不是所有的操作都是非常純的,所以IO函子主要是封裝那些不純的操作;
     * 2,特別是要記下他的map方法
     */
    class IO extends Monad {
        map(f) {
            return IO.of(compose(f, this.val))
        }
    }

    IO.of = x => new IO(x);

    //這裏有三個不純的函數;所以要用IO函子包裹,然後就變純了  一定要包裹到IO函子裏面
    const print = function (x) {
        return new IO(function () {
            console.warn(x + "【step 2】");
            return x;
        });
    }
    const tail = function (x) {
        return new IO(function () {
            console.warn(x[x.length - 1] + "【step 1】");
            return x[x.length - 1] + "【step 1】";
        });
    }
    const readFile = function (data) {
        return new IO(function () {
            console.warn('chain start');
            return localStorage[data];
        });
    };
    //關鍵的核心代碼
    //1.readFile('test') 創建了一個IO函子 值是 return localStorage["test"];
    //2.IO繼承自Monad 所以擁有了flatMap(把它叫chain也行)
    //3.flatMap 接收了tail函數 tail幹了啥呢 接受一個x返回一個新的IO 爲啥呢??因爲tail裏的操作不純啊
    //4.flatMap內部執行了map 這個map是IO的map哦 因爲extend的時候重寫了
    //5.IO.of(compose(f, this.val)) => IO函子(value = function(x){return f(g(x) })
    // var g = function () {
    //         console.log('chain start');
    //         return localStorage[data];
    // })
    // var f = function (x) {
    //     return new IO(function () {
    //         console.log(x[x.length - 1] + "【step 1】");
    //         return x[x.length - 1] + "【step 1】";
    //     });
    // }
    // f(g());
    //6.繼續執行join函數 如果不執行join 最下面要一層層的執行val(可以去掉join試驗一下) 這也是monad精髓所在
    //7.上面實際上返回了 一個新的IO 所以可以鏈式的繼續flatMap 但是萬萬注意的是這個io的value是組合函數傳回來的一個函數 需執行記住啦!!
    //8.所以join return this.val()會繼續返回新的IO方便鏈式 完成了全部的操作
    const result = readFile('test')
        .flatMap(tail)
        .flatMap(print);
    //函數式編程只關心計算 和 數據的映射 並不關注該題的步驟 是舊的範疇到新範疇的映射
    //其餘的什麼curry 懶加載 遞歸 等等都是衍生知識 僅此而已 如果你更關注過程的話 最後的一步解答方式該是如下
    //result.val();

當下函數式編程最熱的庫

RxJS

cycleJS

lodashJS

underscoreJS

ramdajs

函數式編程的實際應用場景

易調試、熱部署、併發

單元測試

總結與補充

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章