函數式編程主食

函數式編程是什麼?html

在函數式編程中,函數就是一個管道(pipe)。這頭進去一個值,那頭就會出來一個新的值,沒有其餘做用。——阮一峯 函數式編程入門教程ajax

函數式編程本質上是一種數學運算。由於是數學運算因此天然就會涉及到加減乘除等運算和交換律結合律同一概分配律等運算法則。若是要函數順利的進行數學運算,就要求函數必須是純的,不能有反作用,即純函數。但若是隻是簡單的將純函數用於複雜的加減乘除運算,則會寫出一堆看起來雜亂無章的、不符合人類閱讀習慣和編碼直覺的代碼。所以函數式編程須要藉助組合函數、柯里化、遞歸、閉包和各類各樣的高階函數讓代碼看起來更符合人類的直覺和邏輯思惟的方式。算法

再說一等公民

當咱們在說函數是「一等公民」的時候,不要想固然的覺得函數就是 js 世界裏的老大了,咱們實際上說的是它們和其餘對象都同樣:就是普通公民。編程

做爲一等公民,函數能夠被賦值給另一個變量,然而編程中卻有不少這樣的神奇操做:json

const hi = name => `Hi, ${name}`
const greeting = name => hi(name)
複製代碼

實際上調用 hi('girl')greeting('gril') 的結果不管如何都是徹底同樣的,greeting 函數所做的不過是調用並返回 hi 函數而已。但實際上徹底不必如此脫褲子放屁畫蛇添足,直接把 hi 函數賦值給 greeting 變量便可:數組

const hi = name => `Hi, ${name}`
const greeting = hi
複製代碼

懂了嗎?那再看下下面這個:緩存

// 太傻了
const getServerStuff = callback => ajaxCall(json => callback(json)) // ajaxCall 是外部封裝好的接口函數
複製代碼

這麼長的函數,看都看不懂,咱們來簡化下:閉包

// 這行
ajaxCall(json => callback(json));

// 等價於這行
ajaxCall(callback);

// 那麼,重構下 getServerStuff
const getServerStuff = callback => ajaxCall(callback);

// ...就等於
const getServerStuff = ajaxCall // <-- 看,沒有括號哦
複製代碼

拆到底就是個賦值操做而已~ 必定要避免這種氣人又尷尬的函數啊~app

純函數

純函數是這樣一種函數,即相同的輸入,永遠會獲得相同的輸出,並且沒有任何可觀察的反作用。函數式編程

從這句話中能夠看出要成爲純函數有兩個充分必要條件:

  1. 相同的輸入會獲得相同的輸出(也叫引用透明性)。好比如果函數的輸入參數是數字返回值是數組,就不會發生輸入參數是數字返回值是數字的狀況。
  2. 沒有任何可觀察的反作用。也就是說函數在執行的過程當中不依賴於外部的狀態 / 也不會改變外部的狀態。

好比對於 slicesplice 函數,輸入數字參數,總能返回數組。可是不一樣之處在於 splice 在執行過程當中會改變原數組,而 slice 在沒有這樣的反作用。因此 slice 是純函數而 splice 不是,以下所示:

let nums = [1, 2, 3, 4, 5]
let a = nums.slice(0,2) // [1, 2]
console.log(nums) // [1, 2, 3, 4, 5]

let nums = [1, 2, 3, 4, 5]
let b = nums.splice(0, 2) // [1, 2]
console.log(nums) // [3, 4, 5]
複製代碼

戲劇性的是:純函數就是數學上的函數,並且是函數式編程的所有。

如何才能編寫無反作用的純函數是學習函數式編程的關鍵。

由於不依賴外部的狀態,因此純函數編程須要藉助一些工具函數,來使函數的傳參看起來優雅且容易理解。

聲明式編程

函數式編程屬於聲明式編程範式,函數式編程的目的是使用函數來抽象做用在數據之上的控制流和操做,從而在系統中消除反作用並減小對狀態的改變。

// 命令式方式
var array = [0, 1, 2, 3]
for(let i = 0; i < array.length; i++) {
    array[i] = Math.pow(array[i], 2)
}
array // [0, 1, 4, 9]

