iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 23
1

覺得前幾篇幾乎只用圖片解釋 Functor 可能不太知道真實程式世界會長什麼樣子,所以決定再加篇幅用程式來舉例加深印象。


Box 這種 data type 除了可以把 value 當參數, function 當然也可以

value 當參數

const Box = x => ({
  map: f => Box(f(x))
});

https://ithelp.ithome.com.tw/upload/images/20200924/20106426XPyuUOtit7.jpg

Function 當參數

const Box = f => ({
  map: g => Box( x => g(f(x)) )
});

https://ithelp.ithome.com.tw/upload/images/20200924/2010642670xpaPINfY.jpg

若今天有一個 impure function logSomething,這也是第五天 Buzz word 2 : Side Effect 的範例

// logSomething :: () -> Number
function logSomething () {
  console.log('Hello World');
  return 0;
}

讓我們看看怎麼用新所學 Functor 包起來!
一開始要創建一個 Box 的 data type

// Box :: Function -> Box
const Box = f => ({
  map: g => Box( x => g(f(x)) ), // 神奇的運算在此
});

const increment = x => x + 1; 
const one = Box(logSomething).map(increment); // return Box data Type

因為 map 裡面是 g(f(x)) 所以會先執行 logSomething 再執行 increment ,不過 one return 的也只是另一個 Box 而已,所以為了方便起見我們會寫一個 runEffects (顧名思義就是執行 Side Effect) 看看到底是 return 了什麼。

// Box :: Function -> Box
const Box = f => ({
  map: g => Box( x => g(f(x)) ), // 神奇的運算在此
  runEffects: x => f(x), // 執行 Effect 看是輸出什麼
});

const increment = x => x + 1; 
const one = Box(logSomething).map(increment); // return Box data Type
one.runEffects(); 
/*
Hello World
1
*/

為了好理解自己來拆一下步驟

result1 = Box(logSomething)

const Box = logSomething => ({
  map: g => Box( x => g(logSomething(x)) ), 
  runEffects: x => logSomething(x), 
});

result2 = result1 .map(increment)

const Box = ( x => increment(logSomething(x) ) => ({
  map: g => Box( x => g( ( x => increment(logSomething(x) )(x)) ),
  runEffects: x => increment(logSomething(x)), // 執行 Effect 看是輸出什麼
});

前面兩個步驟都只是 return 新的一個 Box 而已,要看回傳值需要執行以下 runEffects

result3 = result2 .runEffects()

increment(logSomething(x)) 
/*
Hello World
1
*/

logSomething(x) 會觸發 console.log('Hello World') 以及回傳 0; 而 increment(0) 會回傳 1,也就是最後答案。
用 Functor 思考模式寫程式真的很抽象,腦袋是不是跟我一樣會有點打結呢 XD

Can keep calling map()

這邊可以觀察到 .map() 是可以一直被呼叫很多次的

const double = x => x * 2;
const cube = x => Math.pow(x, 3);
const eight = Box(logSomething) // 0
    .map(increment) // 1
    .map(double) // 2
    .map(cube); // 4

eight.runEffects(); // 4

上面註解的 0124 並不是回傳值,因為其實他們只會回傳 Box 而已並沒有真的去執行 Effect,但你可以用 runEffects() 去看回傳了什麼

Box(logSomething).runEffects()  // 0
Box(logSomething).map(increment).runEffects() // 1

Box(logSomething)
	.map(increment)
	.map(double)
	.runEffects() // 2

Box(logSomething)
	.map(increment)
	.map(double)
	.map(cube)
	.runEffects() // 4

Can use Compose instead?

有認真看之前文章的可能會覺得其實這樣做跟 Compose 蠻像的

cube(double(increment(logSomething())))

但用 Box 這種 functor 能保證每次回傳 "一定都是相同的 data type",並比 compose 擁有額外的 method,例如 maprunEffects ...

To be continued...


參考文章

如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您

上一篇
Applicative
下一篇
Functor 4: 圖解 Box Data Type 之方法 map、flatMap、chain
系列文
Functional Programming in JS30

1 則留言

0
linche0859
iT邦新手 5 級 ‧ 2020-09-23 17:08:17

有關這個範例:

const Box = x => {
	map: fn => Box(fn(x)),
    fold: fn(x) // 取值 remove from the box
}
const nextCharForNumberString = str => 
 Box(str)
  .map(x => x.trim()) 
  .map(x => parseInt(x)) 
  .map(x => x + 1) 
  .map(x => String.fromCharCode(x))

好像要改成這樣,才可以執行成功,不然他會跳 fn is not a function 訊息:

const Box = x => ({
  map: fn => Box(fn(x)),
  fold: fn => fn(x) // 取值 remove from the box
})
const nextCharForNumberString = str => 
  Box(str)
   .map(x => x.trim()) 
   .map(x => parseInt(x)) 
   .map(x => x + 1) 
   .map(x => String.fromCharCode(x)) 
   .fold(x => x)

另外想要問,Ramda 中有與 Functor 結合使用的 API 嗎?

hannahpun iT邦新手 5 級 ‧ 2020-09-24 15:38:13 檢舉

謝謝你幫我找蟲,會再更新程式碼
(前幾篇覺得解釋不夠清楚,所以 exercise 會移到後面篇幅)

hannahpun iT邦新手 5 級 ‧ 2020-10-07 17:37:36 檢舉

對了 關於 Box 的 library 我看比較多人是用這個
https://monet.github.io/monet.js/

我要留言

立即登入留言