前記事がとっちらかっちゃって主旨も変わってきちゃったんで一旦まとめます。
※ 関数名が内容と合ってない気がしてきたので、修正しました。
想定読者:こう書けるとうれしい人
たとえばFizzBuzz。もちろんこのままでは動きませんが、こんな風に書けたらうれしくないですか?
const fizzBuzz = x =>
x % 3 === 0 && x % 5 === 0 ? "FizzBuzz"
: x % 3 === 0 ? "Fizz"
: x % 5 === 0 ? "Buzz"
: x
;
[...attach(console.log, passTimes(15, applyG(fizzBuzz, nest( n=> n+1, 1) )))];
※ がんばってHaskellに寄せてます。ちなみにHaskellだと...
fizzBuzz :: Int -> String
fizzBuzz x
| x `mod` 3 == 0 && x `mod` 5 == 0 = "FizzBuzz"
| x `mod` 3 == 0 = "Fizz"
| x `mod` 5 == 0 = "Buzz"
| otherwise = show x
main = mapM_ putStrLn $ take 15 . map fizzBuzz $ iterate (\n -> n + 1) 1
それぞれ、
- ...attach = mapM_
- console.log = putStrLn
- passTimes = take
- applyG = map
- nest = iterate
に相当するんじゃないか?と想像できますよね。
実際はconsole.log以外だいぶ違いますが。でも全部そろうと同じになります。不思議。
わー! 似てるー! うれしいー! 使ってみたいー! っていうちょっと変った人向けです。
それ、できます。ジェネレーターで。
function* nest( f, x ) {
let y = x
while( true ) {
yield y
y = f( y )
}
}
function* applyG( f, g ) {
let x = g.next();
while( !x.done ){
yield f( x.value );
x = g.next();
}
}
function* passTimes(n,g){
let x
for(let i = 0; i<n; i++){
x = g.next();
if( x.done ) return;
yield x.value;
}
}
function* attach(f, g) {
let x= g.next();
while( !x.done ) {
f(x.value);
yield x.value;
x = g.next();
}
}
const fizzBuzz = x =>
x % 3 === 0 && x % 5 === 0 ? "FizzBuzz"
: x % 3 === 0 ? "Fizz"
: x % 5 === 0 ? "Buzz"
: x
;
[...attach(console.log, passTimes(15, applyG(fizzBuzz, nest( n=> n+1, 1) )))];
//出力結果
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
わーいできたー!
何をしている?
スプレッド構文 "...
" が ジェネレーターの計算結果を順番にならべて[ ]
内に展開してくれてます。
何がいいの?
- 普通に使うと一個ずつしかもらえないものが、いっぺんに全部もらえる。
- 繰り返しのことを考えずに一回分の処理だけ考えて関数がつくれる。
- iterate,nestで一回分の処理を無限ループ > applyGしたりpassIfしたりattachしたり > passTimes,passWhileで条件で終了 > applyGしたりpassIfしたりattachしたり > 結果を配列で取得、 まで一気にできる。
- 見た目が何かかっこいい。※ 個人差あり。
使っている 使える ジェネレーターたち
とりあえず必要なやつをコピペすれば使えます。あるいはまとめてエクスポート/インポートするとか。
// 関数fをくりかえしかけて、その結果を配列にします。スプレッド構文が展開してくれるんで使いどころないかも。
function* iterate(f,xs){
let yss = []
let ys = xs
while( true ) {
yss = yss.concat( [ys] )
yield yss
ys = f( ys )
}
}
// 関数fを繰り返しかけて結果を返します。[... ]と組み合わせてiterate相当。
function* nest( f, x ) {
let y = x
while( true ) {
yield y
y = f( y )
}
}
//ジェネレーターgを呼び出し値に関数fをかけます。
//[... ] と組み合わせてmap相当。
function* applyG( f, g ) {
let x = g.next();
while( !x.done ){
yield f( x.value );
x = g.next();
}
}
// 内部値の初期値をaccとして、2引数関数fに内部値とgの値を適用し、結果を内部値とし、返す。
// reduce相当。[... ]と組み合わせてscan相当。
function* accumG( f, acc, g ){
let x = g.next()
let y = acc
while( !x.done ){
yield y = f( y, x.value )
x = g.next()
}
}
// gの値を判定関数fで調べ、真なら値を返します。偽なら値を返さず動作を継続。
// [... ]と組み合わせてfilter相当。
function* passIf( f, g ) {
let x = g.next();
while( !x.done ) {
if( f(x.value) ) yield x.value;
x = g.next();
}
}
// gの値をn個ブロックします。[... ]と組み合わせてdrop相当。
function* blockTimes(n, g){
let x
for(let i = 0; i < n; i++){
x = g.next();
if( x.done ) return;
}
x = g.next();
while( !x.done ){
yield x.value
x = g.next();
}
}
// gの値をn個とりだします。とりだし終ったら終了メッセージを出す。
// [... ]と組み合わせてtake相当。
function* passTimes(n,g){
let x
for(let i = 0; i<n; i++){
x = g.next();
if( x.done ) return;
yield x.value;
}
}
// gの値をfで調べ、真の間だけ値を返します。偽になったら終了メッセージを出す。
// [... ]と組み合わせてtakeWhile相当。
function* passWhile(f, g){
let x = g.next()
while( !x.done && f( x.value ) ) {
yield x.value;
x = g.next();
}
}
// gの値をfに適用して捨て、gの値をそのまま出力する。fの副作用が必要なとき使う。
// [... ]と組み合わせてforEach相当。
function* attach(f, g) {
let x= g.next();
while( !x.done ) {
f(x.value);
yield x.value;
x = g.next();
}
}
とりあえずこのくらいあれば自分は満足かな。
ちなみに元ネタは主にHaskellのリスト関数です。
使用例
// 無限の等差数列から10個とる。
[...passTimes(10, nest( n=>n+1, 0) )]
//=> [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
// 等差数列に階乗を適用して10個とる。
const fact = n => n<=0 ? 1 : n*fact(n-1);
[...passTimes(10, applyG(fact, nest( n=>n+1, 0) ))]
//=> [ 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880 ]
// passWhileを使って10000未満の階乗。
[...passWhile(e=>e<10000, applyG(fact, nest( n=>n+1, 0) ))]
//=> [ 1, 1, 2, 6, 24, 120, 720, 5040 ]
//10000未満の階乗のうち7で割り切れるもの。
[...passIf(e=>e%7==0, passWhile(e=>e<10000, applyG(fact, nest( n=>n+1, 0) )))]
//=> [ 5040 ]
// 上に同じ。applyGとpassWhileの間で途中経過をコンソール出力。
[...passIf(e=>e%7==0, passWhile( e=>e<10000, attach(console.log, applyG(fact, nest( n=>n+1, 0) ))))]
/* コンソール出力
1
1
2
6
24
120
720
5040
40320 //これを passWhile がブロックし、passIfに終了のメッセージを送る
*/
//=> [ 5040 ] //配列の値
デメリット?
- 「ちょっと何言ってるのかわからない」って言われる。
- 中味を見てわかる通り、裏ではfor,whileがぶんぶん回っています。
パフォーマンス良くないかも。どうなんでしょう? - 要素を一個ずつ受け渡して処理して積み上げる方式なので、ちゃんとした関数合成とは計算の順序が違います。使用例のような要素一個ずつ演算するものでは違いはありませんが、「思ってたんと違う」とかエラーになるとかは多分そのせいです。
- 配列の複数の要素にアクセスするもの(sortとかfindとか)は苦手です。できあがった配列にたいしてあらためてかけた方が幸せになれます。
※ 追記 : [ ]
内で起きてること
上のジェネレーターにはそれぞれ機能に違いがあります。
iterate, nest:
- 左から「一個くれ」というメッセージを受け取るとデータを一個生成して左に送る。
applyG, accumG, passIf, blockTimes, attach:
- 左から「一個くれ」と来ると右に「一個くれ」と送る。
- 右からデータが来るとそれぞれ独自の処理をして左に送る。
- 右から「終了!」というメッセージが来ると左に「終了!」と送る。
passTimes, passWhile:
- 左から「一個くれ」と来ると右に「一個くれ」と送る。
- 右からデータが来ると条件が真ならそのまま左に送る。
- 条件が偽ならデータを左に送らず、「終了!」メッセージを左に送る。
- 右から「終了!」メッセージが来ると左に「終了!」と送る。
このジェネレーター同士がデータを送り合いながらひたすらデータ処理しています。
左から右に伝言ゲーム、右から左にバケツリレーしてるイメージかな?
冒頭のFizzBuzzの例。
[...attach(console.log, passTimes(15, applyG(fizzBuzz, nest( n=> n+1, 1) )))]
行きは左から右、"...
"から nest に向かって「一個くれ」というメッセージを送ります。
- "
...
":「一個くれ」=>attach
:「一個くれ」=>passTimes
:「一個くれ」=>applyG
:「一個くれ」=>nest
帰りは右から左、nestから"...
"に向かってデータを送ります。
-
nest
が一個生成 =>データ=>applyG
が加工=>データ=>passTimes
が数を監視=>データ=>attach
が表示=>データ=>"...
"が並べる
というのを15回繰り返し、16回目の帰りの流れで:
-
nest
が一個生成 =>データ=>applyG
が加工 =>データ」=>passTimes
はデータをブロックし終了メッセージを送る =>「終了!」=>attach
は終了メッセージを送る=>「終了!」=> "...
"は並べたデータを"[ ]
"に送る
みたいな流れじゃないでしょうか。