본문 바로가기

Programmer Jinyo/Scala & FP

scala with cats 책 읽으면서 필기 Chapter 6 (Semigroupal and Applicative)


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

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

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

위 글에서 이어지는 글입니다.

이 글 시리즈는 scala with cats 원문을 보고 쓴 글입니다.

 

*이번 글 부터는 진짜 거의 필기 위주로만 합니다 ㅜ_ㅜ.. 공부하는데에 너무 시간이 오래걸려서..

 


 

이전 챕터에서 우리는 어떻게 map과 flatMap을 사용하여 functors와 monads 가 작업을 이어붙이는지에 대해서 살펴보았다. functor과 모나드가 대단히 유용한 추상화인 반면, 그들이 표현하지 못하는 몇몇 프로그램 타입이 있다.

 

하나의 예시로는 , form validation이 있다. 우리가 유저에게 발생한 모든 에러를 리턴하고자 하는 form 이 있다면, 첫 에러가 발생했을 때 멈추면 안된다. 이 모델을 Either과 같은 것으로 디자인하면, 여러 error들을 잃어버릴 수 있다. 예를 들어, 아래 코드는 parseInt에서 실패한 후 더이상 진행되지 않는다.

import cats.syntax.either._ // for catchOnly
def parseInt(str: String): Either[String, Int] =
  Either.catchOnly[NumberFormatException](str.toInt).
    leftMap(_ => s"Couldn't read $str")

for {
  a <- parseInt("a")
  b <- parseInt("b")
  c <- parseInt("c")
} yield (a + b + c)
// res1: scala.util.Either[String,Int] = Left(Couldn't read a)

map과 flatMap은 서로 이전 결과에 영향을 받으므로 독립적이지만 순차적인 결과들을 처리할 때는 이를 효과적으로 처리할 수 없다. 우리는 우리가 원하는 결과를 위해 더 약한 건설(시퀀싱을 보장하지 않는 어떤 것)이 필요하다.

 

- Semigroupal은 contexts들을 합성하는것에 대한 개념을 담고 있다. Cats는 Semigroupal과 Functor을 사용하여 여러 매개변수와 함께 함수를 시퀀싱하는 cats.syntax.apply모듈을 제공한다.

- Applicative는 Semigroupal과 Functor을 상속받는다. 이것은 context안에서 파라미터에 함수를 적용하는 방법을 제공한다. Applicative는 pure method의 근원이다.

 

 

6.1 Semigroupal

cats.Semigroupal은 context들을 합칠수 있게 해 주는 타입 클래스이다. 만약 우리가 타입 F[A]와 F[B]의 두 객체를 가지고 있다면, Semigroupal [F]는 그들을 F[(A,B)]로 합칠 수 있게 해 준다. 

trait Semigroupal[F[_]] {
  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}

파라미터 fa 와 fb는 서로 독립이다. 우리는 그들을 product 함수를 거치기 전에 어느 쪽이든 계산할 수 있다. 이것은 flatMap과 대조적이다. 

 

6.1.1 Joining Two Contexts

 

Semigroupal이 value들을 합칠 수 있게 해 줬다면, Semigroupal 은 context를 합칠 수 있게 해 준다. Option들을 합쳐보자.

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

Semigroupal[Option].product(Some(123), Some("abc"))
// res0: Option[(Int, String)] = Some((123,abc))

양쪽 파라미터의 타입에 따라 잘 합쳐주는 친구이다.

Semigroupal[Option].product(None, Some("abc"))
// res1: Option[(Nothing, String)] = None

Semigroupal[Option].product(Some(123), None)
// res2: Option[(Int, Nothing)] = None

 

6.1.2 Joining Three or More Contexts

근데 한 10개 합칠때 마다 10겹을 조지는건 말이 안되니깐 이번 주제가 나온 것 같다.

