Blog

【Scala Days 2014】The Reader Monad for Dependency Injection を解説してみた


AMoAdの福原です。アドネットワークの開発/運用を担当しています。

今回は以前アドテクスタジオ内で行われた「ScalaDays2014 発表資料読み会」の内容を記事にしたいと思います。
僕の担当は @jarhartさんが発表した「The Reader Monad for Dependency Injection」でした。
基本的にはスライドの内容を追っていきますが、独自の解説を加えています。
# ていうか僕は英語のヒアリングがダメでして(汗)
英語が出来る人はVideoを見たほうが早いですね。

Slide: http://typesafe.com/blog/scala-days-presentation-roundup
Video: https://parleys.com/play/53a7d2d1e4b0543940d9e56f/chapter0/about

また、以降の解説に出てくるソースコードは実行可能な状態で以下に公開してあります。
https://github.com/fukuo33/ReaderMonad4DI
では、早速解説に入りましょう!

Dependency Injectionとは

Dependency Injection(以下DI)については、様々な記事で紹介されているのでこの記事では詳解はしません。
詳しくは原典である Martin Fowler先生の記事をご覧ください。

Inversion of Control Containers and the Dependency Injection pattern

ScalaでDIのサンプルを簡単に書くと以下のような感じになると思います。

findUsersを仮引数とすることで、依存性として関数呼び出し時に注入できます。

ScalaにおけるMonad

Scala標準APIではMonadを表すクラスは存在しません。
しかしながら、scala.collection.immutable.Listは Functor則Monad則 を満たしていることが分かります。

次にHaskellでは1引数関数をMonadとして扱いますが、Scalaではどうでしょうか?

残念ながら、Scala標準APIでは関数をMonadとして扱うことは出来ません。
しかし、Scalazライブラリには関数をMonadとして扱うことの出来る scalaz.Reader が用意されています。
※ Haskellでは、関数は1引数しか取らないため、以降の説明でも"関数"=="1引数関数"とします。

Reader Monad とは

次に簡単にReader Monad について説明します。

Reader Monadとは単に関数Monadの別名です。
その名の通り関数をMonadとして扱います。

上記のような関数があるとき
文脈は「Intの引数を適用すれば結果が帰ってくる」
値は「Stringの結果」
となるでしょう。

Scalazをimportすることによって関数を Monadとして扱えます。

このように、addTwoという関数があるとき、あたかもその結果に対し関数を適用することが出来ます。
andThenを使った関数合成とよく似ていますね。

Reader Monadのメリットはmapやfor式と合わせて使えることです。

この例では、( + 2)の計算結果であるa と、( * 3)の計算結果であるb を、 a + b します。
for式は「Intの引数を適用すれば結果が帰ってくる」という文脈に包んで戻すので、
res8: Int => Int のような関数が戻ることになります。
res8(7)を式展開すると、以下のようになるでしょう。

"7" という実引数を依存性として見立てると、DIできそうな予感がしてきますね。

Reader Monad を使った DI

では、Reader Monadを使ったDIの方法を見て行きましょう。

これは

と同じ意味です。
Readerの最初の型パラメータ(UserRepo)は引数の型、次の型パラメータ(User)は戻り値の型です。
getUserは関数を返す関数であることに注意してください。
よりHaskell寄りの表現をするなら、部分適用可能な2引数関数であるとも捉えられます。

実際の使用例としては、以下のようになります。

for式と組み合わせて後から依存性(MysqlUserRepo)を注入することが出来るようになりました。
ここまででReaderMonadを使ってDIができることが分かりました。
しかし、話はまだ続きます。

他のMonadをすでに使っていると、、、

もし、他のMonadを使っていた場合はどうなるでしょうか?

findAddress内のfor式は、Futureを扱っていることに注意してください。
だんだん複雑になってきましたね。。。
そこで登場するのが Monad Transformer (モナド変換子) です。

Monad Transformer とは

Monad Transformerについては語れるほど詳しくないので、サンプルから見て行きましょう。

res4のようにMonadが多段になっている時、値に対して関数を適用したい場合どうすればよいでしょうか?

うまくいきましたが、+100するという本来の目的が埋もれがちですね。
# 無理やりfor式で書いてますが ^^;

次にMonad Transformerを使ってみましょう。

一度のforで List, Option それぞれの文脈から値が取り出せました。
便利ですね!

ReaderTを使ったサンプルは、@eed3si9nさんの記事が詳しいです。

http://eed3si9n.com/learning-scalaz/ja/Composing-monadic-functions.html
http://eed3si9n.com/learning-scalaz/ja/Monad-transformers.html

スライド p74 では Kleisli を使って、モナディック関数( A => M[B] みたいな関数)をReaderTを作成する方法。
Reader[A, M[B]]をReaderT[M, A, B]にするliftが紹介されています。

78P 以降はPlay Framework寄りの話になるので割愛します。
mockを使ったテストコードのサンプルは以下に記載したので、参考になれば幸いです。
OtherDependencies3Spec

所感

さて、Reader monad を使ったDIがひと通り分かりました。
が、ここまで書いておいてなんですが、実際にReaderMonad4DIを使って開発するには以下の様な問題があると思います。

  • 依存性を注入するメソッドはReaderと付き合わなければいけない
    実はサンプルコードでは、あえて戻り値の型を明記していませんでした。戻り値の方は常にReader[A, B]と明記する必要があり煩雑なためです。
    また、Monad Transformerを使ってMonad多段問題は解決したものの、逆に言うとMonad Transformerとも付き合うことになるわけです。
    これは、Functional Styleに精通していないプログラマにとって敷居の高いものです。

他にも様々なDIの方法があるので、Reader Monadを使った方法が必ずしもベストではないのですが、Functional Styleについて学ぶのは楽しいですね!

Enjoy, Scala!

Author

アバター
admin

関連記事