LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 5 years have passed since last update.

Scala with Cats 4.4 ~ 4.6

Last updated at Posted at 2018-05-20

4.4 Either

他に有用なMonadをみてみよう。Scala標準ライブラリにもあるEitherだ。
Scala2.11以前は、EitherはmapもflatMapも持っていなかったため、EitherはMonadとはいえなかった。

Scala2.12以降は、Eitherはrightに寄せる(biased)ようになったため、Monadと呼べるようになった。

4.4.1 Left and Right Bias

Scala2.11ではEitherはmapもflatMapメソッドも持っていなかった。そのためScala2.11でのEitherはfor文の中では使いづらかった。各ジェネレータの中で毎回.rightを呼ばなければならなかった。

val either1: Either[String, Int] = Right(10)
val either2: Either[String, Int] = Right(32)
for {
  a <- either1.right
  b <- either2.right
} yield a + b
// res0: scala.util.Either[String,Int] = Right(42)

Scala2.12からは、Eitherは再設計された。Eitherは成功したほうのケースであるRight側でmapとflatMapを使える。これによりfor文もかなり分かりやすくなる。

for {
  a <- either1
  b <- either2
 } yield a + b
// res1: scala.util.Either[String,Int] = Right(42)

Catsを使えばこの振る舞いを2.11で実現できる。cats.syntax.eitherをImportすればright-biasedなEitherを使うことができる。Scala2.12ではこのImportを省略してもいいし、そのままにしておくこともできる。

import cats.syntax.either._ // for map and flatMap
for {
  a <- either1
  b <- either2
} yield a + b

4.4.2 Creating Instances

LeftあるいはRightから直接インスタンスを作るのに加えて、cats.syntax.eitherの拡張メソッドasLeftasRightを使うこともできる。

import cats.syntax.either._ // for asRight
val a = 3.asRight[String]
// a: Either[String,Int] = Right(3)
val b = 4.asRight[String]
// b: Either[String,Int] = Right(4)
for {
x <- a
y <- b
} yield x*x + y*y
// res4: scala.util.Either[String,Int] = Right(25)

これらの"スマートコンストラクタ"を使うことでLeftやRightを返す代わりにEither型を返すようにできる点で、Left.applyやRight.applyを使うよりも利点がある。
以下の例のように、型を過剰に絞り込んでしまうことによって引き起こされる型推論のバグを避けるのに役立つ。

def countPositive(nums: List[Int]) =
  nums.foldLeft(Right(0)) { (accumulator, num) =>
    if(num > 0) {
      accumulator.map(_ + 1)
    } else {
      Left("Negative. Stopping!")
} }
// <console>:21: error: type mismatch;
// found : scala.util.Either[Nothing,Int] // required: scala.util.Right[Nothing,Int] // accumulator.map(_ + 1)
// ^
// <console>:23: error: type mismatch;
// found : scala.util.Left[String,Nothing] // required: scala.util.Right[Nothing,Int] // Left("Negative. Stopping!") // ^

このコードは以下の2点の理由でコンパイルできない。

  1. コンパイラはアキュムレータの型をEitherとする代わりにRightと推論してしまうため。
  2. Right.applyの型パラメータを指定しなかったため、コンパイラはLeftのパラメータをNothingと推論してしまうため。

代わりにasRightを使うことでこれら2つの問題を避けることができる。asRightは戻り型としてEitherを返すため、1つの型パラメータを返すだけでよくなる。

def countPositive(nums: List[Int]) = nums.foldLeft(0.asRight[String]) { (accumulator, num) =>
    if(num > 0) {
      accumulator.map(_ + 1)
    } else {
      Left("Negative. Stopping!")
} }
countPositive(List(1, 2, 3))
// res5: Either[String,Int] = Right(3)
countPositive(List(1, -2, 3))
// res6: Either[String,Int] = Left(Negative. Stopping!)

cats.syntax.either はEitherのコンパニオンオブジェクトにいくつかの便利な拡張メソッドを追加する。
catchOnlyメソッドとcatchNonFatalメソッドは、ExceptionをEitherのインスタンスにしたいときに使える。

Either.catchOnly[NumberFormatException]("foo".toInt)
// res7: Either[NumberFormatException,Int] = Left(java.lang.NumberFormatException: For input string: "foo")
Either.catchNonFatal(sys.error("Badness"))
// res8: Either[Throwable,Nothing] = Left(java.lang.RuntimeException: Badness)

他の型からEitherを作るメソッドもある。