Semigroupal의 동반객체는 product메소드의 위에 또 다른 method들을 정의해 놓았다. 예를 들어, tuple2에서 tuple22까지 쭉 일반화 시키기 위해 정의 해 놓았다. (실질적인 구현은 imap으로 뒤에서부터 둘씩 묶어서 계산하기는 한다.)

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

Semigroupal.tuple3(Option(1), Option(2), Option(3))
// res3: Option[(Int, Int, Int)] = Some((1,2,3))

Semigroupal.tuple3(Option(1), Option(2), Option.empty[Int])
// res4: Option[(Int, Int, Int)] = None

map2에서 map22는 아래와 같다.

Semigroupal.map3(Option(1), Option(2), Option(3))(_ + _ + _)
// res5: Option[Int] = Some(6)

Semigroupal.map2(Option(1), Option.empty[Int])(_ + _)
// res6: Option[Int] = None

contramap 2~22 , imap2~22 등등도 다 존재한다.

 

6.2 Apply Syntax

Cats는 위 함수를 위한 apply syntax를 제공한다. cats.syntax.apply로부터 syntax를 불러올 수 있다.

import cats.instances.option._ // for Semigroupal
import cats.syntax.apply._ // for tupled and mapN

tupled 메소드는 implicit하게 Option들의 튜플로 추가된다. Option 안의 값들을 묶기 위해 Semigroupal을 사용하며 (Semigroupal은 context를 합성해주니까) 이는 Option안의 튜플로 리턴된다.

(Option(123), Option("abc")).tupled
// res7: Option[(Int, String)] = Some((123,abc))

이 함수를 22개의 개수까지는 바로 사용 가능하다. Cats는 각 매개변수의 개수마다 메소드를 정의 해 놓았다.

(Option(123), Option("abc"), Option(true)).tupled
// res8: Option[(Int, String, Boolean)] = Some((123,abc,true))

tupled에 더해서, Cats의 apply syntax는 mapN이라는 implicit Functor을 받아들여서 알맞은 매개변수의 개수를 합성하게 해주는 함수를 제공한다. (Cat.apply 함수를 받아서 Cat 안에 데이터를 채우는 행위를 할 수 있게 됨)

case class Cat(name: String, born: Int, color: String)
(
  Option("Garfield"),
  Option(1978),
  Option("Orange & black")
).mapN(Cat.apply)
// res9: Option[Cat] = Some(Cat(Garfield,1978,Orange & black))

mapN은 내부적으로 Semigroupal을 Option으로부터 추출하기 위해 사용하며 Functor은 함수에 value들을 apply하기 위해 사용한다.

타입 체크가 된다 (파라미터 개수 다르면 오류 뱉음)

val add: (Int, Int) => Int = (a, b) => a + b
// add: (Int, Int) => Int = <function2>

(Option(1), Option(2), Option(3)).mapN(add)
// <console>:27: error: type mismatch;
// found : (Int, Int) => Int
// required: (Int, Int, Int) => ?
// (Option(1), Option(2), Option(3)).mapN(add)
//                                     ^

(Option("cats"), Option(true)).mapN(add)
// <console>:27: error: type mismatch;
// found : (Int, Int) => Int
// required: (String, Boolean) => ?
// (Option("cats"), Option(true)).mapN(add)
//                                  ^

6.2.1 Fancy Functors and Apply Syntax

 

Contravariant 와 invariant functor을 입력받는 contarmapN과 imapN 메소드도 있다. invariant를 써서 Monoid 합성을 해 보자.

import cats.Monoid
import cats.instances.int._ // for Monoid
import cats.instances.invariant._ // for Semigroupal
import cats.instances.list._ // for Monoid
import cats.instances.string._ // for Monoid
import cats.syntax.apply._ // for imapN

case class Cat(
  name: String,
  yearOfBirth: Int,
  favoriteFoods: List[String]
)

val tupleToCat: (String, Int, List[String]) => Cat =
  Cat.apply _

val catToTuple: Cat => (String, Int, List[String]) =
  cat => (cat.name, cat.yearOfBirth, cat.favoriteFoods)

