iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
3
Software Development

Functional Programming in JS系列 第 23 篇

Functor 3: 程式碼解說篇

  • 分享至 

  • xImage
  •  

覺得前幾篇幾乎只用圖片解釋 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

上面註解的 0 、 1 、 2、 4 並不是回傳值,因為其實他們只會回傳 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,例如 map 、 runEffects ...

To be continued...


參考文章

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

歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。


上一篇
Applicative
下一篇
Functor 4: 圖解 Box Data Type 之方法 map、flatMap、chain
系列文
Functional Programming in JS 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

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邦新手 2 級 ‧ 2020-09-24 15:38:13 檢舉

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

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

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

0
杯
iT邦新手 1 級 ‧ 2022-04-17 11:09:58

關於以下這段範例,是不是只有 console.log('Hello World') 才是 Effect?

有再另外 log 出他的結果才會得到 1,所以最後改 console.log(one.runEffects()),才會得出 Hello World & 1 呢?

// 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
*/

我要留言

立即登入留言