Javascript: スプレッド構文とジェネレーターを使ってtransducerっぽいことをする

参照記事:
map, reduce もいいけど transduce もね
衝撃レポート!!map, reduce の本質にせまる!
Transducers(トランスデューサー)

以前書いた記事の焼き直しなのですが、transducerについて上記の記事で勉強して「ああそうか」という部分があったので、そっちに寄せた感じで書きます。

配列を加工してジェネレーターにしたいとか、無限のジェネレーターから条件で配列/ジェネレーターにしたいとかはできます。
たくさん合成するとか、すごい大きい配列とか、処理速度とかは考慮してません。

着想

スプレッド構文にジェネレーターを与えると、順次値を取り出して並べてくれます。

const gen = () => function*(){
    yield 0;
    yield 1;
    yield 2;
}();
[...gen()] //=>[0,1,2]
  • ジェネレーターが値の生成と加工
  • スプレッド構文が蓄積

と考えると、これをうまく使えばtransducerっぽいことができるのでは?

準備:関数合成

本物のtransducerではcomposeを使いますが、このジェネレーター版ではパイプラインを使います。

const pipe = x => (...fs) => fs.reduce( (acc, f) => f(acc), x )

また、初期値と複数のジェネレーターをとって、パイプラインの結果をスプレッド構文で配列にする関数も作っておきます。

const pipeG = x => (...gs) => [...gs, g=>[...g]].reduce( (acc, g) => g(acc), x)

配列の要素を順番に返すジェネレーター

スプレッド構文の中で配列の要素をやりとりできるようにするために使います。

const arrayToG = xs => function*(){
    yield* xs;
}();

const ag = arrayToG( [0, 1, 2] ); 
ag.next(); //=> { value: 0, done: false }
ag.next(); //=> { value: 1, done: false }
ag.next(); //=> { value: 2, done: false }
ag.next(); //=> { value: undefined, done: true }

こんな感じで使います。

[...arrayToG( [0, 1, 2] ) ] //=>[0, 1, 2] 

何もしていないようですが

  • [...]がarrayToGの値を一個ずつ要求し、
  • arrayToGが[0,1,2]の要素を一個ずつ返し、
  • [...]が全部ならべて配列にしようとする

というようなことを裏でしています。

map,filterをするジェネレーター(=transducerっぽいもの)

const mapG = f => g => function*(){
    let x
    while( !( x = g.next() ).done ){
      yield f( x.value );
    }
  }();

const filterG = f => g => function*(){
    let x;
    while( !( x = g.next() ).done ) {
      if( f(x.value) ) yield x.value;
    }
  }();

後々使いやすいように終了判定( ( x = g.next() ).done のくだり)がはいっていますが、本質は

  • 引数のジェネレーターgから新しい値を取り出して
  • 関数fを適用(mapG) or 関数fの結果で値を返すか返さないか(filterG)

ということです。
データの加工だけして、蓄積はスプレッド構文にまかせる。transducerと似てますね。

こんな風に使えます。

//引数に1を足す
const succ = n => n+1;
//引数は偶数かどうか
const even = n => n%2 === 0;
// 配列の要素に1を足して配列にする
[...mapG( succ )( arrayToG( [0, 1, 2, 3] ) ) ];  //=> [ 1, 2, 3, 4 ]
// 配列の要素のうち偶数の配列。
[...filterG( even )( arrayToG( [0, 1, 2, 3] ) ) ]; //=> [ 0, 2 ]

引数にジェネレーターをとるジェネレーターなので合成は簡単です。

//mapGしてfilterGする
[...filterG( even )(mapG( succ )(arrayToG([0, 1, 2, 3])))]; //=> [ 2, 4 ]
// pipeを使って
pipe([0, 1, 2, 3])( arrayToG, mapG( succ ), filterG( even ), g => [...g] ); 
//pipeGを使って
pipeG([0, 1, 2, 3])( arrayToG, mapG( succ ), filterG( even ) );

intoArray : 配列をとって新しい配列を返す

