【面試官問】你懂函數語言程式設計嗎?

小黎也發表於2019-04-27

面試官:你好,你有用過函數語言程式設計嗎?
我:函式式?沒有呢?
面試官:那有了解過嗎?
我:額。。。也沒有,貴公司主要使用函數語言程式設計麼?
面試官:嗯,不怎麼使用,但我就是想問

又到了面試的季節,相比很多小夥伴會被面試官一頓連環問,面試造火箭,進去擰螺絲,可想要順利入職還是得硬著頭皮把火箭造出來才行。

最近不定期寫一些跟面試相關的知識點,為大夥的面試打氣加油。

這篇我們說說函數語言程式設計。

函數語言程式設計是一種程式設計正規化,與物件導向、程式導向都是一種程式設計的方法論,簡單的說就是如何設計我們的程式結構。

函式式的主要思想就是把運算過程寫成一系列的函式呼叫,比如說程式導向我們是這麼寫的

f(msg){
 // 分隔msg
 ...
 // 拼接msg
 ...
 // 其他處理
 ....
}
複製程式碼

函式式就會變成這種形式

a(msg){...// 分隔msg}
b(msg){...// 拼接msg}
c(msg){...// 其他處理}
f(msg) = a(msg).b(msg).c(msg)
複製程式碼

相當於把原本一個大函式拆解成一個個獨立的小函式,然後通過鏈式呼叫,把獨立的小函式串聯起來,已達到輸出的結果與原本的大函式一致,有點類似於管道,或者說流水線,上一個工序處理結束,傳給下一個工序,最後輸出成品。

函數語言程式設計,其函式必須滿足一個基本條件:函式必須是沒有副作用的,不能直接或間接修改除自身外的變數,僅僅是一種資料轉換的行為。片草叢中過,既不沾花也不惹草。

函數語言程式設計有兩個最基本的運算:合成和柯里化。

在之前的文章有詳細介紹過函式柯里化,請戳用大白話介紹柯里化函式,我們在接著看另一個基本運算-合成

什麼是compose

函式合成,英文名叫做 compose ,意思就是一個值要經過多個函式,才能變成另外一個值,將這個多個函式合併成一個函式,就是函式的合成,舉個例子

var name = 'xiaoli'
name = a(name){...}
name = b(name){...}
name = c(name){...}
console.log(name)
複製程式碼

name 經過三個函式才最終輸出我們需要的值,那麼函式合成後,變成如下

var fn = compose(a,b,c)
console.log(fn(name))
複製程式碼

這裡引用阮一峰老師的一張圖

jiao

函式組合還是非常好理解的,標準的函式組合還需要滿足結合律,在引用阮一峰老師的的圖

line

意思就是 compose(a,b) 生成的函式也必須是一個純淨的函式,對呼叫者來說這個生成的函式是透明的,呼叫者只關心 a和b 的實現即可。

compose 實現

我這裡我要推薦把函數語言程式設計玩的最溜的 redux ,也是這些大佬們把函數語言程式設計在前端圈給推了起來。我們看程式碼

// https://github.com/reduxjs/redux/blob/master/src/compose.js
/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製程式碼

是不是很精煉? Array.reduce 大法好啊,巧妙的實現函式巢狀引用。

在看別的函式庫如何實現的,先看看 lodash.js 的實現

/**
 * Composes a function that returns the result of invoking the given functions
 * with the `this` binding of the created function, where each successive
 * invocation is supplied the return value of the previous.
 *
 * @since 3.0.0
 * @category Util
 * @param {Function[]} [funcs] The functions to invoke.
 * @returns {Function} Returns the new composite function.
 * @see flowRight
 * @example
 *
 * function square(n) {
 *   return n * n
 * }
 *
 * const addSquare = flow([add, square])
 * addSquare(1, 2)
 * // => 9
 */
function flow(funcs) {
  const length = funcs ? funcs.length : 0
  let index = length
  while (index--) {
    if (typeof funcs[index] != 'function') {
      throw new TypeError('Expected a function')
    }
  }
  return function(...args) {
    let index = 0
    let result = length ? funcs[index].apply(this, args) : args[0]
    while (++index < length) {
      result = funcs[index].call(this, result)
    }
    return result
  }
}
複製程式碼

loadsh 看著要稍微複雜些,但相容性更高,畢竟有些落後的瀏覽器沒法支援reduce

ramda.js 一個更具有函式式代表的函式庫,這個函式庫非常有意思,每個函式都預設支援柯里化,對酷愛函數語言程式設計的夥伴來說,那就是個大殺器啊,我們看下它 compose 的實現,註釋略長,為了方便大家看,我把註釋簡化下

// compose.js
import pipe from './pipe';
import reverse from './reverse';
/**
 * @func
 * @category Function
 * @sig ((y -> z), (x -> y), ..., (o -> p), ((a, b, ..., n) -> o)) -> ((a, b, ..., n) -> z)
 * @param {...Function} ...functions The functions to compose
 * @return {Function}
 * @see R.pipe
 * @symb R.compose(f, g, h)(a, b) = f(g(h(a, b)))
 */
export default function compose() {
  if (arguments.length === 0) {
    throw new Error('compose requires at least one argument');
  }
  return pipe.apply(this, reverse(arguments));
}

// pipe.js
import _pipe from './internal/_pipe';
import reduce from './reduce';
export default function pipe() {
  if (arguments.length === 0) {
    throw new Error('pipe requires at least one argument');
  }
  return _arity(
    arguments[0].length,
    reduce(_pipe, arguments[0], tail(arguments))
  );
}
// 封裝的層次比較多,就不一一展開了
複製程式碼

ramda 自己實現了reduce,所以相容性也是OK。

至於 compose 的函式執行順序是從左到右還是右到左,這個無非是把執行的順序做個調換。

compose 應用

函式組合的使用場景還是多的,常見的資料處理,只要修改函式因子就可以輸入不同的結果,不需要修改原有流程。我寫個簡單的小栗子

// 因為我喜歡從左到右的順序執行,所以 reduce 裡的順序稍微調換了下
function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }

    if (funcs.length === 1) {
        return funcs[0]
    }

    return funcs.reduce((a, b) => {
      return  (...args) => b(a(...args)) // 從左到右的順序執行
    })
}

function fristName(name){
    return name.split(' ')[0]
}
function upperCase(string){
    return string.toUpperCase()
}
function reverse(string){
    return string.split('').reverse().join('')
}

console.log(compose(fristName,upperCase,reverse)('xiao li')) // OAIX
複製程式碼

用起來還是非常順手的,相信大家基於 compose 寫出很多有意思的程式碼。

我們來看下牛逼的 koa2框架是怎麼通過 compose 實現的洋蔥模型,我覺得非常巧妙,是koa的精髓部分,部分程式碼如下

// https://github.com/koajs/koa/blob/master/lib/application.js
 /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }
// https://github.com/koajs/compose/blob/master/index.js
/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
複製程式碼

小結

composecurry 一起使用可以把函式這東東玩的非常靈活,但對編寫函式就會有些要求,如何避免產生副作用,如何設計函式輸入、輸出,如何設計函式的邊界等等。

在某些場景下使用函數語言程式設計,不僅能程式碼更具擴充套件性,還能讓自己的程式碼看起來逼格更高。

講到這裡,若在面試的時候把上面涉及到的點和栗子大致說出來,面試的這一關那絕對穩啦。

參考文章

www.ruanyifeng.com/blog/2017/0…

相關文章