Either.fromTry(scala.util.Try("foo".toInt))
// res9: Either[Throwable,Int] = Left(java.lang.NumberFormatException: For input string: "foo")
Either.fromOption[String, Int](None, "Badness") 
// res10: Either[String,Int] = Left(Badness)

4.4.3 Transforming Eithers

cats.syntax.eitherは他にもEitherインスタンスに便利なメソッドを加えてくれる。
orElsegetOrElseを使うことで、right側の値を取り出したり、デフォルト値を返すようにしたりできる。

import cats.syntax.either._
"Error".asLeft[Int].getOrElse(0)
// res11: Int = 0
"Error".asLeft[Int].orElse(2.asRight[String]) 
// res12: Either[String,Int] = Right(2)

ensureメソッドはRightの値が条件を満たすかどうかをチェックできる。

-1.asRight[String].ensure("Must be non-negative!")(_ > 0) 
// res13: Either[String,Int] = Left(Must be non-negative!)

recoverメソッドとrecoverWithメソッドは、Futureにある同名のものと似たエラー処理を行う。

"error".asLeft[Int].recover {
  case str: String => -1
}
// res14: Either[String,Int] = Right(-1)
"error".asLeft[Int].recoverWith {
  case str: String => Right(-1)
}
// res15: Either[String,Int] = Right(-1)

mapを補完するleftMapメソッドとbimapメソッドもある。

"foo".asLeft[Int].leftMap(_.reverse)
// res16: Either[String,Int] = Left(oof)
6.asRight[String].bimap(_.reverse, _ * 7)
// res17: Either[String,Int] = Right(42)
"bar".asLeft[Int].bimap(_.reverse, _ * 7)
// res18: Either[String,Int] = Left(rab)

swapメソッドはleftとrightの要素を交換する。

123.asRight[String]
// res19: Either[String,Int] = Right(123)
123.asRight[String].swap
// res20: scala.util.Either[Int,String] = Left(123)

さらには、Catsはたくさんの変換メソッドを追加する。toOption, toList, toTry, toValidatedなど。

4.4.4 Error Handling

よくあるEitherの使いかたとしては、fail-fastなエラー処理がある。flatMapを使って計算をつなげることができるが、どれかの計算がfailすると、その後の計算は実行されないというものだ。

for {
  a <- 1.asRight[String]
  b <- 0.asRight[String]
  c <- if(b == 0) "DIV0".asLeft[Int]
       else (a / b).asRight[String]
} yield c * 100
// res21: scala.util.Either[String,Int] = Left(DIV0)

Eitherをエラー処理に使うときは、エラーを表現するのに使う型を決める必要がある。これにはThrowableを使うことができる。

type Result[A] = Either[Throwable, A]

これはscala.util.Tryと似たセマンティクスだが、問題は、Throwableはとてつもなく広い型だということだ。Throwableという型からはどんな種類のエラーが置きたのかの情報はほとんど得られない。

他のやり方としては、プログラム内で起きるエラーを表す代数型(sealed trait & case class)を定義する方法だ。

sealed trait LoginError extends Product with Serializable

final case class UserNotFound(username: String) extends LoginError
final case class PasswordIncorrect(username: String) extends LoginError
case object UnexpectedError extends LoginError
case class User(username: String, password: String)
type LoginResult = Either[LoginError, User]

このやり方はThrowableの問題を解決はする。これにより固定のエラー型が存在することになり、意図しないエラーも全てそこでキャッチされる。またパターンマッチングにおける網羅性チェックにおける安全性も保証される。

// Choose error-handling behaviour based on type:
def handleError(error: LoginError): Unit =
  error match {
    case UserNotFound(u) =>
      println(s"User not found: $u")
    case PasswordIncorrect(u) =>
      println(s"Password incorrect: $u")
    case UnexpectedError =>
      println(s"Unexpected error")
}
val result1: LoginResult = User("dave", "passw0rd").asRight
// result1: LoginResult = Right(User(dave,passw0rd))
val result2: LoginResult = UserNotFound("dave").asLeft 
// result2: LoginResult = Left(UserNotFound(dave))

result1.fold(handleError, println)
// User(dave,passw0rd)
result2.fold(handleError, println)
// User not found: dave

4.4.5 Exercise: What is Best?

前項の例のようなエラー処理戦略はすべての場合に使えるものだろうか?エラー処理において他に欲しい機能はないだろうか?

(注:決まった答えはない)

4.5 Aside: Error Handling and MonadError

Catsには、MonadErrorというエラー処理に使うEitherのようなものを抽象化した型クラスがある。
MonadErrorはエラーを検知したり処理するための追加の処理を提供する。

  • このセクションはオプショナルだ!

