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のサンプルを簡単に書くと以下のような感じになると思います。
1 2 3 4 5 |
scala> def getOddUserIds(findUsers: => Seq[Int]) = findUsers.filter(_%2 != 0) getOddUserIds: (findUsers: => Seq[Int])Seq[Int] scala> getOddUserIds(1 to 10) res5: Seq[Int] = Vector(1, 3, 5, 7, 9) |
findUsersを仮引数とすることで、依存性として関数呼び出し時に注入できます。
ScalaにおけるMonad
Scala標準APIではMonadを表すクラスは存在しません。
しかしながら、scala.collection.immutable.Listは Functor則、Monad則 を満たしていることが分かります。
1 2 3 4 5 6 |
xs.map(identity) == x xs.map(f).map(g) == xs.map(x => g(f(x))) xs.flatMap(x => List(f(x))) == xs.map(f) List(x).flatMap(f) == f(x) xs.flatMap(List(_)) == xs xs.flatMap(f).flatMap(g) == xs.flatMap(f(_).flatMap(g)) |
次にHaskellでは1引数関数をMonadとして扱いますが、Scalaではどうでしょうか?
1 2 3 4 5 |
def add(x: Int): Int => Int = _ + x val f = add(2) map (_ * 3) error: value map is not a member of Int => Int val f = add(2) map (_ * 3) ^ |
残念ながら、Scala標準APIでは関数をMonadとして扱うことは出来ません。
しかし、Scalazライブラリには関数をMonadとして扱うことの出来る scalaz.Reader が用意されています。
※ Haskellでは、関数は1引数しか取らないため、以降の説明でも"関数"=="1引数関数"とします。
Reader Monad とは
次に簡単にReader Monad について説明します。
Reader Monadとは単に関数Monadの別名です。
その名の通り関数をMonadとして扱います。
1 |
val addTwo: Int => String = { x => (x+2).toString } |
上記のような関数があるとき
文脈は「Intの引数を適用すれば結果が帰ってくる」
値は「Stringの結果」
となるでしょう。
Scalazをimportすることによって関数を Monadとして扱えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ scala -cp ~/.gradle/caches/modules-2/files-2.1/org.scalaz/scalaz-core_2.11/7.1.0/9877bb819627fdeeab575a4ddba961088b958977/scalaz-core_2.11-7.1.0.jar Welcome to Scala version 2.11.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_25). Type in expressions to have them evaluated. Type :help for more information. scala> import scalaz._ import scalaz._ scala> import Scalaz._ import Scalaz._ scala> val addTwo: Int => String = { x => (x+2).toString } addTwo: Int => String = <function1> scala> addTwo map ("value is " + _) res0: Int => String = <function1> scala> res0(5) res1: String = value is 7 |
このように、addTwoという関数があるとき、あたかもその結果に対し関数を適用することが出来ます。
andThenを使った関数合成とよく似ていますね。
Reader Monadのメリットはmapやfor式と合わせて使えることです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
scala> :paste // Entering paste mode (ctrl-D to finish) for { a <- (_ + 2) : Int => Int b <- (_ * 3) : Int => Int } yield { a + b } // Exiting paste mode, now interpreting. res8: Int => Int = <function1> scala> res8(7) res9: Int = 30 |
この例では、( + 2)の計算結果であるa と、( * 3)の計算結果であるb を、 a + b します。
for式は「Intの引数を適用すれば結果が帰ってくる」という文脈に包んで戻すので、
res8: Int => Int のような関数が戻ることになります。
res8(7)を式展開すると、以下のようになるでしょう。
1 |
(7 + 2) + (7 * 3) = 30 |
"7" という実引数を依存性として見立てると、DIできそうな予感がしてきますね。
Reader Monad を使った DI
では、Reader Monadを使ったDIの方法を見て行きましょう。
1 2 |
def getUser(userId: Int) = Reader[UserRepo, User](_.get(userId)) |
これは
1 2 |
def getUser(userId: Int) = (_.get(userId)): UserRepo => Int |
と同じ意味です。
Readerの最初の型パラメータ(UserRepo)は引数の型、次の型パラメータ(User)は戻り値の型です。
getUserは関数を返す関数であることに注意してください。
よりHaskell寄りの表現をするなら、部分適用可能な2引数関数であるとも捉えられます。
1 |
val user: User = getUser(5)(MysqlUserRepo()) |
実際の使用例としては、以下のようになります。
1 2 3 4 5 6 7 |
val getUpdatedUser = for { user <- UserRepo.getUser(5) updated <- UserRepo.updateUser(user) } yield updated val updatedUser = getUpdatedUser(MysqlUserRepo()) |
for式と組み合わせて後から依存性(MysqlUserRepo)を注入することが出来るようになりました。
ここまででReaderMonadを使ってDIができることが分かりました。
しかし、話はまだ続きます。
他のMonadをすでに使っていると、、、
もし、他のMonadを使っていた場合はどうなるでしょうか?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
trait UserRepoAsync { def get(userId: Int): Future[User] def find(email: String): Future[User] def update(user: User): Future[User] } ... object UserService { def getEmail(userId: Int)(implicit ec: ExecutionContext): Reader[Env, Future[String]] = for (userFuture <- UserRepoAsync.getUser(userId)) yield userFuture map (_.email) def findAddress(email: String)(implicit ec: ExecutionContext): Reader[Env, Future[String]] = Env.env map { env => for { user <- UserRepoAsync.findUser(email)(env) address <- AddressRepoAsync.getAddress(user.id)(env) } yield address } } |
findAddress内のfor式は、Futureを扱っていることに注意してください。
だんだん複雑になってきましたね。。。
そこで登場するのが Monad Transformer (モナド変換子) です。
Monad Transformer とは
Monad Transformerについては語れるほど詳しくないので、サンプルから見て行きましょう。
1 2 |
scala> List(Some(1), None, Some(3)) res4: List[Option[Int]] = List(Some(1), None, Some(3)) |
res4のようにMonadが多段になっている時、値に対して関数を適用したい場合どうすればよいでしょうか?
1 2 3 4 5 6 7 8 9 10 11 12 |
scala> :paste // Entering paste mode (ctrl-D to finish) for { oi <- res4 } yield { for { i <- oi } yield i + 100 } // Exiting paste mode, now interpreting. res5: List[Option[Int]] = List(Some(101), None, Some(103)) |
うまくいきましたが、+100するという本来の目的が埋もれがちですね。
# 無理やりfor式で書いてますが ^^;
次にMonad Transformerを使ってみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
scala> OptionT(List(Some(1), None, Some(3))) res6: scalaz.OptionT[List,Int] = OptionT(List(Some(1), None, Some(3))) scala> :paste // Entering paste mode (ctrl-D to finish) for { i <- res6 } yield i + 100 // Exiting paste mode, now interpreting. res7: scalaz.OptionT[List,Int] = OptionT(List(Some(101), None, Some(103))) scala> res7.run res8: List[Option[Int]] = List(Some(101), None, Some(103)) |
一度の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