본문 바로가기

Programmer Jinyo/Scala & FP

scala with cats 책 읽으면서 필기(하다보니 번역급) Chapter 4 (Monads)


투명한 기부를 하고싶다면 이 링크로 와보세요! 🥰 (클릭!)

바이낸스(₿) 수수료 평생 20% 할인받는 링크로 가입하기! 🔥 (클릭!)

이 글은

2020/02/12 - [Programmer Jinyo/Scala & AKKA] - scala with cats 책 읽으면서 필기(하다보니 번역급) Chapter 3

여기에서 이어지는 글이며, 스칼라 with cats 를 보며 작성했습니다.

다음 글은

2020/02/20 - [Programmer Jinyo/Scala & AKKA] - scala with cats 책 읽으면서 필기(하다보니 번역급) Chapter 5 (Monad Transformers)

입니다.

 

* 이 책에서 말하는 모나드의 설명이 사실 실제로 수학적으로 정의된 모나드의 정의랑은 조금 상이한 부분이 있습니다. 따로 정리하지는 않겠지만 알아두시고 공부하시는 것이 좋을 것 같아 말씀드립니다.

Monads

모나드는 Scala에서 가장 일반적인 추상화이다. 많은 스칼라 프로그래머들은 빠르게 모나드 자체를 모르더라도 모나드에 대해서 직감적으로 익숙해진다. 

쉽게 말해, 모나드는 생성자와 flatMap method에 대한 그 어떤 것이라도 모나드이다. 우리가 저번 챕터에서 봤던 functor들도 모나드이다. (Option, List, Future같은) 우리는 monad를 위한 syntax도 있다. for comprehension이 그것이다. 그러나 이런 보편적인 컨셉에도 불구하고, 스칼라 기본 라이브러리에는 "flatMap이 적용될 수 있는 것들"이라는 것이 적용될 수 있는 기반 타입이 부족하다. 이 타입 클래스는 Cats를 사용함으로써 얻어질 수 있는 이득 중 하나이다.

이 챕터에서는 모나드에 대해서 파 볼 것이다. 몇몇 예시를 보면서 공식적인 정의와 그것이 Cats에서 어떻게 구현되어 있는지를 볼 것이다. 

 

4.1 What is Monad?

블로그에 이걸 설명하는 수천개의 비유들과 설명 등등이 있다. 그치만 명확하고 간단하게 설명하고 넘어가도록 하겠다.

 

A monad is a mechanism for sequencing computations.

모나드는 연산을 이어붙이는 방법이다. (라고 해석하는게 제대로 된 해석일까 싶어서 원문도 써놨다.)

 

개꿀ㅋ 그치만.. 좀 더 설명이 필요.

 

3.1 절에서 functors는 우리에게 복잡성을 무시하며 연산을 이어붙일 수 있게 만들어 준다고 했다. 그치만 펑터는 그 시작 지점에서만 조작이 가능하다는 점에서 한계가 있었다. 매 절차마다 추가적인 이런 저런 복잡성을 추가하기가 힘들다.

여기에 모나드가 필요하다. 모나드의 flatMap 메소드는 다음에 무슨 일이 일어날지 특정해주고, 중간의 문제를 고려할 수 있게 해 준다. Option의 flatMap 메소드는 중간의 Option들을 고려한다. List의 flatMap은 List들의 중간을 고려한다. (등등..) flatmap은 계속 그 연산의 결과를 처리하고, 그 이후의 flatMap을 적용하는 연쇄적인 과정을 거쳐간다. 몇가지 예시를 살펴보자.

 

Options

Option은 우리에게 (리턴을 할 수도, 안 할 수도 있는) 연산들을 이어붙일 수 있게 해 준다. 예시가 있다.

def parseInt(str: String): Option[Int] = scala.util.Try(str.toInt).toOption

def divide(a: Int, b: Int): Option[Int] = if(b == 0) None else Some(a / b)

이 메소드는 간혹 None을 리턴하며 실패 할 것이다. flatMap method는 연산 과정 중 이런 결과를 무시하는 처리가 가능하게 해 준다.

def stringDivideBy(aStr: String, bStr: String): Option[Int] =
  parseInt(aStr).flatMap { aNum =>
    parseInt(bStr).flatMap { bNum => divide(aNum, bNum)
  }
}

우리는 저 의미를 알고 있다.

- 첫 parseInt 호출은 None이나 Some을 호출할 것이다.

- Some을 반환하면, flatMap 메소드는 int aNum을 받아서 함수를 호출해 줄 것이다.

- 두번째 parseInt 호출은 None이나 Some을 호출할 것이다.

- 만약 Some을 리턴하면, 그 값을 bNum에 담아서 그 다음 함수를 호출 해 줄 것이다.

- divide는 None이나 Some을 그 결과에 따라 알아서 리턴 해 줄 것이다.

 

 

 

 

각 스텝에서, flatMap은 함수를 호출 할 것인지를 결정하고, 우리의 함수는 다음 순서의 연산을 생성해낸다. 그림 4.1에 표현되어 있다. 

이 연산의 결과는 Option이며, 우리에게 flatMap을 다시 호출하는 것을 허락 해 줌으로써 연산들이 계속될 수 있게 해 준다. 이 fail-fast error handling 방법은 연산의 중간에 None이 한번 나오면 결과를 None으로 만들어버린다.

stringDivideBy("6", "2")
// res1: Option[Int] = Some(3)

stringDivideBy("6", "0")
// res2: Option[Int] = None

stringDivideBy("6", "foo")
// res3: Option[Int] = None

stringDivideBy("bar", "2")
// res4: Option[Int] = None

모든 모나드는 펑터이며, 그래서 우리는 flatMap이나 map을 동시에 적용할 수 있으며 새로운 모나드를 도입하지 않는다. 더해서, 만약 우리가 flatMap과 map을 가지고 있다면 우리는 연속적인 행동을 명확히 표시하기 위해 for comprehensions를 사용할 수 있다.

def stringDivideBy(aStr: String, bStr: String): Option[Int] =
for {
  aNum <- parseInt(aStr)
  bNum <- parseInt(bStr)
  ans <- divide(aNum, bNum)
} yield ans

 

Lists

우리가 Scala developers를 보기 시작하면서 처음 flatMap을 만났을 때, 우리는 Lists를 순회하는 패턴이라고 생각하는 경향이 있다. 이것은 for comprehension의 문법 때문에 (명령형 for loops같아 보이는)의  더욱 그렇게 여겨지고는 한다.

for {
  x <- (1 to 3).toList
  y <- (4 to 5).toList
} yield (x, y)
// res5: List[(Int, Int)] = List((1,4), (1,5), (2,4), (2,5), (3,4), (3,5))

그러나, List의 모나드스러운 행동에 주목해야 하는 또다른 포인트가 있다. List들을 중간 결과의 집합이라고 생각하면, flatMap 은 조합의 순열을 계산하는 생성이 된다. (...)

예를 들어, 위의 for comprehension의 경우, x에는 3가지 가능한 조합, y에는 2가지 가능한 조합이 있다. 그렇다면 6가지의 (x,y)순서쌍에 대한 가능성이 있는 것이다. flatMap은 우리의 코드로부터 다음과 같은 연산을 통해 조합을 생성해낸다.

- get x

- get y

- create a tuple(x,y)

 

Futures

Future은 비동기에 대한 걱정없이 연산을 이어붙이는 모나드이다.

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._

def doSomethingLongRunning: Future[Int] = ???
def doSomethingElseLongRunning: Future[Int] = ???

def doSomethingVeryLongRunning: Future[Int] =
  for {
    result1 <- doSomethingLongRunning
    result2 <- doSomethingElseLongRunning
  } yield result1 + result2

반복하지만, 우리는 각 스텝에 실행할 코드를 특정하고, flatMap이 스케줄러와 쓰레드풀의 무섭고 근본적인 복잡성을 처리 해 준다.

만약 당신이 Future의 더 큰 활용을 해봤다면,  위의 코드는 순서대로 돌아간다는 것을 알 것이다. 

위의 코드는 for comprehension을 늘여서 써보면 더욱 명확해진다.

def doSomethingVeryLongRunning: Future[Int] =
  doSomethingLongRunning.flatMap { result1 =>
    doSomethingElseLongRunning.map { result2 =>
      result1 + result2
    }
  }

우리의 시퀀스의 퓨처는 이전 퓨처들의 결과들로부터 전달받은 함수로부터 만들어졌다. 다른 말로는, 우리의 연산의 각 스텝은 이전의 스텝이 끝나야만 시작된다는 뜻이다. 그림 4.2에 표시되어있다. A => Future[B]로 가는 함수 파라미터를 받는다. (? 맞나..?)

 

4.1.1 Definition of a Monad

우리가 위의 flatMap에 관해서만 말했었는데, monadic 행동은 두 연산에 의해 공식적으로는 알아볼 수 있다.

- pure , of type A => F[A]

- flatMap, of type (F[A], A => F[B]) => F[B].

 

pure은 생성자들에 대해 추상화되어있는 개념인데, plain value로부터 monadic 컨텍스트를 만드는 방법을 제공한다.