implicit val catMonoid: Monoid[Cat] = (
  Monoid[String],
  Monoid[Int],
  Monoid[List[String]]
).imapN(tupleToCat)(catToTuple)

 

우리 모노이드는 "empty" Cats를 만들 수 있음. 그리고 |+| 사용해서 합칠 수 있음.(챕터2에 내용나옴) 

import cats.syntax.semigroup._ // for |+|

val garfield = Cat("Garfield", 1978, List("Lasagne"))
val heathcliff = Cat("Heathcliff", 1988, List("Junk Food"))

garfield |+| heathcliff
// res17: Cat = Cat(GarfieldHeathcliff,3966,List(Lasagne, Junk Food))

 

6.3 Semigroupal Applied to Different Types

Semigroupal이 다른 곳에는 어떻게 적용되는지 살펴보자. (가끔 예상치 못한 짓을 한다)

 

Future

Future은 순차적 실행의 반대 개념인 병렬 실행을 제공한다.

import cats.Semigroupal
import cats.instances.future._ // for Semigroupal
import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.language.higherKinds

val futurePair = Semigroupal[Future].product(Future("Hello"), Future(123))

Await.result(futurePair, 1.second)
// res1: (String, Int) = (Hello,123)

product는 생산(?)이라는 의미가 있다

두개의 Future은 우리가 만들자마자 실행하기 시작한다.  그래서 걔네들은 우리가 product 를 호출할 때 이미 연산을 하고 있다. 우리는 apply syntax를 통해 Future들을 묶을 수 있다.

import cats.syntax.apply._ // for mapN
case class Cat(
  name: String,
  yearOfBirth: Int,
  favoriteFoods: List[String]
)

val futureCat = (
  Future("Garfield"),
  Future(1978),
  Future(List("Lasagne"))
).mapN(Cat.apply)

Await.result(futureCat, 1.second)
// res4: Cat = Cat(Garfield,1978,List(Lasagne))

List

Semigroupal product로 List들을 합치는건 기대하지 않은 결과를 나타낼 수 있다. 우리는 List들을 묶는 것을 기대하겠지만, 사실 cartesian product 연산을 조져버린다;;;

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

Semigroupal[List].product(List(1, 2), List(3, 4))
// res5: List[(Int, Int)] = List((1,3), (1,4), (2,3), (2,4))

개쩔지? 마치 List들 flatMap 조질때랑 비슷한 느낌이다. 왜 이렇게 되는지는 나중에 알아본다.

 

Either

이 챕터 도입부에 fail-fast vs 축적(accumulating)되는 에러 핸들링에 대해 다뤘다. 우리는 Either에 product를 적용해서 fail first를 피해보는걸 시도하고자 해볼 수 있지만 flatMap스럽게 통수를 또 맞아버린다.

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

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

Semigroupal[ErrorOr].product(
  Left(Vector("Error 1")),
  Left(Vector("Error 2"))
)
// res7: ErrorOr[(Nothing, Nothing)] = Left(Vector(Error 1))

아니 분명 뒤에꺼도 실행이 가능한데 왜 이런식이냐?

 

6.3.1 Semigroupal Applied to Monads

List나 Either에 대한 결과의 이유는 그들이 둘다 Monad이기 때문이다. 확실하게 의미(semantics)를 유지하기 위해 Cats의 모나드(Semigroupal 인)는 product에 대한 map과 flatMap으로 이루어진 기본적인 정의를 제공한다. 그래서 우리 생각대로 동작을 안하는 것. (이미 map/flatMap으로 정의되어 있어서)

우리의 Future에 대한 결과 조차도 속임수일 뿐이다. flatMap은 sequential ordering을 제공하므로 product도 같은걸 제공한다. 우리가 본 병렬 처리도 Future이 product를 호출하기 전에 실행되기 시작하기 때문이다.

 

val a = Future("Future 1")
val b = Future("Future 2")

for {
  x <- a
  y <- b
} yield (x, y)

결과는 Future(Success((Future 1,Future 2)))가 나온다;

