點燈坊

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

使用 thunkify() 準備 Function 為參數

Sam Xiao's Avatar 2021-04-27

thunkify() 在 Ramda 算較罕用的 Function,若搭配能接受 Thunk 的 API 或準備 Function 為參數,thunkify() 就能發揮功用。

Version

Ramda 0.27.1

thunkify()

import { thunkify, add } from 'ramda'

let f = thunkify(add)
let g = (x, y) => () => add(x, y)

thunkify(add)(1,2)() // ?
f(1, 2)() // ?

原本 add() 的 signature 為 Number -> Number -> Number,當使用 Ramda 的 thunkify() 後,會變成 Number -> Number -> () -> Number

  • f 使用 thunkify(add) 產生
  • g 則自行建立,fg 完全相同

可發現原本 add() 可直接求值,但 fg 都必須透過 () 才能有結果。

thunkify()
(a, b, ..., j) -> k) -> (a, b, ..., j) -> () -> k)
將原本結果多墊一層 () 才執行

(a, b, ..., j) -> k):原 function

(a, b, ..., j) -> () -> k):signature 都不變,但多墊了一層 ()

thunkify000

fromMaybe()

import { pipe, multiply, map } from 'ramda'
import { create, env } from 'sanctuary'

let { head, fromMaybe } = create ({ checkTypes: true, env })

let data = []

let fib = n => n <= 1 ? n : fib (n - 2) + fib (n - 1)

pipe(
  head,
  map(multiply(2)),
  fromMaybe(fib(30))
)(data) // ?

10 行

pipe(
  head,
  map(multiply(2)),
  fromMaybe(fib(30))
)(data) // ?

使用 pipe() 組合 IIFE:

  • head:回傳 Array 第一筆資料,但包在 Maybe 內
  • map(multiply(2)):對 Maybe 內值乘以 2
  • fromMaybe(fib(30)):從 Maybe 內取出值,若為 Nothing 則為 fib(30)

fromMaybe()
a -> Maybe a -> a
從 Maybe Monad 內部取出值

a:提供 Nothing 的預設值

Maybe a:data 為 Maybe

a:回傳 Maybe 內部值

計算 Fibonacci Number 是很耗 CPU 運算,若沒遇到 Nothing,則 fib(30) 就白浪費 CPU 了。

比較理想的方式是傳入 () -> fib(30),因為是 function,因此 fib(30) 不必求值,等 Nothing 出現時才計算 fib(30),可避免 CPU 無效運算。

thunkify001

fromMaybe_()

import { pipe, multiply, map, thunkify } from 'ramda'
import { create, env } from 'sanctuary'

let { head, fromMaybe_ } = create ({ checkTypes: true, env })

let data = []

let fib = n => n <= 1 ? n : fib (n - 2) + fib (n - 1)

pipe(
  head,
  map(multiply(2)),
  fromMaybe_(thunkify(fib)(30))
)(data) // ?

13 行

fromMaybe_(thunkify(fib)(30))

Sanctuary 另外提供了 frommMaybe_() 支援 Thunk,可用來避免 CPU 無效運算,使用 thunkify()fib() 轉成 Thunk。

fromMaybe_()
(() -> a) -> Maybe a -> a
從 Maybe Monad 內部取出值,但支援 Thunk

() -> a :為 Nothing 的預設值提供 Thunk

Maybe a:data 為 Maybe

a:回傳 Maybe 內部值

fromMaybe()fromMaybe_() 的差異在於第一個 argument 為 () -> a,也就是 Thunk,因此 fib(x) 將不會事先運算好才傳進 fromMaybe_(),而是只將 () -> a 傳進 fromMaybe_(),因為是 function 不會立即計算 Fibonacci Number,直到真的遇到 Nothing 時才會計算。

thunkify002

onMounted()

thunkify003

另外一個常用 thunkify() 的場景是在 Function Pipeline 下,以 function 為參數傳入下一個 function,如 Vue 的 onMounted()watchEffect()

<template>
  {{ price }}
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { pipe, add, thunkify } from 'ramda'
import { read, write } from 'vue3-fp'

let price = ref(100)

let mount = pipe(
  read(price),
  add(20),
  write(price),
)

pipe(
  thunkify(mount),
  onMounted
)()
</script>

12 行

let mount = pipe(
  read(price),
  add(20),
  write(price),
)

使用 pipe() 組合 mount()

  • read(price):讀取 price state
  • add(20):加上 20
  • write(price):寫入 price state

18 行

pipe(
  thunkify(mount),
  onMounted
)()

使用 pipe() 組合 IIFE:

  • thunkify(mount):準備 mount() 傳入 onMounted()
  • onMounted:將 mount() 掛上 mounted hook

Conclusion

  • 只要是很耗 CPU 或耗 memory 的運算,使用 Thunk 的 lazy evaluation 特性可大幅增進執行效率
  • thunkify() 另外一個常用場合是在 Function Pipeline 下,準備另外一個 function 為參數傳入下一個 function,但 Vue 3 在少數 function 使用 thunkify() 會喪失 reactivity,如 guard() 只能使用 always(),但如 onMounted()watchEffect()computed() 都可使用 thunkify()always()

Reference

Ramda, thunkify()