flatMap은 우리가 이미 토의한, 이미 존재하는 context로부터 값을 뽑아내서 다음 시퀀스에 있는 context를 만들어내는 시퀀싱 스텝을 제공한다. Cats의 모나드 타입 클래스의 간소화된 버전의 코드를 보자.

import scala.language.higherKinds

trait Monad[F[_]] {
  def pure[A](value: A): F[A]

  def flatMap[A, B](value: F[A])(func: A => F[B]): F[B]
}

Monad Laws

pure와 flatMap은 다음의 규칙을 만족해야 한다. 이는 우리에게 부수효과나 의도되지 않은 작은 문제들을 신경쓰지 않아도 되게 해 준다.

Left identiy:

pure(a).flatMap(func) == func(a)

Right identity :

m.flatMap(pure) == m

Associativity:

m.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))

 

 

4.1.2 Exercise: Getting Func-y

모든 모나드는 펑터이기도 하다. 모든 모나드는 따라서 이미 존재하는 pure, flatMap 연산을 통해 map 연산을 정의할 수 있다.

import scala.language.higherKinds
trait Monad[F[_]] {
  def pure[A](a: A): F[A]
  def flatMap[A, B](value: F[A])(func: A => F[B]): F[B]
  def map[A, B](value: F[A])(func: A => B): F[B] = ???
}

머 걍

  trait Monad[F[_]] {
    def pure[A](a: A): F[A]
    def flatMap[A, B](value: F[A])(func: A => F[B]): F[B]
    def map[A, B](value: F[A])(func: A => B): F[B]  = flatMap(value)(a=>pure(func(a)))
  }

일케함 댄다. (다만 내가 알기로는 모든 map 연산이 pure(flatMap())은 아니다.)

 

4.2 Monads in Cats

모나드에 cats를 뿌려보자. 늘 그래왔듯, 타입클래스, 인스턴스, syntax를 볼 것이다.

 

4.2.1 The Monad Type Class

모나드 타입클래스는 cats.Monad이다. Monad는 FlatMap (flatMap 메소드를 제공하는)과 Applicative(pure을 제공하는)을 상속받는다. Applicative는 또한 Functor을 상속받기 때문에 Monad는 위에서 본 대로, map method를 제공받는다. Applicatives는 6장에서 살펴본다.

아래에 pure와 flatMap과 map을 직접적으로 사용한 예시가 있다.

import cats.Monad
import cats.instances.option._ // for Monad
import cats.instances.list._ // for Monad

val opt1 = Monad[Option].pure(3)
// opt1: Option[Int] = Some(3)

val opt2 = Monad[Option].flatMap(opt1)(a => Some(a + 2))
// opt2: Option[Int] = Some(5)

val opt3 = Monad[Option].map(opt2)(a => 100 * a)
// opt3: Option[Int] = Some(500)

val list1 = Monad[List].pure(3)
// list1: List[Int] = List(3)

val list2 = Monad[List].flatMap(List(1, 2, 3))(a => List(a, a*10))
// list2: List[Int] = List(1, 10, 2, 20, 3, 30)

val list3 = Monad[List].map(list2)(a => a + 123)
// list3: List[Int] = List(124, 133, 125, 143, 126, 153)

모나드는 Functor의 모든 메소드를 포함하여 많은 다른 메소드를 제공한다. 

 

4.2.2 Default Instances

Cats 는 cats.instances를 통해 많은 standard library(Option, List, Vector 등등)안에 있는 모든 모나드들을 위한 인스턴스들을 제공한다.

import cats.instances.option._ // for Monad

Monad[Option].flatMap(Option(1))(a => Option(a*2))
// res0: Option[Int] = Some(2)

import cats.instances.list._ // for Monad

Monad[List].flatMap(List(1, 2, 3))(a => List(a, a*10))
// res1: List[Int] = List(1, 10, 2, 20, 3, 30)

import cats.instances.vector._ // for Monad

Monad[Vector].flatMap(Vector(1, 2, 3))(a => Vector(a, a*10))
// res2: Vector[Int] = Vector(1, 10, 2, 20, 3, 30)

Cats는 Future을 위한 Monad도 제공한다. Future class 자신에 있는 method들과는 다르게, 모나드에 있는 pure와 flatMap 메소드는 implicit ExecutionContext 파라미터를  받을 수 없다. (왜냐하면 파라미터는 모나드 트레이트에 정의되어 있지 않기 때문이다) 이것을 해결하려면, Cats는 Future을 위한 Monad를 부를 때 ExecutionContext가 스코프 내에 있어야 한다.

import cats.instances.future._ // for Monad
import scala.concurrent._
import scala.concurrent.duration._

val fm = Monad[Future]
// <console>:37: error: could not find implicit value for parameter instance: cats.Monad[scala.concurrent.Future]
// val fm = Monad[Future]
//               ^

ExecutionContext를 스코프 내로 가져오면 해결!

import scala.concurrent.ExecutionContext.Implicits.global

val fm = Monad[Future]
// fm: cats.Monad[scala.concurrent.Future] = cats.instances.FutureInstances$$anon$1@3c5f21ab

모나드 인스턴스는 pure와 flatMap에 하위 호출을 위해 캡쳐된 ExecutionContext를 사용한다.

val future = fm.flatMap(fm.pure(1))(x => fm.pure(x + 2))

Await.result(future, 1.second)
// res3: Int = 3

위에 더해서, Cats 는 기본 라이브러리에 없는 새로운 모나드도 제공 해 준다. 요 친구들이랑 친해져보자.

 

4.2.3 Monad Syntax

모나드의 syntax는 세 곳에서 온다.

 

- cats.syntax.flatMap provides syntax for flatMap;

- cats.syntax.functor provides syntax for map;

- cats.syntax.applicative provides syntax for pure

 

이걸 cats.implicits에서 import해서 쓰는게 실제로는 훨씬 편하다. 그러나 예시에서는 각각의 명확함을 위해 그냥 각각 임포트해서 쓸 것이다.

우리는 pure을 모나드의 인스턴스를 생성하기 위해 쓸 수 있다. 우리는 우리가 원하는 특정 타입 파라미터의 애매모호함을 없애기 위해 이를 주로 사용하게 될 것이다.

import cats.instances.option._ // for Monad
import cats.instances.list._ // for Monad
import cats.syntax.applicative._ // for pure

1.pure[Option]
// res4: Option[Int] = Some(1)

1.pure[List]
// res5: List[Int] = List(1)

Scala monad에서 List나 Option과는 다르게 flatMap과 map method를 직접적으로  표현하는 것은 힘들다. 왜냐하면 그들은 그들의 메소드의 명확한 버전을 정의하기 때문이다. 대신 우리는 user의 선택에 따라 모나드로 감싸진 파라미터를 계산하는 제네릭 함수를 작성 할 것이다.

import cats.Monad
import cats.syntax.functor._ // for map
import cats.syntax.flatMap._ // for flatMap
import scala.language.higherKinds

def sumSquare[F[_]: Monad](a: F[Int], b: F[Int]): F[Int] = a.flatMap(x => b.map(y => x*x + y*y))

import cats.instances.option._ // for Monad
import cats.instances.list._ // for Monad

sumSquare(Option(3), Option(4))
// res8: Option[Int] = Some(25)

sumSquare(List(1, 2, 3), List(4, 5))
// res9: List[Int] = List(17, 26, 20, 29, 25, 34)

위의 함수를 for comprehension을 사용해서 재 작성할 수 있다. 컴파일러는 우리의 comprehension을 다시 작성하여 정확히 일치하는 작업을 할 것이다.

def sumSquare[F[_]: Monad](a: F[Int], b: F[Int]): F[Int] =
for {
  x <- a
  y <- b
} yield x*x + y*y

sumSquare(Option(3), Option(4))
// res10: Option[Int] = Some(25)

sumSquare(List(1, 2, 3), List(4, 5))
// res11: List[Int] = List(17, 26, 20, 29, 25, 34)

이것은 Cats의 monad에 대해 알아야 할 일반적인 것의 모든것이다. 이제 스칼라 표준 라이브러리에 있는 더 유용한 모나드들의 종류에 대해 살펴보도록 하자.

 

4.3 The Identity Monad

이전 절에서 다른 모나드들을 사용하여 추상화된 메소드를 작성함으로써 Cats의 flatMap과 map syntax를 보여주었다.

import scala.language.higherKinds
import cats.Monad
import cats.syntax.functor._ // for map
import cats.syntax.flatMap._ // for flatMap

def sumSquare[F[_]: Monad](a: F[Int], b: F[Int]): F[Int] =
  for {
    x <- a
    y <- b
  } yield x*x + y*y

요 친구들은 Options나 Lists에서는 잘 동작하지만 plain values에서는 잘 동작하지 않는다.

sumSquare(3, 4)
// <console>:22: error: no type parameters for method sumSquare: (a: F[Int], b: F[Int])(implicit evidence$1: cats.Monad[F])F[Int] exist so that it can be applied to arguments (Int, Int)
// --- because ---
// argument expression's type is not compatible with formal parameter type;
// found : Int
// required: ?F[Int]
// sumSquare(3, 4)
//            ^
// <console>:22: error: type mismatch;
// found : Int(3)
// required: F[Int]
// sumSquare(3, 4)
//            ^
// <console>:22: error: type mismatch;
// found : Int(4)
// required: F[Int]
// sumSquare(3, 4)
//            ^

