Object.prototypeにメソッドを生やしてパイプライン演算子っぽくしてみた
をやってみて、こりゃなしだな...とつくづく思ったのですが、
【Swift】鉄道指向プログラミング(Railway Oriented Programming)でResultの使い方を学ぶ
JavaScriptでパイプライン演算子が欲しい余り頭がおかしくなった人間の末路
ネストしすぎの()を何とかしたい
を読んで、なるほど!と思い、もうちょっと遊んでみました。
配列を使ってパイプラインっぽいことをする
たとえばこんなことです。
const plus = x => y => y + x
plus('C')( plus('B')( plus('A')('hoge') ) ) // 'hogeABC'
これを
['hoge']
.map(plus('A'))
.map(plus('B'))
.map(plus('C'))
[0] // 'hogeABC'
と書ける、ということです。
は? 何してるの?
と一瞬思うかもしれませんが、
配列に値をひとつだけいれる
-> .map() で、配列から値を取り出し、関数を適用して、新しい配列に入れて返す
-> [0] で、配列から値をとりだす
というようなことをしています。
値を入れたり出したり手間がかかるし、字数も多いのでアレですけど、ちゃんと連鎖的に関数適用できています。
これでいいじゃん。
これを...
オブジェクトでまねしてみる
const Result = class{
constructor( value ) { this.value = value }
_(f) { return new Result( f(this.value) ) }
}
// 用例:
const plus = x => y => y + x
new Result('hoge')
._ ( plus('A') )
._ ( plus('B') )
._ ( plus('C') )
.value // 'hogeABC'
それ用のオブジェクトResultを作って、配列のかわりにしてみました。
見た目でそれっぽくなるように工夫はしてみましたが、特にメリットはありません。配列のと同じです。
これを元にして...
失敗 Failure と 成功 Success を定義して、ちょっとかしこいパイプラインにしてみる
ちょっと 鉄道指向 っぽい方に寄せていってます。
パイプラインは、一気に処理をすすめるので、途中でよく失敗しちゃったりします。
上のようなシンプルなパイプラインだと、それでも律儀に計算を続けようとして結果、無意味な値を返してきます。
// うっかり未定義の値を渡してしまう:
new Result(undefined)
._ ( plus('A') )
._ ( plus('B') )
._ ( plus('C') )
.value // 'undefinedABC'
// 途中で、計算が失敗して無意味な値を返す:
new Result('hoge')
._ ( plus('A') )
._ ( _=> NaN )
._ ( plus('C') )
.value // 'NaNC'
また、何か実行時にエラーがあれば止まってしまいます。
- パイプラインの途中で望まない値になっちゃったり、エラーを投げたりをいい感じにしてくれないかなあ
- ついでに、途中の値を出力できたら便利かも
失敗Failure と成功Success を定義して、何か失敗したら、そこで計算をやめて失敗を返すようにしてみました。
const Failure = class {
constructor(value) { this.value = value }
_(f) { return this }
}
const Success = class {
constructor(value) { this.value = value }
_(f) { return result(f)(this.value) }
}
// 値が失敗した値なのか判定する関数
const isFailed = x =>
x===null || x===undefined || Number.isNaN(x)
// 値を関数に適用して失敗か成功に包んで返す
const result = f => x => {
try {
const a = f(x)
return isFailed(a) ? new Failure(a) : new Success(a)
}
catch(e){ return new Failure(e) }
}
// 値を 値によって失敗か成功で包んで返す
const pipe = result( x=>x )
// 関数に値を適用して返り値を捨て、値を返す
const attach = f => x => ( f(x), x )
// 用例:
const plus = x => y => y + x
// 成功:
pipe('hoge')
._ ( plus('A') )
._ ( plus('B') )
._ ( plus('C') ) // Success { value: 'hogeABC' }
// 失敗な値:
pipe('hoge')
._ ( x=>NaN )
._ ( x=>undefined )
._ ( plus('C') ) // Failure { value: NaN }
// エラー とか attach とか:
pipe('hoge')
._ (attach(console.log)) // 'hoge'を出力
._ ( x=>{throw Error('wow!')} ) // Failure { value: Error: wow! /* 以下略 */}
._ (attach(console.log)) // 出力しない
._ ( x=>undefined )
._ ( plus('C') ) // Failure { value: Error: wow! /* 以下略 */}
こんな仕組みです。
- 失敗のメソッド _() は、自分の値に引数の関数を適用せず、自分自身を返す。
- 成功のメソッド _() は、自分の値に引数の関数を適用した結果で成功か失敗を返す。
- 関数 result() で成功か、失敗かを返す。
- 関数を適用したときエラーを投げたら、失敗。
- 失敗した値かはisFailed() で判断する。ここでは、null / undefined / NaN が失敗。
結果は成功Successか失敗Failureに包まれているので、それによって後続の処理を分岐できるし、とくに問題がなければ直接 .value で値をとってもよい。
attach()は、関数と値を引数にして、値に関数を適用し、返り値を捨て、値を返す補助的な関数です。
何か副作用を発生させたいときに便利な関数です。
これをパイプラインの途中で使うと、成功の値だけを、表示したり、DBに登録したり、みたいなことができます。