點燈坊

失くすものさえない今が強くなるチャンスよ

使用 either() 實現 OR Combinator

Sam Xiao's Avatar 2020-08-28

若第一個 Function 結果為 Truthy Value 則回傳第一個 Function,否則回傳第二個 Function,在 FP 稱為 OR Combinator,可使用 Ramda 的 either() 實現。

Version

macOS Catalina 10.15.6
Ramda 0.27.1

Imperative

let f = n => {
  if (n > 10 || !(n % 2)) return true
  else return false
}

f(8) // ?
f(20) // ?
f(7) // ?

若要判斷 大於 10 且為 偶數,ECMAScript 可使用 if 一一判斷並搭配 ||

either000

Extract Function

let isGt10 = n => n > 10
let isEven = n => !(n % 2)

let f = n => isGt10(n) || isEven(n)

f(8) // ?
f(20) // ?
f(7) // ?

也可將判斷 expression 抽成 isGt10()isEven(),再搭配 || 判斷。

either001

either()

let isGt10 = n => n > 10
let isEven = n => !(n % 2)

let either = f1 => f2 => n => f1(n) || f2(n)

let f = either(isGt10)(isEven)

f(8) // ?
f(20) // ?
f(7) // ?

可自行以 || 實作 either() 回傳適當 function。

either008

Ramda

import { either, gt, flip } from 'ramda'
import { isEven } from 'wink-fp'

let isGt10 = flip(gt)(10)

let f = either(isGt10)(isEven)

f(8) // ?
f(20) // ?
f(7) // ?

Ramda 已提供 either() 可直接使用。

either()
(*… → Boolean) → (*… → Boolean) → *... → Boolean
若第一個 function 結果為 truthy value 則回傳第一個 function,否則回傳第二個 function

(*… → Boolean):傳入第一個 function

(*… → Boolean):傳入第二個 function

*...:傳入 data

Boolean:回傳任一 function 結果

either002

Example

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
]

let f = a => {
  let result = []

  for (let x of a)
    if (x.price === 100 || x.price === 200)
      result.push(x)

  return result
}
  
f(data) // ?

第 1 行

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
]

只要是 price100200 都屬於合理價錢。

第 7 行

let f = a => {
  let result = []

  for (let x of a)
    if (x.price === 100 || x.price === 200)
      result.push(x)

  return result
}

Imperative 會先建立 result empty array,使用 for loop 對 data 一筆一筆處理,然後使用 if 判斷 price 是否為 100200,最後 push 進 resultarray 回傳。

either003

Array.prototype.filter()

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
]

let f = a => a.filter(x => x.price === 100 || x.price === 200)
  
f(data) // ?

也可使用 ECMAScript 自帶的 filter(),只要在 predicate 判斷 price100200 即可。

either004

Ramda

import { filter } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
]

let f = filter(x => x.price === 100 || x.price === 200)

f(data) // ?

也可使用 Ramda 的 filter() 使 f() 能 point-free。

either006

Point-free

import { filter, either, propEq } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
]

let f = filter(either(
  propEq('price', 100), 
  propEq('price', 200))
)

f(data) // ?

以 FP 角度,我們必須提供兩個 predicate,但只要其中一個 predicate 成立即可,因此使用 Ramda 的 either() 將兩個使用 propEq() 產生的 predicate 組合成單一 predicate。

注意新 predicate 接受的 data 與原本 predicate 都相同,一樣是 object。

either005

Example

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
]

let f = n => a => {
  let result = a.find(x => x.title === n)
  
  if (result === undefined)
    result = { price: 0 }

  return result.price
}

f('Speaking JavaScript')(data) // ?
f('RxJS')(data) // ?

either() 另外一個應用是搭配會回傳 undefined 的 function,如 find(),當找不到資料時提供預設值。

Imperative 會使用 if 判斷是否回傳 undefined,這雖然可行,但會終止 pipeline 而改用 variable。

either009

Ramda

import { find, either, pipe, prop, identity, always } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
]

let f = n => pipe(
  find(x => x.title === n),
  either(identity, () => ({ price: 0 })),
  prop('price'),
)

f('Speaking JavaScript')(data) // ?
f('RxJS')(data) // ?

由於 find() 可能回傳 undefined,可使用 either() 判斷若為 falsy value 時提供 default value,如此可使 Function Pipeline 不中斷。

either010

Point-free

import { find, either, pipe, prop, propEq, identity, always } from 'ramda'

let data = [
  { title: 'FP in JavaScript', price: 100 },
  { title: 'RxJS in Action', price: 200 },
  { title: 'Speaking JavaScript', price: 300 }
]

let f = n => pipe(
  find(propEq('title', n)),
  either(identity, always({ price: 0 })),
  prop('price'),
)

f('Speaking JavaScript')(data) // ?
f('RxJS')(data) // ?

可將 find()either() 進一步 point-free。

either011

ADT

either013

在 Ramda 文件中 either() 最後一行提到支援 Applicative Functor,且範例還用到 Maybe,這是什麼呢 ?

either014

根據 fantasy-land 定義,Applicative 必須支援 Apply 與 Functor 所有特性,而 Monad 亦需支援 Applicative 所有特性,either() 既然支援 Applicative,這表示 either() 亦支援 Monad。

Maybe Monad

import { either } from 'ramda'
import { create, env } from 'sanctuary'

let { Just } = create ({ checkTypes: false, env })

either(Just(false), Just(20)) // ?
either(Just(true), Just(20)) // ?
either(Just(10), Just(20)) // ?

either() 可直接比較 Just 內的值,若第一個 Just 內為 true 則直接回傳 Just,若為 false 則回傳第二個 Just。

either015

import { either } from 'ramda'
import { create, env } from 'sanctuary'

let { Just, Nothing } = create ({ checkTypes: false, env })

either(Nothing, Just(20)) // ?
either(Just(20), Nothing) // ?

若其中之一為 Nothing 則直接回傳 Nothing。

either016

Conclusion

  • or()|| 的 value 版本;而 either()|| 的 function 版本
  • either() 在 FP 也稱為 OR combinator
  • 若只有兩個 predicate,則 either() 等效於 anyPass()
  • either() 由於底層是 || 因此支援 truthy value 與 falsy value,不必如 ifElse() 一定要傳入 transfer function
  • either() 由於底層是 ||,因此支援 short-circuitited,也就是第二個 function 有可能不執行,因此可提高執行效率
  • either() 由於是傳入兩 function,因此實務上有兩種用法:一種是將兩個只會用到其一的 predicate 合併為一;另一種時搭配會回傳 undefind 的 function 使其能繼續 Function Pipeline
  • either() 除了能用於 ECMAScript 原生型別外,還支援 Applicative Functor,可直接搭配 Maybe Monad 使用

Reference

Ramda, either()
Ramda, ifElse()
Sanctuary, Maybe