LoginSignup
7
6

More than 5 years have passed since last update.

JavaScript(ECMAScript)とRamdaで関数型プログラミング

Posted at

さっき「関数型プログラミング」でGoogle検索したら最初に表示された「関数型プログラミングはまず考え方から理解しよう」というページ、本文はもちろん、活発な議論がなされたコメント部分も、とても面白かったです。

ただ、コメント中のHaskellの美しいコードは、無限リストが使えるからこその書き方なんですよね……。このやり方は、JavaScriptだとちょっと辛い。コメント中でHaskellと同様のやり方をJavaScriptで書いてくださっている方も、繰り返す数を指定することで対処しています。

うんやっぱりHaskellはすげーなぁというのは当然なんだけど、JavaScriptを気に入っている私としては、実はJavaScriptもそれなりにすごいんだよということを示しておきたい。だから、JavaScript(EcmaScript)に関数型プログラミング向けライブラリのRamdaをインポートして、同じお題でプログラミングしてみました。

元ページのお題

詳しくは元ページを見ていただくとして、簡単に書くとこんな感じ。

  • 唐揚げ弁当がいくつかあります。唐揚げを何個か、つまみ食いしたいです。
  • バレづらいように、最も唐揚げの数が多い弁当からつまみ食いしましょう。

とりあえず、関数型で書いてみた

#後で述べますけど、このコードはかなりヘッポコです……。後で修正しますから、ここで見捨てないで。

というわけで、とりあえず関数型プログラミングした結果は以下の通り。

// ES6なのでrequireじゃなくてimport。
import R from 'ramda';

// 元ネタから、データ構造をちょっと変更。
const lunchBoxes = [{'唐揚げ': {count: 10}, '玉子焼き': {count: 1}},
                    {'唐揚げ': {count:  8}},
                    {'唐揚げ': {count:  6}}];

// つまみ食い。
function eatWithFinger(foodName, lunchBoxes) {
  // foodNameの数量が最大の弁当のインデックスを取得します。
  const canEatIndex = R.apply(R.reduce(R.maxBy(([lunchBox, i]) => lunchBox[foodName].count)),
                              R.juxt([R.head, R.tail])(R.addIndex(R.map)(R.constructor(Array), lunchBoxes)))[1];

  // canEatIndexの弁当のfoodNameの数量を1減らした、新しいリストを返します。
  return R.assocPath([canEatIndex, foodName, 'count'], lunchBoxes[canEatIndex][foodName].count - 1, lunchBoxes);
}

export default function main() {
  // 唐揚げを5回つまみ食い。
  const eatKaraageWithFingerFiveTimes = R.apply(R.pipe)(R.repeat(R.curry(eatWithFinger)('唐揚げ'), 5));

  console.log(eatKaraageWithFingerFiveTimes(lunchBoxes));
}

// [10, 8, 6] → [9, 8, 6] → [8, 8, 6] → [7, 8, 6] → [7, 7, 6] → [6, 7, 6]となるので、
// 各弁当の唐揚げの数は、6個、7個、6個になります。

以下、Ramdaの使い方の解説です。Ramdaをご存知の方は飛ばしてください。

R.applyは、f(a, b, c)という関数を[a, b, c]という引数で呼び出せるように変換する関数です。で、Ramdaが提供する関数はすべてカリー化されていて、だからR.apply(f)とやると[a, b, c]を引数にとる関数が返ってきます(R.apply(f(a))なら、[b, c]が引数になる)。で、上のコードでR.reduceR.applyしているのは、R.reduceの引数はR.reduce(f, initialValue, xs)となっていて、初期値(initialValue)を必ず指定しなければならないためです(JavaScriptのArrayreduceでは、初期値を指定しない場合は自動でArrayの最初の要素が初期値になるのに……)。

R.maxByは、R.maxBy(pred, x, y)とするとpred(x)pred(y)を比較して、大きな方(xy)を返す関数です。これも当然カリー化されているので、R.maxBy(pred)すると引数を2個とる関数が返されて、それはR.reduceの第一引数の関数にちょうどよいというわけ。なので、上のコードではR.reduce(R.maxBy())して最大の要素を求めています。

R.juxtは、複数の関数に同じ引数を渡すための処理です。R.juxt([foo, bar])(x)とすると[foo(x), bar(x)]が返ってきます。R.headは最初の要素、R.tailは二番目から最後までのリストを返すので、これでやっとR.reduceの引数が揃います。

で、そのR.juxtした関数に渡しているのは、R.addIndex(R.map)(R.constructor(Array))したlunchBoxesです。R.addIndexはインデックス対応バージョンの関数を返しますので、これで[[lunchBox[0], 0], [lunchBox[1], 1] ...]が手に入るというわけ。あ、R.constructorは、扱いが面倒なコンストラクタを関数化する関数です。あと、[lunchBox, index]のうち、canEatIndexとして表現したいのはindexの方なので、最後に[1]を追加しています。

あとは、R.assocPathで、lunchBoxesを修正した新しいlunchBoxesを返すだけ。この関数をR.curryでカリー化して、R.repeatで5個並べて、R.pipeで順に呼び出すようにしています(R.pipe(a, b, c)(x)とするとc(b(a(x)))になります。aしてbしてcするのを順序どおりに表現できるので、私はこの関数が大好きです)。

これで、繰り返し回数をつまみ食いする関数から分離できました(R.repeatの引数に移動しただけという気もしますが……)。元ページみたいにつまみ食いした途中のlunchBoxesを取得したい場合は、R.tapするとか(デバッグのときに便利です)、R.pipeじゃなくてR.reduceするとかで大丈夫かと。

以上、Ramdaの解説終わり。ふう、これで関数型プログラミングのコードができあがりました。