エラー処理をするMonadたちを抽象的に扱いたいということが無いかぎり、MonadErrorを使う必要はない。
例えば、FutureとTryとか、EitherとEitherTのエラー処理を抽象化したいときにはMonadErrorが使える。
このような抽象化がひとまず必要ではないなら、このセクションはスキップしてかまわない。

4.5.1 The MonadError Type Class

以下がシンプルなバージョンのMonadErrorの定義となる。

package cats
trait MonadError[F[_], E] extends Monad[F] {
  // Lift an error into the `F` context:
  def raiseError[A](e: E): F[A]
  // Handle an error, potentially recovering from it:
  def handleError[A](fa: F[A])(f: E => A): F[A]
  // Test an instance of `F`,
  // failing if the predicate is not satisfied:
  def ensure[A](fa: F[A])(e: E)(f: A => Boolean): F[A]
}

MonadErrorは、2つの型パラメータで定義される。

  • FはMonadの型を指定する。
  • EはFに含まれるエラーの型を指定する。

これら2つのパラメータがどのようにフィットするかを示すため、Eitherの型クラスをインスタンス化する例を示す。

import cats.MonadError
import cats.instances.either._ // for MonadError
type ErrorOr[A] = Either[String, A]
val monadError = MonadError[ErrorOr, String]
  • ApplicativeError

実際は、MonadErrorはApplicativeError型クラスを継承したものだ。Applicativeは第6章で登場する。セマンティクスとしては同じなため、いまは詳細には踏み込まない。

4.5.2 Raising and Handling Errors

MonadErrorで最も重要なメソッドはraiseErrorhandleErrorの2つ。
raiseErrorはMonadでいうpureメソッドのようなもので、failureを表現するインスタンスを作成する。

val success = monadError.pure(42)
// success: ErrorOr[Int] = Right(42)

val failure = monadError.raiseError("Badness")
// failure: ErrorOr[Nothing] = Left(Badness)

(pureはRight側、raiseErrorはLeft側)

handleErrorはraiseErrorを補完するもので、errorを使って成功に変換することを可能にする。Futureのrecoverメソッドに近い。

monadError.handleError(failure) {
  case "Badness" =>
    monadError.pure("It's ok")
  case other =>
    monadError.raiseError("It's not ok")
}
// res2: ErrorOr[ErrorOr[String]] = Right(Right(It's ok))

他に便利なメソッドとして、ensureという、フィルタのような振る舞いをするメソッドがある。成功側のMonadの値を条件式にかけて、それがfalseを返すときに返すエラーを指定できる。

import cats.syntax.either._ // for asRight
monadError.ensure(success)("Number too low!")(_ > 1000)
// res3: ErrorOr[Int] = Left(Number too low!)

CatsはraiseErrorとhandleErrorをcats.syntax.applicativeErrorにおいて提供している。ensureはcats.syntax.monadErrorにある。

import cats.syntax.applicative._ // for pure
import cats.syntax.applicativeError._ // for raiseError etc import cats.syntax.monadError._ // for ensure
val success = 42.pure[ErrorOr]
// success: ErrorOr[Int] = Right(42)
val failure = "Badness".raiseError[ErrorOr, Int]
// failure: ErrorOr[Int] = Left(Badness)
success.ensure("Number to low!")(_ > 1000)
// res4: Either[String,Int] = Left(Number to low!)

4.5.3 Instances of MonadError

CatsはEither, Future, Tryなどの多くの型に対してMonadErrorインスタンスを提供している。Eitherのインスタンスはどのエラータイプにもカスタマイズ可能だが、FutureとTryのインスタンはエラーをThrowablesとして表現する。

import scala.util.Try
import cats.instances.try_._ // for MonadError
val exn: Throwable =
  new RuntimeException("It's all gone wrong")
exn.raiseError[Try, Int]
// res6: scala.util.Try[Int] = Failure(java.lang.RuntimeException: It' s all gone wrong)

4.5.4 Exercise: Abstracting

※未実装

4.6 The Eval Monad

cats.Evalはさまざまな評価のモデルを抽象化するMonadだ。主なものとしてeagerとlazyがある。Evalは結果がmemoizedされているかによってさらに区別される。

4.6.1 Eager, Lazy, Memoized, Oh My!

そのことは何を意味するのだろうか?
Eagerを使った計算は即座に評価されるのに対し、Lazyの計算はアクセスされたときに評価される。Memoizedな計算は最初にアクセスされたときに実行され、その後は結果がキャッシュされる。