이따군데 왜 Semigroupal을 쓰냐? 왜냐면 Semigroupal의 인스턴스를 가지는 유용한 데이터 타입은 만들 수 있는데 모나드로는 안되기 떄문이다. 이것은 우리가 product를 다른 방법으로 구현(implement)할 수 있게 해 준다. 에러 핸들링을 위한 대체 타입을 보면서 알아보자.

 

6.3.1.1 Exercise: The Product of Monads

flatMap의 관점에서 product를 구현 해 보자. 

import cats.Monad
def product[M[_]: Monad, A, B](x: M[A], y: M[B]): M[(A, B)] =
    ???

아래와 같이 하면 된다. (flatMap / map을 사용해도 동일)

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

  def product[M[_]: Monad, A, B](x: M[A], y: M[B]): M[(A, B)] =
    {
      for {
        xa <- x
        ya <- y
      } yield(xa,ya)
    }

그리고 이렇기 때문에

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

product(List(1, 2), List(3, 4))
// res12: List[(Int, Int)] = List((1,3), (1,4), (2,3), (2,4))

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

product[ErrorOr, Int, Int](
  Left(Vector("Error 1")),
  Left(Vector("Error 2"))
)
// res13: ErrorOr[(Int, Int)] = Left(Vector(Error 1))

이런 결과가 나온다.

 

6.4 Validated

지금까지는 우리는 Either의 fail-fast error handling에 익숙해져 있다. 아직 우리는 Either Monad의 flatMap등을 사용하는 방법을 깨뜨리지 않으면서 에러들을 합산하여 보여줄 방법이 없다.

다행히, Cats는 Validated라는 Semigroupal의 인스턴스를 제공한다.(Monad아님) 그래서 product의 구현이 에러들을 취합하기에 좋은 형식이다.

import cats.Semigroupal
import cats.data.Validated
import cats.instances.list._ // for Monoid

type AllErrorsOr[A] = Validated[List[String], A]

Semigroupal[AllErrorsOr].product(
  Validated.invalid(List("Error 1")),
  Validated.invalid(List("Error 2"))
)
// res1: AllErrorsOr[(Nothing, Nothing)] = Invalid(List(Error 1, Error2))

만약 하나만 invalid인 경우,

Semigroupal[AllErrorsOr].product(
    Validated.valid(List("Error 1")),
    Validated.invalid(List("Error 2"))
  )
//res: Invalid(List(Error 2))

Validated로 Either을 잘 대체할 수 있어욥! 

 

6.4.1 Creating Instances of Validated

Validated에는 두개의 subtype이 있다. Validated.Valid 와 Validated.Invalidated 이며, 각각 Right , Left의 역할을 담당한다.이 타입들의 인스턴스를 만드는 방법은 직접 만들거나 메소드를 실행해서 만들 수 있다.

val v = Validated.Valid(123)
// v: cats.data.Validated.Valid[Int] = Valid(123)

val i = Validated.Invalid(List("Badness"))
// i: cats.data.Validated.Invalid[List[String]] = Invalid(List(Badness))

그치만 똑똑한 생성자가 있기 떄문에 이걸 쓰는게 더 편할떄가 많다.

val v = Validated.valid[List[String], Int](123)
// v: cats.data.Validated[List[String],Int] = Valid(123)

val i = Validated.invalid[List[String], Int](List("Badness"))
// i: cats.data.Validated[List[String],Int] = Invalid(List(Badness))

세번째 옵션으로는 extension method를 쓰는 것이다.

import cats.syntax.validated._ // for valid and invalid

123.valid[List[String]]
// res2: cats.data.Validated[List[String],Int] = Valid(123)

List("Badness").invalid[Int]
// res3: cats.data.Validated[List[String],Int] = Invalid(List(Badness))

네번째 옵션으로는 cats.syntax.applicative와 cats.syntax.applicativeError로부터 pure와 raiseError를 각각 받아와서 쓰는 것이다.

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

