Cats Monad

Published: by

Cats Monad .

Monad

monad 관하여는 Monad를 참조

Cats Monad type class

Monad는 전의 글에서 Monad에 대하여 설명했다.
여기서는 cats에서 어떻게 Monad를 쉽게 이용할 수 있는지를 살펴보자.
우선 Monad의 type class는 아래와 같으며, Moand는 Functor이므로 아래와 같이 map 도 구현 가능 해야 한다.

trait Monad[F[_]] {
 def pure[A](a: A): F[A]
 def flatMap[A,B](ma: F[A])(f: A => F[B]): F[B]

 //Monad 는 Funtor이다 따라서 pure와 flatMap으로 map를 제공할 수 있엉야 한다.
 def map[A,B](ma: F[A])(f: A => B): F[B] = flatMap(ma)(a => pure(f(a)))
}

cats의 Monad type class는 아래와 같다.

trait Monad[F[_]] extends FlatMap[F] with Applicative[F]

//Left 항등법칙 
//pure(a).flatMap(f) == f(a)

//right 항등법칙 
//ma.flatMap(pure) == ma

//associativity 결합법칙
//ma.flatMap(f).flatMap(g) == ma.flatMap(a => f(a).flatMap(g))

위에의 코드를 보면 FlatMap에서 flatMap를 Applicative fuctor에서 pure, map method를 상속 받는다.

Cats Monad instance

//Monad[F[_]] extends FlatMap[F] with Applicative[F]
// FlatMap으로 부터 flatMap method
// Applicative으로 부터 pure, map method
import cats.Monad
import cats.instances.list._
import cats.instances.option._ //for Monad

val opt1 = Monad[Option].pure(3)
val opt2 = Monad[Option].flatMap(opt1)(a => Some(a + 2))
val opt3 = Monad[Option].map(opt2)(a => a * 100)

val list1 = Monad[List].pure(3)
val list2 = Monad[List].flatMap(List(1,2,3))(a => List(a, a * 10))
val list3 = Monad[List].map(list2)(a => a + 123)

Future인경우 implicit 가 필요한데 ExecutionContext 이것이 scope에 없으면
cats.instances.future를 해도 implicit Monad[Future]가 없다고 한다.

import scala.concurrent.Future
import cats.instances.future._
import scala.concurrent.ExecutionContext.Implicits.global

val futureMonad: Monad[Future] = Monad[Future]
val future: Future[Int] = Monad[Future].pure(1)

Cats Monad Syntax

3개의 cats.syntax에서 얻는다.

  • cats.syntax.flatMap : flatMap 의 사용
  • cats.syntax.functor : map 의 사용
  • cats.syntax.applicative : pure 의 사용
    3개을 모두 사용 할경우 일일이 하지 않고 다음과 같이 하면 한번에
    import cats.implicits
import cats.syntax.applicative._
val op4: Option[Int] = 1.pure[Option]
val rs: List[Int] = 1.pure[List]

flatMap이나 map같은 경우는 pure 와 같이 쉽게 할 수 가 없다.
왜냐면 type에 맞는 연산이 정의된 implicit가 없기 때문이다.

import cats.syntax.flatMap._ //for flatMap
import cats.syntax.functor._ //for map
def sum[F[_]: Monad](ma: F[Int], mb: F[Int]): F[Int] =
  ma.flatMap(a => mb.map(b => a * b))

val r = sum(Monad[Option].pure(2), Option(5))
println(r)
//Some(10)
val rs2:List[Int] = sum(List(1,2,3), Monad[List].pure(5))
println(rs2)
//List(5, 10, 15)

위에서 sum(1,2)는 작동하지 않는다. 이런 경우 type alias를 주면 해결 할 수 있는데 전에 Traversable 예에서 traverse method를 가지고 map method를 만들때 Applicative가 필요 했는데 이때 type Id[A] = A를 이용한 적이 있었다. 다만 cats에서 이미 이에 대한 type 를 만들어 놓은 것을 import 하면 된다.

//now work sum(1,2)
/ type Id[A] = A
// cats에서 Id 제공
import cats.Id
import cats.instances.int._
val rs3:Int = sum(3: Id[Int], 5: Id[Int])
println(rs3)

val ex01:Id[Int] = 1
val ex02:Id[String] = "abcd"
val rs4:Int = Monad[Id].pure(2).flatMap(i => i * 5)
val rs5:Id[Int] = Monad[Id].pure(2).flatMap(i => i * 5)

import cats.syntax.eq._
println(rs4 === rs5)

cats Either syntax 유용용한 method

asRight[LeftT], asLeft[RightT]

아래의 코드는 compile 되지 않는다.
이유는 접기 결과 유형이 Either가 아닌 Right로 타입 추론이 되면서 Left가 문제가 된다. 또 Right의 Left type parameter를 Nothing으로 추론 된다.

val xs = List(1,2,3,4,5)
xs.foldLeft(Right(0))((acum,i) => 
 if(i < 3) acum.map(n => n + 1) else Left("Not Supported"))

따라서 compile error가 나지 않게 하기 위해서 다음과 같이 하면 된다.

val xs = List(1,2,3,4,5)
xs.foldLeft[Either[String,Int]](Right(0))((acum, i) 
 => if(i < 3) acum.map(n => n + i) else Left("Not Supported"))

이를 좀더 쉽게 cats의 asRight[LeftT], asLeft[RightT]를 사용하면 같은 결과를 가질 수 있다.

import cats.syntax.either._
  // asRight[LeftT]  asLeft[RightT]
xs.foldLeft(0.asRight[String])((acum, i) => 
	if(i < 3) acum.map(n => n + i) else "Not Supprted".asLeft[Int])
// Either[String,Int]

cats.syntax.either.EitherObjectOps