例えば、Scalaのvalはeagerかつmemoizedだ。観察可能な副作用を起こしてみることでそのことを確認できる。次の例では、xの値を計算するコードはアクセス時ではなく定義時に実行され(Eager)、xにアクセスすると再度計算が走ることなく値が呼び出される(memoized)。

val x = {
  println("Computing X")
  math.random
}
// Computing X
// x: Double = 0.32119158749503807
x // first access
// res0: Double = 0.32119158749503807
x // second access
// res1: Double = 0.32119158749503807

それに対して、defはlazyでnot memoizedだ。下記ではyの計算は実際にアクセスするまで実行されず(lazy)、アクセスするたびに毎回実行される(not memoized).

def y = {
  println("Computing Y")
  math.random
}
// y: Double
y // first access
// Computing Y
// res2: Double = 0.5179245763430056
y // second access
// Computing Y
// res3: Double = 0.8657077812314633

大事なことを言い残したが、lazy valsはlazyかつmemoizedだ。下記ではzの計算は最初にアクセスされるまで実行されない(lazy)が、結果はキャッシュされ、それ以降のアクセスでは再利用される(memoized)。

lazy val z = {
  println("Computing Z")
  math.random
}
// z: Double = <lazy>
z // first access
// Computing Z
// res4: Double = 0.027165389120539563
z // second access
// res5: Double = 0.027165389120539563

4.6.2 Eval's Models of Evaluation

EvalはNow, Later, Alwaysの3つのサブタイプを持っている。それぞれのコンストラクタメソッドを使ってクラスをインスタンス化できるが、それらの型はEvalとして返される。

import cats.Eval
val now = Eval.now(math.random + 1000)
// now: cats.Eval[Double] = Now(1000.6884369117727)
val later = Eval.later(math.random + 2000)
// later: cats.Eval[Double] = cats.Later@71175ee9
val always = Eval.always(math.random + 3000)
// always: cats.Eval[Double] = cats.Always@462e2fea

Evalの結果をvalueメソッドを用いて取り出すことができる。

now.value
// res6: Double = 1000.6884369117727
later.value
// res7: Double = 2000.8775276106762
always.value
// res8: Double = 3000.6943184468

どの型のEvalも上に挙げたいずれかの評価モデルを使って結果を計算する。Eval.nowは値を即座にキャプチャする。そのセマンティクスはval、つまりeagerかつmemoizedに近い。

val x = Eval.now {
  println("Computing X")
  math.random
}
// Computing X
// x: cats.Eval[Double] = Now(0.8724950064732552)
x.value // first access
// res9: Double = 0.8724950064732552
x.value // second access
// res10: Double = 0.8724950064732552

Eval.alwaysは遅延した計算をキャプチャする。defに近い。

val y = Eval.always {
  println("Computing Y")
  math.random
}
// y: cats.Eval[Double] = cats.Always@5212e1f5
y.value // first access
// Computing Y
// res11: Double = 0.8795680260041828
y.value // second access
// Computing Y
// res12: Double = 0.5640213059400854

Eval.laterはlazyかつmemoizedな計算をキャプチャする。lazy valに近い。

val z = Eval.later {
  println("Computing Z")
  math.random
}
// z: cats.Eval[Double] = cats.Later@33eda11
z.value // first access
// Computing Z
// res13: Double = 0.5813583535421343
z.value // second access
// res14: Double = 0.5813583535421343

3つの型の振る舞いは下記のように要約できる。

Scala Cats Properties
val Now eager, memoized
lazy val Later lazy, memoized
def Always lazy, not memoized

4.6.3 Eval as a Monad

他のMonadと同じように、EvalのmapとflatMapは計算をチェーンに追加する。ただしこの場合は、チェーンはfunctionのリストとして保存される。それらのfunctionsはEvalのvalueメソッドが呼ばれて結果がリクエストされるまで実行されない。

val greeting = Eval.
  always { println("Step 1"); "Hello" }.
  map { str => println("Step 2"); s"$str world" }
// greeting: cats.Eval[String] = cats.Eval$$anon$8@3a67c76e
greeting.value
// Step 1
// Step 2
// res15: String = Hello world

注意しなければいけない点としては、元のEvalインスタンスのセマンティクスは維持されるものの、マッピングされたfunctionは常にオンデマンドで遅延的に実行される(defのセマンティクス)。

val ans = for {
  a <- Eval.now { println("Calculating A"); 40 }
  b <- Eval.always { println("Calculating B"); 2 }
} yield {
println("Adding A and B") a+b
}
// Calculating A
// ans: cats.Eval[Int] = cats.Eval$$anon$8@2d96144d
ans.value // first access
// Calculating B
// Adding A and B
// res16: Int = 42
ans.value // second access
// Calculating B
// Adding A and B
// res17: Int = 42

