在函數式程式設計風格中,資料的串接通常利用pipe、flow或compose將函數式合成,因此程式開發人員比較難在「接管」(pipe)過程中觀察各個函數輸出的結果,一旦程式出現邏輯上的錯誤時,較難偵錯。
我們在介紹pipe、flow和compose的時候,介紹了trace這個函數,我們可任意將它安置在pipe中任一個函數之後,以觀察該函數輸出結果來協助偵錯,了解程式的邏輯問題。
const trace =
(label?: string) =>
<A>(a: A) => {
console.log(label ? `${label}:` : 'trace:', a);
};
當函數輸出的型別是一般型別,trace函數可以將值直接log在Console中,當輸出結果為Option或Either建構的型別時,Console中出現的不是單純我們所想觀察的值,會是Option和Either建構出來的物件,我們必須從物件中找出正確屬性的值,雖然較麻煩,但還是能看到函數輸出的值。
如果我們函數的輸出型別是IO和Task時,這個容器建構出來的是尚未執行的函數,上面的trace函數就無用武之地。我們可以修改上面的trace函數,讓我們知道IO型別和Task型別執行的結果,從中理解程式邏輯錯誤的原因。
const traceIO =
<A>(label?: string) =>
(fa: IO<A>): IO<A> => {
console.log(label ? `${label}:` : 'trace:', fa());
return fa;
};
除了自行設計trace函數,我們也可以使用各個模組提供的tap或其它tap相關的函數來協助我們除錯,下面我們就來介紹各個tap函數及其相關函數的使用方法。假設F是一個型別建構子,tap函數第一個參數是一個函數f,而f是一個A => F<A>
的函數,tap的第二個參數是F<A>
型別,tap最後會回傳型別F<A>
的結果。以Option.tap的型別簽名為例:
tap: <A>(f: (a: A) => Option<any>) => (self: Option<A>) => Option<A>
tap執行邏輯如下:
tapEither的基本行為和tap一樣,不同的是tapEither中f的輸出型別為Either,如果f輸出right則tapEither輸出some,f輸出left則tapEither輸出none。
下列程式碼你可以進行測試和練習。
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
const tapOption: string = pipe(
some(2),
O.tapEither((x) => {
console.log(`Value is ${x}`);
if (x > 30) return E.right('son');
return E.left(0);
}),
O.match(
() => 'none',
(v) => String(v)
)
);
各個模組中的tap函數的邏輯都是相似,IO和Task因為沒有失敗的情形,只要輸出型別正確,f執行之後,會把原輸入直接輸出,這個f通常是一些Side effect,協助偵錯用。tap另外有一個別名(alias),稱之為chainFirst,使用邏輯一樣,掌握了tap函數的用法,可以讓你的偵錯過程更為順利。今天的分享內容就到這邊,明天再見。