모나드이던지 아니던지 sumSquare을 쓸 수 있으면 훨씬 유용할 것 같다. 이것은 우리에게 monadic 과 non-monadic code를 동시에 추상화 할 수 있게 할 것이다. 운좋게, Cats는 이 간격을 이어줄 Id type을 가지고 있다.

import cats.Id

sumSquare(3 : Id[Int], 4 : Id[Int])
// res2: cats.Id[Int] = 25

Id 는 plain values를 사용하여 우리의 monadic 메소드를 호출할 수 있게 해준다. 그러나 구체적인 의미는 이해하기 힘들다. 우리는 파라미터를 sumSquare에 Id[Int]로 캐스팅하고 Id[Int]로 결과로 받아온다!

 

?

 

정의를 보면 이해가 간다.

package cats

type Id[A] = A

Id는 사실 단일 파라미터 타입 생성자를 원자 타입으로 바꿔주는 타입 얼라이어스이다. 우리는 어떤 값이라도 Id에 해당하는 타입으로 캐스팅 할 수 있다.

"Dave" : Id[String]
// res3: cats.Id[String] = Dave

123 : Id[Int]
// res4: cats.Id[Int] = 123

List(1, 2, 3) : Id[List[Int]]
// res5: cats.Id[List[Int]] = List(1, 2, 3)

Cats는 Id를 위한 Functor, Monad를 포함하는 다양한 타입 클래스들을 제공한다.이것들이 우리로 하여금 map, flatMap, pure을 plain value를 넘길 수 있게 해 준다.

val a = Monad[Id].pure(3)
// a: cats.Id[Int] = 3

val b = Monad[Id].flatMap(a)(_ + 1)
// b: cats.Id[Int] = 4

import cats.syntax.functor._ // for map
import cats.syntax.flatMap._ // for flatMap

for {
  x <- a
  y <- b
} yield x + y
// res6: cats.Id[Int] = 7

모나딕 / non-모나딕 값을 둘다 사용할 수 있는 점은 굉장히 유용하다. 예를 들어, 배포 환경에서는 Future을 사용해서 비동기적으로 코드를 실행시키면서 테스트는 Id를 사용하여 동기적으로 동작시킬 수 있다.

 

4.3.1 Exercise: Monadic Secret Identities

Id를 위해 pure , map , flatMap 를 만들어라! 

  def pure[A](value: A): Id[A] = {
    value
  }
  def flatMap[A, B](value: Id[A])(func: A => Id[B]): Id[B] = {
    func(value)
  }
  def map[A, B](value: Id[A])(func: A => B): Id[B] = {
    func(value)
  }

우선은 위와 같이 함수만 정의하면 된다.

번외 : 

더보기

* 난 또 헷갈려버리고 말았다. 이전에 나온 방법들에 대해 다시 생각 해 보자.

implicit val 타입클래스_타입2 : 타입클래스[타입2] = new 타입클래스[타입2] {

  def 미리_정의된_함수들_오버라이딩

}

-> 이 경우는 Id에 미리 정의된 함수를 임의의 특별한 타입에 대해서 오버라이딩 하는게 아니기 때문에 이런 식으로 하는게 아니다.

 

 

(감싸진 타입은 Box나 Id같은 친구들)

implicit def 감싸진타입_타입클래스[A](implicit ev:타입클래스[A]):타입클래스[감싸진타입[A]] = new 타입클래스[감싸진타입[A]]{

  def 미리_정의된_함수_오버라이딩

}

-> 뭐 이런 식으로 가능은 한데, 함 해보자. 모나드를 상속받는게 자연스러워 보인다. 근데 모나드는 F[_] 요렇게 안에 빵꾸 있는 타입 자체를 받는 친구이기 때문에

  implicit def IdMonad:Monad[Id] = new Monad[Id] {
    override def pure[A](a: A): Id[A] = {
      a
    }
    override def flatMap[A, B](value: Id[A])(func: A => Id[B]): Id[B] = {
      func(value)
    }

    override def map[A, B](value: Id[A])(func: A => B): Id[B] = this.flatMap(value)(a=>this.pure(func(a)))
  }

이렇게 작성하면 될 것이다. 굿굿~ thanks professor Ji !

-

4.4 Either

또다른 조흔 모나드를 보자. Either은 스칼라 표준 라이브러리 출신이다. 스칼라 2.11이하 버전에서는 map이나 flatMap 메소드가 없었다. 그치만... 2.12에서는 Either은 right biased가 되었다.

 

4.4.1 Left and Right Bias

스칼라 2.11에서, Either은 기본적인 map이나 flatMap 메소드가 없었다. 이것은 스칼라 2.11버전의 Either을 for comprehensions에서 사용하기 불편하게 만들었다. 우리는 따라서 매번 .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)

스칼라 2.12에서, Either은 재 디자인 되었다. 최신 Either은 오른쪽이 성공 케이스를 나타내게 됨으로써 직접적으로 map과 flatMap을 지원하게 되었다. 따라서 더 편하게 프로그래밍이 가능하게 됐다.

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

4.4.2 Creating Instances

Left 나 Right 의 인스턴스를 만들기 위한 또 한가지 방법으로는, cats.syntax.either에서 asLeft 와 asRight 메소드를 import하는 것이다.

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.apply와 Right.apply라는 장점을 가지고 있는데, 그들은 Left 나 Right 대신 Either 타입을 리턴하기 때문이다. 이것은 아래의 예시같은 over-narrowing에 의한 타입 추론(inference) 버그를 피할 수 있게 해 준다.

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!")
//                 ^

위 코드는 두가지 이유로 버그가 발생한다.

1. 컴파일러는 accumulator을 Either대신 Right으로 추론한다.

2. 우리는 Right.apply를 정해주지 않았기 때문에 컴파일러는 left 파라미터를 Nothing이라고 인지한다.

그러므로 우린 똑똑이 타입 생성자 asRight를 사용하면 된다.

 

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 메소드는 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 인스턴스를 위한 유용한 메소드를 제공한다. orElse나 getOrElse를 사용해서 default를 리턴하거나 오른쪽 값을 뽑아주거나 하는데에 쓸 수 있다.

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 method는 right-hand value가 predicate(서술부? 조건문?)을 만족하는지 체크할 수 있게 한다.

-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 메소드는 우리에게 왼쪽과 오른쪽을 바꿀 수 있게 한다.

123.asRight[String]
// res19: Either[String,Int] = Right(123)

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

마지막으로, 변환 메소드들도 있는데, toOption,toList,toTry,toValidated 등이 그것이다.

 

4.4.4 Error Handling

Either은 일반적으로 fail-fast error handling을 구현하기 위해 쓰인다. 우리는 flatMap을 사용해 보통 연산을 시퀀싱한다. 하나의 연산이 실패하면, 나머지 연산들은 동작하지 않을 것이다.

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이 굉장히 광범위한 타입이라는 것이다. 우리는 거의 어떤 타입의 에러가 발생하는지 알 수가 없다.

 

또다른 접근은 프로그램 안에서 발생할 수 있는 에러를 표현할 대수적인 데이터 타입을 정의하는 것이다. 

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.5 Aside: Error Handling and MonadError

Cats는 MonadError라는 추가적인 타입클래스를 제공하는데, 에러핸들링을 위해 사용하는 Either같은 데이터 타입들에 대한 추상화를 제공한다. MonadError은 에러를 발생시키고 조절할 수 있는 추가적인 연산을 제공한다.

 

This Section is Optional!

난 공부 할거야 응 수고~

 

4.5.1 The MonadError Type Class

모나드 에러의 간단한 버전의 정의가 있다.

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은 두 타입 파라미터들로 정의되어 있다.

- F는 모나드 타입이다.

- E는 F안에 포함되어있는 에러 타입이다.

 

어떻게 이 파라미터들이 같이 작동하는지 보려면  아래의 Either을 위한 타입클래스의 에시를 보자.

import cats.MonadError
import cats.instances.either._ // for MonadError

type ErrorOr[A] = Either[String, A]

val monadError = MonadError[ErrorOr, String]

그냥 느낌으로 보니, 에러 헨들링하는 타입들을 감싸는 친구인 것 같다.

 

ApplicativeError

실제로는, MonadError는 ApplicativeError라는 타입 클래스를 상속받는다. 그러나 챕터6 전에는 Applicatives를 보지 않을 것이다. 의미는 각 타입클래스와 동일하기에 디테일한건 지금은 무시하도록 한다.

 

4.5.2 Raising and Handling Errors

두가지 중요한 MonadError의 메소드는 raiseError와 handleError이다. raiseError은 그것이 실패를 표현하는 인스턴스를 만들어낸다는 것만 빼면 Monad를 위한 pure method와 비슷하다. 감싸진 안쪽의 타입(ErrorOr)과의 상관관계를 주의깊게 보는것이 좋을 것 같다.

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

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

 

