前置き
関数型プログラミングでDIする為のDIコンテナをJavaScriptで実験的に作ってみました、という記事です。
恐らく同様のオープンソースがすでに存在しているのだろうと思いましたが、軽く探しても見つからなかった(追記:やっぱりありました!末尾に追記してます)ので実験的にDIコンテナを作成しながら考察していきます。
私自身はそこまで関数型プログラミングの理論に詳しいわけではないので、FPニキからの補足やツッコミをお待ちしております。
考察
DIとDIコンテナは、オブジェクト指向プログラミングにおいてとても便利な道具であり、パラダイムであると思います。
DIコンテナを使うためにオブジェクト指向を使うといってもいいぐらいじゃなかろうかと思うくらいですが、これ、関数型プログラミングだとどうなるんだろうと思いました。
例えば次のような、リソースの取得用関数ライブラリがあるとします。
// 現在の言語を取得
function getLanguage() {
// 実際には設定ファイルやブラウザの設定から読み込まれる
return 'ja'; // 日本
}
// リソースの取得
function getResource(language) {
return {
'en': {
'Hello': 'Hello',
},
'ja': {
'Hello': 'こんにちは',
},
'fr': {
'Hello': 'Bonjour',
},
}[language];
}
// リソース文字列の取得
function getResourceString(messageId) {
const language = getLanguage();
const resource = getResource(language);
return resource[messageId];
}
ここで、リソース文字列の取得用関数getResourceString(messageId)
に注目しましょう。
// リソース文字列の取得
function getResourceString(messageId) {
const language = getLanguage();
const resource = getResource(language);
return resource[messageId];
}
この関数は、内部でgetLanguage()
とgetResource(language)
に依存しています。
これらの依存を外部から注入できるようにするには、これらを関数の引数にすれば良いでしょう。
// リソース文字列の取得
function getResourceString(getLanguage, getResource, messageId) {
const language = getLanguage();
const resource = getResource(language);
return resource[messageId];
}
いやいや、なんか変です。関数の中身がなんにも変わっていません。
そもそも、getResourceString
に絶対必要なのはresource
だけです。次のようになるのが自然でしょう。
// リソース文字列の取得
function getResourceString(resource, messageId) {
return resource[messageId];
}
すっきりしました。
しかし、これは単に、getResourceString
を使う側にresource
の取得責任を丸投げしているだけです。
呼び出し側は以下のように煩雑になってしまいます。
const language = getLanguage();
const resource = getResource(language);
const hello = getResourceString(resource, 'Hello');
例えば、Hello
というリソース文字列を内部で使用する関数sayHello
を定義してみましょう。
sayHello
は、getResourceString
を必要とします。なので、そのためにresource
も必要とします
これまでと同様に、依存性を全て引数から受け取ります。
function sayHello(resource, getResourceString, name) {
const hello = getResourceString(resource, 'Hello');
return `${hello}, ${name}!`;
}
なんでしょうか。ちっとも世界が良くなっていきません。それもそのはず、「関数の依存性を引数に出したところで、それを呼び出す側はなんにもうれしくない」からです。
これがオブジェクト指向のDIであれば、コンストラクタ引数はDIコンテナによって解決され、生成済のオブジェクトを入手することができます。そのオブジェクトの利用側は、コンストラクタ引数がどれだけ増えようと問題ありません。それらはDIコンテナが全て解決してくれるからです。
ですから、関数型の世界であっても、こんな風にDIコンテナから関数を取り出すと依存性が解決された状態になっていて欲しいわけです。
// FDIはDIコンテナ
const sayHello = FDI.getRequiredFunction('sayHello');
const hello = sayHello('jun1s'); // 依存性のある引数は既に解決された状態になっている
console.log(hello); // こんにちは, jun1s
こんな風になっているためにはどうしたらいいのでしょうか。
つまり:
sayHello(resource, getResourceString, name)
上記を、以下のように呼び出せるようにした上で、内部的にはresource
とgetResourceString
が解決された状態にしたいわけです。
sayHello(name)
しかし、オリジナルのsayHello自身は元の形のまま残しておき、DIコンテナには依存させたくないのです。
ここまで考えて、ふとひらめきました。
このような、「引数の一部だけを解決して残りの引数を取る関数に変換する」方法が関数型プログラミングに存在します。それが「カリー化」です。
// 元の関数
const original_func = (a, b, c) => a + b + c;
// a, b を引数にとって、残りの c だけを引数に取る関数を返す
const curry_func = (a, b) => (c) => original_func(a, b, c);
// curry_funcを使って a, b を部分適用し、残った c のみを取る関数を作る
const new_func = curry_func(1, 2);
const result = new_func(3); // 1 + 2 + 3 = 6 が返る
このカリー化を使って、依存性部分だけを部分適用し、残った引数だけを持つ関数を返す「関数型DIコンテナ」を作れば、万事解決するのではないでしょうか。
具体的には次のようなイメージでDIコンテナに格納していきます。
const functions = [];
// カリー化して登録: 引数を依存性のある部分とそうでない部分に分ける
functions['sayHello'] = (resource, getResourceString) => (messageId) => sayHello(resource, getResourceString, messageId);
それを取り出す際には、次のように、依存している引数のみを部分適用して返します。
function getRequiredFunction(functionName) {
// カリー化された関数をDIコンテナから取り出す
// (dep1, dep2, ... ) => (param1, param2, ...) => original_func(dep1, dep2, ..., param1, param2, ...);
const func = functions[functionName];
// ["dep1", "dep2", ... ] を取得
const depArgNames = なんらかの方法でfuncの引数名リストを取得;
// 各引数をDIコンテナから引数名で参照して再帰的に取得
const args = depArgNames.map(arg => getRequiredFunction(arg));
// カリー化された関数に依存性引数を部分適用して返す
return func(...args);
}
これで、全ての引数がDIコンテナによって解決された関数を取得できるようになりました!
しかし、そのためには、sayHello
が依存しているresource
やgetResourceString
もDIコンテナに登録する必要がありますね。やってしまいましょう!
functions['getLanguage'] = () => () => getLanguage());
functions['getResource'] = (getLanguage) => () => getResource(getLanguage()));
functions['getResourceString'] = (getResource) => (messageId) => getResourceString(getResource(), messageId));
上記のDIコンテナへの定義を見ていて気付くことがあります。getResource
への引数はもう必要ありません。language
はDIコンテナが解決してくれます。
そしてそうなると、getResourceString
に渡すresource
もDIコンテナに解決してもらえます。
そして、今やsayHello
にはresource
を渡す必要もなくなったのです。
function sayHello(getResourceString, name) {
const hello = getResourceString('Hello');
return `${hello}, ${name}!`;
}
DIコンテナへの登録は以下のようになるでしょう。
functions['sayHello'] = (getResourceString) => (messageId) => sayHello(getResourceString, messageId);
なんて素敵!
というわけで、なんだかうまく動いたので、Functional Dependency Injectionという名前のライブラリにして、次のように使えるようにしてみました。
https://github.com/jun1sfukui/functional-dependency-injection
FDI.addFunction('getLanguage', () => () => getLanguage());
FDI.addFunction('getResource', (getLanguage) => () => getResource(getLanguage()));
const getResource = FDI.getRequiredFunction('getResource');
もちろん、getRequiredFunction
を使ったDIコンテナからの直接の取得はプログラムの根本のところで呼び出す関数のみで、その内部で使う関数については各関数の引数を自動的に取得して部分適用し続けてくれます。この記事を読めばわかりますよね。
関数の引数リストを取得する部分は関数を文字列化して無理やり取得しているので、もっとどうにかならないものかとも思ったのですが、引数リストを別途文字列で与えるのもなんだかなーという感じなので、現状はこのようになっています(いちおう、引数リストは事前にキャッシュするようにはしてあります)。
実際にプロジェクトで運用しようとするといろいろと問題が起こる気もします(そもそも毎回部分適用で関数生成しているから結構重いのではという気がします)が、とりあえず実験的にやってみたら少ないコードの割には思ったよりうまく動きました、というご報告にて。
こういう機能を言語やコンパイラ側で用意してくれて、効率的に実行してくれると嬉しいですね。
追記
カリー化と部分適用という同じアイデアで、既に上等なものが存在していました…!
https://github.com/frouriojs/velona
こちらの記事が詳しいです。
https://qiita.com/m_mitsuhide/items/34474fcf5402e8606297
なるほど、実行時に依存性を解決するのではなく、関数定義の時点で解決してしまうわけですか・・・!
実行時のオーバーヘッドを考えるとこれが最適なのかもしれません。
依存性リストを {dep1, dep2, ...} のオブジェクト形式で渡すのはスマートですよね。これだと文字列として依存対象の名前を取得できます。Function.toString() からの文字列解析で依存性を取得する方法に限界を感じたら、こっちの方法に切り替えてもいい気がしました。
ただvelonaの方法だと、一部の関数をテスト用に差し替えるとかができないのかな? どうなんでしょうか。
あと、当記事のように「関数そのものではなく関数の呼び出し結果のみを注入したい場合」にやりにくいように見受けましたが、これもざっとみただけではわかりませんでした。
でも、関数型プログラミングするなら、そもそも結果のみを注入とか中途半端なこと言ってないで全部関数にしろよってことなのかも。
この辺は関数型プログラミングにそこまで精通していないため、感覚が分かりません。
依存を値で受け取れる方が、大元の関数はシンプルになることは割と多いのではないかなぁとは思っています。
とりあえず、この考え方自体は関数型プログラミングの世界では昔から存在しているようで、むしろ安心しました。