はじめに
パイプライン演算子 がまだなかなか来そうにないので。
似たようなことをするのに配列使ったりとか、Promise 使ってみたりとか、パイプライン用のクラスを作ったりライブラリ使ったりしてもいいのだけれど、そこまで凝ったの要らないんだよなあ、ってときのお手軽版。
お手軽パイプライン
const pipe = x => f => f ? pipe(f(x)) : x;
type Pipe<T> = {
(): T;
<U>(f: (x: T) => U): Pipe<U>
};
const pipe: <T>(x: T) => Pipe<T> =
<T>(x: T) => (<U>(f?: (x: T) => U) => f ? pipe(f(x)) : x) as Pipe<T>;
使うときはこう。
pipe(1)
(x => x + 1)
(x => x * 3)
(x => console.log(x)); // 6
あるいは、
console.log(
pipe(1)
(x => x + 1)
(x => x * 3)
()
);
お手軽!
関数の配列を適用するみたいな実装だと、引数と戻り値が同じ型じゃないときに面倒になるけれど、これなら途中で型が変わっても大丈夫。
以下応用例。
バリエーション
Optional っぽいもの
Nullish (null
や undefined
) だったら何もしない or 二つ目の関数を実行
const optPipe = x => (f, g) => f ? optPipe(x == null ? g && g() : f(x)) : x;
type OptPipe<T> = {
(): T | null | undefined;
<U>(f: (x: T) => U | null | undefined, g?: () => U): OptPipe<U>
};
const optPipe: <T>(x?: T) => OptPipe<T> =
<T>(x?: T) => (
<U>(f?: (x: T) => U, g?: () => U) =>
f ? optPipe(x == null ? g && g() : f(x)) : x
) as OptPipe<T>;
最後にデフォルト値挟み込んだりとか。
const inp = () => Math.random() < 0.5 ? 1 : undefined;
optPipe(inp())
(x => x + 1)
(x => x * 3)
(String, () => 'no value')
(x => console.log(x)); // '6' or 'no value'
値を変えずに処理を挟む
ブロック or カンマ演算子で tap 相当。
pipe(1)
(x => x + 1)
(x => {
console.log(x); // 2
return x;
})
(x => x * 3)
(x => console.log(x)); // 6
// または
pipe(1)
(x => x + 1)
(x => (console.log(x), x)) // 2
(x => x * 3)
(x => console.log(x)); // 6
なんとなく見栄えを気にしてみる。
const tap = x => {
const self = f => f ? (f(x), self) : x;
return self;
};
type Tap<T> = {
(f: (x: T) => void): Tap<T>;
(): T;
};
const tap = <T>(x: T) => {
const self = (f?: (x: T) => void) => f ? (f(x), self) : x;
return self as Tap<T>;
};
pipe(1)
(x => x + 1)
(x => tap(x)
(x => console.log('tap1'))
(x => console.log('tap2'))
(x => console.log('tap3'))
()
)
(x => x * 3)
(x => console.log(x)); // 6
fork して join
const fork = x => {
const self = r => f => f ? self([f(x), ...r]) : r;
return self([]);
};
type Cons<H, T extends any[]> =
((head: H, ...tail: T) => any) extends ((...args: infer R) => any) ? R : [];
type Fork<T, R extends any[] = []> = {
(): R;
<U>(f: (x: T) => U): Fork<T, Cons<U, R>>;
};
const fork = <T>(x:T) => {
const self = <R extends any[]>(r: R) => <U>(f: (x: T) => U) =>
f ? self([f(x), ...r]) : r;
return self([]) as Fork<T>;
};
// (x + 1) * (x + 2) * (x + 3) * 3
pipe(1)
(x => fork(x)
(x => x + 1)
(x => x + 2)
(x => x + 3)
().reduce((x, y) => x * y)
)
(x => x * 3)
(x => console.log(x)); // 72
簡単な例なので、やりすぎ感が否めない。
式が複雑になりそうなときに分割して縦に並べやすくなるので、
一応それなりに使えたり。
型が配列でなくタプルになっているあたりが一応のメリット。
例だと reduce
の前で number[]
ではなく [number, number, number]
と推論してくれる。
異なる型が混ざっていても OK。
順序が逆になってるけど、正順にしようとすると TypeScript の型定義が長くなるのでやめました。
追記: コメント欄に正順版も載せました。お手軽感あまりありませんが。
戻り値が同じ型の fork なら、関数を配列にするのもあり。
// (x + 1) * (x + 2) * (x + 3) * 3
pipe(1)
(x => [
x => x + 1,
x => x + 2,
x => x + 3
].map(f => f(x)).reduce((x, y) => x * y)
)
(x => x * 3)
(x => console.log(x)); // 72
追記:
書き忘れ。
関数を配列にする場合、TypeScript だと推論効かないので。
const forkArray = <T, U>(x: T, fs: ((x: T) => U)[]) => fs.map(f => f(x));
// (x + 1) * (x + 2) * (x + 3) * 3
pipe(1)
(x => forkArray(x, [
x => x + 1,
x => x + 2,
x => x + 3
]).reduce((x, y) => x * y)
)
(x => x * 3)
(x => console.log(x)); // 72
ちなみに、あくまで pipe っぽい記法にするなら、と言う話で、
今回みたいな簡単な例なら、当然 fork せずに直に配列作っちゃったほうが楽です。
おわりに
と言うわけでお手軽パイプライン実装と使用例でした。
個人的には、書き捨てスクリプトなんかで時々こういうことやります。