handleError은 raiseError의 보완물이다. 그것은 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이라고, filter-like 행동을 구현하는 메소드도 있다. 우리의 조건문에 따라 성공/실패한 모나드의 값인지를 나누고 조건문이 false일 때는 어떤 에러를 띄울지 특정한다.

 

Cats 는 riseError와 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등을 포함한 다양한 데이터 타입에 대한 인스턴스들을 제공한다. 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.6 The Eval Monad

cats.Eval은 평가 모델에 대한 추상화를 가능하게 해준다. 우리는 예전부터 eager, lazy라는 두개의 모델에 대해 들어보았을 것이다. Eval은 결과가 memoized 되는지 안되는지에 대해 더욱 뚜렷히 구분한다.

 

4.6.1 Eager, Lazy, Memoized, Oh My!

이 용어들이 뭘 의미할까? 

Eager 연산은 바로 발생하는 반면 lazy 연산은 접근시 이루어진다. Memoized 연산은 처음만 실행되며 그 결과가 캐시된다.

예를 들어 스칼라 val 은 eager이며 memoized이다. 아래의 예시를 보면, x는 정의 시점에 계산되며 그 값은 다시 실행되지 않는다.

val x = {
  println("Computing X")
  math.random
}
// Computing X
// x: Double = 0.828821237871653

x // first access
// res0: Double = 0.828821237871653

x // second access
// res1: Double = 0.828821237871653

 

반면, def 들은 lazy이며 memoized가 아니다.  lazy하며 not memoized라 할 수 있다.

def y = {
  println("Computing Y")
  math.random
}
// y: Double

y // first access
// Computing Y
// res2: Double = 0.8068798927021629

y // second access
// Computing Y
// res3: Double = 0.9741888122553769

 

마지막으로, lazy val은 lazy하며 memoized이다. 처음 접근 시 캐싱된다.

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

z // first access
// Computing Z
// res4: Double = 0.8103134694961557

z // second access
// res5: Double = 0.8103134694961557

 

4.6.2 Eval’s Models of Evaluation

Eval은 Now, Later, Always 라는 세 개의 subtype이 있다. 우리는 이것들을 세개의 Eval type인 클래스의 인스턴스들을 만들어 리턴하는 세개의 생성자 매소드를 통해 만든다.

import cats.Eval

val now = Eval.now(math.random + 1000)
// now: cats.Eval[Double] = Now(1000.6093560712978)

val later = Eval.later(math.random + 2000)
// later: cats.Eval[Double] = cats.Later@b38e255

val always = Eval.always(math.random + 3000)
// always: cats.Eval[Double] = cats.Always@7aa03230

우리는 Eval의 value메소드를 통해 결과를 뽑아낼 수 있다.

 

now.value
// res6: Double = 1000.6093560712978

later.value
// res7: Double = 2000.2568996853545

always.value
// res8: Double = 3000.7835394144035

각 타입의 Eval은 정의된 대로 계산을 진행한다. Eval.now는 값을 그 시점에 바로 캡쳐하한다. eager and memorized인 val과 비슷하다.

val x = Eval.now {
  println("Computing X")
  math.random
}
// Computing X
// x: cats.Eval[Double] = Now(0.8561858941490939)

x.value // first access
// res9: Double = 0.8561858941490939

x.value // second access
// res10: Double = 0.8561858941490939

Eval.always는 lazy 하게 연산한다. def와 비슷.

val y = Eval.always {
  println("Computing Y")
  math.random
}
// y: cats.Eval[Double] = cats.Always@3d65aec1

y.value // first access
// Computing Y
// res11: Double = 0.20044347463534973

y.value // second access
// Computing Y
// res12: Double = 0.6306326024648614

마지막으로, Eval.later은 lazy val과 같이 lazy, memoized 연산을 한다.

val z = Eval.later {
  println("Computing Z")
  math.random
}
// z: cats.Eval[Double] = cats.Later@6059069c

z.value // first access
// Computing Z
// res13: Double = 0.11754104909945928

z.value // second access
// res14: Double = 0.11754104909945928

 

참고~

 

4.6.3 Eval as a Monad

모든 모나드들과 마찬가지로, Eval의 map과 flatMap 메소드는 연산을 체인에 더해준다. 그러나 이 경우에는 체인이 명백히 함수들의 리스트로서 저장되어 있다. 함수들은 결과를 보기 위해 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@497e6146

greeting.value
// Step 1
// Step 2
// res15: String = Hello world

 

다만, Eval 인스턴스라고 할지라도 mapping functions는 항상 lazy하게 요청시에 불려진다 (def semantics)

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@6ac067b7

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

 

Eval은 연산의 체인을 memoize할 수 있게 해주는 method가 있다. call 시점부터 memoize 연산 까지에 대한 결과가 캐시되며, 호출 이후의 연산이 원래의 상태를 유지하고 있다.

 

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@225eed8c

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 Trampoling and Eval.defer

Eval의 유용한 속성은 map과 flatMap 메소드들이 trampolined 된다는 것이다. 그 말은, 우리가 스택 프레임을 소모하지 않고서도 임의로 map과 flatMap을 중첩하여 호출할 수 있다는 의미이다. 우리는 이것을 stack safety라고 부른다.

Trampoline
https://en.wikipedia.org/wiki/Trampoline_(computing)
연관검색어: #tail-recursive function

예를 들어, factorial을 연산하는 과정을 생각 해 보자.

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

이것은 스택오버플로우가 나기 쉽다.

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

우리는 이 메소드를 Eval을 사용해서 stack safe 하게 만들 수 있다.

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

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

는 - 실패 ㅋ

그 이유는 우리가 여전히 Eval의 map method를 사용하기 시작하기 전 factorial을 재귀적으로 호출하는 것을 전부 마킹하고 있기 때문이다.  이걸 Eval.defer이라는 이미 존재하는 Eval의 인스턴스를 사용하여 계산을 defer(미루다)할 수 있다. defer method는 map이나 flatMap처럼 trampolined 되기 때문에 우리는 이미 존재하는 연산을 stack safe하게 만드는 데에 빠르게 적용해볼 수 있다.

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 = 33473205095971448369154760940714864779127732238104548077301003219901680221443656

Eval은 stack safety를 강제하기 좋은 툴로, 큰 연산량, 데이터구조를 다루기 좋다. 그러나 trampolining이 공짜가 아니라는걸 알아두자. 이 함수의 체인은 힙 메모리에 쌓인다. 중첩 연산이 갚어지면 한계가 있지만 힙이 스택보단 훨씬 사이즈가 크다.

 

4.6.5 Exercise: Safer Folding using Eval

아래의 foldRight는 안전하지 않다. Eval을 써서 안전하게 만들어봐라.

 

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
}

 

난 요로케

  def foldRight[A, B](as: List[A], acc: B)(fn: (A, B) => B): Eval[B] =
    as match {
      case head :: tail =>
        Eval.defer(foldRight(tail, acc)(fn).map(x=>fn(head, x)))
      case Nil =>
        Eval.now(acc)
    }

  println(foldRight(List(1,2,3,4),0)((x,y)=>x+y).value)

했는데 이거 말고

 

  def foldRightEval[A, B](as: List[A], acc: Eval[B])(fn: (A, Eval[B]) => Eval[B]): Eval[B] =
    as match {
      case head :: tail =>
        Eval.defer(fn(head, foldRightEval(tail, acc)(fn)))
      case Nil =>
        acc
    }
  def foldRight[A, B](as: List[A], acc: B)(fn: (A, B) => B): B =
    foldRightEval(as, Eval.now(acc)) { (a, b) =>
      b.map(fn(a, _))
    }.value

이렇게도 짤 수 있다고 한다.

 

4.7 The Writer Monad

cats.data.Writer은 연산중에 로그를 찍을 수 있게 해준다. 레코드, 메시지, 에러, 추가적인 데이터 등등을 찍을 수 있고 마지막에 연산 결과와 함께 로그를 뽑을 수 있다.

Writers가 많이 쓰이는 예시로는, 멀티 쓰레드 연산시 기존의 명령형 로깅 기술은 서로 다른 컨텍스트에서 교차하여 나타나기 때문에 이를 스텝별 시퀀스를 기록하기 위한 용도로 쓴다. Writer과 함께라면 연산의 로그는 결과와 묶이므로 섞인 로그를 보지 않을 수 있다.

 

Cats Data Types

Writer은 cats.data 패키지에서 처음 살펴본 데이터 타입이다. 이 패키지는 유용한 기호를 생산하는 여러 타입 클래스 인스턴스를 제공하는 패키지이다. 다음 챕터에서 cats.data 친구들은 더 살펴 볼 것이고, Validated type은 챕터6에..!

 

4.7.1 Creating and Unpacking Writers

Writer[W,A]는 두 값을 가진다. type W의 로그, type A의 결과가 그것이다. 우리는 다음과 같이 타입을 가지는 Writer을 만들 수 있다.

