函數語言程式設計4-高階函式

瘋狂的小蘑菇發表於2017-06-26

以其他函式作為引數的函式

本章的所有程式碼,均在github.com/antgod/func…

關於傳遞函式的思考

  • max
    在很多程式語言的核心庫,都包含一個叫做max的函式。包括underscore也有這樣的函式。
const max = (data) => {
  return data.reduce((maxer, next) => {
    return maxer > next ? maxer : next
  })
}

console.log(max([5, 1, 3, 4, 2])) // 輸出5複製程式碼

執行結果並沒有什麼奇怪,但是這樣特定的函式存在一個限制,如果我們不是從數字而是從物件中尋找最大值,該怎麼辦?

const max = (data, compare = item => item) => {
  return data.reduce((maxer, next) => {
    return compare(maxer) > compare(next) ? maxer : next
  })
}

console.log(max([{ age: 64 }, { age: 32 }, { age: 50 }], item => item.age))複製程式碼

但是,在某些方面,這個函式仍然受限,並不是真正的函式式,讀者想想看,為什麼呢?

這個函式的大於號,是定死的。

我們可以構建一個新的函式,一個用來生成可比較的值,另一個用來比較兩個值返回最佳值。

const finder = (data, need, compare) => {
  return data.reduce((last, next) => {
    return compare(last) === need(compare(last), compare(next)) ? last : next
  })
}

const identity = prop => prop

console.log(finder([1, 2, 3, 4, 5], Math.max, identity))複製程式碼

當我們要比較物件大小時候,就可以傳遞第三個引數了。

const finder = (data, need, compare) => {
  return data.reduce((last, next) => {
    return compare(last) === need(compare(last), compare(next)) ? last : next
  })
}

const plucker = prop => item => item[prop]

console.log(finder([{ age: 64 }, { age: 32 }, { age: 50 }], Math.max, plucker('age')))複製程式碼

當我們要查詢以B開頭的名字,怎麼辦呢?

const finder = (data, need, compare) => {
  return data.reduce((last, next) => {
    return compare(last) === need(compare(last), compare(next)) ? last : next
  })
}

const plucker = prop => item => item[prop]

console.log(finder([{ name: 'A', age: 64 }, { name: 'B', age: 32 }, { name: 'C', age: 50 }], (x, y) => {
  return x.charAt(0) === 'B' ? x : y
}, plucker('name')))複製程式碼

發現問題

我們發現,函式雖然短小精幹,並且也按照了我們的預期工作。但是卻有一些重複性程式碼。

// in finder
return compare(last) === need(compare(last), compare(next)) ? last : next

// in used
return x.charAt(0) === 'B' ? x : y複製程式碼

你會發現,這兩段邏輯完全相同,也就是說,這兩種演算法都是返回最佳值或者當前值。

我們完全可以按照以下思路縮減程式碼:

  • 如果第一個引數比第二個引數更好,返回第一個引數。
const bester = (data, need) => {
  return data.reduce((last, next) => {
    return need(last, next) ? last : next
  })
}

console.log(bester([{ name: 'A', age: 64 }, { name: 'B', age: 32 }, { name: 'C', age: 50 }], (x, y) => {
  return x.age < y.age
}))複製程式碼

關於傳遞函式的思考:重複、與條件

上一章中,我們建立了接受兩個函式引數的finder,並且簡化成一個函式引數的bester。事實上,在大多數js函式設計中,都不需要返回多個函式。

但是某些情況下,我們需要返回多個函式。讓我們以repeat開始,介紹為什麼要使用多個函式為引數。

const range = (times) => {
  const ranges = []
  for (let idx = 0; idx < times; idx++) {
    ranges.push(null)
  }
  return ranges
}

const repeat = (value, time) => {
  return range(time).map(() => value)
}

console.log(repeat(4, 3))複製程式碼

range函式對underscore的range做了下刪減,返回一個包含n個值為null的陣列。

使用函式,而不是值

作為repeat的常規實現方式,我們仍然有提高的空間。如果將重複值運算,那樣使用場景更廣泛。

比如我們要隨機生成10個10以內的數字(range函式與前文相同)。

const repeatness = (createValue, time) => {
  return range(time).map((value, index) => createValue(index))
}


console.log(repeatness(() => Math.floor(Math.random() * 10) + 1, 10))複製程式碼
一個函式是不夠的,需要多參函式

有的時候,我們不知道函式需要呼叫多少次,我們只有一個條件。比如說,當我們不斷重複呼叫一個函式,當函式超過某個閾值或者條件,我們停止呼叫。使用repeatness明顯達不到我們的需求。

比如,我們想計算1024以下所有的2的整數指數值(2, 4, 8, 16, 32, 64, 128, 256, 512)。

const iterate = (createValue, checker, init) => {
  const ret = []
  let result = createValue(init)
  while (checker(result)) {
    ret.push(result)
    result = createValue(result)
  }
  return ret
}

console.log(iterate(n => n + n, n => n < 1024, 1))複製程式碼