別の例。1つめをnow, 2つめをnowにしても、1つめは即座に評価されるが、2つめはvalueを取るときに呼び出される。

@ val ans = for {
    a <- Eval.now { println("Calculating A"); 40 }
    b <- Eval.now { println("Calculating B"); 2 }
  } yield {
  println("Adding A and B");
  a+b
  }
Calculating A
ans: Eval[Int] = cats.Eval$$anon$9@778fde44

@ ans.value
Calculating B
Adding A and B
res38: Int = 42

@ ans.value
Calculating B
Adding A and B
res39: Int = 42

さらに別の例。1つめをlaterにすると、たしかに遅延評価される(lazyのセマンティクス)。
しかし2つめのnowは即座に評価されず、valueを取るときに毎回呼び出される(defのセマンティクス)。

@ val ans = for {
    a <- Eval.later { println("Calculating A"); 40 }
    b <- Eval.now { println("Calculating B"); 2 }
  } yield {
  println("Adding A and B");
  a+b
  }
ans: Eval[Int] = cats.Eval$$anon$9@2b8c4bed

@ ans.value
Calculating A
Calculating B
Adding A and B
res41: Int = 42

@ ans.value
Calculating B
Adding A and B
res42: Int = 42

@ ans.value
Calculating B
Adding A and B
res43: Int = 42

@ ans.value
Calculating B
Adding A and B
res44: Int = 42

Evalはmemoizedメソッドを持ち、一連の計算をメモ化することができる。memoizedが呼ばれるまでの一連の計算結果はキャッシュされ、その後の計算は元のセマンティクスを保持する。

val saying = Eval.
  always { println("Step 1"); "The cat" }.
  map { str => println("Step 2"); s"$str sat on" }.
  memoize.
  map { str => println("Step 3"); s"$str the mat" }
// saying: cats.Eval[String] = cats.Eval$$anon$8@7a0389b5
saying.value // first access
// Step 1
// Step 2
// Step 3
// res18: String = The cat sat on the mat
saying.value // second access
// Step 3
// res19: String = The cat sat on the mat

4.6.4 Trampolining and Eval.defer

Evalの便利な特性の1つは、mapとflatMapのメソッドがトランポリンされて(Trampolining)いることだ。つまり、スタックフレームを消費することなく、mapとflatMapの呼び出しを任意にネストすることができる。この特性は「スタックの安全性(stack safety)」と呼ばれる。

例として、累乗を計算する関数を考えてみよう。

def factorial(n: BigInt): BigInt =
  if(n == 1) n else n * factorial(n - 1)

このメソッドは簡単にスタックオーバーフローになる。

factorial(50000)
// java.lang.StackOverflowError
//   ...

これをEvalを使ってスタック安全に書き直すことができる。

def factorial(n: BigInt): Eval[BigInt] =
  if(n == 1) {
    Eval.now(n)
  } else {
    factorial(n - 1).map(_ * n)
  }
factorial(50000).value
// java.lang.StackOverflowError
//   ...

あれ!動作しなかった。これはfactorialへの再帰呼び出しをEvalのmapメソッドが動き始める前にやってしまっているためだ。これを回避するにはEval.deferを使用する。Eval.deferは、Evalの既存のインスタンスを取り、その評価を遅延させる。
deferメソッドはmapやflatMapのようにトランポリングされているので、既存のオペレーションをスタック安全にするための手軽な方法として使える。

def factorial(n: BigInt): Eval[BigInt] =
 if(n == 1) {
   Eval.now(n)
 } else {
   Eval.defer(factorial(n - 1).map(_ * n))
 }

factorial(50000).value
// res20: BigInt = 334732050959714483691547609407148647791277322381045480773010032199016802214436564

Evalは、非常に大きな計算とデータ構造を扱う際にスタックの安全性を強化するための便利なツールだ。しかし、トランポリン化は無制限にできるわけではなく、ヒープ上に関数オブジェクトの連鎖を作成することでスタックの消費を回避している。計算を入れ子にできる深さにはまだ限界があるが、スタックではなくヒープのサイズによって制限されている。

4.6.5 Exercise: Safer Folding using Eval

foldRightのネイティブ実装はstack safeではない。Evalを使ってstack safeにせよ。

def foldRight[A, B](as: List[A], acc: B)(fn: (A, B) => B): B = as match {
    case head :: tail =>
      fn(head, foldRight(tail, acc)(fn))
    case Nil =>
      acc
}
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up