29
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Bacon.jsと組み合わせる場合、lodash-fpを使うとよい

Last updated at Posted at 2015-05-14

lodash-fpとは

lodashの各関数について、引数の順序をiterateeが最初になるように、あるいは処理対象データが最後になるように変更した上で予め全てcurryingしたもの。flydというFRPライブラリを評価してる時にRamda関連の情報を眺めている時に存在を知った。

例の題材

例えば、ドミニオンというボードゲームのプレイ時に必要な「王国カードの選択」というテーマを考えてみる。ドミニオンは、ゲームを始める前にそのゲームで使うカードを多数の中から10枚だけ選ぶようになっており、この選択によってゲーム毎のプレイ感覚を変えることができる。

そのカードの情報が、例えば以下のようになっているとする。

cards.js
module.exports = [
    {name: 'Moat', cost: 2, type: 2},
    {name: 'Village', cost: 3, type: 0},
    {name: 'Militia', cost: 4, type: 1},
    // ...
]

例えば、10枚選ぶ前にまずcostが4以上のものだけをピックアップするにはどうすればいいか?ただ_.filterを使えばいいだけだが、lodashとlodash-fpで何が違うのか。

普通のlodashの場合

lodashが提供する(特に、ArrayやObjectに対して何らかの関数を繰り返し適用する)関数の引数順序は基本的に処理対象データが先で、iteratee、つまり適用される関数が後に来る。なので、

var _ = require('lodash'),
    cards = require('cards.js'),
    filtered = _.filter(cards, function(x) { return x.cost >= 4; });

とすればよい。

lodash-fpの場合

lodash-fpが提供する関数は、最初に書いた通り同じ名前でも引数順序が変わっているので、まず単純に上の逆になる。

var _ = require('lodash-fp'),
    cards = require('cards.js'),
    filtered = _.filter(function(x) { return x.cost >= 4; }, cards);

そして、デフォルトでcurryingされているため、例えば以下のような書き方を特に何も準備せずにできる。

var _ = require('lodash-fp'),
    cards = require('cards.js'),
    costGte4 = _.filter(function(x) { return x.cost >= 4; }),
    filtered = costGte4(cards);

だから何?

引数の順序で関数の方が先になっていたりデフォルトでcurryingされていると何がいいかということについては正直Why Ramda?を読むのが一番早いと思うが、とにかく関数合成がしやすいということに尽きるのではないかと思う。

関数型っぽい実装の仕方を積極的にしようとすると、問題解決のスコープをできるだけ小さい関数に狭めていってそれを組み合わせるような書き方になっていく。少なくとも私はそうだ。なので、関数を合成しやすい、そのために余計なことをしなくていいということは実装しやすさ、コードのS/N比の高さに直結する。

「他のアーティクルを読めば分かるよ」では投稿する意味がないので、もう一度ドミニオンの例を使ってみる。例えば、もう自分で10枚考えるのめんどくさいからボタン押したら10枚ランダムで選ばれるウェブアプリを作ろうということになったとする。その実装にはBacon.jsを使うことにし、以下のようなレンダリング用の関数が定義済みとする。

var _ = require('lodash');
module.exports = function render(data) {
    $('#cards').html(_.template('some template...'), data);
};

まず共通の考え方

lodashにはshuffleという与えられたcollectionの順序をランダムにしたArrayを返す関数と、takeという与えられたArrayの最初n個のみを持つArrayを返す関数があるため、cardsからランダムな10枚を選択する処理はこれらを組み合わせるだけでできそうだ。関数として切り出すなら、一番簡単に書くなら以下のようになるだろう。

function takeRandom10(cards) {
    return _.take(_.shuffle(cards), 10);
}

あるいは、chainな書き方をするなら以下のようになるだろう。

function takeRandom10(cards) {
    return _(cards).shuffle().take(10).value();
}

とりあえずこのような関数をそのまま使ってBacon.jsで実装するなら、以下のようになる。

$('#button').asEventStream('click') // クリックによって発生するBacon.jsのイベントストリームを生成
    .map(cards) // ストリームデータを、上流データを無視して定義済みの変数(この場合cards)に変換する
    .map(takeRandom10) // ストリームデータを、上流データをtakeRandom10に適用したものに変換する
    .onValue(render); // ストリームデータを消費して、何かを描画する

いちいち関数を定義したくない

