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