Scalazを使おう #1
ポニー村山
はじめに
Scalazを導入するメリットを書いていきたいと思います。
今回は、モナドの有用性について書きたいと思います。
Scalazお試し
REPLでScalazを動かすにはまず次のようなbuild.sbtファイルを作ります。
scalaVersion := "2.11.6"
libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.1.1"
initialCommands += "import scalaz._, Scalaz._"
sbt console
コマンドによりREPLでScalazが使えます。
モナドって何
モナドってなんだ???
次の3つの法則(モナド則)を満たしていれば、それはモナドです。
(Monad[F].point(x)) >>= f == f x
m >>= Monad[F].point == m
(m >>= f) >>= g == m >>= (_ => f(_) >>= g)
直感的には、Monad[F].point
が単位元であり、>>=
が結合的であることを示しています。
一部を簡略化すると次のように捉えることができます。
point >>= ○ == ○
△ >>= point == △
(○ >>= △) >>= □ == ○ >>= (△ >>= □)
あれ、これって・・・
0 + ○ == ○
△ + 0 == △
(○ + △) + □ == ○ + (△ + □)
足し算とそっくりですね。
さて、実際の動作を見ていきましょう。
Monad[F].point
という見慣れない関数が出てきましたが、これはなんでしょう?Monad[F]
とは、Monadが型パラメータを受け取れることを示しています。
具体的なpoint
の動作を見ていきましょう。
scala> Monad[Option].point(3) res0: Option[Int] = Some(3) scala> Monad[List].point("x") res1: List[String] = List("x") scala> Monad[Function0].point("x") res2: () => String = <function0> scala> res2() res3: String = "x"
point
は1つの要素にある文脈を持たせます。
例えば、Optionモナドは「失敗するかもしれない計算」という文脈を意味します。
文脈付き計算
>>=
は、Scalazが提供するflatMap
のエイリアスです。
scala> def success = (x:Int) => Some(x * 2)
success: Int => Some[Int]
scala> def fail = (x:Int) => None
fail: Int => None.type
scala> Monad[Option].point(5) >>= success >>= success
res6: Option[Int] = Some(20)
scala> Monad[Option].point(5) >>= fail >>= success
res7: Option[Int] = None
>>=
による計算の合成を行うことにより、Monadは文脈を持ったまま計算を継続する機能を提供します。
これこそが、モナドを利用する意義となります。
便利なfor文
ScalaにはflatMap
を使うのに便利なfor構文が用意されています。
scala> for (x <- Some(2); y <- Some(5)) yield (x + y)
res14: Option[Int] = Some(7)
これは、下記のコードと同じ意味を持ちます。
scala> Some(2) flatMap ((x:Int) => Some(x + 5))
res15: Option[Int] = Some(7)
flatMapやmap, filterが連鎖しているコードは理解しにくいので、可能な限りfor文を使いましょう。
エラー処理
Monadの有用な利用例として、エラー処理などに用いるEitherモナドを見ていきましょう。
※Scala標準ライブラリのEitherはモナドではありません。
Scalazでは、残念なScala標準ライブラリを補完するために、様々なモナドインスタンスを提供します(訳:様々な型をモナドとして利用可能にします)。その1つが、Eitherモナドです。
scala> 1.right
res8: scalaz.\/[Nothing,Int] = \/-(1)
scala> "error".left
res9: scalaz.\/[String,Nothing] = -\/(error)
scala> "error".left[Int]
res10: scalaz.\/[String,Int] = -\/(error)
scala> 3.right[String] >>= (x => (x + 1).right)
res11: scalaz.\/[String,Int] = \/-(4)
scala> "error".left[Int] >>= (x => (x + 1).right)
res12: scalaz.\/[String,Int] = -\/(error)
ScalazのEitherは\/という名前です(標準ライブラリのと被らせないため)。
Eitherは概念的に直和をあわらしており、leftかrightの状態を持ちます。通常、エラー時はleftにエラーメッセージを、正常時はrightに正常値を入れることで利用します。
具体的な利用例は次のようになります。
scala> for (x <- "success".right; y <- "error at y".left[String]; z <- "error at z".left[String]) yield z
res13: scalaz.\/[String,String] = -\/(error at y)
Optionモナドとの違いは、どこでエラーが発生したかを特定できることです。
playによる例
ここで、実際にplayのActionにおけるエラー処理コード実装を考えてみましょう。
まず、手続き的な実装は次のようになります。
def doAction1 = Action { implicit req =>
def handle: Result = {
val headerOption = authorizeHeaders(req.headers)
if (headerOption.isEmpty) {
return BadRequest("Unauthorized request")
}
val jsonOption = req.body.asJson
if (jsonOption.isEmpty) {
return BadRequest("Wrong POST body")
}
val valueOption = validateJson(jsonOption.get)
if (valueOption.isEmpty) {
return BadRequest("Invalid JSON")
}
doSomething(valueOption.get)
Ok("Success")
}
handle
}
このコードの問題点は、実際に処理を行う箇所とエラー処理を行う箇所がコード上で一緒くたになってしまっていることです。また、isEmptyの後にgetを呼んでいる箇所がありますが、型システム上でこのget呼び出しが安全ではあることを保証できません。
次に、パターンマッチを利用したコードを見ていきましょう。
def doAction2 = Action { implicit req =>
authorizeHeaders(req.headers) match {
case Some(_) => req.body.asJson match {
case Some(json) => validateJson(json) match {
case Some(user) => {
doSomething(user)
Ok("Success")
}
case _ => BadRequest("Invalid JSON")
}
case _ => BadRequest("Wrong POST body")
}
case _ => BadRequest("Unauthorized request")
}
}
このコードではOption型から安全に値を取り出していますが、ネストが深く可読性が悪いという問題があります。case _ => BadRequest...
が3つ並んでいますが、どれがどのmatch式に対応しているのかパッと見ではわかりませんよね。
続いて、Scalaのfor文を用いた実装を見ていきます。
def doAction3 = Action { implicit req =>
(for {
_ <- authorizeHeaders(req.headers)
json <- req.body.asJson
user <- validateJson(json)
} yield user) match {
case Some(user) => {
doSomething(user)
Ok("Success")
}
case _ => BadRequest("Something wrong")
}
}
このコードでは、Option型を利用して安全に計算しつつ、エラー処理と本来行いたい処理を綺麗に分離できています。しかし、エラー内容を取得できないためBadRequest("Something wrong")
としてまとめてしまっています。
そこで、エラー内容による分岐が可能なEitherを利用したfor文実装を考えます。
def doAction4 = Action { implicit req =>
(for {
_ <- authorizeHeaders(req.headers)
.toRight(BadRequest("Unauthorized request")).right
json <- req.body.asJson
.toRight(BadRequest("Wrong POST body")).right
user <- validateJson(json)
.toRight(BadRequest("Invalid JSON")).right
} yield user) match {
case Left(error) => error
case Right(user) => {
doSomething(user)
Ok("Success")
}
}
}
これで、正常処理とエラー処理の分断と、エラー内容の判別という目的を達成できました。しかし、なんだか冗長ですね。各行にある.rightというのは、Scala標準ライブラリのEitherがモナドでないことによる余計な記述です。
Scalazを用いると、次のようにさらに簡潔に書けます。
def doAction5 = Action { implicit req =>
(for {
_ <- authorizeHeaders(req.headers) /> BadRequest("Unauthorized request")
json <- req.body.asJson /> BadRequest("Wrong POST body")
user <- validateJson(json) /> BadRequest("Invalid JSON")
} yield user) match {
case -\/(error) => error
case \/-(user) => {
doSomething(user)
Ok("Success")
}
}
}
このように、モナドを利用することで型システムの恩恵を受けた強力で簡潔なエラー処理コードを書くことができました。
モナドを自作しよう
モナドを作るのは簡単です。早速作ってみましょう。
case class MyClass[A](state: Int, value: A)
trait MyClassInstances {
implicit val MyClassInstance = new Monad[MyClass] {
def point[A](x: => A): MyClass[A] = MyClass (0, x)
def bind[A, B](fa: MyClass[A])(f: (A) => MyClass[B]): MyClass[B] = {
val fb = f(fa.value)
MyClass(fa.state + fb.state, fb.value)
}
}
}
case object MyClassInstances extends MyClassInstances
このモナドは、行いたい計算の裏側でついでに足し算を行います。
足し算は単位元「0」を持ち、結合的なので簡単にモナドにできました。
モナドを自作する際は、「単位元」と「結合則」について意識するといいでしょう。
上のコードをREPL上でテストするには、:pasteモードでコピペすればOKです。
早速モナド則をテストしてみましょう。
scala> import MyClassInstances._
import MyClassInstances._
scala> Monad[MyClass].point(3) >>= ((x:Int) => MyClass(2, x))
res14: MyClass[Int] = MyClass(2,3)
scala> ((x:Int) => MyClass(2, x))(3)
res15: MyClass[Int] = MyClass(2,3)
scala> MyClass(3, 4) >>= ((x:Int) => Monad[MyClass].point(x))
res16: MyClass[Int] = MyClass(3,4)
scala> val f = (x:Int) => MyClass(5, x)
f: Int => MyClass[Int] = <function1>
scala> val g = (x:Int) => MyClass(2, x)
g: Int => MyClass[Int] = <function1>
scala> (MyClass(1, 2) >>= f) >>= g
res17: MyClass[Int] = MyClass(8,2)
scala> MyClass(1, 2) >>= ((x:Int) => f(x) >>= g)
res18: MyClass[Int] = MyClass(8,2)
どうやらモナド則を満たしているようです。もっとしっかり試したい人はScalaCheckとか使ってみましょう!
さらに実用的な例が見たい方は、次のScalazコードを読みましょう。
おわりに
さて、Monad則の話に戻りましょう。
(Monad[F].point(x)) >>= f == f x
m >>= Monad[F].point == m
(m >>= f) >>= g == m >>= (_ => f(_) >>= g)
モナドが持つ強力な機能は、これらのモナド則を満たすことによって保証されています。モナド則を破ったモナドインスタンスを作ることはたやすいですが、やめましょう。それはモナドではありません。Scalazが提供するモナドインスタンスは、モナド則を満たすよう作られています。
Scalazには、モナドを対象とした様々な便利関数が用意されています。
モナド則を守って、楽しいモナドライフを送りましょう!