import cats.data.Writer
import cats.instances.vector._ // for Monoid
Writer(Vector(
  "It was the best of times",
  "it was the worst of times"
), 1859)
// res0: cats.data.WriterT[cats.Id,scala.collection.immutable.Vector[String],Int] = WriterT((Vector(It was the best of times, it was the worst of times),1859))

 콘솔에 찍힌 건 Writer[Vector[String], Int]이 아니라 WriterT[Id, Vector[String], Int]임을 주목하자. 코드 재사용성 측면에서 Cats는 Writer을 다른 타입으로 구현 해 놓았다. WriterT는 monad transformer이라는 새로운 컨셉의 예시인데 다음 챕터에서 다룬다.

 

일단은 자세한건 무시하기로 하자. Writer은 WriterT의 type alias이다. 그러므로 우리는 WriterT[Id,W,A]를 Writer[W,A]라고 쓸 수 있다.

 

type Writer[W, A] = WriterT[Id, W, A]

 

편의를 위해 Cats는 Writers를 log나 result만을 지정해도 만들 수 있게 하였다. 만약 우리가 standard pure syntax에 우리가 원하는 result를 가지고 있기만 하면 말이다. 그러기 위해서 우리는 Cats가 empty log를 어떻게 만들지 알게하기 위해서 Monoid[W]를 스코프 내에 가지고 있어야 한다.

import cats.instances.vector._ // for Monoid
import cats.syntax.applicative._ // for pure

type Logged[A] = Writer[Vector[String], A]

123.pure[Logged]
// res2: Logged[Int] = WriterT((Vector(),123))

우리가 로그를 가지고 있고 결과가 없으면 우리는 cats.syntax.writer의 tell syntax를 사용하여 Writer[Unit]를 만들 수 있다.

import cats.syntax.writer._ // for tell
Vector("msg1", "msg2", "msg3").tell
// res3: cats.data.Writer[scala.collection.immutable.Vector[String],Unit] = WriterT((Vector(msg1, msg2, msg3),()))

result / log 둘다 가지고 있으면 Writer.apply나 cats.syntax.writer의 writer syntax를 전부 사용할 수 있다.

import cats.syntax.writer._ // for writer

val a = Writer(Vector("msg1", "msg2", "msg3"), 123)
// a: cats.data.WriterT[cats.Id,scala.collection.immutable.Vector[String],Int] = WriterT((Vector(msg1, msg2, msg3),123))

val b = 123.writer(Vector("msg1", "msg2", "msg3"))
// b: cats.data.Writer[scala.collection.immutable.Vector[String],Int] = WriterT((Vector(msg1, msg2, msg3),123))

우리는 value와 written 메소드를 각각 사용하여 result와 log를 writer에서 뽑아낼 수 있다. 

val aResult: Int = a.value
// aResult: Int = 123

val aLog: Vector[String] = a.written
// aLog: Vector[String] = Vector(msg1, msg2, msg3)

둘 다는 run 메소드를 사용하면 된다.

val (log, result) = b.run
// log: scala.collection.immutable.Vector[String] = Vector(msg1, msg2, msg3)
// result: Int = 123

4.7.2 Composing and Transforming Writers

Writer안의 log는 그것을 map이나 flatMap 할 때 제공된다. flatMap은 유저의 시퀀싱 함수의 결과와 source Wirter로부터 로그를 이어붙인다. 이런 이유로 append type이 잘 되어있는 타입으로 log type을 정하는게 좋다 (Vector같이)

val writer1 = for {
  a <- 10.pure[Logged]
  _ <- Vector("a", "b", "c").tell
  b <- 32.writer(Vector("x", "y", "z"))
} yield a + b
// writer1: cats.data.WriterT[cats.Id,Vector[String],Int] = WriterT((Vector(a, b, c, x, y, z),42))

writer1.run
// res4: cats.Id[(Vector[String], Int)] = (Vector(a, b, c, x, y, z),42)

결과를 map과 flatMap을 통해서 변환하기 위해 mapWritten 메소드로 Writer의 log를 변환할 수 있다.

val writer2 = writer1.mapWritten(_.map(_.toUpperCase))
// writer2: cats.data.WriterT[cats.Id,scala.collection.immutable.Vector[String],Int] = WriterT((Vector(A, B, C, X, Y, Z),42))

writer2.run
// res5: cats.Id[(scala.collection.immutable.Vector[String], Int)] = (Vector(A, B, C, X, Y, Z),42)

슈거를 없앤 여기까지의 중간정리.

  import cats.data.Writer
  import cats.syntax.writer._ // for tell
  import cats.instances.vector._ // for Monoid
  import cats.syntax.applicative._ // for pure

  type Logged[A] = Writer[Vector[String], A]

  val writer1 = 10.pure[Logged]
    .flatMap(a =>
      Vector("a", "b", "c").tell
        .flatMap(_ =>
          32.writer(Vector("x", "y", "z"))
            .map(b => a + b)
        )
    )
  // writer1: cats.data.WriterT[cats.Id,Vector[String],Int] = WriterT((Vector(a, b, c, x, y, z),42))

  println(writer1.run)
  // res4: cats.Id[(Vector[String], Int)] = (Vector(a, b, c, x, y, z),42)
  val writer2 = writer1.mapWritten(_.map(_.toUpperCase))
  // writer2: cats.data.WriterT[cats.Id,scala.collection.immutable.Vector[String],Int] = WriterT((Vector(A, B, C, X, Y, Z),42))
  println(writer2.run)
  // res5: cats.Id[(scala.collection.immutable.Vector[String], Int)] = (Vector(A, B, C, X, Y, Z),42)

*** flatMap이나 map을 하면 기존의 "로그들"은 자동으로 뒤로 가서 이어 붙는다

val writer1 = Vector("a", "b", "c").tell
    .flatMap(_ =>
      10.pure[Logged]
        .flatMap(a =>
          32.writer(Vector("x", "y", "z"))
            .map(b => a + b)
        )
    )

위의 결과도 (Vector(a, b, c, x, y, z),42) 이기 때문.

 

암튼, 아까의 writer2로 돌아와서,

우리는 log와 result를 동시에 bimap이나 mapBoth를 사용해서 변경할 수 있다. bimap은 두 함수 파라미터들을 가지는데, 하나는 로그를 위한 것이고 하나는 result를 위한 것이다.

 

val writer3 = writer1.bimap(
  log => log.map(_.toUpperCase),
  res => res * 100
)
// writer3: cats.data.WriterT[cats.Id,scala.collection.immutable.Vector[String],Int] = WriterT((Vector(A, B, C, X, Y, Z),4200))

writer3.run
// res6: cats.Id[(scala.collection.immutable.Vector[String], Int)] = (Vector(A, B, C, X, Y, Z),4200)

val writer4 = writer1.mapBoth { (log, res) =>
  val log2 = log.map(_ + "!")
  val res2 = res * 1000
  (log2, res2)
}
// writer4: cats.data.WriterT[cats.Id,scala.collection.immutable.Vector[String],Int] = WriterT((Vector(a!, b!, c!, x!, y!, z!),42000))

writer4.run
// res7: cats.Id[(scala.collection.immutable.Vector[String], Int)] = (Vector(a!, b!, c!, x!, y!, z!),42000)

마지막으로, reset 메소드를 통해서 로그를 지울 수 있고, swap메소드를 통해서 log와 result를 swap 할 수 있다(ㄷㄷ).

val writer5 = writer1.reset
// writer5: cats.data.WriterT[cats.Id,Vector[String],Int] = WriterT((Vector(),42))

writer5.run
// res8: cats.Id[(Vector[String], Int)] = (Vector(),42)

val writer6 = writer1.swap
// writer6: cats.data.WriterT[cats.Id,Int,Vector[String]] = WriterT((42,Vector(a, b, c, x, y, z)))

writer6.run
// res9: cats.Id[(Int, Vector[String])] = (42,Vector(a, b, c, x, y, z))

4.7.3 Exercise: Show Your Working

 

 

Writers는 멀티 쓰레드 환경에서 작업 로그를 찍는데에 유용하다. factorial 연산을 로깅하며 확인 해 보자.

아래의 factorial 함수는 팩토리얼을 계산하고 돌다가 바로바로 그 스탭을 찍어낸다. 도움을 줄 slowly 함수가 동작하는데에 확실히 일정 이상 시간을 걸리게 해준다. 아무리 작은 계산일지라도.

def slowly[A](body: => A) = try body finally Thread.sleep(100)

def factorial(n: Int): Int = {
  val ans = slowly(if(n == 0) 1 else n * factorial(n - 1))
  println(s"fact $n $ans")
  ans
}

여기에 아웃풋이 있다.

factorial(5)
// fact 0 1
// fact 1 1
// fact 2 2
// fact 3 6
// fact 4 24
// fact 5 120
// res11: Int = 120

우리가 만약 여러 팩토리얼을 병렬로 실행한다면, log message는 교차하여 남겨질 수 있다. 이건 메시지를 보기 힘들게 한다.

import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._

Await.result(Future.sequence(Vector(
  Future(factorial(3)),
  Future(factorial(3))
)), 5.seconds)
// fact 0 1
// fact 0 1
// fact 1 1
// fact 1 1
// fact 2 2
// fact 2 2
// fact 3 6
// fact 3 6
// res14: scala.collection.immutable.Vector[Int] =
// Vector(120, 120)

