
本記事は、Yaphi Berhanu、Vildan Softic、Jani Hartikainen、Dan Princeが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
JavaScriptがおもしろいのは、関数型言語としての側面です。JavaScriptの世界では関数は当初から第一級オブジェクトとして扱われてきました。それにより、さまざまな方法でエレガントで表現に富むコードを書くことができます。
しかし、単に関数型プログラミングの能力を持っているからといって、自動的に関数型プログラミングになるわけではありません。Ramda.jsは、JavaScriptを使って関数型プログラミングを始めるサポートをしてくれる人気のライブラリー です(GitHubで4000のスターを獲得しています)。
はじめに
Ramda.jsを最大限に有効活用するため、最初に小さなNode.jsプロジェクトを構築してその特徴に触れてみます。Node Package Manager (npm)で簡単にインストールできます。
npm install ramda
通常、ライブラリーの関数をネームスペースRにインポートします。この方法だと、Ramdaのメソッドへのすべてのコールは、Rプレフィックスを持つことになります。
var R = require('ramda');
もちろんフロントエンドのコードでRamda.jsを使用できない理由はありません。ブラウザーで最低限必要なのは、ライブラリーのコピーへの適切なパスを含めることだけです。以下のHTMLスニペットのようにシンプルです。
<script src="ramda.min.js"></script>
Ramada.jsはDOMやNode.jsの特定の機能は使いません。Ramada.jsはあくまでも言語のライブラリー/延長であり、JavaScriptのランタイム(ECMAScript 5で標準化されている)の構造やアルゴリズムに基づいて構築するものです。
さて、本題に入る準備はできました。これから実際の機能を説明していきます。
コンセプト
関数型プログラムでもっとも重要なコンセプトは、pure関数です。pure関数は、べき等(訳注:ある操作を何回しても常に同じ結果が得られること)であり、状態を変更することはありません。これは数学的には理解できることで、外部の状態に依存しないsin(x)などの関数として自然だと思えます。
pure関数に加え、引数が単独の関数が欲しいところです。引数が単独の関数はもっとも原始的なものです。引数がゼロの関数は、通常外部の状態が変更されており、それゆえ純粋ではないことを意味します。しかし、JavaScriptのような言語では通常、引数が単独の関数があります。
カリー化
高階関数(関数を引数や戻り値とする関数)が持つ能力はクロージャー(ローカル変数を捕らえる)と組み合わせ、カリー化という優れた解決法を生み出します。カリー化とは、複数の(たとえば)引数を持つ関数が、1つの引数を持つ関数に変換し返すプロセスのことです。必要とされるすべての引数が収集されるまで続きます。
引数がstringであるかをテストするラッパーを書くために、Ramda.jsのヘルパーisを使いたいとします。次のようになります。
function isString (test) {
return R.is(String, test);
}
var result = isString('foo'); //=> true
同様のことがカリー化でずっと簡単にできます。R.isは、Ramada.jsの一部であるため、関数が取るより少ない引数を与えると、ライブラリーが自動的にカリー化された関数を返します。
var isString = R.is(String);
var result = isString('foo'); //=> true
これは、とても意味のあることです。1つの引数を持つR.isを使用したので、1つの関数を受け取りました。そして、2回目のコールで結果を得ます(オリジナル関数のコールには、2つの引数が必要なのを思い出してください)。
しかし、もし最初の段階でRamda.jsヘルパーを使っていなかったらどうなるのでしょうか。仮に、コードに次の関数が定義されているとします。
var quadratic = (a, b, c, x) => x * x * a + x * b + c;
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> TypeError: quadratic(..) is not a function
このコードは、完全な第2次多項式です。すべての可能な値を許可する4つのパラメーターがあります。しかし通常、唯一のパラメートの固定セットa、b、cのためにxを変更したいと思うはずです。Ramda.jsで変換すると以下のようになります。
var quadratic = R.curry((a, b, c, x) => x * x * a + x * b + c);
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> 4
ここでも特定のサブセットを簡単に使えるエイリアスに引数の評価を渡します。たとえば、x - 1の方程式は、次にのようになります。
var xOffset = quadratic(0, 1, -1);
xOffset(0); //=> -1
xOffset(1); //=> 0
引数の数が、関数のパラメーターによって与えられない場合、curryNを使用し引数の数を明確にする必要があります。
カリー化はRamda.jsの核となるものですが、しかし、それだけではライブラリーは面白味にかけてしまいます。関数型プログラムで重要なもう1つのコンセプトは、不変性なのです。
不変の構造
状態が変更されてしまうのを防ぐもっとも簡単な方法は、変更できないデータ構造だけで動作するようにすることです。シンプルなオブジェクトには、次のようなことは許されないように読み取り専用のアクセサが必要です。
var position = {
x: 5,
y: 9
};
position.x = 10; // works!
読み取り専用のプロパティを宣言する以外に、プロパティをgetter関数に変更できます。
var position = (function (x, y) {
return {
getX: () => { return x; },
getY: () => { return y; }
};
})(5, 9);
position.getX() = 10; // does not work!
ここまでで、すでに少し良くなりましたが、オブジェクトはまだ変更の余地があります。getX関数のカスタム定義を加えられるということです。
position.getX = function () {
return 10;
};
不変性を実現するもっとも良い方法は、Object.freezeを使用することです。constのキーワードと一緒に、変更できない不変変数を導入できます。
const position = Object.freeze({ x: 5, y: 9 });
ほかの例としては、リストを伴うことです。イミュターブルなリストに要素を追加すると、その後、最後に追加された新要素と元のリストのコピーを作成する必要があります。もちろんできるだけ最適化するため、元のオブジェクトの不変性についての知識を活用できます。この方法では、単純に参照してコピーを置き換えられます。本質的には、連結リストのようになります。標準のJavaScriptの配列は可変なので、正確性を担保するためコピーを取っておく必要があることに注意してください。
たとえばappend()のようなメソッドはJavaScriptの配列で働き、その配列を返します。その処理は、べき等です。同様の引数で関数を複数回コールした場合、常に同じ結果になります。
R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']
R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']
R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']
指定されたエントリーがなくても、与えられた配列を返すremoveメソッドがあります。次のように動作します。
R.remove('write', 'tests', ['write', 'more', 'tests']); //=> ['more']
フレキシブルな引数を持つので、カリー化を適用するために前述したcurryN関数が必要です。一般的なヘルパーのセットもあります。
ユーティリティメソッド
すべてのヘルパー関数のもっとも重要なコンセプトは、引数をカリー化しやすいようにすることです。引数を変えなければならない頻度が高いほど、ほかの引数の前に位置付けられる可能性が低くなります。
sum()関数とrange()関数
たとえば、sum関数やrange関数のように常時監視が必要な関数は、もちろんRamda.jsで見つけられます。
R.sum(R.range(1, 5)); //=> 10
range()ヘルパーには、カリー化を利用してラッパーを作れます。
var from10ToExclusive = R.range(10);
from10ToExclusive(15); //=> [10, 11, 12, 13, 14]
これを固定の(排他的な)最大値でラップしたい場合はどうでしょうか。Ramda.jsはR.__で表示される特別なパラメーターを使って助けます。
var to14FromInclusive = R.range(R.__, 15);
to14FromInclusive(10); //=> [10, 11, 12, 13, 14]
map()関数
さらに、Ramda.jsはJavaScriptの主要な関数に対して、たとえばArray.prototype.mapなど、より良い解決策を持ったほかのオプションの提供を図ります。オプションには、異なる引数のオーダーと独創的なカリー化が付随します。
map関数は次のようになります。
R.map(x => 2 * x, [1, 2, 3]); //=> [2, 4, 6]
prop()関数
そのほかに役に立つ関数はprop関数です。prop関数は指定されたプロパティの値を取得しよう図ります。指定されたプロパティが存在しない場合、undefinedが返されます。その値が本当に定義されていない場合(undefined)は曖昧なことかもしれませんが、実際はほとんど気にしません。
R.prop('x', { x: 100 }); //=> 100
R.prop('x', { y: 50 }); //=> undefined
zipWith()関数
もし先に説明したRamda.jsのメソッドが有用だと感じなかった場合、次に紹介する関数のほうが興味深いかもしれません。今回は具体的な事例ではなく任意に選んだシナリオを例にします。
2つのリストがあり、組み合わせたいとします。zip関数を利用すれば簡単にできます。しかし、得られる結果(それ自体が配列に2つの値があるとされる配列の要素)は欲しいものではないかもしれません。このようなとき、zipWith関数の出番です。任意関数を使って値を単一にします。
var letters = ["A", "B", "C", "D", "E"];
var numbers = [1, 2, 3];
var zipper = R.zipWith((x, y) => x + y);
zipper(letters, numbers); // ["A1", "B2", "C3"]
同様に、ベクトルにドット積を導入できます。
var dot = R.pipe(R.zipWith((x, y) => x * y), R.sum);
dot([1, 2, 3], [1, 2, 3]) // 14
掛け算([1, 4, 9]を算出する)で2つの配列をジップし、結果をsum関数につなぎます。
enumerableでの作業は、とにかく大きなテーマです。Ramda.jsが作業に役立つ多くの機能をもたらしているのは驚きではありません。すでに、各要素に関数を適用するためR.mapを導入しました。同様に、要素の数を減らすヘルパーもあります。最も一般的な別の配列を得たfilter関数かreduce関数のいずれかかを経由して、単一の値に設定します。
chain()関数
配列を動かすことは、効果的かつ補助的な関数が付随します。たとえばchain関数を使うと、簡単に配列を結合できます。インプットとして数を使用し、アウトプットとして主要な因子で配列を与えるprimeFactorization関数があります。次の数のセットで関数を適用した結果を結合できます。
R.chain(primeFactorization, [4, 7, 21]); //=> [2, 2, 7, 3, 7]
実用例
ここで大きな質問です。Ramda.jsによって導入されたコンセプトの日常的な仕事でのメリットはなんでしょうか。次の(すでに見栄えがかなり良いですが)コードスニペットがあるとします。
fetchFromServer()
.then(JSON.parse)
.then(function (data){ return data.posts })
.then(function (posts){
return posts.map(function (post){ return post.title })
});
Ramda.jsがこれをより解読しやすくするにはどうすればよいのでしょうか。最初の列はすでにベストな状態です。2番目の列は雑然としてしまっています。供給された引数からpostsプロパティを抽出したいと考えているスニペットです。
最後の3列目は乱雑です。引数によって供給される、すべてのポストを繰り返します。いま一度言いますが、目的は特定のプロパティを抽出することです。次の解決策はどうでしょうか。
fetchFromServer()
.then(JSON.parse)
.then(R.prop('posts'))
.then(R.map(R.prop('title')));
コードを読みやすくする最適な解決策です。Ramda.jsによる関数プログラムのおかげです。しかし、ECMAScript 6で導入されたシンタックス「fat arrow」が簡潔で読みやすいコードを導くことも覚えておきましょう。
fetchFromServer()
.then(JSON.parse)
.then(json => json.posts)
.then(posts => posts.map(p => p.title));
上のコードは、Ramda.jsの知識がなくてもほとんど読めるはずです。さらにパフォーマンスや保守性の目的にしか有益でない抽象的概念を減らしています。
lens関数
最後に、便利なオブジェクトヘルパーについて触れておきます。オブジェクトヘルパーのlens関数は覚えておいて損はありません。
lens関数は、オブジェクトや配列とともに特定のRamda.js関数に送られる特別なオブジェクトです。lens関数は、それぞれのオブジェクトまたは配列の特定のプロパティまたはインデックスからデータを取得したり変換することができます。
記事の冒頭で挙げた不変性の例のように、2つのキーxとyを持つオブジェクトがあるとします。getter関数やsetter関数のメソッドでオブジェクトをラッピングするのではなく、対象のプロパティに「焦点をあてる」レンズのような関数を作れます。
オブジェクトのプロパティxにアクセスするlens関数を作成するには、次のことができます。
var x = R.lens(R.prop('x'), R.assoc('x'));
prop関数が標準的なgetter関数(これはすでに紹介されています)なので、assocはsetter関数(3つの値のシンタックス:key, value, object)です。
Ramda.jsからの関数を使って、lens関数によって定義されたプロパティにアクセスできます。
var xCoordinate = R.view(x, position);
var newPosition = R.set(x, 7, position);
動作は、与えられたpositionオブジェクトをそのまま(それをフリーズしたかどうかは無関係)にしておくと覚えておいてください。
set関数は特殊なover関数であるだけなことに注意してください(2つは似ていますがset関数は任意の値の代わりに関数をとります)。set関数は、値を変換するのに使われます。たとえば、次のコールはx座標を3で掛けます。
var newPosition = R.over(x, R.multiply(3), position);
Ramda.js、lodash、あるいは…?
ここで出てくる疑問は、Ramda.jsを選ぶ理由は何だろうか? ということです。なぜlodashやほかのライブラリーを使用すべきではないのでしょうか。もちろん、Ramda.jsがより新しいからより良いと思うかもしれませんが、Ramda.jsが関数の原則を念頭に作られたものだという事実を知れば分かります(JavaScriptライブラリーにとって新しい引数の配置や選択方法です)。
たとえば、Ramda.jsのイテレータのリストは、アイテムをリストではなくデフォルトで渡す一方、そのほかのライブラリー(lodashのように)の基準は、アイテムとインデックスをコールバック関数に渡します。ささいな問題に思えるかもしれませんが、parseInt()(選択的な2番目の引数を取るもの)のような役に立つビルトイン関数の使用を妨げてしまいます。その反面、Ramda.jsは適切に動作します。
結局、何を選ぶべきかという判断は、特定の要求やチームの経験、知識によりますが、Ramda.jsは注目に値するものです。
参考文献
- Higher-order functions(高階関数)
- Why Curry Helps(なぜカリーが役立つのか)
- Immutability(不変性)
- Why Ramda?(なぜRamda?)
- Ramda documentation(Ramdaドキュメント)
- Functional Programming with Ramda.js(Ramda.jsの関数プログラミング)
最後に
関数型プログラミングを、難題を解決してくれる得策として考えてはいけません。その代わり、構成のしやすさや柔軟性、障害耐性/堅牢さを得られるツールボックスとして捉えられるべきです。モダンなJavaScriptライブラリーはすでにこれらの利点を利用する関数のコンセプトを備えています。Ramda.jsは関数型を活用する人のレパートリーを拡大する強力なツールです。
(原文:Hands-on Functional Programming with Ramda.js)
[翻訳:こう7]
[編集:Livit]
