본문 바로가기

Programmer Jinyo/Scala & FP

scala with cats 책 읽으면서 필기(하다보니 번역급) Chapter 2 (Monoids and Semigroups)


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

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

2020/02/07 - [전체보기] - scala with cats 책 읽으면서 필기(하다보니 번역급) Chapter 1

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

다음 글은

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

입니다.

 

이 글은 scala with cats책을 읽으면서 거의 번역하다시피 한 글임을 먼저 밝히고요, https://books.underscore.io/scala-with-cats/scala-with-cats.pdf 여기서 보실 수 있습니다.

 

 

 

Monoids and Semigroups

 

이 챕터에서는 monoid와 semigroup 타입 클래스에 대해서 알아 볼 것이다. 이 것들은 value들을 더하거나 조합할 수 있게 해준다. 우리가 유추해낼 수 있는 일반적인 원칙이나 연산들을 살펴보도록 하자.

 

Integer addition

Int의 Addition은 2진 연산자이며 닫혀있다. (Int끼리 연산하면 Int가 나온다는 뜻)

2 + 1
// res0: Int = 3

또한 identity 원소 (이걸 항등원이라 하나? 기억이 가물가물..) 0 이 있다. 이는 모든 인트 a 에 대해 a + 0 == 0 + a  == a 인 속성 때문이다.

2 + 0
// res1: Int = 2

0 + 2
// res2: Int = 2

그리고 그 외에도 여러 속성이 있다. 예를 들어, 결합법칙이라던지 하는 등등의 법칙을 만족한다.

(1 + 2) + 3
// res3: Int = 6

1 + (2 + 3)
// res4: Int = 6

Integer multiplication

같은게 곱에서도 적용된다. 0이었던걸 1로 바꾸면

1 * 3
// res5: Int = 3

3 * 1
// res6: Int = 3

추가적으로 결합법칙도 만족!

(1 * 2) * 3
// res7: Int = 6

1 * (2 * 3)
// res8: Int = 6

String and sequence concatenation

우리는 문자열도 더할 수 있다.

"One" ++ "two"
// res9: String = Onetwo

그리고 공백 문자열이 항등원이다.

"" ++ "Hello"
// res10: String = Hello

"Hello" ++ ""
// res11: String = Hello

그리고 다시, 결합법칙을 만족한다.

("One" ++ "Two") ++ "Three"
// res12: String = OneTwoThree

"One" ++ ("Two" ++ "Three")
// res13: String = OneTwoThree

우리가 위에서 시퀀스에서 병렬화를 권장하기 위해 ++을 더 익숙한 + 대신 썼다는 것을 기억하자.

 

2.1 Definition of a Monoid

우리는 "addition"시나리오들을 보았다. 각각은 이진 결합 덧셈(?이거머냐 associative binary addition) 이며 항등원을 가진다. 이것이 모노이드(monoid)임을 배우는 것은 새삼 특별할 것이 없을 것이다. 일반적으로, 타입 A 를 위한 모노이드는 다음과 같다.

- 타입 (A,A) => A로 만들어주는 결합 연산

- 타입 A의 빈 원소

이 정의는 스칼라 코드에 잘 번역될 수 있다. 아래는 Cats로 부터 정의된 모노이드이다.

trait Monoid[A] {
  def combine(x: A, y: A): A
  def empty: A
}

결합과 빈 연산을 제공하는것에 더해서, 모노이드는 반드시 몇 가지 법칙에 부합해야 한다. A안의 모든 값 x,y,z의 경우 결합 법칙을 만족해야 하며 empty는 항등원이어야 한다.

def associativeLaw[A](x: A, y: A, z: A) (implicit m: Monoid[A]): Boolean = {
  m.combine(x, m.combine(y, z)) == m.combine(m.combine(x, y), z)
}
def identityLaw[A](x: A) (implicit m: Monoid[A]): Boolean = {
  (m.combine(x, m.empty) == x) && (m.combine(m.empty, x) == x)
}

정수의 뺼셈은 결합법칙을 만족하지 않기에 모노이드가 아니다.

(1 - 2) - 3
// res15: Int = -4

1 - (2 - 3)
// res16: Int = 2

실제로는, 우리가 직접 모노이드를 만들 때만 이 Laws에 대해 생각하면 된다. 이걸 만족 안하면 Cats의 시스템을 사용하다 예기치 못한 오류가 발생할 수 있다.

 

2.2 Definition of a Semigroup

세미그룹은 모노이드의 combine(결합)파트만을 뗀 것이다. 많은 세미그룹이 모노이드인데, 몇몇 상황에 대해서 empty element를 정의하지 못하는 경우가 있다. 예를 들어, 우리가 위에서 보였던 예시중에 양의 정수인 type에 대해서 모노이드를 만들 수는 없다. 항등원이 없기 때문이다. 

따라서, 더 간단하게  Cats에서 모노이드는 아래와 같이 정의된다.

trait Semigroup[A] {
  def combine(x: A, y: A): A
}
trait Monoid[A] extends Semigroup[A] {
  def empty: A
}

