JavaScript 函數語言程式設計

SRIGT發表於2024-09-09

0x01 函數語言程式設計

(1)概述

  • 函數語言程式設計(Functional Programming,簡稱 FP)是一種程式設計正規化,它將計算機運算視為數學上的函式計算,強調將計算過程看作是函式之間的轉換而不是狀態的改變

    • ❗ “函式” 的意思是指對映關係
    • 其他常見程式設計正規化包括程序導向程式設計、物件導向程式設計等
  • 核心思想:將函式視為一等公民

    “函式是一等公民”(First-class Function):

    • 函式可以像變數一樣傳遞和使用

      const success = (response) => console.log(response);
      const failure = (error) => console.error(error);
      
      $.ajax({
        type: "GET",
        url: "url",
        success: success,
        error: failure,
      });
      
    • 函式可以作為引數傳遞給其他函式(高階函式)

    • 函式可以作為返回值返回(高階函式)

  • 優勢:

    • 可以脫離 this
    • 更好地利用 tree-shaking 過濾無用程式碼
    • 便於測試、並行處理等
  • 舉例:

    • 非函式式

      let a = 1;
      let b = 2;
      let sum = a + b;
      console.log(sum);
      
    • 函式式

      function add(a, b) {
        return a + b;
      }
      
      let sum = add(1, 2);
      console.log(sum);
      

(2)高階函式

  • 高階函式(Higher-order Function)指可以把函式作為引數傳遞給其他函式或作為函式返回值返回

  • 函式作為引數:

    Array.prototype.myForEach = function (callback) {
      for (let i = 0; i < this.length; i++) callback(this[i], i, this);
    };
    
    const arr = [1, 2, 3];
    arr.myForEach((item, index, arr) => {
      console.log(item, index, arr);
    });
    
  • 函式作為返回值:

    function once(fn) {
      let called = false;
      return function () {
        if (!called) {
          called = true;
          return fn.apply(this, arguments);
        }
      };
    }
    
    const logOnce = once(console.log);
    logOnce("hello world");	// 正常輸出
    logOnce("hello world");	// 未輸出
    
  • 意義:透過抽象通用的問題,遮蔽函式的細節,實現以目標為導向

(3)閉包

  • 閉包(Closure)指函式與其詞法環境的引用打包成一個整體

  • 特點:可以在某個作用域中呼叫一個函式的內部函式並訪問其作用域中的成員

  • 實現方法:將函式作為返回值返回

    function add(a) {
      return function (b) {
        return a + b;
      };
    }
    
    const add5 = add(5);
    console.log(add5(3));	// 8
    
  • 本質:函式在執行的時候會放到一個執行棧上當函式執行完畢後會從棧上移除,而堆上的作用域成員因為被外部引用而不能釋放,從而使得內部函式可以訪問外部函式的成員

  • 包含關係:

    graph TB subgraph 函數語言程式設計 subgraph 高階函式 subgraph 閉包 a[ ] end end end

(4)純函式

  • 純函式(Pure Function)指相同的輸入會永遠得到相同的輸出,即一一對映

  • 舉例:Array.slice 是純函式(不會修改原陣列),而 Array.splice 是非純函式

  • 優勢:

    • 可快取

      const memoize = (fn) => {
        let cache = {};
        return function () {
          let arg_str = JSON.stringify(arguments);
          cache[arg_str] = cache[arg_str] || fn.apply(fn, arguments);
          return cache[arg_str];
        };
      };
      
      const calcArea = (radius) => {
        return Math.PI * Math.pow(radius, 2);
      };
      
      const memoizedCalcArea = memoize(calcArea);
      console.log(memoizedCalcArea(5));
      
    • 可測試

    • 並行處理

(5)副作用

  • 含義:當讓一個函式變為非純函式時,會帶來的副作用
  • 來源:配置檔案、資料庫、輸入的內容
  • 隱患:
    • 降低擴充套件性、重用性
    • 增加不確定性

(6)柯里化

  • 柯里化(Currying)是把接受多個引數的函式變換成接受一個單一引數函式,並且返回接受餘下的引數且返回結果的新函式的技術

  • 舉例:

    const compareTo = (target) => {
      return function (current) {
        return current >= target;
      };
    };
    
    const compareTo5 = compareTo(5);
    console.log(compareTo5(4));		// flase
    console.log(compareTo5(6));		// true
    
    // ES6 柯里化
    const compareTo = (target) => (current) => current >= target;
    
  • 優勢:

    • 傳遞較少的引數得到一個已快取某些固定引數的新函式
    • 降低函式的粒度
    • 將多元函式轉換為一元函式

(7)函式組合

  • 函式組合(Compose)是指將細粒度的函式組合為一個新函式

  • 函式組合預設從右到左執行

  • 舉例:

    const compose = (f, g) => {
      return function (value) {
        return f(g(value));
      };
    };
    const reverse = (array) => array.reverse();
    const first = (array) => array[0];
    const last = compose(first, reverse);
    
    console.log(last([1, 2, 3, 4]));	// 4
    
  • 結合律:compose(compose(f, g), h) === compose(f, compose(g, h))