type ErrorsOr[A] = Validated[List[String], A]

123.pure[ErrorsOr]
// res5: ErrorsOr[Int] = Valid(123)

List("Badness").raiseError[ErrorsOr, Int]
// res6: ErrorsOr[Int] = Invalid(List(Badness))

마지막으로, 여러 다른 소스(source)들로부터 validate instance들을 만드는 helper 메소드들이 있다. Exceptions , Try, Either, Option으로부터 만들 수 있다.

Validated.catchOnly[NumberFormatException]("foo".toInt)
// res7: cats.data.Validated[NumberFormatException,Int] = Invalid(java.lang.NumberFormatException: For input string: "foo")

Validated.catchNonFatal(sys.error("Badness"))
// res8: cats.data.Validated[Throwable,Nothing] = Invalid(java.lang.RuntimeException: Badness)

Validated.fromTry(scala.util.Try("foo".toInt))
// res9: cats.data.Validated[Throwable,Int] = Invalid(java.lang.NumberFormatException: For input string: "foo")

Validated.fromEither[String, Int](Left("Badness"))
// res10: cats.data.Validated[String,Int] = Invalid(Badness)

Validated.fromOption[String, Int](None, "Badness")
// res11: cats.data.Validated[String,Int] = Invalid(Badness)

6.4.2 Combining Instances of Validated

위의 Semigroupal을 위한 syntax나 method를 통해 Validated instance를 합성할 수 있다.

Validated는 Semigroup을 사용해서 error을 누적한다. 그래서 in scope시켜줘야 한다.

import cats.instances.string._ // for Semigroup

Semigroupal[AllErrorsOr]
// res13: cats.Semigroupal[AllErrorsOr] = cats.data.ValidatedInstances$$anon$1@5efc8c3e

컴파일러가 올바른 타입에 대한 Semigroupal 을 불러오기 위한 implicit이 in scope상황일 때, 우리는 Semigroupal method들을 error을 누적하기 위해서 apply를 포함한 모든 메소드를 쓸 수 있다.

import cats.syntax.apply._ // for tupled

(
  "Error 1".invalid[Int],
  "Error 2".invalid[Int]
).tupled
// res14: cats.data.Validated[String,(Int, Int)] = Invalid(Error 1 Error 2)

볼 수 있듯, String이 error을 누적하는데에 이상적인 타입은 아니다. 주로 Vector나 List를 쓴다.

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

(
  Vector(404).invalid[Int],
  Vector(500).invalid[Int]
).tupled
// res15: cats.data.Validated[scala.collection.immutable.Vector[Int],(Int, Int)] = Invalid(Vector(404, 500))

cats.data 패키지는 NonEmptyList와 NonEmptyVector타입을 통해 하나의 에러도 없을 경우 실패를 막아주기도 한다.

import cats.data.NonEmptyVector

(
  NonEmptyVector.of("Error 1").invalid[Int],
  NonEmptyVector.of("Error 2").invalid[Int]
).tupled
// res16: cats.data.Validated[cats.data.NonEmptyVector[String],(Int,Int)] = Invalid(NonEmptyVector(Error 1, Error 2))

6.4.3 Methods of Validated

Validated는 Either의 그 편한 메소드들과 비슷한 것들을 가지고 있다. 우리는 map, leftMap, bimap을 valid와 invalid안의 값을 바꾸기 위해 사용할 수 있다.

123.valid.map(_ * 100)
// res17: cats.data.Validated[Nothing,Int] = Valid(12300)

"?".invalid.leftMap(_.toString)
// res18: cats.data.Validated[String,Nothing] = Invalid(?)

123.valid[String].bimap(_ + "!", _ * 100)
// res19: cats.data.Validated[String,Int] = Valid(12300)

"?".invalid[Int].bimap(_ + "!", _ * 100)
// res20: cats.data.Validated[String,Int] = Invalid(?!)