cats.syntax.either 에는 Either companion object type를 받는 method들이 있다.

  1. catchOnly[A], catchNonFatal[A]
val rs03 = Either.catchOnly[NumberFormatException]("foo".toInt)
println(rs03)
//Left(java.lang.NumberFormatException: For input string: "foo")

//def catchNonFatal[A](f: => A): Either[Throwable, A]
val rs04 = Either.catchNonFatal(sys.error("Occur error"))
println(rs04)
//Left(java.lang.RuntimeException: Occur error)
  1. fromTry[A](t: Try[A]): Either[Throwable, A]
val rs05 = Either.fromTry(Try("foo".toInt))
println(rs05)
//Left(java.lang.NumberFormatException: For input string: "foo")
  1. def fromOption[A, B](o: Option[B], ifNone: => A): Either[A, B]
val rs06 = Either.fromOption[String,Int](None, "Oops!")
println(rs06)
//Left(Oops!)

Transforming Either

cats.syntax.ethier 에는 변환 method들이 많다.

  1. orElse, getOrElse
//1. orElse, 2. getOrElse
val rs07 ="Error".asLeft[Int].getOrElse(0)
println(rs07) //0
val rs08 = "Error".asLeft[Int].orElse(3.asRight[String])
println(rs08) //Right(3)
  1. ensure Right의 조건 검증
val rs09 = -1.asRight[String].ensure("must not be negative value")(i => i >= 0)
println(rs09)
//Left(must not be negative value)
  1. recover, recoverWith
val rs10 = "Error".asLeft[Int].recover{
  case _: String => -1
}
println(rs10)
//-1

val rs11 = "Error".asLeft[Int].recoverWith {
  case _: String => Right(-1)
}
println(rs11)
//Right(-1)
  1. leftMap, bimap
val rs12 = "leftMap".asLeft[Int].leftMap(_.reverse)
println(rs12)//Left(paMtfel)

val rs13 = 5.asRight[String].bimap(msg => msg.reverse, i => i * 10)
val rs14 = "leftMap".asLeft[Int].bimap(ms => ms.reverse, i => i * 10)
println(rs13)//Right(50)
println(rs14)//Left(paMtfel)
  1. swap
//swap
val rs15 = "foo".asLeft[Int].swap
println(rs15)//Right(foo)

Eval Monad

평가

eager : 즉시 평가(값으로서)
lazy : 실행시 평가
memoized : 실행시 평가 이후 cache

//vals = eager + memoized
val x = {
  println("Computing X")
  math.random()
}
//Computing X
//x: Double = 0.7810079442262585

println(x)
//0.7810079442262585
println(x)
//0.7810079442262585
println(x)
//0.7810079442262585

//def = lazy + not memoized
def y = {
  println("Computing Y")
  math.random()
}
//y: Double
println(y)
//Computing Y
//0.5771256769413449
println(y)
//Computing Y
//0.21990502916453714
println(y)
//Computing Y
//0.4958650011258595

//lazy vals = lazy + memoized
lazy val z = {
  println("Computing Z")
  math.random()
}
//z: Double = <lazy>
println(z)
//Computing Z
//0.614124247006023
println(z)
//0.614124247006023
println(z)
//0.614124247006023

cats에서 Now 는 즉시평가(val과 같이),
Always는 실행시 매번 평가(def 같이),
Later는 처음 실행시 평가 이후 cached처럼(momoized 같이) 작동 한다.

import cats.Eval
val cx = Eval.now(math.random())
//cx: cats.Eval[Double] = Now(0.7542159377020204)
val cy = Eval.always(math.random())
//cy: cats.Eval[Double] = cats.Always@3cc198f7
val cz = Eval.later(math.random())
//cz: cats.Eval[Double] = cats.Later@531d6244

println(cx.value)//0.7542159377020204
println(cy.value)//0.5143690057772041
println(cz.value)//0.012411074690963253

println(cx.value)//0.7542159377020204
println(cy.value)//0.5093222535099888
println(cz.value)//0.012411074690963253

Eval Monad

import cats.Eval
val greeting: Eval[String] = Eval.always{ println("Step 01"); "Hellow";}.map{s => println("Step 02"); s"$s world";}

println(greeting.value)
println(greeting.value)
/* 결과
start evaluation
Step 01
Step 02
Hellow world
Step 01
Step 02
Hellow world
*/

위에 결과를 보면 always는 def 처럼 평가 됨을 알 수 있다.

val ans: Eval[Int] = for {
  a <- Eval.now{ println("Step01"); 10} //바로 평가 val 즉 값처럼 
  b <- Eval.always{println("Step02"); 5}
} yield {
  println("Add A and B");
  a + b
}

println("start evaluation")
println(ans.value)
println(ans.value)

/*결과
Step01 //선언과 동시에 출력
start evaluation
Step02
Add A and B
15
Step02
Add A and B
15
*/

위에 결과를 보면 평가전에 Step01 이 찍히는 것으로 봐서 선언과 동시에 값으로서 평가가 되어 진후 아래의 사용할 때 step01은 찍히지 않는 것을 보아 다시 평가 하지 않는다.

val saying = Eval.always{ println("Step01"); "The cat"}
  .map{s => println("Step02"); s" $s sat on"}.memoize
  .map{s => println("Step03"); s"$s the mat"}

println("start evaluation")
println(saying.value)
println(saying.value)

/*결과
start evaluation
Step01
Step02
Step03
 The cat sat on the mat
Step03 //memoize 로 인하여 위의 연산 chain결과는 cache처럼 
 The cat sat on the mat
*/

위의 예제에서 memoize 로 인하여 앞에서의 연산chain이 한번 평가 이후 2번째 부터는 cahce처럼 행동