0x02 第三方庫

(1)Lodash

a. 概述

  • 官網:https://lodash.com/

  • Lodash 是一個現代的 JavaScript 實用工具庫,提供模組化、效能和附加功能

    • 純函式的功能庫
  • 安裝:

    1. 使用命令 npm init -y 初始化 NodeJS 環境

    2. 使用命令 npm i --save lodash 安裝 Lodash

    3. 在 js 檔案中匯入並使用 Lodash

      const _ = require('lodash');
      
      const arr = [1, 2, 3]
      console.log(_.first(arr));
      console.log(_.last(arr));
      

b. 柯里化

  • Lodash 中的柯里化:curry(func)

  • 功能:

    flowchart TB a[建立一個可以接收一個或多個 func 引數的函式]-->b{func 所需的引數都被提供} b--是-->c[執行 func 並返回執行的結果] b--否-->d[繼續返回該函式並等待接收剩餘的引數]
  • 舉例:

    const _ = require('lodash');
    
    const add = (a, b, c) => a + b + c;
    const curried = _.curry(add);
    
    console.log(curried(1)(2)(3));	// 6
    console.log(curried(1, 2)(3));	// 6
    console.log(curried(1, 2, 3));	// 6
    
  • 復現:

    /**
     * 函式柯里化
     * @param {Function} func 需要柯里化的函式
     * @returns {Function} 柯里化後的函式
     */
    const curry = (func) => {
      /**
       * 柯里化函式
       * 遞迴地處理引數,直到達到原始函式所需的引數數量
       * @param {...any} args 當前函式呼叫時傳入的引數
       * @returns {*} 如果引數足夠則呼叫原始函式返回結果,否則返回下一個接收更多引數的函式
       */
      return function curriedFn(...args) {
        // 判斷當前傳入的引數數量是否足夠呼叫原始函式
        if (args.length >= func.length) {
          // 引數足夠,呼叫原始函式並返回結果
          return func.apply(this, args);
        } else {
          // 引數不足,返回一個新的函式,合併當前引數和新傳入的引數
          return function (...args2) {
            // 遞迴呼叫柯里化函式,合併引數
            return curriedFn.apply(this, args.concat(args2));
          };
        }
      };
    };
    

c. 函式組合

  • Lodash 中有兩種函式組合方法

    1. flow():從左到右執行
    2. flowRight():從右到左執行
  • 舉例:

    const _ = require("lodash");
    
    const reverse = (array) => array.reverse();
    const first = (array) => array[0];
    const fn = _.flow(reverse, first);
    
    console.log(fn([1, 2, 3, 4]));吧
    
  • 復現:

    const flow =
      (...args) =>
      (value) =>
        args.reduce((acc, fn) => fn(acc), value);
    
    const flowRight =
      (...args) =>
      (value) =>
        args.reduceRight((acc, fn) => fn(acc), value);
    
  • 除錯:使用柯里化函式

    const _ = require("lodash");
    
    const trace = _.curry((tag, v) => {
      console.log(tag, v);	// 除錯 [ 4, 3, 2, 1 ]
      return v;
    });
    
    const reverse = _.curry((array) => array.reverse());
    const first = _.curry((array) => array[0]);
    const last = _.flow(reverse, trace("除錯"), first);
    
    console.log(last([1, 2, 3, 4]));	// 4
    

d. FP 模組

  • Lodash 的 FP 模組提供函數語言程式設計的方法

  • 提供了不可變的 auto-curriediteratee-firstdata-last 方法

  • 舉例:

    const fp = require("lodash/fp");
    
    console.log(fp.map(fp.toUpper, ["a", "b", "c"]));	// [ 'A', 'B', 'C' ]
    console.log(fp.filter((x) => x % 2 === 0, [1, 2, 3, 4]));	// [ 2, 4 ]
    console.log(fp.split(" ", "Hello world"));	// [ 'Hello', 'world
    

e. Point Free

  • Point Free 是一種程式設計風格,把資料處理的過程定義成與資料無關的合成運算

  • 舉例:

    const fp = require("lodash/fp");
    
    const last = fp.flow(fp.reverse, fp.first);
    console.log(last([1, 2, 3, 4]));
    

(2)Folktale

  • 官網:https://folktale.origamitower.com/

  • Folktale 是一個標準的函數語言程式設計庫,僅提供一些函式式處理操作等

  • 使用命令 npm i --save folktale 安裝

  • 舉例:

    const { curry, compose } = require("folktale/core/lambda");
    const { toUpper, first } = require("lodash/fp");
    
    const f = curry(2, (x, y) => x + y);
    console.log(f(3, 4) === f(3)(4));	// true
    
    const g = compose(toUpper, first);
    console.log(g(["a", "b"]));	// A
    