Validated가 monad가 아니므로 flatMap은 사용할 수 없다. 그러나 Cats는 flatMap을 대신할 andThen을 가지고 있다. andThen의 타입 시그니처는 flatMap의 그것과 동일하지만 구현상에서 monad laws를 만족하지 않기 때문에 다른 이름을 가지고 있다.

32.valid.andThen { a =>
  10.valid.map { b =>
    a + b
  }
}
// res21: cats.data.Validated[Nothing,Int] = Valid(42)

만약 flatMap이상의 것을 하고 싶다면, 우리는 toEither과 toValidated으로 Validate와 Either을 왔다갔다 하면서 변환해야 한다. toValidate는 cats.syntax.either로 부터 온다는걸 기억하자.

import cats.syntax.either._ // for toValidated
// import cats.syntax.either._

"Badness".invalid[Int]
// res22: cats.data.Validated[String,Int] = Invalid(Badness)

"Badness".invalid[Int].toEither
// res23: Either[String,Int] = Left(Badness)

"Badness".invalid[Int].toEither.toValidated
// res24: cats.data.Validated[String,Int] = Invalid(Badness)

Either와 같이, ensure 메소드를 통해 조건문(predicate)이 맞지 않으면 특정 에러를 발생시킬 수도 있다.

// 123.valid[String].ensure("Negative!")(_ > 0)

마지막으로, getOrElse나 fold를 통해 valid나 invalid case에서 값을 뽑아올 수 있다.

"fail".invalid[Int].getOrElse(0)
// res26: Int = 0

"fail".invalid[Int].fold(_ + "!!!", _.toString)
// res27: String = fail!!!

6.4.4 Exercise: Form Validation

request from a client in a Map[String, String]인 상태

case class User(name: String, age: Int)

이걸 민들고 싶음. 아래의 규칙 지켜야 함.

- name , age는 반드시 있어야 함.

- name 은 blank가 아니어야 함.

- age는 non-negative int여야 함.

 

만약 모든 규칙이 우리의 parser을 통과하면 User을 리턴해야 한다. 만약 하나라도 실패하면 error 메시지들이 담긴 List를 리턴해야 한다.

시퀀셜하게 조합하는걸로 시작 해 보자. 

우선 name이랑 age field를 읽어오는 메소드부터 정의하자.

 

- readName은 Map[String, String] 파라미터를 받고, "name"필드를 추출하고, 관계된 validation 규칙을 체크하고, Either[List[String], String] 리턴하기.

- readAge는 Map[String, String]파라미터를 받아서 "age" 필드를 추출하고, 관계된 validation 규칙을 체크하고, Either[List[String],Int]를 리턴한다.

 

작은 building block으로부터 만들기 시작하자. getValue 라는, 필드 이름을 주면 Map으로부터 String을 읽어들이는 메소드 정의부터 시작하라. 

우선 타입을 먼저 정의하자.

import cats.data.Validated

type FormData = Map[String, String]
type FailFast[A] = Either[List[String], A]
type FailSlow[A] = Validated[List[String], A]

참고로 Either의 toRight 함수의 정의는 아래와 같다.

def toRight[X](left: => X): Either[X, A] =
    if (isEmpty) Left(left) else Right(this.get)

그렇다면 구현은 아래와 같이 할 수 있다.

 

  def getValue(key : String)(data: FormData) : FailFast[String] = {
    data.get(key).toRight(List(s"$key field not specified"))
  }
  val getName = getValue("name") _
  // getName: FormData => FailFast[String] = <function1>

  println(getName(Map("name" -> "Dade Murphy")))
  // res29: FailFast[String] = Right(Dade Murphy)

(언더바 키워드가 어색하다면 부분 적용 함수를 검색해봐요!)

이것도 가능

  def getValue(key : String, data: FormData) : FailFast[String] = {
    data.get(key).toRight(List(s"$key field not specified"))
  }
  val getName = getValue("name", _ : FormData)
  // getName: FormData => FailFast[String] = <function1>

  println(getName(Map("name" -> "Dade Murphy")))
  // res29: FailFast[String] = Right(Dade Murphy)