타입 클래스를 다룰 때 이런 상속을 많이 볼 것이다. 우리에게 모듈성과 재사용성을 늘려준다. 모노이드 get => 세미그룹까지 한큐에 get! 비슷하게, Semigroup[B]를 넘겨야 할 상황에서 Monoid[B]를 넘길 수 있다.

 

2.3 Exercise: The Truth About Monoids

몇몇 예시를 봤지만 좀 더 보자. Boolean에 대해 생각 해 보자. 얼마나 많은 모노이드를 정의할 수 있냐? 아래의 정의로부터 시작해보자.

trait Semigroup[A] {
  def combine(x: A, y: A): A
}
trait Monoid[A] extends Semigroup[A] {
  def empty: A
}
object Monoid {
  def apply[A](implicit monoid: Monoid[A]) =
    monoid
}

나는 and / or 만 생각했는데

    implicit val MonoidBooleanOr : Monoid[Boolean] = new Monoid[Boolean] {
      def empty: Boolean = false
      def combine(x: Boolean, y: Boolean): Boolean = x || y
    }
    
    implicit val MonoidBooleanAnd : Monoid[Boolean] = new Monoid[Boolean] {
      def empty: Boolean = true
      def combine(x: Boolean, y: Boolean): Boolean = x && y
    }

솔루션 보니 이것저것 더 있는 모양이다.

 

2.4 Exercise: All Set for Monoids

집합에 대한 모노이드나 세미그룹은 뭐가 있을까?

    implicit def MonoidSet[A] : Monoid[Set[A]] = new Monoid[Set[A]] {
      def empty: Set[A] = Set()
      def combine(x: Set[A], y: Set[A]): Set[A] = x ++ y
    }

난 이렇게 썼다.

2.5 Monoids in Cats

이제 모노이드가 뭔지 알아봤고, Cats에서 어떻게 구현 되어 있는지 알아보자. 다시한번 세가지 주요한 요소들에 대해 생각 해 보자. type class / instances / interface !

 

2.5.1 The Monoid Type class

모노이드 타입클래스는 cats.kernel.Monoid 이다. 이는 cats.Monoid로 불린다. Monoid는 cats.kernel.Semigroup을 extend하는데, 이는 cats.Semigroup으로도 불린다(가칭(alised)되어 있다). 우리가 Cats를 사용할 때, 우리는 일반적으로 cats package에서 타입클래스들을 불러온다.

import cats.Monoid
import cats.Semigroup

 

Cats Kernel?

Cats Kernel은 Cats의 하위 프로젝트로서, 적은 typeclass 집합을 제공한다. 그치만 동시에 Cats의 모든 툴박스를 요구하지 않는다. 이 코어 타입 클래스들이 cats.kernel 패키지에 등록되어 있지만, cats 패키지에 전부 aliased 되어 있기 때문에 그 구분을 인지할 일은 실제로 적다.

Cats Kernel 타입 클래스들을 다룬건 Eq, Semigroup, Monoid 등이다. 나머지는 모두 cats패키지 내부 친구들이다.

 

2.5.2 Monoid Instances

모노이드는 유저 인터페이스를 위한 standard Cats pattern을 따른다. 동반객체는 apply method를 가지며 특정 타입에 대한 타입 클래스 인스턴스를 리턴한다. 예를 들어, 스트링을 위한 모노이드 인스턴스를 원한다면, 그리고 우리가 scope 내에 올바른 implicit를 가지고 있다면, 우리는 아래와 같이 쓸 수 있다. 

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

Monoid[String].combine("Hi ", "there")
// res0: String = Hi there

Monoid[String].empty
// res1: String = ""

이것은 아래와도 똑같다.

Monoid.apply[String].combine("Hi ", "there")
// res2: String = Hi there

Monoid.apply[String].empty
// res3: String = ""

우리가 알고있듯이, 모노이드는 Semigroup을 extends하고 있다. 만약 empty가 필요 없다면 

import cats.Semigroup

Semigroup[String].combine("Hi ", "there")
// res4: String = Hi there

이렇게도 쓸 수 있다.

모노이드를 위한 타입 클래스 인스턴스는 cats.instances에 챕터 1에 설명한 것 처럼 일반적인 방법으로 구현되어 있다. 예를 들어,  Int를 위한 인스턴스를 받아오고 싶다면 cats.instances.int를 임포트 하면 된다.

import cats.Monoid
import cats.instances.int._ // for Monoid

Monoid[Int].combine(32, 10)
// res5: Int = 42

비슷하게, Monoid[Option[Int]]를 모으고 싶다면 cats.instances.int와 cats.instances.option을 쓰면 된다.

import cats.Monoid
import cats.instances.int._ // for Monoid
import cats.instances.option._ // for Monoid

val a = Option(22)
// a: Option[Int] = Some(22)

val b = Option(20)
// b: Option[Int] = Some(20)

Monoid[Option[Int]].combine(a, b)
// res6: Option[Int] = Some(42)

2.5.3 Monoid Syntax