// 聲明式方式
[0, 1, 2, 3].map(num => Math.pow(num, 2)) // [0, 1, 4, 9]

複製代碼

命令式編程注重代碼執行過程,上面代碼使用了命令控制結構 for 循環,正是這種命令控制語句致使了代碼的死板和難以複用。函數式編程不關注代碼具體如何執行和數據如何穿過函數,而關注代碼執行結果。這也決定了函數式編程須要倚重一些工具函數( 如 map filter ruduce find 等數組函數和 curry compose 等工具函數 )。

柯里化

俗話說一口吃不成胖子,記得第一次接觸柯里化是在紅寶書裏面,當時看了好幾遍仍是一臉懵逼,不知所云。因此柯里化函數是須要必定能力的抽象思惟的。

柯里化是一種將使用多個參數的函數轉換成一系列使用一個或多個參數的函數的技術。你能夠一次性地傳遞全部參數調用函數,也能夠每次只傳一個參數分屢次調用。

function sub_curry(fn, ...args) { // sub_curry 用來緩存傳入的參數
  return function() {
    return fn.apply(this, args.concat([...arguments])
  }
}
function curry(fn, length) {
  length = length || fn.length
  return function(...args) {
    if (args.length < length) {
      var combined = [fn].concat(args)
      return curry(sub_curry.apply(this, combined), length - args.length) // 遞歸調用自身返回一個固定部分參數的函數
    } else {
      return fn.apply(this, args)
    }
  }
}
var fn = curry(function(a, b, c) {
  return [a, b, c]
})
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
複製代碼

curry 函數的原理是利用閉包將原函數的一部分參數存起來,若是參數小於原函數形參的數量就返回一個新函數以便繼續傳參調用,若是參數等於原函數的參數了就執行原函數。

import { curry } from 'lodash'

function add(a, b) {
  return a + b
}
var addCurry = curry(add) // 柯里化add

var increment = addCurry(1) // 生成新函數 increment 執行會將入參增長 1
increment(10) // 11

var addTen = addCurry(10)) // 生成新函數 increment 執行會將入參增長 1
addTen(1) // 生成新函數 addTen 執行會將入參增長 10
複製代碼

能夠看到利用柯里化技術,可以生成固定了部分參數的新函數。

組合

咱們認爲組合是高於其餘全部原則的設計原則,這是由於組合讓咱們的代碼簡單而富有可讀性。

組合顧名思義就是將不一樣的函數組合起來生成一個新的函數。組合的參數必須都是純函數。

function compose (...fns) {
  return function (...args) {
    return fns.reduceRight((arg , fn, index) => {
      if (index === fns.length - 1) {
        return fn(...arg)
      }
      return fn(arg)
    }, args)
  }
}

function toUpperCase(str) {
    return str.toUpperCase()
}
function split(str){
  return str.split('');
}
function reverse(arr){
  return arr.reverse();
}
function join(arr){
  return arr.join('');
}

const turnStr = compose(join, reverse, split, toUpperCase)
turnStr('emosewa si nijeuj') // JUEJIN IS AWESOME
複製代碼

組合函數好像一個管道同樣,將參數從右向左依次執行並將結果傳遞給左邊的參數。

組合函數還符合結合律,組合的調用分組不重要,全部結果都是同樣的:

const turnStr1 = compose(compose(join, reverse), split, toUpperCase)
turnStr1('emosewa si nijeuj') // JUEJIN IS AWESOME

const turnStr2 = compose(compose(join, reverse), split, toUpperCase)
turnStr2('emosewa si nijeuj') // JUEJIN IS AWESOME
複製代碼

結合律的一大好處是任何一個函數分組均可以被拆開來,而後再以它們本身的組合方式打包在一塊兒。固然這個就看我的的抽象能力和編碼能力了。

總結

函數式編程不是一朝一夕就能學會的,而是要在實際開發中逐漸學習和熟練的。在實際編程中,儘可能的利用 curry compose 等工具函數和遞歸將代碼控制邏輯抽先化,逐漸捨棄命令式開發而習慣編寫聲明式的代碼。

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息