이번 글에서는 모노이드와 세미그룹에 대해 다룰 예정입니다.
여전히, 레퍼런스는 Scala with Cats [무료 PDF] 입니다.
Recap.
모노이드와 세미그룹은 값을 더하거나 합치는데 사용됩니다. 들어가기에 앞서, 몇 가지 기초적인 수학 법칙들에 대해서 짚고 넘어가고자 합니다. 우리는 덧셈과 곱셈 연산에 대해 결합법칙과 교환법칙이 성립하는 것과 덧셈 연산의 항등원은 0, 곱셈 연산의 항등원은 1이라는 사실을 이미 알고 있습니다.
더불어, 문자열의 덧셈 연산은 결합법칙이 성립하며, 항등원을 가지고 있습니다.
"One" ++ "Two" // "OneTwo"
"Two" ++ "One" // "TwoOne" 교환법칙은 성립하지 않는다.
("One" ++ "Two") ++ "Three" // "OneTwoThree"
"One" ++ ("Two" ++ "Three") // "OneTwoThree" 결합법칙의 성립
"Hello" ++ "" == "Hello" // "Hello"
"" ++ "Hello" == "Hello" // "Hello" 문자열 덧셈 연산의 항등원은 빈 문자열 "" 이다.
문자열 덧셈 연산의 이 특성은 Sequence 에도 적용됩니다.
Seq(1, 2) ++ Seq(3, 4) // List(1, 2, 3, 4)
Seq(3, 4) ++ Seq(1, 2) // List(3, 4, 1, 2) 교환법칙은 성립하지 않는다.
Seq(1, 2) ++ (Seq(3, 4) ++ Seq(5, 6)) // List(1, 2, 3, 4, 5, 6)
(Seq(1, 2) ++ Seq(3, 4)) ++ Seq(5, 6) // List(1, 2, 3, 4, 5, 6) 결합법칙의 성립
Seq(1, 2) ++ Seq() // List(1, 2)
Seq() ++ Seq(1, 2) // List(1, 2) Seq 합 연산의 항등원은 빈 Seq 이다.
갑자기 수학 법칙 이야기는 왜 하는거에요?
위에서 언급한 구조들이 모노이드이기 때문입니다.엥?
위키피디아 모노이드 : https://ko.wikipedia.org/wiki/모노이드
일반적으로 타입 A에 대한 모노이드는 다음과 같이 정의합니다 :
- 연산 결합이 가능하고 (A, A) ⇒ A
- 타입A에 대해 비어있는 상태의(element empty of type A) 정의가 있다.
우리는 이러한 연산들을 모노이드라고 부릅니다.
모노이드를 이룰 때, 교환법칙은 필수가 아닙니다.
따라서 문자열의 덧셈 연산도 모노이드를 이룬다고 할 수 있습니다.
Cats 라이브러리에서 모노이드는 다음과 같이 정의되었습니다 (간략한 버전)
trait Monoid[A] {
def combine(x: A, y: A): A
def empty: A
}
combine 과 empty 함수를 제공할 때, 모노이드는 몇 가지 법칙을 따라야 합니다.
- 타입 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)
}
따라서 항등원도 존재하지 않고 결합 법칙을 만족하지 않는 뺄셈과 나눗셈 연산은 모노이드가 아닙니다.
실제로 새로운 모노이드 인스턴스를 정의하고자 할 때에 위 두 가지 법칙에 대해서만 고려하면 됩니다.
두 법칙을 준수하지 않는 인스턴스는 Cats 라이브러리의 다른 함수들과의 상호작용 후에 예측할 수 없는 결과를 반환할 수 있습니다.
세미그룹 (Semigroup, 군집)
세미그룹(군집)은 결합법칙이 성립하는 이항연산을 의미합니다.
많은 세미그룹이 모노이드이기도 하지만, 항등원을 정의해 줄 수 없는 경우를 세미그룹이라고 합니다.
즉, 비어있는 상태의 원소를 정의해 줄 수 없는 경우가 발생하는 경우를 대비하여 사용하는 타입 클래스입니다.
우리는 Sequence 자료형의 결합 및 정수 덧셈 연산이 모노이드인것을 알고 있습니다.
하지만 프로그램 로직 상 빈 Sequence를 사용할 수 없거나 양의 정수만을 사용해야 한다면 어떨까요?
이 때 우리는 empty 메소드를 정의해 줄 수 없게 됩니다. (일례로 Cats에서는 세미그룹의 구현체지만 모노이드는 아닌 NonEmptyList라는 데이터 타입이 존재합니다.)
세미그룹 추가에 따른 Cats 라이브러리 내 모노이드의 (조금 더) 자세한 구현체는 다음과 같습니다.
trait Semigroup[A] {
def combine(x:A, y: A): A
}
trait Monoid[A] extends Semigroup[A] {
def empty: A
}
타입 클래스에 대해 이야기할 때 종종 이런 형태의 상속을 보게 될텐데, 이는 모듈성과 재사용성을 부여하기 위함입니다.
만약 우리가 타입 A에 대한 모노이드를 구현한다면 세미그룹은 자동으로 같이 구현됩니다. 즉, 메소드가 세미그룹을 타입을 인자로 받는다면 모노이드를 세미그룹 대신 사용해도 됩니다.
Cats의 Monoid
Monoid Type Class
Cats의 모노이드 타입 클래스는 cats.kernel.Monoid 패키지에, 세미그룹 타입 클래스는 cats.kernel.Seimgroup 패키지에 정의되어 있습니다. 각각의 패키지는 cats.Monoid, cats.Semigroup으로 aliased 되었습니다.
* Cats Kernel 이란?
Cats Kernel은 Cats의 subproject로써 Cats toolbox 전체를 필요로 하지 않는 라이브러리를 위해 몇 가지 타입클래스만을 제공합니다. 본 시리즈에서 다루는 커널 타입 클래스는 Eq, Semigroup, Monoid 뿐입니다.
import cats.Monoid
import cats.Semigroup
Monoid Type Instances
모노이드는 interface를 사용하기 위해 표준 Cats 패턴을 따릅니다 : 특정 타입에 대한 타입 클래스를 반환하는 apply 메소드가 정의된 컴패니언 오브젝트를 가지고 있습니다. 만약 String 모노이드 인스턴스가 필요하다면 다음과 같은 import문을 사용하면 됩니다.
import cats.Monoid
import cats.instances.string._
Monoid[String].combine("Hello, ", "World!") // "Hello, World!"
Monoid[String].empty // ""
Monoid.apply[String].combine("Hello, ", "World!") // "Hello, World!"
Monoid.apply[String].empty // ""
모노이드는 Semigroup을 상속받기때문에 다음과 같이 사용할 수도 있습니다.
import cats.Semigroup
Semigroup[String].combine("Hello, ", "World!") // "Hello, World!"
Option 타입을 사용할 수도 있습니다.
import cats.Monoid
import cats.instances.int._
import cats.instances.option._
val a = Option(3) // Option[Int] = Some(3)
val b = Option(5) // Option[Int] = Some(5)
Monoid[Option[Int]].combine(a, b) // Option[Int] = Some(8)
Monoid Syntax
Cats는 combine 메소드를 사용하기 위한 Syntax로 |+| 연산자를 제공합니다.
combine은 Semigroup의 메소드이기 때문에 cats.syntax.semigroup을 import 해 사용합니다.
import cats.instances.string._
import cats.syntax.semigroup._
val helloWorld = "Hello, " |+| "World!" |+| Monoid[String].empty
그럼 이 모노이드와 세미그룹은 도대체 언제 사용하는 건가요?
위에서 언급한 특성을 다시 나열해 봅시다.
- 결합 법칙이 성립힌다.
- 항등원이 존재한다.
만약 Sequence의 모든 원소에 대한 계산이 필요한데, 그 원소들이 위 성질들을 만족한다면, 병렬로 연산을 실행한 다음 마지막에 결과를 합쳐주기만 하면 되지 않을까요?
실제로 모노이드는 위와 같은 상황에서 병렬 실행 안정성을 제공할 수 있습니다.
'Languages > Scala' 카테고리의 다른 글
[Scala Cats] 펑터 Functors, Higher Kinded Types (0) | 2021.01.06 |
---|---|
[Scala Cats] 타입 공변성/반공변성 다루기 (0) | 2020.12.15 |
[Scala Cats] Cats의 타입 클래스 (0) | 2020.12.07 |
[Scala Cats] 스칼라 타입 클래스 기초 (0) | 2020.11.13 |
[스칼라] 함수 - 기초 (0) | 2020.07.21 |