Cats 는 combine method를 |+| 연산 syntax로 제공한다. 왜냐하면 combine의 경우 기술적으로 보면 Semigroup으로부터 오는 것이고, 우리는 cats.syntax.semigroup을 통해서 접근하게 되기 때문이다.

import cats.instances.string._ // for Monoid
import cats.syntax.semigroup._ // for |+|

val stringResult = "Hi " |+| "there" |+| Monoid[String].empty
// stringResult: String = Hi there

import cats.instances.int._ // for Monoid

val intResult = 1 |+| 2 |+| Monoid[Int].empty
// intResult: Int = 3

2.5.4  Exercise: Adding All The Things

def add(items: List[Int]): Int를 작성해라. 내부 숫자들을 싹 더하자. (왜 갑자기?)

  def add(items:List[Int]):Int = {
    items.foldLeft(0)((x,y)=>x+y)
  }

List[Option[Int]]를 더하는 함수를 작성해라. 단, 코드 복제가 일어나지 않게 조심하자!.

  import cats.Monoid
  import cats.implicits._

  def add2(items:List[Option[Int]]):Option[Int] = {
    items.foldLeft(Monoid[Option[Int]].empty)((x,y)=>x|+|y)
  }
  println(add2(List(Some(1),Some(2),Some(3))))

기왕 하는거 모노이드를 써 보았다.

case class Order(totalCost: Double, quantity: Double) 타입으로 add를 커스터마이징 하고 싶다. 잘 해보자.

  case class Order(totalCost: Double, quantity: Double)
  object MonoidInstance {
    implicit val MonoidOrder:Monoid[Order] = new Monoid[Order] {
      override def empty: Order = Order(0,0)
      override def combine(x: Order, y: Order): Order = Order(x.totalCost+y.totalCost,x.quantity+y.quantity)
    }
  }
  
  import MonoidInstance._
  def add3(items:List[Order]):Order = {
    items.foldLeft(Monoid[Order].empty)((x,y)=>x|+|y)
  }
  println(add3(List(Order(1,2),Order(1,2),Order(1,2),Order(1,2))))

난 위같이 풀었당

 

2.6 Applications of Monoids

우린 이제 모노이드가 뭔지 안다. (더하거나 합치는 것의 추상화이다) 그치만 어디다 쓸까? 케이스 스터디로 예시를 보자.

(자세하게 알고 싶으면 원문 글을 보자! 난 흥미가 별로 없다!)

2.6.1 Big Data

하둡이나 스파크같은 큰 데이터 처리할 때, 다양한 분산처리 안에서도 무결성이나 이런 부분들에 견고해야 하므로 상당히 많은 케이스들이 모노이드로서 바라볼 수 있게 처리된다.

2.6.2 Distributed Systems

분산 시스템에서 다양한 처리 결과를 합치는 과정에서도 모노이드의 개념이 들어갈 수 밖에 없다.

2.6.3 Monoids in the Small

위 두 예시는 모노이드들이 시스템 아키텍쳐 전반적으로 녹아 있는 예시였다. 그리고 아주 많은 상황에서 모노이드는 작게 작게 코드에서 유용하게 쓰일 수 있다. (걍 데이터 combine에 대한 규칙이 필요할 때 Monoid 임포트해서 조지면 댈 것 같다 ㅎ)

 

2.7 Summary

cats에서 모노이드는 아래의 세개의 import를 통해서 쓸 수 있다.

import cats.Monoid
import cats.instances.string._ // for Monoid
import cats.syntax.semigroup._ // for |+|

"Scala" |+| " with " |+| "Cats"
// res0: String = Scala with Cats

올바른 인스턴스들이 scope에 있다면, 우리는 우리가 원하는 것에 대해 전부 더할 수 있다.

 

import cats.instances.int._ // for Monoid
import cats.instances.option._ // for Monoid

Option(1) |+| Option(2)
// res1: Option[Int] = Some(3)

import cats.instances.map._ // for Monoid

val map1 = Map("a" -> 1, "b" -> 2)
val map2 = Map("b" -> 3, "d" -> 4)

map1 |+| map2
// res3: Map[String,Int] = Map(b -> 5, d -> 4, a -> 1)

import cats.instances.tuple._ // for Monoid

val tuple1 = ("hello", 123)
val tuple2 = ("world", 321)

tuple1 |+| tuple2
// res6: (String, Int) = (helloworld,444)

또한 우리는 우리가 가지고 있는 어떤 타입과도 모노이드와 잘 동작하는 제네릭 코드를 작성할 수 있다.

def addAll[A](values: List[A])(implicit monoid: Monoid[A]): A = values.foldRight(monoid.empty)(_ |+| _)

addAll(List(1, 2, 3))
// res7: Int = 6

addAll(List(None, Some(1), Some(2)))
// res8: Option[Int] = Some(3)

굿굿 ㅎ

 

챕터 2는 좀 짧다

 

 

 

 

 

오역 / 틀리게 적은 부분에 대한 지적은 환영입니닷 영어가 짧아서...