覺得前幾篇幾乎只用圖片解釋 Functor 可能不太知道真實程式世界會長什麼樣子,所以決定再加篇幅用程式來舉例加深印象。
Box 這種 data type 除了可以把 value 當參數, function 當然也可以
const Box = x => ({
map: f => Box(f(x))
});
const Box = f => ({
map: g => Box( x => g(f(x)) )
});
若今天有一個 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
*/
為了好理解自己來拆一下步驟
Box(logSomething)
const Box = logSomething => ({
map: g => Box( x => g(logSomething(x)) ),
runEffects: x => logSomething(x),
});
.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
.runEffects()
increment(logSomething(x))
/*
Hello World
1
*/
logSomething(x)
會觸發 console.log('Hello World')
以及回傳 0; 而 increment(0)
會回傳 1,也就是最後答案。
用 Functor 思考模式寫程式真的很抽象,腦袋是不是跟我一樣會有點打結呢 XD
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
Compose
instead?有認真看之前文章的可能會覺得其實這樣做跟 Compose 蠻像的
cube(double(increment(logSomething())))
但用 Box 這種 functor 能保證每次回傳 "一定都是相同的 data type",並比 compose 擁有額外的 method,例如 map
、 runEffects
...
To be continued...
如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您
歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。
有關這個範例:
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 嗎?
謝謝你幫我找蟲,會再更新程式碼
(前幾篇覺得解釋不夠清楚,所以 exercise 會移到後面篇幅)
對了 關於 Box 的 library 我看比較多人是用這個
https://monet.github.io/monet.js/
關於以下這段範例,是不是只有 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
*/