函式接收兩個函式引數,一個用來執行動作,一個用來校驗結果。當結果滿足時返回true。這算是repeatness的升級版了,連重複次數都是開放的,受到一個函式的執行結果影響。

返回其他函式的函式

回憶下之前repeatness返回三個常量的情況

repeatness(() => 'Odelay', 3)複製程式碼

這種返回常量的函式非常有用,是函數語言程式設計的一種設計模式,通過函式來返回值。我們經常稱之為k,為了清晰起見,我們稱之為alwasy。

const always = value => () => value
console.log(repeatness(always('Odelay'), 3))複製程式碼

always的行為可以用來解釋閉包。閉包用來捕獲一個值,並多次返回相同的值。每一個新的閉包都會返回不一樣的值

const f = always(() => {})

console.log(f() === f())
// => true
const g = always(() => {})

console.log(g() === f())
// => false複製程式碼

像always這樣的函式被稱為組合子。

高階函式捕獲引數

高階函式的引數用來配置返回函式的行為。
注意觀察以下程式碼:

const makeAdder = init => rest => init + rest
const add100 = makeAdder(100)
console.log(add100(38))
// => 138複製程式碼

你會經常看到一個函式返回了一個捕獲變數的函式。

捕獲變數的好處

比如你要生成一個特定字首的隨機字串。

const uniqueString = prefix => [prefix, new Date().getTime()].join('')

console.log(uniqueString('ghosts'))
console.log(uniqueString('turkey'))複製程式碼

看起來不錯,但是如果我們需要自增的索引,而不是時間戳作為字尾,怎麼辦?

可以使用閉包來實現:

const generator = (init, prefix) => {
  let counter = init
  return (pre = prefix) => {
    return [pre, counter++].join('')
  }
}

const g1 = generator(0, 'prefix')

console.log(g1())
console.log(g1())
console.log(g1('new'))
/*
=>
prefix0
prefix1
new2
*/複製程式碼

我們也可以用物件來實現:

const generator2 =  (init, prefix) => {
  return {
    count: init,
    uniqueString: function(pre = prefix) {
      return [pre, this.count++].join('')
    },
  }
}

const g2 = generator2(0, 'prefix')

console.log(g2.uniqueString.call(g2))
console.log(g2.uniqueString.call(g2))
console.log(g2.uniqueString('new'))
/*
=>
prefix0
prefix1
new2
*/複製程式碼

注意這裡使用了this訪問資料,那麼函式不能再使用箭頭函式。物件的缺點是不安全,因為可以隨意訪問 count的值。很多時候隱藏實現細節是很重要的。事實上,我們可以把count像閉包一樣隱藏在函式內部。

const generator3 = (init, prefix) => {
  let counter = init
  return {
    uniqueString: (pre = prefix) => {
      return [pre, counter++].join('')
    },
  }
}

const g3 = generator3(0, 'prefix')

console.log(g3.uniqueString())
console.log(g3.uniqueString())
console.log(g3.uniqueString('new'))
/*
=>
prefix0
prefix1
new2
*/複製程式碼

閉包的方式乾淨、簡單,但是也充滿了陷阱。

改值時候要小心

雖然對於外界操作來說,該變數是安全的。但是它會增加複雜度,當一個函式的返回值只依賴引數時,被稱為引用透明。

這個詞看起來很花哨,意味著不破壞程式碼結構的情況下,用預期的值替換函式的任意呼叫。當你使用會改變內部程式碼變數的閉包時,你不一定能做到這一點。因為很多閉包的返回值是依賴於呼叫次數的。也就是說,呼叫uniqueString10次與10000次,返回值是不同的。

所以我們在任何地方呼叫閉包的函式時,都需要格外小心,有必要時,需要增加監控或者日誌。否則在閉包返回值值任意變化時,我們往往找不到變化原因。

防止不存在的函式: fnull(grund)

我們建立幾個高階函式的例子,第一個叫做fnull,我們先來舉例說明下它的目的。

假設我們有一組需要乘法的陣列:

const nums = [1, 2, 3, null, 5]

console.log(nums.reduce((total, n) => (total * n)))複製程式碼

很顯然null不會給我們任何有用的答案。這時候fnull函式很有用。

const nums = [1, 2, 3, null, 5]
const fillnull = (handle, ...args) => (...argvs) => handle(...argvs.map((argv, i) => argv || args[i]))

console.log(nums.reduce(fillnull((total, n) => { return total * n }, 1, 1)))複製程式碼

函式檢查每個傳入的引數是否是null或者undefined。如果是,則用預設值替換掉,然後再呼叫函式。

如果要查詢的目標是物件,可以用以下方式使用:

const defaults = d => (o, k) => {
  const val = fillnull(identity, d[k])
  return o && val(o[k])
}

const ages = [{ age: 100 }, { age: 120 }, { age: 150 }, { }, { age: 30 }]

const lookup = defaults({ age: 0 })

console.log(ages.reduce((total, age) => {
  return total + lookup(age, 'age')
}, 0))複製程式碼

其中defaults函式用來配置預設值,返回一個函式lookup,之後每次執行lookup函式,如果遇到空值,都會返回預設值代替。

相關文章