위의 코드를 다시 작성해서 로그를 잘 남겨보자.

 

  import cats.data.Writer
  import cats.syntax.writer._ // for tell
  import cats.instances.vector._ // for Monoid
  import cats.syntax.applicative._ // for pure

  type Logged[A] = Writer[Vector[String], A]

  import scala.concurrent._
  import scala.concurrent.ExecutionContext.Implicits.global
  import scala.concurrent.duration._
  def slowly[A](body: => A) = try body finally Thread.sleep(100)

  def factorial(n: Int): Logged[Int] = {
    val ans = slowly(if(n == 0) 1.pure[Logged] else factorial(n - 1).map( a => a * n))
    ans.flatMap(x => 0.writer(Vector(s"fact $n $x")).map( y => x) )
  }
  println(Await.result(Future.sequence(Vector(
    Future(factorial(5)),
    Future(factorial(3))
  )), 5.seconds))

난 위같이 풀었다.

 

4.8 The Reader Monad

cats.data.Reader은 input에 따라서 작업을 시퀀싱 할 수 있게 해주는 모나드이다. Reader의 인스턴스들은 단일 매개변수인 함수를 감싸서 그들을 합성하는 다양한 유용한 메소드들을 제공한다.

Reader의 유용한 기능 중 하나는 dependency injection이다. 우리가 외부 환경설정에 의존하는 여러 작업들을 해야 하면, 우리는 그 작업들을 하나의 큰 작업으로 묶어서 환경변수를 파라미터로 입력받고 프로그램을 명시된 순서대로 실행하게 할 수 있다.

 

4.8.1 Creating and Unpacking Readers

우리는 Reader[A, B]를 Reader.apply 생성자를 통해 함수 A=>B로부터 생성할 수 있다.

import cats.data.Reader

case class Cat(name: String, favoriteFood: String)
// defined class Cat

val catName: Reader[Cat, String] = Reader(cat => cat.name)
// catName: cats.data.Reader[Cat,String] = Kleisli(<function1>)

우리는 Reader의 run method를 사용해서 함수를 다시 추출할 수 있고, apply를 사용하여 익숙한 방식으로 호출할 수 있다.

catName.run(Cat("Garfield", "lasagne"))
// res0: cats.Id[String] = Garfield

여기까지는 간단한데 별 이득이 없다.

 

4.8.2 Composing Readers

Reader의 능력은 map, flatMap 메소드에서 오는데, 얘는 서로 다른 함수의 합성을 대표한다. 우리는 보통 Reader들을 생성하고 같은 타입의 환경변수를 입력받고, map과 flatMap을 통해서 합성하고, 맨 마지막에 run을 호출해서 config를 주입한다.

val greetKitty: Reader[Cat, String] = catName.map(name => s"Hello ${name}")

greetKitty.run(Cat("Heathcliff", "junk food"))
// res1: cats.Id[String] = Hello Heathcliff

위의 경우 기존에 cat의 name을 알려주는 Reader안의 함수와 인사해주는 함수를 합성 한 것이다. 그리고 마지막에 run으로 config를 주입하여 실행해주는 것을 볼 수 있다.

 

flatMap 메소드는 더 흥미로와! 이친구는 같은 인풋 타입에 의존하는 Reader들을 합성할 수 있게 해준다. 고양이 밥주는 예제를 통해 어떻게 일어나는지 봐보자.

val feedKitty: Reader[Cat, String] = 
   Reader(cat => s"Have a nice bowl of ${cat.favoriteFood}")

val greetAndFeed: Reader[Cat, String] =

for {
  greet <- greetKitty
  feed <- feedKitty
} yield s"$greet. $feed."

greetAndFeed(Cat("Garfield", "lasagne"))
// res3: cats.Id[String] = Hello Garfield. Have a nice bowl of lasagne.

greetAndFeed(Cat("Heathcliff", "junk food"))
// res4: cats.Id[String] = Hello Heathcliff. Have a nice bowl of junkfood.

 

4.8.3 Exercise: Hacking on Readers

 

Reader의 클래식한 사용은 프로그램에게 환경변수를 파라미터로 넘겨주는 것이었다. 간단한 로그인 시스템을 가지고 생각 해 보자. 우리의 환경변수는 두 데이터베이스에 있을 것이다. valid user들과 그들의 암호로 이루어져 있다.

case class Db(
  usernames: Map[Int, String],
  passwords: Map[String, String]
)

Reader을 위한 DbReader 타입 얼라이어스를 만드는 것으로 시작하자. 얘는 Db를 입력으로 받는다. 이것을 통해 나머지 코드를 훨씬 짧게 만들 수 있을 것이다.

이렇게 했다.

  case class Db(
                 usernames: Map[Int, String],
                 passwords: Map[String, String]
               )
  type DbReader[A] = Reader[Db,A]

이제 username을 userId Int를 통해 찾고, username String에 통해  password를 찾는 DbReaders를 만드는 메소드를 만들어라. 아래와 같은 타입이어야 할 것이다. (case class Db의 usernames passwords의 타입이 Map임을 주목하자)

  def findUsername(userId: Int): DbReader[Option[String]] =
    ???
  def checkPassword(
                     username: String,
                     password: String): DbReader[Boolean] =
    ???

Map은 get이라는 함수가 있다!

  def findUsername(userId: Int): DbReader[Option[String]] =
    Reader(db => db.usernames.get(userId))
  def checkPassword(
                     username: String,
                     password: String): DbReader[Boolean] =
    Reader(db => db.passwords.get(username).contains(password))

마지막으로 userId가 주어지면 checkLogin method를 구현하자.

def checkLogin(
  userId: Int,
  password: String): DbReader[Boolean] = ???

나는 결국 이렇게 했다리~

  import cats.syntax.applicative._ // for pure
  import cats.data.Reader

  case class Db( usernames: Map[Int, String],
                 passwords: Map[String, String]
               )
  type DbReader[A] = Reader[Db,A]

  def findUsername(userId: Int): DbReader[Option[String]] =
    Reader(db => db.usernames.get(userId))

  def checkPassword( username: String,
                     password: String): DbReader[Boolean] =
    Reader(db => db.passwords.get(username).contains(password))

  def checkLogin( userId: Int,
                  password: String): DbReader[Boolean] = {
    for {
      user <- findUsername(userId)
      result <- {
        user match {
          case Some(x) => checkPassword (x, password)
          case None => false.pure[DbReader]
        }
      }
    } yield result
  }
  /*
  result <- {
        user match {
          case Some(x) => {
            checkPassword(x, password)
          }
          case None => false
        }
      }
  */
  val users = Map(
    1 -> "dade",
    2 -> "kate",
    3 -> "margo"
  )
  val passwords = Map(
    "dade" -> "zerocool",
    "kate" -> "acidburn",
    "margo" -> "secret"
  )
  val db = Db(users, passwords)

  println(checkLogin(5,"1u1u").run(db))
  println(checkLogin(3,"secret").run(db))

 

4.8.4 When to Use Readers?

 

Reader는 의존성 주입에 대한 도구를 제공해준다. 우리는 우리의 프로그램 스텝을 Reader 인스턴스로 작성하고, map과 flatMap으로 체인으로 묶어서 의존성을 주입할 수 있는 함수를 만든다. 

스칼라에는 많은 의존성 주입을 구현할 방법이 있다. 간단하게는 multiple 파라미터 리스트들을 통하거나, implicit parameter과 type class들, 혹은 cake 패턴이나 DI 프레임워크 같은 복잡한 테크닉을 통한다.

 

Reader은 다음과 같은 상황에 제일 유용하다.

 

• we are constructing a batch program that can easily be represented by a function;

• we need to defer injection of a known parameter or set of parameters;

• we want to be able to test parts of the program in isolation.

 

프로그램의 절차를 Reader로 표현하면 우리는 순수함수로서 그들을 테스트하기가 편하고, map과 flatMap 콤비네이터에 대한 접근성을 얻는다.

더 많은 의존성을 갖는다거나, pure function으로 쉽게 표현되지 않는 심화된 문제의 경우 다른 의존성 주입 테크닉들이 어 적절할 수 있다.

 

Kleisli Arrows

콘솔 아웃풋에서 Reader은 Kleisli 타입으로 구현되었다는 것을 읽었을 것이다. Kleisli arrows는 걸과 타입의 타입 생성자에 대한 더 일반적인 Reader의 모양을 제공한다. (?) 챕터 5에서 더 알아보자.

 

4.9 The State Monad

cats.data.State 는 연산의 일부분에 추가적인 state를 넘기게 해준다. 우리는 State 인스턴스를 atomic state operations(뭐라고 번역하지..)를 나타내도록 정의하고 map과 flatMap을 사용해서 쓰레드(thread)한다. 이 방식을 통해 우리는 mutation을 사용하지 않고 순수하게 함수형으로 mutable state를 모델링할 수 있다.

 

4.9.1 Creating and Unpacking State

가장 간단한 형태인 State[S, A]는 S => (S, A) 형태의 함수를 나타낸다. S는 State 의 Type이고 A는 결과의 Type이다.

