ふと、「Identity Functorってパイプライン演算子っぽい」と思ったんですよ。何言ってんだって感じですが。とりあえず、JavaScript で下記のような関数を定義するとパイプラインっぽいことができるんじゃないかと思った次第です。
const wrap = x => ({
unwrap: x,
map: f => wrap(f(x)),
tap: f => wrap((f(x), x))
});
tap と unwrap がある理由
Identity Functor にtap
は不要です。Scala がpipe
(上のmap
に対応するメソッド)とともにtap
も導入したことを参考に入れてみました。副作用が身近な言語ならtap
は確かに便利ですね。
Scala では、pipe/tap
を呼び出すと暗黙の変換が発動して元の値がChainingOps
に包まれます。また、pipe/tap
の戻り値はChainingOps
に包まれていません。そのため明示的に包んだり、包装をほどいたりする必要はありません。それに比べると、上の JavaScript の実装は明示的なwrap/unwrap
が必要になります。なぜなら、暗黙的な変換はされませんし、map/tap
の戻り値が包まれた状態で返されるからです。
余談ですが、tap
は Ruby が源流です。逆に言うと Ruby のthen
はパイプラインっぽいかもしれませんね。
map をパイプラインとして使ってみる
次のコードの「通常の関数適用」と「map を使った関数適用」の処理結果はどちらも202
になります。
const addOne = x => x + 1;
const timesTow = x => 2 * x;
// 通常の関数適用
timesTow(addOne(100));
// map を使った関数適用
wrap(100).map(addOne).map(timesTow).unwrap;
map
を使うと関数適用の入れ子が解消され、処理の順にそって関数を左から右に書けています。パイプラインっぽいことは十分できていますね。
this
を考慮しなければ、fn(…(f1(f0(x))…)
の形の関数適用の入れ子は一般に、wrap(x).map(f0).map(f1)….map(fn).unwrap
へ書き換え可能です。
tap を使ってみる
上の map 使用例のコードにtap
を挟み込むと、処理途中の結果をコンソールに表示できます。具体的に、処理結果は202
で変わりませんが、コンソールには100
, 101
, 202
と途中結果が表示されていきます。
const addOne = x => x + 1;
const timesTow = x => 2 * x;
// tap を使って処理の途中結果をコンソールに出力
wrap(100) .tap(console.log)
.map(addOne) .tap(console.log)
.map(timesTow) .tap(console.log)
.unwrap;
一次変数を用意したり、引数をそのまま返す関数を用意すれば通常の関数適用でもtap
と同じ事はできるかもしれません。ただ、tap
の方がコードの追加は容易でしょうし、不要になった際の除去も簡単でしょう。
ちなみに、応用すると下記のようなおどろおどろしい FizzBuzz を書くこともできます。
const toFizzBuzz = n => wrap(n)
.map(num => ({num, str: ''}))
.tap(x => {
if(x.num % 3 === 0)
x.str += 'Fizz';
})
.tap(x => {
if(x.num % 5 === 0)
x.str += 'Buzz';
})
.tap(x => {
if(x.str === '')
x.str += x.num.toString();
})
.map(x => x.str)
.unwrap;
関数合成
使いどころは不明ですが、同じような考えで関数合成のみを扱うオブジェクトも生成できます。
const unary = (f = x => x) => ({
run: f,
mappend: g => unary(x => g(f(x))),
tappend: g => unary(x => {
const y = f(x);
g(y);
return y;
})
});
const addOne = x => x + 1;
const timesTow = x => 2 * x;
const compose = unary()
.tappend(console.log)
.mappend(addOne)
.tappend(console.log)
.mappend(timesTow)
.tappend(console.log)
.run;
compose(100);
型付け
TypeScript で型を意識するなら次のようになるでしょうか。wrap
は素直に型が付きますね。unary
は、型が合わないためデフォルト引数を設定できません。別途、emptyUnary
を用意すれば、同じようにemptyUnary()
を起点に関数を合成していく事ができます。
type Identity<A> = {
unwrap: A,
map: <B>(f: (x: A) => B) => Identity<B>,
tap: <U>(f: (x: A) => U) => Identity<A>
}
const wrap = <A>(x: A): Identity<A> => ({
unwrap: x,
map: f => wrap(f(x)),
tap: f => wrap((f(x), x))
});
type UnaryBuilder<A, B> = {
run: (x: A) => B,
mappend: <C>(g: (x: B) => C) => UnaryBuilder<A, C>,
tappend: <U>(g: (x: B) => U) => UnaryBuilder<A, B>
}
const emptyUnary = <A>(): UnaryBuilder<A, A> => unary(x => x);
const unary = <A, B>(f: (x: A) => B): UnaryBuilder<A, B> => ({
run: f,
mappend: g => unary(x => g(f(x))),
tappend: g => unary(x => {
const y = f(x);
g(y);
return y;
})
});
おわり
まあ、ごちゃごちゃ言ってないで、パイプライン演算子|>
が使える環境なら、それを使えばいいですし。使えなくても次のような関数を定義すれば十分ですね。
const pipe = (x, ...fs) => fs.reduce((v, f) => f(v), x);