JavaScript
es6
関数型プログラミング

JavaScript:関数を無限iterate して map して filter して take とかできるジェネレーターを考えてみた 2

前の記事ではあまりに意味不明の構文に見えるので、もうすこし見やすいようにしてみました。
だいぶいじりましたが主旨は同じです。

想定読者 : こう書けるとうれしい人

FizzBuzzです。パイプライン演算子の雰囲気です。

const fizzBuzz = x =>
 x % 3 === 0 && x % 5 === 0 ? "FizzBuzz"
 : x % 3 === 0 ? "Fizz"
 : x % 5 === 0 ? "Buzz"
 : x
;

pipe(1)(nestG(n=>n+1), applyG(fizzBuzz), passTimesG(15), attachG(console.log), spreadG)

//=>こんな結果になればいいな。
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

あるいはこっち。関数合成の雰囲気です。

dot(spreadG, attachG(console.log), passTimesG(15), applyG(fizzBuzz), nestG(n=>n+1))(1)

もはや何言語だかわからなくなりそうですが、こうなるとうれしいー!っていうだいぶ変わった人向けです。

これを読みこめばOK

それぞれ使う関数をコピペするか、エクスポート/インポートするかすれば使えます。

//関数fをくりかえしかけて、その結果を配列にします。[...]が展開してくれるんで使いどころないかも。
function iterateG(f){
  return function*(xs){
    let yss = []
    let ys = xs
    while( true ) {
      yss = yss.concat( [ys] )
      yield yss
      ys = f( ys )
    }
  }
}
//関数fを繰り返しかけて結果を返します。[... ] と組み合わせてiterate相当。
function nestG( f ) {
  return function*( x ){
    let y = x
    while( true ) {
      yield y
      y = f( y )
    }
  }
}
//ジェネレーターgを呼び出し値に関数fをかけます。
//[... ] と組み合わせてmap相当。
function applyG( f ) {
  return function*( g ){
    let x = g.next();
    while( !x.done ){
      yield f( x.value );
      x = g.next();
    }
  }
}
// 内部値の初期値をaccとして、2引数関数fに内部値とgの値を適用し、結果を内部値とし、返す。
//reduce相当。[... ] と組み合わせてscan相当。
function accumG( f ){
  return function( acc ){
    return function*( g ){
      let x = g.next()
      let y = acc
      while( !x.done ){
        yield y = f( y, x.value )
        x = g.next()
      }
    }
  }
}
//gの値を判定関数fで調べ、真なら値を返します。偽なら値を返さず動作を継続。
//[... ] と組み合わせてfilter相当。
function passIfG( f ) {
  return function*( g ){
    let x = g.next();
    while( !x.done ) {
      if( f(x.value) ) yield x.value;
      x = g.next();
    }
  }
}
// gの値をn個ブロックします。[... ] と組み合わせてdrop相当。
function blockTimesG( n ){
  return function*( 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 passTimesG( n ){
  return function*( g ){
    let x
    for(let i = 0; i<n; i++){
      x = g.next();
      if( x.done ) return;
      yield x.value;
    }
  }
}
//gの値をfで調べ、真の間だけ値を返します。偽になったら終了メッセージを出す。
//[... ] と組み合わせてtakeWhile相当。
function passWhileG( f ){
  return function*( g ){
    let x = g.next()
    while( !x.done && f( x.value ) ) {
      yield x.value;
      x = g.next();
    }
  }
}
// gの値をfに適用して捨て、gの値をそのまま出力する。fの副作用が必要なとき使う。
function attachG( f ) {
  return function*( g ){
    let x= g.next();
    while( !x.done ) {
      f(x.value);
      yield x.value;
      x = g.next();
    }
  }
}

//関数合成のための関数
const pipe = x => (...f) => f.reduce( (acc, e) => e(acc), x )
const dot = (...f) => x => f.reduceRight( (acc, e) => e(acc), x )
//スプレッド構文、メソッドを関数にしておく
const spreadG = xs => [...xs]
const sortC = f => xs => xs.sort( f )
const mapC = f => xs => xs.map( f )
const filterC = f => xs => xs.filter( f )
const forEachC = f => xs => xs.forEach( f )

使用例:

// 無限の等差数列から10個とる。
pipe(0)(nestG(n=>n+1), passTimesG(10), spreadG)
=> [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
// 等差数列に階乗を適用して10個とる。
const fact = n => n<=0 ? 1 : n*fact(n-1));
pipe(0)(nestG(n=>n+1),applyG(fact,  passTimesG(10), spreadG)
=> [ 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880 ]
// passWhileGを使って10000未満の階乗。
   pipe(0)(nestG(n=>n+1),applyG(fact,  passWhileG(e=>e<10000), spreadG)
=> [ 1, 1, 2, 6, 24, 120, 720, 5040 ]
//10000未満の階乗のうち7で割り切れるもの。
   pipe(0)(nestG(n=>n+1),applyG(fact,  passWhileG(e=>e<10000), passIfG(e=>e%7==0), spreadG)
=> [ 5040 ]
// 上に同じ。applyGとpassWhileGの間で途中経過をコンソール出力。
   pipe(0)(nestG(n=>n+1),applyG(fact, attachG(console.log), passWhileG(e=>e<10000), passIfG(e=>e%7==0), spreadG)
/* コンソール出力
1
1
2
6
24
120
720
5040
40320//これを passWhileGがブロックし、passIfGに終了のメッセージを送る
*/
=> [ 5040 ]

使い方 :

  • 関数合成の仕組みを使っていますが、やってることは要素を一個ずつ処理して並べていくいわば大きなwhileループです。spreadGが適用されるまでは配列ですらありません。この辺りは前の記事と同じです。適宜、読み替えてください。
  • spreadGが適用された後は、pipe、dotの外側ですが普通の配列のメソッドが使えます。
  • 使いそうな配列メソッドを関数にしたバージョン(sortC,mapCなど)を最後に付けておきました。これらはspreadGが適用された後、pipe、dotの内側で使えます。

一番使いそうな場面は、確定した配列にmapとかfilterするのではなく、逆に何かまだ確定してないものにmapとかfilterしてその結果をある条件で区切って配列にしたい、というような場合でしょうか。

たとえばこんなことができます。
フィボナッチ数列のうち奇数を1000未満までの配列にしてFizzBuzzするとか。

const fizzBuzz = x =>
 x % 3 === 0 && x % 5 === 0 ? "FizzBuzz"
 : x % 3 === 0 ? "Fizz"
 : x % 5 === 0 ? "Buzz"
 : x
;
// 初期値を二つとるジェネレーター。nestGをちょっと変形したもの。
const nest2G = f => x =>function*( y ){ 
  yield x 
  yield y 
  while(true){ 
    yield x = f(x)(y); 
    [x, y] = [y, x] 
  } 
}
// フィボナッチ数列のもと。前二つを足すと次の要素になる。
const fibo = x => y => x + y;

pipe(1)(
    nest2G(fibo)(0)
    , passIfG(e=>e%2===1)
    , passWhileG(n=>n<1000)
    , applyG(fizzBuzz)
    , spreadG
    , forEachC(e=>console.log(e))
)
//=>
1
1
Fizz
Buzz
13
Fizz
Buzz
89
233
377
Fizz