import cats.data.State

val a = State[Int, String] {
  state => (state, s"The state is $state")
}
// a: cats.data.State[Int,String] = cats.data.IndexedStateT@6cca9e53

다른 말로, State의 인스턴스는 다음의 두가지 일을 한다.

 

• 입력 state를 출력 state로 바꾼다. (transforms an input state to an output state;)

• 결과를 계산한다. (computes a result.)

 

우리는 initial state를 제공함으로써 우리의 monad를 "run"할 수 있다. State는 -run, runS, runA의 세가지 다른 state와 result를 리턴하는 메소드를 제공한다. 각 메소드는 Eval인스턴스를 리턴하는데, 이는 State가 stack safety를 유지하기 위해 사용한다. 실제 값을 뽑아내기 위해서 value method를 사용한다. * 아래의 예시를 보면 run은 state,result를 다 반환하고 runS는 state만, runA는 result만 반환한다.

// Get the state and the result:
val (state, result) = a.run(10).value
// state: Int = 10
// result: String = The state is 10
// Get the state, ignore the result:

val state = a.runS(10).value
// state: Int = 10
// Get the result, ignore the state:

val result = a.runA(10).value
// result: String = The state is 10

(번외로 쓰는 생각인데, 왜 굳이 약자는 State / Action이 생각나게 S / A로 써놓고 의미하는건 state / result 일까? .. 알 수 없는 노릇이다. 혹시 저 위에 atomic 을 의미하는건가..?)

 

4.9.2 Composing and Transforming State

우리가 Reader과 Writer에서 본 대로, State 모나드의 힘은 인스턴스들을 결합하는 데에서 나온다. map 과 flatMap 메소드는 state를 한 인스턴스를 다른 인스턴스로 이어붙인(thread)다. 인스턴스 각각은 atomic state 변환을 나타내며 그 결합은 연속적인 변화의 총 합(완성)을 나타낸다.

  import cats.data.State

  val step1 = State[Int, String] { num =>
    val ans = num + 1
    (ans, s"Result of step1: $ans")
  }
  // step1: cats.data.State[Int,String] = cats.data.IndexedStateT@e2d98c

  val step2 = State[Int, String] { num =>
    val ans = num * 2
    (ans, s"Result of step2: $ans")
  }
  // step2: cats.data.State[Int,String] = cats.data.IndexedStateT@5982d592

  val both = for {
    a <- step1
    b <- step2
  } yield (a, b)
  // both: cats.data.IndexedStateT[cats.Eval,Int,Int,(String, String)] = cats.data.IndexedStateT@2d336af6

  val (state, result) = both.run(20).value
  // state: Int = 42
  // result: (String, String) = (Result of step1: 21,Result of step2: 42)

볼 수 있듯이, final state가 시퀀스 안의 변환을 둘 다 적용한 result이다. State는 우리가 for comprehension에서 무슨 상호작용을 따로 주지 않더라도 한 스텝에서 다음 스텝으로 이어붙어져서 가는것(threaded)을 볼 수 있다. 

아, 참고로 state는 무조건 그냥 넘어가고 result는 yeild 하는 친구만 나온다 아래 코드를 보자.

  val both = for {
    a <- step1
    b <- step2
  } yield (a)

//참고로 위와 같이 하면 
//state : 42
//result : Result of step1: 21

 

State 모나드를 사용하는 일반적인 모델은 연산의 각 스텝을 인스턴스로 표현하고 standard 모나드 연산을 통해서 step을 합성한다. Cats는 원시적인 스텝을 만들기 위한 몇몇 편리한 생성자를 제공한다. 

 

 

• get은 state를 result와 같은 것으로 만들어 뽑아낸다. (get extracts the state as the result;)

• set은 state를 업데이트하고 result로 unit을 반환한다. (set updates the state and returns unit as the result;)

• pure은 state를 무시하고 공급된 result를 반환한다. (pure ignores the state and returns a supplied result;)

• inspect는 transformation함수로부터 state를 추출한다. (inspect extracts the state via a transformation function;)

• modify는 update 함수를 사용해서 state를 업데이트 한다(modify updates the state using an update function.)

 

val getDemo = State.get[Int]
// getDemo: cats.data.State[Int,Int] = cats.data.IndexedStateT@5f10cd3e

getDemo.run(10).value
// res3: (Int, Int) = (10,10)

val setDemo = State.set[Int](30)
// setDemo: cats.data.State[Int,Unit] = cats.data.IndexedStateT@18654165

setDemo.run(10).value
// res4: (Int, Unit) = (30,())

val pureDemo = State.pure[Int, String]("Result")
// pureDemo: cats.data.State[Int,String] = cats.data.IndexedStateT@7da49f73

pureDemo.run(10).value
// res5: (Int, String) = (10,Result)

val inspectDemo = State.inspect[Int, String](_ + "!")
// inspectDemo: cats.data.State[Int,String] = cats.data.IndexedStateT@24ad766f

inspectDemo.run(10).value
// res6: (Int, String) = (10,10!)

val modifyDemo = State.modify[Int](_ + 1)
// modifyDemo: cats.data.State[Int,Unit] = cats.data.IndexedStateT@3f81d8a3

modifyDemo.run(10).value
// res7: (Int, Unit) = (11,())

우리는 이 빌딩 블록들을 for comprehension으로 조합할 수 있다. 우리는 보통 state에 변형만을 가하는 중간 스테이지를 무시한다.

import State._
val program: State[Int, (Int, Int, Int)] = for {
  a <- get[Int]                   // result = state (Int,Int)로 만들어줌
  _ <- set[Int](a + 1)            // state를 업데이트하고 unit return
  b <- get[Int]                   // Int,Int로 만들어줌
  _ <- modify[Int](_ + 1)         // 함수를 통해 state 업데이트
  c <- inspect[Int, Int](_ * 1000)// 함수를 통해 state를 변환해서 결과로
} yield (a, b, c)
// program: cats.data.State[Int,(Int, Int, Int)] = cats.data.IndexedStateT@528336cd

val (state, result) = program.run(1).value
// state: Int = 3
// result: (Int, Int, Int) = (1,2,3000)

 

4.9.3 Exercise: Post-Order Calculator

 

State 모나드는 복잡한 표현과 변경가능한 레지스터를 통과시키는 간단한 인터프리터를 만들 수 있게 해 준다. 우리는 이것의 간단한 예시를 post-order integer 산술 표현 계산기를 만들어보면서 경험해볼 수 있다. 

 

- 숫자가 들어오면 스택에 푸시

- 연산자가 들어오면 두 숫자를 pop해서 계산하고 그 결과를 다시 스택에 푸시

 

1 2 + 3 * // see 1, push onto stack
2 + 3 *   // see 2, push onto stack
+ 3 *     // see +, pop 1 and 2 off of stack,
          // push (1 + 2) = 3 in their place
3 3 *     // see 3, push onto stack
3 *       // see 3, push onto stack
*         // see *, pop 3 and 3 off of stack,
          // push (3 * 3) = 9 in their place

 

단일 심볼을 State 인스턴스로 파싱해주는 evalOne이라는 함수를 작성하는 것으로 시작하자. 아래의 템플릿을 사용하자. (에러 핸들링은 우선은 고려하지 말자. 만약 스택이 잘못된 배열일때면 exception을 뱉어도 된다.)

import cats.data.State

type CalcState[A] = State[List[Int], A]

def evalOne(sym: String): CalcState[Int] = ???

어려워 보이지만, 각 인스턴스는 스택과 결과를 합치기 위한 스택으로부터의 함수형 변환을 나타낸다. 

State[List[Int], Int] { oldStack =>
  val newStack = someTransformation(oldStack)
  val result = someCalculation
  (newStack, result)
}

잘 해 봅시다. * evalOne(값) 들을 여러번 호출하면서 1 2 + 등을 차례로 넣어주기 때문에 String split은 고려할 필요가 없다. 나는 이것 때문에 처음에 헷갈려서 적어둠.

 

 

뭐 적당히 정답을 컨닝하긴 했는데 아래와 같이 했다.

 

  import cats.data.State
  type CalcState[A] = State[List[Int], A]

  def Calculation(func:(Int,Int) => Int) : CalcState[Int] = State[List[Int],Int] {
    case b :: a :: tail =>
      val res = func(a,b)
      (res::tail,res)
    case _ => sys.error("Fail!")
  }
  def makeOperationFunc(sym: String) : ((Int,Int) => Int) = {
    sym match {
      case "+" => ((x,y)=>x+y)
      case "-" => ((x,y)=>x-y)
      case "*" => ((x,y)=>x*y)
      case "/" => ((x,y)=>x/y)
    }
  }
  def Push(sym:Int):CalcState[Int] = State[List[Int],Int] { stack =>
    (sym :: stack, sym)
  }
  def evalOne(sym: String): CalcState[Int] = { // 본인이 다른 것으로 갈아끼워지는 패턴이다.
    sym match {
      case "+" | "-" | "*" | "/" => Calculation(makeOperationFunc(sym))
      case num => Push(num.toInt)
    }
  }

  println(evalOne("42").runA(Nil).value)
  val program = for {
    _ <- evalOne("1")
    _ <- evalOne("2")
    ans <- evalOne("+")
  } yield ans
  println(program.runA(Nil).value)

