超強力な関数型プログラミング用ライブラリ Ramda.js を学ぼう #1 - これから始める人のための導入編
wakamsha
Ramda.js とは
平たく言うと JavaScript 標準 API である map
や filter
といったコレクション操作系と Object.assign
のような値の不変性担保という二つのパラダイムを併せ持ったライブラリです。類似するライブラリに Underscore.js / Lodash や Immutable.js などがありますが、Underscore.js / Lodash はコレクション操作寄りであり、 Immutable.js は名前の通り不変性担保に重きを置いているという点で少々コンセプトが異なります。
非常に多くの API を備えており1)v0.25.0
の時点で 246! 、それらは全て不変性と副作用のない機能 ( Immutable ) として設計されています。また API 名も Haskell や Scala のそれにインスパイアされた命名となっているので、関数型プログラミングに長けたエンジニアと会話する時にも役立ったりします。
「JavaScript で ●● な実装したいんですが、いまいち綺麗に書けないんですよね。こういう時 Haskell や Scala だったらどうします?」
「Haskell だと ▲▲▲ という便利な機能がありましてですね……」
「ほう、なるほど? ( Ramda.js のドキュメント検索……、」
「お?同じ名前の API がある!これか!!」
なんて会話が出来たりするのです。
はじめての Ramda.js を書いてみよう
百聞は一見にしかず。実際にコードを書いてみてその魅力を体験してみましょう。
TypeScript のすゝめ
サンプルコードは全て TypeScript で記述します。Ramda.js は副作用の無いライブラリですが、入力 ( 引数 ) によっては出力 ( 戻り値 ) の構造を大きく変えた戻り値を返す性質を持っています。そのため少しでも入力と出力に関する情報を明確にするために型を指定できる TypeScript が適しているのです2)Haskell や Scala も型安全な言語ですよね。。
環境構築
まずは適当なディレクトリを作成して Node.js プロジェクトを構築しましょう。
# ディレクトリ作成
$ mkdir hello-ramda
# ディレクトリに移動
$ cd hello-ramda/
# Node.js プロジェクト作成
$ npm init -y
# ライブラリインストール
$ npm i --save ramda
# TypeScript、型定義ファイル、バンドルツールをインストール
$ npm i -D typescript @types/ramda webpack webpack-cli ts-loader
オブジェクトを操作してみる
以下のような簡単なオブジェクトデータを用意し、 Ramda.js の API を使って操作してみます。
type User = {
name: string;
email: string;
age: number;
address: {
zipcode: string;
};
};
const user: User = {
name: 'Bret',
email: 'bred@april.biz',
age: 22,
address: {
zipcode: '92998-3874',
},
};
prop - 値を取得
手始めに任意のプロパティの値を取得してみます。
import { prop } from 'ramda';
prop('name', user); //=> "Bred"
prop()
は、第二引数に渡したオブジェクトから第一引数に渡したプロパティ名の値を返します。指定したプロパティ名が存在しない場合は undefined
を返します。上記の例では、user
オブジェクトに対し name
というプロパティ名を指定して Bred
という値を取得しました。なんてことはありませんね。
assoc - 値を上書く
では次はより関数型に寄せた例をやってみます。任意のプロパティの値を上書きしてみましょう。
import { assoc } from 'ramda';
assoc<number, User, string>('age', 23, user);
{
"name": "Bred",
"email": "Sincere@april.biz",
"age": 23,
"address": {
"zipcode": "92998-3874"
},
}
assoc()
は、最終引数に渡したオブジェクトの指定プロパティを指定した値で上書きします。オブジェクト内に指定のプロパティが存在すればその値を上書きし、無ければ新しいプロパティとして追加した結果を返します。
これだけですとなんてことないように思えます。しかし戻り値のオブジェクトは入力をクローンして新規に生成されたもので、元の user
オブジェクト自体は変更されていないのがポイントです。つまり不変性の担保です。そして指定したプロパティ名が存在しなかった場合は、その key / value
を新たに追加した結果値を生成して返します。ということは、どのような構造が返ってくるのかを明確にするために型情報を明記することが重要になってくるというわけです。
如何でしょう?簡単にではありますが、Ramda.js のポテンシャルがお分かりいただけたのではないでしょうか?
ほぼ全ての API がカリー化を備えている
カリー化とは、期待される数より少ない引数で関数を呼び出した場合に、残りの引数を取るためにその呼び出された関数が別の関数を返すような関数にすることを指します。
先ほどの assoc
を例にします。assoc は『プロパティ名』『上書きする値』『対象となるオブジェクト』の三つを引数に取ることで最終結果値を戻り値として返します。
しかし、もし三つ目の『対象となるオブジェクト』を渡さなかったらどうなるでしょう?普通なら実行エラーが起きるものですが、Ramda.js の API はその殆どがカリー化対応されてるため、少ない引数を受け取るとその数に応じて自動的にカリー化した関数を戻り値として返します。
assoc<number, string>('age', 23); // 第三引数を渡していない
// カリー化された関数が生成される
function r(e){return 0===arguments.length||n(e)?r:t.apply(this,arguments)}
よってこのように記述することが出来ます。結果は引数三つを一度に渡したときと全く同じです。
assoc<number, string>('age', 23)(user);
// もちろんこのように書くことも可能
assoc<string>('age')(23)(user);
非常に強力な機能です。わざわざ自分でカリー化された関数を別途用意する必要はありません。全てライブラリがよしなに巻き取ってくれるというわけです。
あくまで JavaScript らしさはそのままに、ネイティブであるがの如く振る舞う
Ramda.js には先ほどご紹介した以外にも map
や filter
といった JavaScript 標準のと同名の API も備えてます。本来であればこれらは配列に対してしか使えませんが、Ramda.js のそれは配列だけでなくオブジェクトハッシュに対しても同様のことが可能となっており、その振る舞いは標準のそれをそのまま違和感なく拡張したものとなっています。あくまで JavaScript らしさをそのまま維持することで、導入コストを下げようとしてるのがうかがい知れます。
また、関数型プログラミングはその特性上、関数呼び出しの頻度が非常に高くなる傾向にあります。ネイティブにコンパイルする言語であればそこである程度の最適化が施されますが、その場で都度実行される JavaScript においては関数型のお作法は『富豪プログラミング』といえなくもないでしょう。Ramda.js もその辺りは理解しており、可能な限りパフォーマンスを意識した実装を目指しているようです。
締め
JavaScript は、その言語仕様に関数型ライクなエッセンスを一部含んではいるものの、あくまでプロトタイプベースであり3)ES2015 以降はオブジェクト指向の毛色が強まっていますが。、決して Haskell や Scala のような関数型言語ではありません。しかし Ramda.js のような強力なライブラリの力を借りることで、かなりそれに親しいものを手に入れることが出来ます。
今回は導入編ということで Ramda.js の簡単な概要とさわりだけをご紹介しました。次回からは Ramda.js の様々な API をデモとともにご紹介するなどして、本格的な JavaScript 式関数型プログラミングの世界に入っていきたいと思います。
参考リンク
なんだかんだで公式ドキュメントのお世話になるのが一番です。決して丁寧な解説とは言えませんが、必要最低限の情報は網羅されています。GitHub のスター数は 10,000 を超えているにも関わらず、実践的なノウハウは殆ど見当たりません。当記事が皆さまのお役に立てれば幸いです。