念のため、手続き型で書いてみた

でも、関数型プログラミングのコードだけあっても、良いか悪いか判断できませんよね? 異なる手法でプログラミングしたコードと比較しないと。というわけで、手続き型で同じ処理を書いてみました。

const lunchBoxes = [{'唐揚げ': {count: 10}, '玉子焼き': {count: 1}},
                    {'唐揚げ': {count:  8}},
                    {'唐揚げ': {count:  6}}];

function eatWithFinger(foodName) {
  let canEatIndex = 0;
  for (let i = 1; i < lunchBoxes.length; ++i) {
    canEatIndex = lunchBoxes[i][foodName].count > lunchBoxes[canEatIndex][foodName].count ? i : canEatIndex;
  }

  lunchBoxes[canEatIndex][foodName].count--;
}

export default function main() {
  for (let i = 0; i < 5; ++i) {
    eatWithFinger('唐揚げ');
  }

  console.log(lunchBoxes);
}

……あれ? さっきの関数型より、この手続き型の方が簡単で分かりやすい?

もう一度、関数型で書いてみた

……冷静になれ、私。

最初のコードをよく見てみると、Ramdaがxだからyしているという部分があって、その結果としてコードが複雑になっています。たとえばR.reduceの引数に初期値が必要とかね。でもこれはRamda的にはしょうがなくて、Ramdaはカリー化を前提にしているので引数の数によるオーバーローディングができないんですよ。だから、R.reduceでは必ず初期値を指定するしかない。

でもね、私が今書いているのはJavaScriptで、JavaScriptでは引数の数によるオーバーローディングは当たり前の技術で、Arrayreduceは初期値の省略が可能になっています。だから、サクっとreduceの別バージョンを作っちゃいました(Ramdaの公式サイトのサンプルでも、その場で関数をじゃかじゃか追加しているしね)。内容が単純なので、ラムダ式で書きます。

const reduce = (pred, xs) => R.apply(R.reduce(pred), R.juxt([R.head, R.tail])(xs));

あとあれだ、最大の要素を探すときにR.maxByR.reduceを組み合わせるのも面倒くさかった。なので、別バージョンを。

const maxBy = (pred, xs) => reduce(R.maxBy(pred), xs);
// カリー化を活用してconst maxBy = reduce(R.maxBy(pred))の方が短いけど、引数が消えるとわかりづらくなりそうだったので……。

インデックス化もね。

const indexed = (xs) => R.addIndex(R.map)(R.constructor(Array), xs);

あれ、今作ったmaxByindexedを組み合わせると、最大の要素のインデックスを取得する関数も作れちゃう?

const maxIndexBy = (pred, xs) => maxBy(R.apply(pred), indexed(xs))[1];
// JavaScriptは引数の数が足りない場合は単純に無視するので、R.apply(pred)するだけで大丈夫。

ついでに、assocPathの値ではなくて関数を取るバージョンを、adjustPathとして定義しちゃいましょう。

const adjustPath = (path, func, xs) => R.assocPath(path, func(R.path(path, xs)), xs);

関数型プログラミングは関数という小さな単位が基本要素なので、組み合わせの際に小回りが利いて実に便利ですな。サクサク新しい関数を作れちゃう。今回作成した関数群は唐揚げ弁当問題に特化していない、汎用的なものなので再利用できそうですしね。

というわけで、これらの関数群をutility.mjsとしてまとめた上で、もう一度関数型プログラミングしてみましょう。

import R                        from 'ramda';
import {adjustPath, maxIndexBy} from './utility';

const lunchBoxes = [{'唐揚げ': {count: 10}, '玉子焼き': {count: 1}},
                    {'唐揚げ': {count:  8}},
                    {'唐揚げ': {count:  6}}];

const canEatIndex = (foodName, lunchBoxes) => maxIndexBy(R.path([foodName, 'count']), lunchBoxes);
const eatWithFinger = (foodName, lunchBoxes) => adjustPath([canEatIndex(foodName, lunchBoxes), foodName, 'count'],
                                                           R.dec,
                                                           lunchBoxes);

export default function main() {
  const eatKaraageWithFingerFiveTimes = R.apply(R.pipe)(R.repeat(R.curry(eatWithFinger)('唐揚げ'), 5));

  console.log(eatKaraageWithFingerFiveTimes(lunchBoxes));
}

おお、ちょー短い。canEatIndexは数量が最も大きい食材のインデックスを返すこと、eatWithFingerはその食材の数量をデクリメントすることが、コードからすぐに分かります(adjustPathという関数が何するのかは、R.adjustから推測できるはず……ということにしてください)。

で、上のコードで注目して頂きたい点が、もう一つあります。

今回書き直した際にcanEatIndexを別の関数に分割したわけですけど、手続き型の場合は、このように関数を分割するのはかなり勇気がいります。だって、別にした関数の中でlunchBoxesを変更していないことを確認するには、その関数の中身を確認しなければならないわけですからね。今回みたいに単純なプログラムならいいですけど、でも、大きなプログラムだったり、複数人で開発していたりしたら? 関数型プログラミングは副作用を嫌いますから、関数型プログラミングしていればそんな心配は不要となります(ちなみにeatWithFingerにも副作用はありません。新しいリストを返しますから)。うん、私は関数型プログラミングな人で良かったなぁと。

参考

関数型プログラミングはまず考え方から理解しよう ←元ネタ。
唐揚げつまんでみた ←Haskellやっぱりすげー。Haskell的な書き方をJavaScriptでもやっていてすげー。
PHPでも唐揚げつまんでみた ←PHPでHaskellライクにやってます。すげー。
Ramda ←とても便利。おすすめ!
functional-programming-with-es6-and-ramda ←本稿で作成したコードです。

7
6
1

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
7
6