자 이제, evalAll이라는 List[String]을 입력받아서 연산을 수행하는 함수를 만들어라. evalOne 사용하는거 허용이고, State 모나드들을 같이 flatMap을 통해 이어붙여(thread)라. 당신의 함수는 다음의 모양과 같아야 겠다.

def evalAll(input: List[String]): CalcState[Int] =
     ???

아래같이 하면 된다.

맨 처음의 타입을 CalcState로 해놓으면(0.pure 부분) 이제 foldLeft 하면서 flatMap할 때 첫 원소부터 챡 챡 챡 챡 예쁘게 합치기 쌉가능 ㅇㅈ? ㅇㅇㅈ

  import cats.syntax.applicative._ // for pure

  def evalAll(input: List[String]): CalcState[Int] = {
    input.foldLeft(0.pure[CalcState])((a,b)=>{
      a.flatMap(_ => evalOne(b))
    })
  }

저기에서 이제 0.pure의 0은 사실 의미가 없다. 0이 result로서 들어가는 것이고, 실질적으로 쓰이는 state는 run 해줄때 넣어주기 때문이다. 그러므로 그냥 Int.pure이라는 점에 의의를 두어서 보자. 이를 실질적으로 아래와 같이 실행 시킨다.

  val program = evalAll(List("1", "2", "+", "3", "*"))
  // program: CalcState[Int] = cats.data.IndexedStateT@2a6631ab
  println(program.runA(Nil).value)

9가 나오는지 확인 해 보자.

  val program2 = for {
    _ <- evalAll(List("1", "2", "+"))
    _ <- evalAll(List("3", "4", "+"))
    ans <- evalOne("*")
  } yield ans

evalAll이나 evalOne가 state이다보니 이런 것도 가능하다. 

run하면 값이 제대로 나온다.

 

자 이제 evalInput 함수를 만들어서 input String을 symbol로 나누고 evalAll을 불러서 inital stack과 함께 실행해봐라.

 

  def evalInput(input:String): Int = {
    val inputSymbols = input.split(" ").toList
    evalAll(inputSymbols).runA(Nil).value
  }
  println(evalInput("1 2 + 3 *"))

Easy~

 

4.10 Defining Custom Monads

우리는 세 개의 메소드를 구현함으로써 custom Monad를 정의할 수 있다.  flatMap, pure, 그리고 우리가 아직 보지 못한 tailRecM이라는 메소드 들이다. 아래에 모나드 Option의 구현을 예로 보자.

import cats.Monad
import scala.annotation.tailrec

val optionMonad = new Monad[Option] {
  def flatMap[A, B](opt: Option[A])(fn: A => Option[B]): Option[B] =
    opt flatMap fn

  def pure[A](opt: A): Option[A] =
    Some(opt)

  @tailrec
  def tailRecM[A, B](a: A)(fn: A => Option[Either[A, B]]): Option[B] =
    fn(a) match {
      case None => None
      case Some(Left(a1)) => tailRecM(a1)(fn)
      case Some(Right(b)) => Some(b)
    }
}

tailRecM 메소드는 Cats에서 중첩된 flatMap 호출에 대한 제한된 stack 공간 소모에 대한 최적화를 위해 쓰인다. 2015년에 http://functorial.com/stack-safety-for-free/index.pdf

여기에서 논문으로 나왔다고 한다. 이 A => Option[Either[A,B]]를 받아서 fn이 Right를 리턴하기 전까지 스스로를 재귀적으로 불러야만 한다.

 

우리가 tailRecM 을 tail-recursive하게 만들 수 있다면, Cats는 큰 List등을 folding하는 등의 재귀 상황에서 stack safety를 보장할 수 있다 (Section 7.1에 나온다).  Cats에서 제공하는 모든 모나드는 tailRecM을 가지고 있다. 다만..  이걸 커스텀 모나드에 대해서 작성하는 것은 굉장히 쉽지 않을 것이다... 

 

4.10.1 Exercise: Branching out Further with Monads

마지막 챕터로 우리의 Tree data type에 대한 모나드를 작성 해 보자. 기본 타입은 아래와 같았었다.

sealed trait Tree[+A]

final case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]

final case class Leaf[A](value: A) extends Tree[A]

def branch[A](left: Tree[A], right: Tree[A]): Tree[A] = Branch(left, right)

def leaf[A](value: A): Tree[A] = Leaf(value)

코드가 브랜치와 Leaf 인스턴스에서 동작하는지 검증해보고, 모나드가 Functor-like 행위를 바로 할 수 있는지 확인해라.

그리고 scope내의 모나드가 우리가 직접 flatMap이나 map을 구현하지 않았더라도 for comprehension를 사용하게 해주는지도 봐라.

 

tailRecM tail-recursive를 만들려고 생각하지 말자. 솔루션에 있으니까 한번 확인 해 보면 된다.

 

tailRecM을 제대로 짜지는 않았지만 기능은 할 수 있도록 짜면

sealed trait Tree[+A]

  final case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
  final case class Leaf[A](value: A) extends Tree[A]

  def branch[A](left: Tree[A], right: Tree[A]): Tree[A] = Branch(left, right)
  def leaf[A](value: A): Tree[A] = Leaf(value)

  import cats.Monad
  implicit val treeMonad = new Monad[Tree] {
    def pure[A](value: A):Tree[A] = Leaf[A]
    def flatMap[A,B] (tree:Tree[A])(func: A => Tree[B]) : Tree[B] = {
      tree match {
        case x : Branch[A] => {
          Branch(flatMap(x.left)(func),flatMap(x.right)(func))
        }
        case x : Leaf[A] => {
          func(x.value)
        }
      }
    }
    override def tailRecM[A, B](a: A)(func: A => Tree[Either[A, B]]): Tree[B] =
      flatMap(func(a)) {
        case Left(value) =>
          tailRecM(value)(func)
        case Right(value) =>
          Leaf(value)
      }
  }

이렇다.

 

올바른 방법은

  import cats.Monad
  import scala.annotation.tailrec
  implicit val treeMonad = new Monad[Tree] {
    def pure[A](value: A): Tree[A] =
      Leaf(value)
    def flatMap[A, B](tree: Tree[A])
                     (func: A => Tree[B]): Tree[B] =
      tree match {
        case Branch(l, r) =>
          Branch(flatMap(l)(func), flatMap(r)(func))
        case Leaf(value) =>
          func(value)
      }
    def tailRecM[A, B](arg: A)
                      (func: A => Tree[Either[A, B]]): Tree[B] = {
      @tailrec
      def loop(
                open: List[Tree[Either[A, B]]],
                closed: List[Option[Tree[B]]]): List[Tree[B]] =
        open match {
          case Branch(l, r) :: next =>
            loop(l :: r :: next, None :: closed)
          case Leaf(Left(value)) :: next =>
            loop(func(value) :: next, closed)
          case Leaf(Right(value)) :: next =>
            loop(next, Some(pure(value)) :: closed)
          case Nil =>
            closed.foldLeft(Nil: List[Tree[B]]) { (acc, maybeTree) =>
              maybeTree.map(_ :: acc).getOrElse {
                val left :: right :: tail = acc
                branch(left, right) :: tail
              }
            }
        }
      loop(List(func(arg)), Nil).head
    }
  }

이렇다고 한다.

기본적으로, 왜 되지? 왜 안되지? 뭐가 차이지? 라고 생각할 수 있는데, 그 차이는 tailrec에서 온다.

 

기본적으로, 저 @ 어노테이션은 포멧을 강제 해 준다. (@scala.annotation.tailrec)

tailRecursion 상태로 프로그래밍 하는 과정이 tailRecM 을 제대로 프로그래밍 하는데에 필요하다..

 

(나도 잘 몰라서.. 정리해보자면)

기본적으로 factorial을 짤 때, 아래와 같이 짜면 tailrec을 만족하지 않는데

  def factorial(n: Int): Int = {
    if(n==1) n
    else n * factorial(n-1)
  }

그 이유는 factorial이 리턴 된 후에 추가적으로 해야 할 작업이 (*n )있기 때문에 stack memory를 사용해야 하는 문제가 있기 때문이다.

이는 아래와 같이 고치면 된다.

  def factorial(n: Int): Int = {
    @scala.annotation.tailrec
    def _factorial(n: Int, acc: Int): Int = {
      if (n == 1) acc
      else _factorial(n - 1, acc * n)
    }
    _factorial(n, 1)
  }

위 친구는 그냥 리턴 되자마자 한큐로 passing through 되기 때문에 만족한다.

 

이런 방식으로 tailRecM도 짜면 되는 것이다~

 

4.11 Summary

이 챕터에선 모나드를 봤다.

flatMap도 봤다. 

Either List Id Reader Writer State 다 봤다.

tailRecM 짱힘들다.  화이팅..

 

 

 

 

 

오역 / 오개념 탑재에 대한 제보는 언제나 환영입니다~