0x03 函子

(1)概述

  • 函子(Functor)是一個特殊的容器,透過一個普通物件來實現;該物件具有 map 方法,可以執行變形關係

    • 容器包含值的變形關係
  • 作用:將副作用控制在可控範圍內

  • 舉例:

    class Container {
      static of(value) {
        return new Container(value);
      }
    
      constructor(value) {
        this._value = value;
      }
    
      map(fn) {
        return Container.of(fn(this._value));
      }
    }
    
    const obj = Container.of(1)
      .map((x) => x + 1)
      .map((x) => x * x);
    console.log(obj);	// Container { _value: 4 }
    

(2)MayBe

  • 作用:處理外部空值清空

  • 實現並舉例:

    class MayBe {
      static of(value) {
        return new MayBe(value);
      }
    
      constructor(value) {
        this._value = value;
      }
    
      map(fn) {
        return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value));
      }
    
      isNothing() {
        return this._value === null || this._value === undefined;
      }
    }
    
    console.log(MayBe.of("Hello world").map((x) => x.toUpperCase()));	// MayBe { _value: 'HELLO WORLD' }
    console.log(MayBe.of(null).map((x) => x.toUpperCase()));	// MayBe { _value: null }
    

(3)Either

  • 作用:用於異常處理,類似 if...else

  • 實現並舉例:

    class Left {
      static of(value) {
        return new Left(value);
      }
    
      constructor(value) {
        this._value = value;
      }
    
      map(fn) {
        return this;
      }
    }
    
    class Right {
      static of(value) {
        return new Right(value);
      }
    
      constructor(value) {
        this._value = value;
      }
    
      map(fn) {
        return Right.of(fn(this._value));
      }
    }
    
    const parseJSON = (str) => {
      try {
        return Right.of(JSON.parse(str));
      } catch (e) {
        return Left.of(`Error parsing JSON: ${e.message}`);
      }
    }
    console.log(parseJSON('{ "id": "0" }'));
    console.log(parseJSON({ id: 0 }));
    

(4)IO

  • 作用:將非純函式作為值,惰性執行該函式

  • 實現並舉例:

    const fp = require("lodash/fp");
    
    class IO {
      static of(value) {
        return new IO(function () {
          return value;
        });
      }
    
      constructor(fn) {
        this._value = fn;
      }
    
      map(fn) {
        return new IO(fp.flowRight(fn, this._value));
      }
    }
    
    console.log(
      IO.of(process)
        .map((p) => p.execPath)
        ._value()
    );	// path\to\node.exe
    

(5)Task

  • Folktale 提供用於執行非同步任務的 Task 函子

    Folktale 2.x 與 Folktale 1.x 的 Task 區別較大

    當前 Folktale 版本為 2.3.2

  • 舉例:

    const fs = require("fs");
    const { task } = require("folktale/concurrency/task");
    const { split, find } = require("lodash/fp");
    
    const readFile = (filename) => {
      return task((promise) => {
        fs.readFile(filename, "utf-8", (err, data) => {
          if (err) promise.reject(err);
          else promise.resolve(data);
        });
      });
    };
    
    readFile("package.json")
      .map(split("\n"))
      .map(find((x) => x.includes("version")))
      .run()
      .listen({
        onRejected: (err) => console.log(err),
        onResolved: (data) => console.log(data),
      });
    

(6)Pointed

  • 作用:實現 of 靜態方法的函子
  • of 用於避免使用 new 建立物件,並將值放入上下文從而使用 map 處理值

(7)Monad

  • 定義:當一個函子具有 ofjoin 方法,並且遵守一些定律,則這個函子是 Monad 函子

    • 可以“變扁”的 Pointed 函子
  • 實現並舉例:

    class IO_Monad {
      static of(value) {
        return new IO_Monad(() => value);
      }
    
      constructor(fn) {
        this._value = fn;
      }
    
      // map方法,用於在IO_Monad例項的函式執行結果上應用給定的函式
      map(fn) {
        // 使用flowRight函式組合fn和this._value,確保fn在this._value執行後應用
        return new IO_Monad(require("lodash/fp").flowRight(fn, this._value));
      }
    
      // join方法,用於執行IO_Monad例項內部的函式並返回結果
      join() {
        return this._value();
      }
    
      // flatMap方法,用於先應用map方法,然後執行結果中的函式
      flatMap(fn) {
        // 先應用map方法,然後透過join執行結果中的函式
        return this.map(fn).join();
      }
    }
    
    const readFile = (filename) => {
      return new IO_Monad(() => {
        return require("fs").readFileSync(filename, "utf-8");
      });
    };
    
    const print = (value) => {
      return new IO_Monad(() => {
        console.log(value);
        return value;
      });
    };
    
    readFile("package.json").flatMap(print).join();
    

-End-

相關文章