takeRandom10を当面他で使いまわす予定が無いのなら、いちいち関数を定義するのは若干面倒だ。とりあえず別に定義したくないだけなら無名関数をその場で渡してあげればいいが、迂遠な感じは残る。

    // snip
    .map(function(cards) {
        return _(cards).shuffle().take(10).value();
    })
    // snip

takeRandom10の中身は関数呼び出しを組み合わせたもの(だからchainな書き方をすることができるわけだが)であり、関数合成を有効に利用できそうな典型的な例だ。

関数合成の機能は、関数型プログラミング用のライブラリではよくcomposeという名前で提供されている。lodashではflow/flowRightという名前で提供されていて(composeという名前も、flowRightに対するエイリアスとして提供されている)、引数として複数の関数を渡すとそれを合成したものを返す。flowはpipeという名前で提供されることが多い実装だが、どちらかというとflowRight(compose)よりもflowの方が人間の思考順序的には自然かも知れない。

_.flow(f, g, h)(x) // h(g(f(x)))と同じ
_.flowRight(f, g, h)(x) // f(g(h(x)))と同じ

普通のlodashの場合

takeRandom10の場合、shuffleは一引数だからそのままflowに渡せるが、takeが引数を二つ取るためそのまま単純には関数合成できない。複数の引数を取る関数を関数合成できるよう一引数の関数にするには、引数の部分適用をするのが手っ取り早いだろう。

lodashはpartial/partialRightという関数で部分適用をサポートしているので、とりあえずtakeRandom10を明示的な関数定義でなく関数合成で実現するなら以下のようになるだろう。

var takeRandom10 = _.flow(_.shuffle, _.partial(_.take, _, 10));
// 引数を_.shuffle(x)に渡した結果を、_.take(x, 10)に渡す合成関数を生成

partialの引数適用は左から順に行われるので、二つ目の引数のみ適用したい場合は一つ目の引数として'_'を使えば、引数適用をスキップするためのプレースホルダーとして扱われる。あるいは、partialRightであれば適用が右側から行われるので、以下のようにも書ける。

var takeRandom10 = _.flow(_.shuffle, _.partialRight(_.take, 10));

このように合成された関数をBacon.jsのmapに直接渡せばよいので、以下のような実装になる。

$('#button').asEventStream('click')
    .map(cards)
    .map(_.flow(_.shuffle, _.partialRight(_.take, 10))
    .onValue(render);

人によって感じ方は違うかも知れないが、partialRightがノイズになっている感じはする。そういう余計なことをしなくても自動的に引数が部分適用されていたらもっと楽だし、もっと言うなら、takeの引数順序が逆になっていればわざわざ一つ目の引数をプレースホルダーを使って飛ばしたりpartialRightを使ったりしなくてもただ左側から補助的な引数のみを部分適用することができる。

最終的な合成関数の引数(を変換されたもの)は、合成される前の関数においては右端の引数になっている方が合成しやすいということが分かる。

lodash-fpの場合

最初に書いた通り、lodash-fpのtakeは先に要素数、次に対象Arrayと順番が逆になっている。また、curryingされているためわざわざpartialなどを使わなくても一つ目の引数のみ指定すれば自動的に一つ目の引数のみが適用された一引数のtakeが返ってくるので、以下のように書くことができる。

    // snip
    .map(_.flow(_.shuffle, _take(10)))
    // snip

flowはもうどうしようもないので、lodashの文脈で言う限りこれでノイズは最小になったと言えるだろう。

ただ、この例の場合…

ここまで書いて気づいたのだが、Bacon.jsのストリームはそれ自体が所謂パイプ処理(ここでいうflow)を実現しているし、Bacon.jsのFunction construction rulesに書いた通りmapなどによる擬似的な引数部分適用が行われるので、実は以下のようによりBaconicに書くことができる。

$('#button').asEventStream('click')
    .map(cards)
    .map(_.shuffle)
    .map(_.take, 10)
    .onValue(render);

それでも、この書き方はtakeの引数順序が逆になっているからこそできることなので、それだけでもlodash-fpの恩恵を受けることができていると言えるだろう。逆に言うと、これができないがためにBacon.jsのストリームに直接lodashの関数をmapすることができないでいた(_.partialRightすればいいのだが、なんかいちいち邪魔くさくて…)。

終わり

lodash-fpはlodashと共存させることも普通にできるので(_にlodashを、__にlodash-fpをアサインするとか)、現在実装しているコードにもまずは部分的にでも利用してみようと思う。

29
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?