引数の順番をほかの言語、ライブラリにあるintoに合せてみるとこんな感じです。

const intoArray = (...gs) => xs => pipeG( xs )( arrayToG, ...gs )

intoArray( mapG( succ ), filterG( even ) )( [0, 1, 2, 3] )  //=> [ 2, 4 ]

seq : 配列をとってジェネレーターを返す

場合によってはジェネレーターの方が良い場合もあるので。

const seq = (...gs) => xs => pipe( xs )( arrayToG, ...gs )

const a = seq( mapG( succ ), filterG( even ) )( [0, 1, 2, 3] );
a.next(); //=> { value: 2, done: false }
a.next(); //=> { value: 4, done: false }
a.next(); //=> { value: undefined, done: true }

中間まとめ

ここまでが参照記事の内容になぞらえた部分。
既に確定している配列を加工して配列(やジェネレーター)にする話。

この先は、まだ配列としては確定していない無限のジェネレーターを、加工しつつ条件で制限して配列(やジェネレーター)を得るには?という話。

無限を扱う

ネタ元にarrayToGを使いましたが、別のジェネレーターでもいいわけです。

//配列の要素を循環して出力
const cycleG = xs => function*(){
    while(true){
        yield* xs;
    }
}();
//関数fを繰り返しかけて結果を返します。
const iterateG = f =>  x => function*(){
    while( true ) {
      yield x;
      x = f( x )
    }
  }();

ただしこいつらは危険です。[... ]にかけると無限のものを全部とりだそうとして固まってしまいます。
なにか条件で数を制限しなければいけません。

制限する仕組み

//gの値をn個とりだします。
const takeG = n => g => function*(){
    let x
    for(let i = 0; i<n; i++){
      if( ( x = g.next() ).done ) return;
      yield x.value;
    }
  }();
//gの値をfで調べ、真の間だけ値を返します。
const takeWhileG = f => g => function*(){
    let x 
    while( !( x = g.next() ).done && f( x.value ) ) {
      yield x.value;
    }
  }();

ちょっとわかりにくいですが、ここに条件で終了させるしくみがあります。

  • arrayToG、takeG、takeWhileGはそれぞれの条件によって終了し、{value:undefined,done:true}を発生させる。
  • ジェネレーターを受け取るジェネレーターは、この done:true を受けとった時に終了するように作られている。また終了時には同じく {value:undefined,done:true} を返す。
  • これによって done:true が次々と伝播していって、ジェネレーター全体を閉じる動きになる。

こんな風に使えます。

// 0,1,2,.... => 5未満 => 1を足す=> 偶数 => 配列にする
pipeG( 0 )( 
    iterateG( succ )
    , takeWhileG( n => n<5 )
    , mapG( succ )
    , filterG( even ) 
) //=> [ 2, 4 ]

// 0,1,2,.... => 5未満 => 1を足す=> 偶数 => ジェネレーターにする
const a = pipe( 0 )( 
    iterateG( succ )
    , takeWhileG( n => n<5 )
    , mapG( succ )
    , filterG( even ) 
);

a.next(); //=> { value: 2, done: false }
a.next(); //=> { value: 4, done: false }
a.next(); //=> { value: undefined, done: true }

使えそうなジェネレーター

その他、あったらいいな的なものを考えてみました。

// 内部値の初期値をaccとして、2引数関数fに内部値とgの値を適用し、結果を内部値とし、返す。
const scanG = f => acc => g => function*(){
      let x
      let y = acc
      while( !( x = g.next() ).done ){
        yield y = f( y, x.value )
      }
    }();
// gの値をn個ブロックします。
const dropG = n => g => function*(){
    let x 
    for(let i = 0; i < n; i++){
      if( (x = g.next()).done ) return;
    }
    while( !(x = g.next()).done ){
      yield x.value
    }
  }();
// gの値をfに適用して捨て、gの値をそのまま出力する。fの副作用が必要なとき使う。
const attachG = f => g => function*(){
    let x
    while( !(x= g.next()).done ) {
      f(x.value);
      yield x.value;
    }
  }();
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.