吃藥時,藥包上都會註明可能會有什麼副作用,Side Effect 就是這個意思,也就是除了主要的作用外,可能會產生的額外作用
function 內服藥(症狀){
console.log('肚子痛')
return `治療${症狀}`;
}
內服藥('流鼻水');
// '肚子痛'
// '治療流鼻水'
function 本身做的功能,也就是治療流鼻水
任何在運算的過程中改變了 function 內服藥
以外的變動就是 Side Effects,console.log('肚子痛')
會印在 function 外瀏覽器的 console tab 所以這就是 Side Effect
其實 Side Effect 不止是運用在 Function 裡,會變動到原本的變數值也算是 Side Effect
var x = 0;
x += 3
num++
y = "Hello " + x
Side Effect 會導致不易維護程式碼、得到無法預期結果等等。而平常撰寫 javaScript 容易造成的 Side Effect 非常之多,就先列常見的
修改外部的 state
// 改了 global 變數
var a = 0;
a++;
發送 HTTP Request
Rendering screen
使用會改變原陣列/物件的 JS method (eg. splice)
修改任何外部變數
DOM 操作
讀取 input 的值
Changing DB value
logging & console: 改變了系統狀態
驚! 問題來了,以上根本就是我們平常在做的事啊!改一下 DOM 然後點擊按鈕發出 HTTP Request 再來印出結果改動頁面,這些
全部都有 SIDE EFFECT
javaScript 世界太容易有 Side Effects,但它又是造成 Bug 的主要來源之一,我們很難完全避掉但可以運用一些技巧,例如把 Side Effect 用 Monad 封裝起來、讓 Side Effect 只作用在一定的範圍內、避免使用本身就有 Side Effect 的原生方法,讓程式碼是可預期且穩定的。
除了避免使用有 Side Effect 的 js 原生方法 (去年曾經分享過哪些 Array 原生方法會造成 Side Effect 哪些不會)
let xs = [1, 2, 3, 4, 5]
❌
xs.splice(0, 3) // xs -> [1, 2, 3]
xs.splice(0, 3) // xs -> [4, 5]
xs.slice(0, 3) // xs -> []
✅
xs.slice(0, 3) // xs -> [1, 2, 3]
xs.slice(0, 3) // xs -> [1, 2, 3]
xs.slice(0, 3) // xs -> [1, 2, 3]
這個方法我們把所有 impure code (會造成 Side Effect 的) 都放到參數裡,然後就丟著不管了,把責任丟給別人
function logSomething(something) {
const dt = (new Date()).toISOString();
console.log(`${dt}: ${something}`);
return something;
}
可以看到上面 impurities code 有
若把以上兩個抓起來丟進參數裡
function logSomething(d, cnsl, something) {
const dt = d.toISOString();
return cnsl.log(`${dt}: ${something}`);
}
const something = 'Hannah';
const d = new Date();;
logSomething(d, console, something)
這個 function 就變 Pure 了?!其實不然,因為你每次丟進去的 input 還是會導致不同的 output,哪門子解決方法。
寫成下面就才真的是 Pure,"one input, one output"。不去訪問 global 物件例如 console
或 Date
,讓資料維持穩定。
const d = {toISOString: () => '1865-11-26T16:00:00.000Z'};
const cnsl = {
log: (msg) => {
return msg;
},
};
function logSomething(d, cnsl, something) {
const dt = d.toISOString();
return cnsl.log(`${dt}: ${something}`);
}
logSomething(d, cnsl, "Off with their heads!");
// 1865-11-26T16:00:00.000Z: Off with their heads!
但你可能會說怎麼可能不用 Date,我就是要偵測今天日期啊,請看下一個解法
把 impure function 再包進另一個 function 就好了!
怎麼做呢?先來看一個例子
function logSomething() {
console.log('Hello World');
return 0;
}
logSomething();
// 'Hello World'
// 0
上面很清楚是 impure function,因為 log 會造成 Side Effect,那若換成
function logSomething() {
function Something() {
console.log('Hello World');
return 0;
}
return Something;
}
// ----- 或更常見改成 arrow function
function logSomething = () => () => {
console.log('Hello World');
return 0;
}
logSomething(); // Just return the same function
logSomething(); // Just return the same function
logSomething(); // Just return the same function
就完全變 Pure Function 了耶,因為符合
自己認為是偷吃步,但完全可以運用在實務中
Note. 感謝 良葛格 補充說明這種將副作用的世界與無副作用世界連結在一起的邊界手法,只是最低限度為了滿足函式有輸入有輸出特性,而不得不為的方式。因為嚴格說,若你的函式傳回的函式是不純的,那麼該函式也會是不純的。
這個比第一個複雜很多,會在講到 Functor 時再說
FP 要追求的…其實不是整個程式都是 Pure,而是 Pure 與 Impure 有明顯的界線,Side Effect 不全然是壞事也不是說有 Side Effect 的函式就不要使用,而是儘可能讓 Side Effect 能有好的管控,不要出現預期外的 side effect。
如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您
歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。
FP 要追求的…其實不是整個程式都是 Pure,而是 Pure 與 Impure 有明顯的界線。
其實這個範例還是 Impure 的,畢竟 log
本身代表的就是 Impure 的動作,也就是對外界的輸出(螢幕、檔案…):
const d = {toISOString: () => '1865-11-26T16:00:00.000Z'};
const cnsl = {
log: () => {
// do something
},
};
function logSomething(d, cnsl, something) {
const dt = d.toISOString();
return cnsl.log(`${dt}: ${something}`);
}
logSomething(d, cnsl, "Off with their heads!");
// ← "Off with their heads!"
Dependency injection 是 OO 的概念,FP 其實在分離純與不純,因此我會建議,接下來的手法可以說是「抽取副作用」,從你的這個起始範例開始:
function logSomething(something) {
const dt = (new Date()).toISOString();
console.log(`${dt}: ${something}`);
return something;
}
依你想表達的內容,若要令 logSomething
成為 Pure,我會這麼做:
// 改個名以符合其作用
function formatMessage(dt, something) {
return `${dt}: ${something}`;
}
// 以上是純的
// 以下是不純的
const something = 'Hannah';
const d = new Date();
console.log(formatMessage(d, something));
你的這個範例:
function logSomething() {
function Something() {
console.log('Hello World');
return 0;
}
return Something;
}
logSomething(); // Just return the same function
logSomething(); // Just return the same function
logSomething(); // Just return the same function
某些程度上,這種手法類似 Haskell 中的 IO
(參考 Haskell Tutorial(20)初探 IO 型態),將副作用的世界與無副作用世界連結在一起的邊界手法,只是最低限度為了滿足函式有輸入有輸出特性,而不得不為的方式(也就是你提及的 Effect Functor,Haskell 中,IO
就是一種 Functor)。
在 Haskell 中,傳回 IO
的就是不純的,而一個函式若呼叫的另一函式會傳回 IO
,那麼它也是不純的,在 Haskell 中,迫使你這個函式也得傳回 IO
,也就是說,若你的函式傳回的函式是不純的,那麼該函式也會是不純的。
也就是說,回到你的範例,並不會因為 logSomething
傳回函式,而令其變成純的,不談 Haskell,純就 JavaScript 來看,意義上其實等同於:
function logSomething() {
console.log('Hello World');
return 0;
}
logSomething(); // 不純的,因為對外界輸出了
logSomething(); // 不純的,因為對外界輸出了
logSomething(); // 不純的,因為對外界輸出了
如一開始我說的,FP 要追求的…其實不是整個程式都是 Pure,而是 Pure 與 Impure 有明顯地界線,只要一個函式中有不純的操作,那麼該函式就也是不純的,並不會因為函式可以傳遞而變成純綷的,你把它改為以下也是不純的:
function logSomething(log, message) {
log(message);
return 0;
}
// 也許你有不同的 log 方式
logSomething(console.log, 'Hello World'); // 不純的,因為 console.log 不純
logSomething(abc.log, 'Hello XXX'); // 不純的,因為 abc.log 不純
logSomething(xyz.log, 'Hello OOO'); // 不純的,因為 xyz.log 不純
在 Haskell 中,函式中呼叫了傳回 IO
的函式,該函式也必須傳回 IO
,這是個極為強制性的手法,會逼你一定要劃分純與不純的兩個世界。
不過,JavaScript 並沒有這種強制性,使用這種手法的話,我個人覺得沒有實質意義,也令人困惑。
也許你分出 logSomething
的目的,是為了對 message
做些額外處理,那麼會建議這麼寫:
function prefix_logging(message) {
return 'LOGGING:' + message;
}
const prefixed1 = prefix_logging('Hello, World');
const prefixed2 = prefix_logging('Hello, XXX');
const prefixed3 = prefix_logging('Hello, OOO');
// 以上是純的
// 以下是不純的
console.log(prefixed1);
abc.log(prefixed2);
xyz.log(prefixed3);
在 js 世界真的是沒這麼容易去處理啊
謝謝指教,先把以下改成 return 就不會產生 Side Effect
const cnsl = {
log: (msg) => {
return msg;
},
};
然後有補充進去您解釋的部分了