이렇게 하면 실패 메시지도 잘 뜬다.

getName(Map())
// res30: FailFast[String] = Left(List(name field not specified))

그리고 parseInt도 구현하자.

import cats.syntax.either._ // for catchOnly

type NumFmtExn = NumberFormatException

def parseInt(name: String)(data: String): FailFast[Int] =
  Either.catchOnly[NumFmtExn](data.toInt).
    leftMap(_ => List(s"$name must be an integer"))

저 exception catchOnly 친구들은 잘 모르겠긴 한데 잘 동작하네?^^ 정답 배낌 ㅎ 

parseInt("age")("11")
// res33: FailFast[Int] = Right(11)

parseInt("age")("foo")
// res34: FailFast[Int] = Left(List(age must be an integer))

 

이제 validation check를 구현하자. nonBlank는 Strings를, nonNegative는 Int를 위해서 구현 ㄱㄱ

key , data를 입력받아서 에러 메시지를 추가하게 만들어 줘야 한다.

  def nonBlank(value : String) : Boolean = {
    !value.isEmpty()
  }
  def nonNegative(value : Int) : Boolean = {
    value >= 0
  }

이렇게 구현하고 싶은 충동이 들겠지만 그러면 안된다.

  def nonBlank(key : String)(data:String) : FailFast[String] = {
    
  }
  def nonNegative(key : String)(data:Int) : FailFast[Int] = {

  }

요런 느낌으로다가.. 해야한다.

  def nonBlank(key : String)(data:String) : FailFast[String] = {
    Right(data).ensure(List(s"$key must be non empty"))(_.nonEmpty)
  }
  def nonNegative(key : String)(data:Int) : FailFast[Int] = {
    Right(data).ensure(List(s"$key must be non negative"))(_>=0)
  }

그러면 결과도 일케 나온다

nonBlank("name")("Dade Murphy")
// res36: FailFast[String] = Right(Dade Murphy)

nonBlank("name")("")
// res37: FailFast[String] = Left(List(name cannot be blank))

nonNegative("age")(11)
// res38: FailFast[Int] = Right(11)

nonNegative("age")(-1)
// res39: FailFast[Int] = Left(List(age must be non-negative))

다음으로, getValue, parseInt, nonBlank, nonNegative를 쓰까서 readName, readAge를 만들자.

  def readName(data: FormData) : FailFast[String] = {
    getValue("name", data).flatMap(nonBlank("name"))
  }
  def readAge(data: FormData) : FailFast[Int] = {
    getValue("age", data).flatMap(nonBlank("name")).flatMap(parseInt("age")).flatMap(nonNegative("age"))
  }

사실 나는 찐따같이 이렇게 (_) 을 다 붙였었다.

  def readName(data: FormData) : FailFast[String] = {
    getValue("name", data).flatMap(nonBlank("name")(_))
  }
  def readAge(data: FormData) : FailFast[Int] = {
    getValue("age", data).flatMap(nonBlank("name")(_)).flatMap(parseInt("age")(_)).flatMap(nonNegative("age")(_))
  }
  println(readName(Map("name" -> "Dade Murphy")))
  // res41: FailFast[String] = Right(Dade Murphy)
  println(readName(Map("name" -> "")))
  // res42: FailFast[String] = Left(List(name cannot be blank))
  println(readName(Map()))
  // res43: FailFast[String] = Left(List(name field not specified))
  println(readAge(Map("age" -> "11")))
  // res44: FailFast[Int] = Right(11)
  println(readAge(Map("age" -> "-1")))
  // res45: FailFast[Int] = Left(List(age must be non-negative))
  println(readAge(Map()))
  // res46: FailFast[Int] = Left(List(age field not specified))

안붙여도 동작 하네 ㅎ;

 

