JavaScript で触れる関数型プログラミング

プレイドのエンジニア兼ハンターの @algas です。
Ramda.js の関数を例にして JavaScript による関数型プログラミングの考え方を紹介します。
本記事では Ramda.js 自体を紹介するのではなく、より深い関数型プログラミングの考え方を学ぶきっかけを作れることを意図しています。

想定読者

  • Node.js でアプリケーションを実装したことがある
  • 関数型プログラミングに興味がある

JavaScript でなぜ関数型プログラミングが必要なのか

JavaScript のモジュールの多くは Mutable で副作用の影響を受けやすい実装になっています。
副作用の影響を受けやすければ、そのモジュールを複数の場所で利用するのが難しくなります。
さらに関数などのインターフェースが適切でなかったり、意図したとおりに機能しなければ再利用するのが難しく冗長なコードが増えてしまうことになります。
冗長なコードは可読性が低く理解するのにも時間がかかります。
それによって不具合を混入しやすくなったり、不具合を解消するのに不要な時間がかかってしまいます。
関数型プログラミングを導入することで、コードを短く保ち素早く実装して不具合を混入しにくくなることが期待できます。
これはもちろん手法の一つであり、すべての問題を解決する万能の魔法ではありません。

Ramda.js と関数型プログラミング

Ramda.js は JavaScript の実用的な関数のライブラリです。
ramdaFilled_200x235
以下のような特徴を持っています。

  1. 純粋関数型
    不変で副作用の影響を受けにくいような設計になっています。
  2. 自動カリー化
    関数の再利用がしやすくなります。
  3. 引数の最適化
    基本的には最後の引数にデータが来るようになっています。

Ramda.js は便利な関数を数多く揃えていて、このライブラリを使ってアプリケーションを実装するだけでも非常に有益です。

Ramda.js を使うとどのように便利なのかについては、Thinking in Ramda のブログに良くまとめられています。

map の定義

Ramda.js の map 関数の定義を見てみましょう。

map :: Functor f => (a -> b) -> f a -> f b

上記は JavaScript の文法ではありません。
特に Haskell に馴染みがない読者向けに上記の定義を詳しく説明します。
ちなみに Haskell では Ramda.js の map が fmap という関数に該当します。

'=>' の左側は関数の型制約です。
Functor f とは f は Functor であることを示しています。
Functor 自体が何かについては後ほど詳しく説明します。

-> は関数型コンストラクタです。
a -> b とは型 a の値を引き数に1つ取り、型 b の値を返す関数であることを示しています。
例えば a -> b -> c は第一引数に型 a の値を、第二引数に型 b の値を取り、型 c の値を返す関数です。
a, b のように小文字で書かれた型に特に制約がなければ任意の型であることを表しています。

小文字がスペース区切りで連続している場合には、関数を適用したことを表しています。
つまり f a は関数 f を a に適用した値です。

上記を踏まえてもう一度 map の定義を見直してみます。
map は第一引数に a -> b という値(すなわち関数)、第二引数に f a という値を取り、f b という値を返します。

詳しくは Fantasy Land Specification をご参照ください。

logo

Functor とは何か?

Fantasy Land Specification に記載されている Functor の定義を見てみましょう。

  1. Identity
    map(a => a, u) == u
    a => a という「何もしない」関数を map に適用しても、適用する前と同じです。
  2. Composition
    map(x => f(g(x)), u) == map(f, map(g, u))
    これは x => f(g(x)) という x に関数 g と関数 f を map で適用した値は、
    関数 g を map した値にさらに関数 f を map した値と同じです。

Functor とは上記2つの性質 (Identity, Composition) を示すような型制約のことを指します。
上記の map は Functor の例のために仮においたもので、map 以外の Functor の性質を持つ関数も同様の性質を示すことが期待されます。
また map 関数の定義における f には任意の Functor を適用できます。

Functor 制約を持つ関数の実装例

Ramda Fantasy では Functor として Maybe, Either など8つの実装が紹介されています。
また Array, Object も Functor の性質を満たすことが分かっています。
これらはすべて Functor の性質を満たしているので、Ramda.js の map 関数を適用できます。

以下で map 関数を使った実装例を紹介します。

const R = require('ramda');
const S = require('sanctuary');

const mapFunc = R.map(R.compose(S.mult(2), S.add(1)));
const a = [1,2,3];
const z = S.Just(42);
const n = S.Nothing;
const w = S.Right(99);
const e = S.Left("error");

console.log(mapFunc(a));
console.log(mapFunc(z));
console.log(mapFunc(n));
console.log(mapFunc(w));
console.log(mapFunc(e));

mapFunc は Number を値に持つ Functor の中身に "1 を足して 2 倍する" 関数です。
上記で mapFunc という関数を異なる Functor に適用していることに注目してください。
ここでは List Number, Maybe Number, Either Number のそれぞれの値に全く同じ形式で mapFunc を適用しています。

出力結果は以下のとおりです。

[ 4, 6, 8 ]
Just (86)
Nothing
Right (200)
Left ("error")

各値に "1を足して2倍した" 結果が得られていますね。

そもそも Functor を使うと何がうれしいの?

Functor 自体は抽象的な存在であり、そのものが実践的な役割を持つわけではありません。
Functor である Maybe などが実際に便利さを示します。
したがって、ここでは Maybe を使うと何が便利になるかということを例に説明します。
Maybe aNothingJust a という2つの値コンストラクタを持ちます。

Maybe a = Nothing | Just a

Sanctuary.js の実装では S.NothingS.Just(value) という関数がコンストラクタです。
ここで a は Non-Nullable Value になります。null は Nothing に変換されます。
Maybe の世界には null は存在しないので、値が null であるかどうかをチェックする必要はありません。
実装例で示したように Functor (Maybe) であることを前提とした関数を適用した場合には、Nothing か Just かを気にすることなく関数を適用することができます。
Maybe を使う場合に null または Nothing を気にしなければならないのは、以下の2つの時だけです。

  1. 外界から Maybe の世界に値を変換する時
  2. Maybe の世界から外界に値を変換する時

つまり、Maybe を使うと多くのプログラマにとって煩わしい null チェックの回数を大幅に減らすことができます。

本記事では Functor を主な例として説明しましたが、Fantasy Land に記載されているように Functor よりもさらに複雑な条件を持つ Applicative や Monad などの概念も存在します。
これらを詳しくするには余白が足りないので他書にその役割を譲ることにします。

dependencies

上記は Fantasy Land Specification から引用した図です。

まとめ

本記事では Ramda.js, Fantasy-Land を中心に Functor を使った JavaScript による関数型プログラミングに触れる試みをしました。
現実的に現段階において本記事の概念に近づく実装をするためには以下を実践するほうがよいでしょう。

また本記事では説明できなかった項目やさらに発展的な内容を知るには Haskell などのプログラミング言語の仕様や実装を学ぶことをオススメします。

参考文献

最後に

CX(顧客体験)プラットフォーム「KARTE」を運営するプレイドでは、KARTEを使ってこんなアプリケーションが作りたい! KARTE自体の開発に興味がある!というエンジニア(インターンも!)を募集しています。
詳しくは弊社採用ページまたはWantedlyをご覧ください。 もしくはお気軽に、下記の「話を聞きに行きたい」ボタンを押してください!