마지막으로, Semigroupal 을 사용해서 readName과 readAge의 결과를 통해 User을 만들어내자. Either을 Validated로 accumulate error 하기 위해 사용하는거 잊지 말라.

  case class User(name:String,age:Int)

  def readUser(data: FormData):FailSlow[User] = {
    (readName(data).toValidated,
      readAge(data).toValidated
    ).mapN(User.apply)
  }
  println(readUser(Map("name" -> "Dave", "age" -> "37")))
  // res48: FailSlow[User] = Valid(User(Dave,37))
  println(readUser(Map("age" -> "-1")))
  // res49: FailSlow[User] = Invalid(List(name field not specified, agemust be non-negative))

ㅇㅇ

리마인드 하자면, mapN은 implicit Functor을 받아들여서 적절히 합성해주는 함수이다 ^_^

 

6.5 Apply and Applicative

Semigroupal들은 함수형 프로그래밍 문헌들에서는 잘 언급되지 않는 편이기는 하다. 그들은 연관된 타입 클래스인 Applicative functor의 기능성(Functionality)의 부분집합을 제공한다.

Semigroupal과 Applicative는 context를 join하는 것과 동일한 개념의 대체 인코딩을 효과적으로 제공한다.  두 인코딩 모두 http://www.staff.city.ac.uk/~ross/papers/Applicative.html 같은 논문에 소개되어 있다. (Semigroupal은 여기에 monoidal이라고 소개되어 있다)

Cats 는 두 타입 클래스를 사용해 Applicatives 를 모델링한다. 우선, cats.Apply이다. 이것은 Semigroupal과 functor을 상속받으며 context 내에서 파라미터를 적용하는 ap메소드를 추가한다. 두번째로, cats.Applicative인데, Apply를 상속받으며, pure메소드를 추가한다. 간단 정의는 아래에.

trait Apply[F[_]] extends Semigroupal[F] with Functor[F] {
  def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] =
    ap(map(fa)(a => (b: B) => (a, b)))(fb)
}

trait Applicative[F[_]] extends Apply[F] {
  def pure[A](a: A): F[A]
}

자세히 분석 해 보면, ap 메소드는 파라미터 fa를 함수 ff로 context F[_] 안에서 적용한다. Semigroupal로부터 온 product 메소드는 ap와 map으로 정의되어 있다.

product 의 구현은 넘 마음 쓰지 말자. 디테일한 부분은 글케 안중요하다. 아무튼 map을 통해 벗긴 a와 받은 b를 튜플로 묶어준다고 보면 된다.

Applicative 는 pure method를 준다. 이것은 우리가 Monad에서 보았던 그 pure이다. 

-> 머 결국 Applicative에서 pure만 없으면 Apply이다.

 

6.5.1 The Hierarchy of Sequencing Type Classes

Apply 와 Applicative를 소개 했으니, 우리는 zoom out 해서 서로 다른 방법으로 그들의 연산을 시퀀싱 하는 타입 클래스 가족들을 살펴볼 수 있게 됐다. 그림 6.1을 보자.

아래 타입클래스가 위 타입클래스를 상속받는 상태이다.

 

각 타입 클래스 계층은 특정한 시퀀싱 방법을 나타내며 특징적인 메소드들을 알려주고 이를 통해 다음과 같은 정의를 알 수 있다.

- 모든 모나드는 applicative이다.

- 모든 applicative 는 semigroupal 이다. (위 그림에서는 Cartesian이라고 쓰여 있다)

- 등등

 

Apply는 prodct를 ap와 map을 통해 정의한다. Monad는 product, ap, map을 pure와 flatMap을 통해 정의한다. 

 

 

 

머 여튼

더 많이 상속받을수록 유연성은 낮아지고 기능이 많아진다

덜 상속받으면 유연해지는데 기능이 없고 기능이 적어진다. 당연한 trade off

 

 

 

여튼 그니까 클래스 잘 선택해서 써라 이말.

 

 

 

 

 

 

대충 이쯤 정리하고 그럼 이만.

 

 

 

 

 

오역 / 오개념 적은것에 대한 제보 / 정정요청 환영 ☆