[번역] Point-Free #4 대수적으로 알아보는 타입 체계
Swift의 타입 체계와 대수의 연관점이 있을까요? 네, 아주 많이요! 이번 에피소드에선 둘 사이의 공통점을 알아보고 그 공통점을 이용해서 타입 안정성을 가지는 데이터 구조를 만들어보겠습니다.
시작하며
오늘은 두 세계 사이의 연결점에 대해 알아봅시다. 바로 Swift의 타입과 대수입니다. 우리 모두 두 세계에 익숙하지만, 이들이 아주 깊이 연관돼있고 아름답기까지 하다는 사실을 아시나요? 두 세계의 공통점을 사용하면 데이터 구조가 얼마 만큼의 복잡도를 가지는지 이해할 수 있으며, 일어날 수 없는 상태에 대해 컴파일 시간에 예방하는 더 나은 구조를 생성할 수 있습니다.
대수적으로 알아보는 구조체
먼저 간단한 예시인 구조체부터 시작하겠습니다.
struct Pair<A, B> {
let first: A
let second: B
}
이 구조체는 두 개의 제네릭을 받는 구조입니다. 그리고 두 개의 필드가 존재하는데 각 필드가 제네릭 데이터를 하나씩 받습니다. 이 구조체의 A
와 B
에 타입을 넣고 그에 맞는 경우의 수를 계산해보겠습니다.
Pair<Bool, Bool>(first: true, second: true)
Pair<Bool, Bool>(first: true, second: false)
Pair<Bool, Bool>(first: false, second: true)
Pair<Bool, Bool>(first: false, second: false)
Pair<Bool, Bool>
는 정확히 4개의 값을 가질 수 있습니다. 다른 값을 넣는다면 Swift의 컴파일러가 허락하지 않을 것입니다. 또 다른 예제를 만들어봅시다. Bool
과 짝을 이룰 세 개의 값을 가질 수 있는 열거형을 만들겠습니다.
enum Three {
case one
case two
case three
}
Bool
과 Three
를 이용하면 가질 수 있는 값의 경우의 수가 어떻게 될까요?
Pair<Bool, Three>(first: true, second: .one)
Pair<Bool, Three>(first: true, second: .two)
Pair<Bool, Three>(first: true, second: .three)
Pair<Bool, Three>(first: false, second: .one)
Pair<Bool, Three>(first: false, second: .two)
Pair<Bool, Three>(first: false, second: .three)
6가지네요! 흥미롭습니다.
Swift에는 특이한 타입인 Void
가 존재합니다. 이 타입이 특이한 이유는 두 가지나 있습니다. 하나는 Void
의 타입과 값을 동일한 방식으로 표현할 수 있다는 점입니다.
_: Void = Void()
_: Void = ()
_: () = ()
두 번째는 Void
는 단 하나의 값만 가진다는 점입니다! 그러한 이유로 ()
에 아무 값을 넣지 않아도 문제가 없습니다. 이는 Void
의 단 하나의 값을 표현하는 것이지만 ()
로 무언가를 할 수는 없습니다. 반환 값이 없는 함수에서 반환값을 명시적으로 지정하지 않아도 비밀리에 Void
를 반환하는 이유이기도 합니다.
func foo(_ x: Int) /* -> Void */ {
// return ()
}
Swift 컴파일러가 숨겨진 부분을 채워줍니다.
그럼 이제 Void
를 Bool
과 연결해봅시다.
Pair<Bool, Void>(first: true, second: ())
Pair<Bool, Void>(first: false, second: ())
두 가지 값을 가지네요.
Void
와 Void
의 쌍은 어떨까요?
Pair<Void, Void>(first: (), second: ())
한 가지 값을 가집니다!
Swift에는 이상한 타입이 하나 더 있습니다. 바로 Never
입니다. Never
의 정의는 아주 간단합니다.
enum Never {}
이게 무슨 의미일까요? 열거형인데 케이스가 없습니다. 이런 타입을 보통 “uninhabited type”이라고 부릅니다. 아무런 값도 가지지 않는 타입이라는 뜻입니다. 이런 타입은 다음과 같이 작성할 수 있는 방법이 없습니다.
_: Never = ???
위의 물음표에 넣을 수 있는 값도 없는데다가 Swift 컴파일러도 허락하지 않습니다.
만약 Never
와 Pair
를 쌍으로 연결하면 어떻게 될까요?
Pair<Bool, Never>(first: true, second: ???)
앞에서 말한대로 ???
자리에 넣을 수 있는 값이 없습니다!
또한 Never
는 컴파일러의 특별한 관리를 받습니다. 만약 어떤 함수가 Never
를 반환하면 아무것도 반환하지 않는 함수라고 이해합니다. 예를 들어, fatalError
함수는 Never
를 반환합니다.
앞의 예제를 바탕으로 A
와 B
로 이루어진 쌍인 Pair<A, B>
의 경우의 수를 계산해볼까요?
// Pair<Bool, Bool> = 4
// Pair<Bool, Three> = 6
// Pair<Bool, Void> = 2
// Pair<Void, Void> = 1
// Pair<Bool, Never> = 0
패턴이 보이네요. 이건 곱셈입니다!
// Pair<Bool, Bool> = 4 = 2 * 2
// Pair<Bool, Three> = 6 = 2 * 3
// Pair<Bool, Void> = 2 = 2 * 1
// Pair<Void, Void> = 1 = 1 * 1
// Pair<Bool, Never> = 0 = 2 * 0
Pair
는 값의 수를 곱한 수만큼의 값을 가질 수 있습니다. 앞에서 본 것 처럼 Pair<A, B>
가 가질 수 있는 값의 경우의 수는 A
가 가질 수 있는 값의 수와 B
가 가질 수 있는 값의 수를 곱한 것과 동일합니다.
이러한 현상을 대수적으로 풀어낸 또 다른 해석이 존재합니다. 바로 논리곱(logical conjunction)입니다. Pair
타입은 두 타입의 “and” 값을 모아놓은 것입니다. 즉, Pair<A, B>
는 반드시 A
의 어떤 값과 B
의 어떤 값을 가집니다.
그리고 이것은 Pair
에서만 일어나는 일이 아닙니다. 다른 예시도 보시죠.
enum Theme {
case light
case dark
}
enum State {
case highlighted
case normal
case selected
}
struct Component {
let enabled: Bool
let state: State
let theme: Theme
}
Component
를 대수적으로 풀면 어떻게 될까요?
// Bool * Theme * State = 2 * 3 * 2 = 12
Component
가 가질 수 있는 값의 경우의 수는 12네요!
이제 타입의 이름은 신경쓰지 말고 어떤 데이터가 저장되어 있는지에 집중해봅시다. 지금까지 Pair<A, B>
라고 쓰던 것이 A * B
로 간결해졌습니다. 이런 모습이 이상해보일 수 있지만, 직관을 키우는데는 이만한 것이 없습니다.
// Pair<A, B> = A * B
// Pair<Bool, Bool> = Bool * Bool
// Pair<Bool, Three> = Bool * Three
// Pair<Bool, Void> = Bool * Void
// Pair<Bool, Never> = Bool * Never
저희는 A * B
를 A
와 B
의 곱(product)이라고 부릅니다. Pair<A, B>
를 풀어서 A
와 B
가 가질 수 있는 값의 수를 곱하는 직관에 대해 조금 더 추상적으로 생각해봅시다. 타입이 가질 수 있는 값이 한정적일 때는 이런 식의 방식이 참인 반면, 다음과 같은 상황에선 도움이 되지 않습니다.
// Pair<Bool, String> = Bool * String
위 예시가 가질 수 있는 값의 경우의 수는 계산하지 않겠습니다. 왜냐하면 String
이 가질 수 있는 값의 수는 무한대이기 때문입니다.
다음과 같이 무한 개의 값을 가질 수 있는 타입끼리 곱하는 경우도 있을 것입니다.
// String * [Int]
// [String] * [[Int]]
이제는 Void
, Never
그리고 Bool
의 이름을 없애고 각 타입이 가질 수 있는 값의 경우의 수를 표현해보겠습니다.
// Never = 0
// Void = 1
// Bool = 2
이제 우리는 특정 타입에 대해서만 고민하지 않고 추상적이고 대수적인 개체에 대해 생각할 수 있게 되었습니다!
대수적으로 알아보는 열거형
지금까지 Swift의 구조체가 타입의 곱셈에 대응한다는 것을 알아보았습니다. 그렇다면 곱셈의 쌍대(dual)인 덧셈에 해당하는 것은 무엇일까요? 이를 Swift의 타입 체계로 본다면 어떤 것일까요?
Swift에는 정확히 이런 기능을 지원하는데요, 바로 열거형입니다! 일단 아주 일반적인 열거형을 정의해보겠습니다.
enum Either<A, B> {
case left(A)
case right(B)
}
구조체에서 봤던 것과 동일하게 Bool
로 예제를 구성해보겠습니다.
Either<Bool, Bool>.left(true)
Either<Bool, Bool>.left(false)
Either<Bool, Bool>.right(true)
Either<Bool, Bool>.right(false)
구조체와 동일하게 결과는 네 개네요. Three
의 경우는 어떨까요?
Either<Bool, Three>.left(true)
Either<Bool, Three>.left(false)
Either<Bool, Three>.right(.one)
Either<Bool, Three>.right(.two)
Either<Bool, Three>.right(.three)
이번엔 다섯 개네요. 흥미롭습니다! 그럼 Void
는 어떨까요?
Either<Bool, Void>.left(true)
Either<Bool, Void>.left(false)
Either<Bool, Void>.right(Void())
세 개입니다!
Never
는요?
Either<Bool, Never>.left(true)
Either<Bool, Never>.left(false)
Either<Bool, Never>.right(???)
마지막 결과는 특히나 흥미롭습니다. 구조체에서 Never
가 들어간 쌍(Pair)으로 예시를 들 때는 Pair<A, Never>
와 같은 모양을 가져서 아무런 쌍도 얻을 수 없었습니다. 하지만 Either
의 경우엔 하나의 경우만 불가능한 경우이고 다른 값은 Bool
을 받는 가능한 경우입니다.
그렇다면 여기에 숨어있는 대수적인 비밀은 무엇일까요? Either<A, B>
에서 A
와 B
의 값 사이의 관계를 알아봅시다!
Either<Bool, Bool> = 4 = 2 + 2
Either<Bool, Three> = 5 = 2 + 3
Either<Bool, Void> = 3 = 2 + 1
Either<Bool, Never> = 2 = 2 + 0
위의 예시에서 알 수 있듯이, Either<A, B>
는 A
의 값의 수와 B
의 값의 수를 더한 결과입니다. 이 결과를 통해 Either
는 타입의 덧셈에 직접적으로 대응한다는 것을 알 수 있습니다. 그리고 이게 바로 열거형이 “합 타입(sum types)”이라고 불리는 이유이기도 합니다. 또한, Pair
에 했던 것처럼 Either
를 논리적인 측면에서 해석할 수도 있습니다. Either
타입은 두 타입 중 하나를 값으로 가집니다. 즉, Either<A, B>
는 A
타입이 나타낼 수 있는 값의 수와 B
타입이 나타낼 수 있는 값의 수를 합한 수 만큼의 값을 가질 수 있다는 것을 의미합니다.
Swift 문법에 맞지는 않지만 더욱 직관적으로 이해하기 위해 기존의 코드를 좀 더 풀어서 써보겠습니다.
Either<Bool, Bool> = Bool + Bool = 2 + 2 = 4
Either<Bool, Three> = Bool + Three = 2 + 3 = 5
Either<Bool, Void> = Bool + Void = 2 + 1 = 3
Either<Bool, Never> = Bool + Never = 2 + 0 = 2
Void라는 단어를 조심하세요!
몇몇 언어(Haskell, PureScript, Idris 등)에서 값이 없는 타입을 표현하기 위해 Void
(Swift에선 Never
입니다)를 사용합니다. 그리고 이는 다른 언어로 갔을 때 혼란을 만들기도 합니다. 생각해보면 “Void”라는 단어의 의미는 아무것도 없는 공허를 뜻하기 때문에 아무것도 없는 타입을 표현하기 위한 이름으로 적절한 것 같기도 합니다.
Swift의 Void
처럼 단 하나의 값을 가지고 있는 타입을 표현하는 더 나은 이름은 Unit
이 있습니다. Unit
은 다음과 같이 정의할 수 있습니다.
struct Unit {}
let unit = Unit()
Unit
이 좋은 점은 드디어 타입의 이름(Unit
)과 타입의 값(unit
)을 분리할 수 있게 되었다는 점입니다. 또 다른 좋은 점은 Unit
에 대한 실제 구조체 타입이 생겨서 익스텐션을 추가할 수 있게 되었다는 점입니다.
extension Unit: Equatable {
static func == (lhs: Unit, rhs: Unit) -> Bool {
return true
}
}
위 코드를 통해 우리는 unit
을 비교가 가능한(Equatable) 값만 입력으로 받는 함수에 넣을 수 있게 되었습니다. Swift의 Void
는 할 수 없는 일이라 더욱 끝내주네요. Void
에 익스텐션을 추가하려고 하면 다음과 같은 에러를 만나게 됩니다.
Non-nominal type 'Void' cannot be extended
왜냐하면 Void
는 빈 튜플로 정의되었기 때문입니다.
typealias Void = ()
Swift의 튜플은 non-nominal 타입입니다. 다시 말하자면, 이름으로는 접근할 수 없고 구조로만 접근할 수 있다는 의미입니다. 이것은 Swift의 정말 아픈 손가락 중 하나라고 생각됩니다. 꼭 빠른 시일내에 보수되기를 바랍니다.
빈 구조체 vs 빈 열거형
이제부터는 아주 이상한 일을 해볼 것입니다. 일단 Unit
과 Never
의 정의를 꼼꼼히 살펴보겠습니다.
struct Unit {}
enum Never {}
아무런 경우가 없는 열거형이라는 점과 아무런 필드가 없는 구조체라는 점에서 비슷한 점이 존재하네요. 그러면 아무런 경우가 없는 열거형은 값이 없는 반면, 아무런 필드가 없는 구조체는 값이 하나 존재할까요? 위 코드만 봐서는 Unit
에도 아무런 값이 존재하지 않는다고 생각하는 것이 충분히 가능해보입니다.
이러한 이유를 직관적으로 이해할 수 있으신가요?
이 질문에는 Swift의 타입과 대수 사이의 대응을 사용하면 더욱 쉽게 답변을 낼 수 있습니다. 직관적으로, “빈 열거형과 빈 구조체에는 어떤 값이 존재할까?”라는 질문은 “정수로 이루어진 빈 배열의 합과 곱은 무엇일까?”라는 질문과 동일하다고 생각할 수 있습니다.
정수로 이루어진 배열이 있다고 해보겠습니다. 다음의 함수에 들어가야 할 내용은 무엇일까요?
func sum(_ xs: [Int]) -> Int {
fatalError()
}
func product(_ xs: [Int]) -> Int {
fatalError()
}
let xs = [1, 2, 3]
sum(xs)
product(xs)
우리가 할 일은 배열의 모든 원소를 돌면서 합하거나 곱하는 것이겠죠?
func sum(_ xs: [Int]) -> Int {
var result: Int
for x in xs {
result += x
}
return result
}
func product(_ xs: [Int]) -> Int {
var result: Int
for x in xs {
result *= x
}
return result
}
위 코드는 result
의 기본값이 지정되지 않아서 당장은 컴파일되지 않습니다. 그러면 result
의 기본값은 무엇이 되어야 할까요? 이 질문에 답변하기 위해선 sum
과 product
가 어떤 속성을 가져야하는지 이해해야 합니다. 그러면 저절로 result
가 어떤 값으로 시작해야하는지 정해질 것입니다. 몇 개의 배열을 받았을 때 sum
과 product
가 가져야 할 가장 기본적인 속성은 다음과 같을 것입니다.
sum([1, 2]) + sum([3]) == sum([1, 2] + [3])
product([1, 2]) * product([3]) == product([1, 2] + [3])
그러면 여기에 빈 배열을 넣어볼까요?
sum([1, 2]) + sum([]) == sum([1, 2] + [])
product([1, 2]) * product([]) == product([1, 2] + [])
위 등식을 만족시키려면 sum([])
은 0이 되어야 하고, product([])
는 1
이 되어야 합니다. 다른 선택지는 존재하지 않습니다. 그러니 합의 기본값은 0
이고 곱의 기본값은 1
이라고 할 수 있습니다.
sum([1, 2]) + 0 == sum([1, 2] + [])
product([1, 2]) * 1 == product([1, 2] + [])
sum([]) == 0
product([]) == 1
이제 이 개념을 타입 세계로 옮겨볼까요? 기본값을 타입으로 옮겨보면 “빈 합 타입”은 값이 없고(불가능하고) “빈 곱 타입”은 하나의 값을 가집니다! 드디어 대수를 이용해서 진퇴양난의 상황을 해결할 수 있게 되었네요! 정말 끝내줍니다!
대수적 속성
여기까지 Swift의 타입과 대수 사이의 대응을 이해하기 위해 몇 가지 개념을 익혔습니다. 그러면 이제 이 지식을 활용해서 타입 구성에 대한 조금 더 높은 단계의 직관을 얻어볼 준비가 되었습니다.
쉬운 것부터 시작하겠습니다. Void
가 1
에 대응한다는 사실은 기억하실 것입니다. 그리고 대수 세계에선 1
로 곱하는 것은 아무일도 일으키지 않는다는 것을 알고 계실 것입니다. 그렇다면 이것을 타입에 적용하면 어떤 일이 일어날까요?
// Void = 1
// A * Void = A = Void * A
위 내용은 구조체의 필드에 Void
값을 사용하면 본질적으로는 타입이 변하지 않는 순 효과를 일으킨다는 의미입니다.
반대로 0
에 대응하는 Never
를 곱에 사용하면 0
이라는 결과가 나옵니다. 타입 세계에서 보면 다음과 같습니다.
// Never = 0
// A * Never = Never = Never * A
그러니 구조체의 필드에 Never
를 넣는 것은 구조체에 Never
타입 하나만 넣는 것과 같은 결과를 냅니다. 완전히 소멸시켜버리는 것이죠.
그러나 0
을 더하는 것은 결과값을 바뀌지 않게 해주며, 다음과 같이 표현할 수 있습니다.
// A + Never = A = Never + A
다음과 같은 타입 표현이 있다고 해보겠습니다.
// 1 + A = Void + A
Either
로 표현하면 다음과 같은 모양일 것입니다.
// Either<Void, A> {
// case left(())
// case right(A)
// }
이 타입은 오른쪽엔 A
의 모든 값을 가지고 왼쪽엔 단 하나의 특별한 값인 left(Void())
를 가지게 될 것입니다. Swift에서 이런 모양을 가진 타입이 무엇이 있을까요? 바로 Optional
입니다!
enum Optional<A> {
case none
case some(A)
}
none
의 경우는 left
에 대응하고, some
의 경우는 right
에 대응합니다. 그러니 다음과 같이 표현할 수 있겠죠.
// 1 + A = Void + A = A?
이제 아래와 같은 표현식을 만나게 되었다고 해보겠습니다.
// Either<Pair<A, B>, Pair<A, C>>
지금까지 했던 방식으로 표현해보면 다음과 같겠네요.
// A * B + A * C
기본적인 대수를 사용해서 간단한 표현식으로 바꿀 수 있을 것 같습니다.
A * (B + C)
이를 다시 원래대로 표현하면 열거형을 가진 Pair
로 표현할 수 있겠네요.
Pair<A, Either<B, C>>
이렇게 대수적인 직관은 데이터 구조를 간단하게 만들 수 있는 능력을 길러줍니다.
이전 예제의 Pair
와 Either
를 뒤집어 봅시다.
Pair<Either<A, B>, Either<A, C>>
수학 세계의 말로 표현하면 다음과 같을 것입니다.
// (A + B) * (A + C)
위 식은 인수분해할 것이 없기 때문에 더 이상 간단하게 만들 수 없습니다.
물론 확장할 수는 있습니다.
// A * A + A * C + B * A + B * C
이 결과를 타입 세계의 언어로 표현하면 네 개의 Pair
를 경우로 가지는 열거형이 될 것입니다. 만약 이게 여러분이 원하던 결과라면 대수의 힘을 한 번 더 느끼셨겠군요!
그래서 요점이 무엇인가요?
지금까지는 직관에 도움을 주기 위해 Swift에서 작동하지 않는 많은 의사코드를 작성해왔습니다. 도움이 좀 되셨나요?
이제는 실제 상황인 URLSession
의 메소드를 살펴보겠습니다.
// URLSession.shared
// .dataTask(with: url, completionHandler: (data: Data?, response: URLResponse?, error: Error?) -> Void)
이 컴플리션 핸들러는 모두 옵셔널인 세 개의 값을 반환합니다. Swift의 튜플은 곱이니 세 개의 필드의 곱 타입이라고 생각할 수 있습니다. 그럼 대수적으로 적어볼까요?
// (Data + 1) * (URLResponse + 1) * (Error + 1)
이해하기 어려우니 완전히 풀어보겠습니다.
// (Data + 1) * (URLResponse + 1) * (Error + 1)
// = Data * URLResponse * Error
// + Data * URLResponse
// + URLResponse * Error
// + Data * Error
// + Data
// + URLResponse
// + Error
// + 1
여기엔 일어날 수 없는 상태에 대한 표현도 존재합니다. URLResponse
는 Error
와 동시에 존재할 수 없기 때문에 URLResponse * Error
라는 경우는 생길 수 없는 경우입니다. 같은 이유로 Data * Error
도 불가능한 경우입니다. 모든 값이 nil
인 경우 Void
라는 뜻이니 1
은 얻을 수 있는 값입니다. 그리고 절대 일어날 수 없지만 모든 값이 잘 왔을 때는 Data * URLResponse * Error
모양의 값을 얻을 수 있을 것입니다.
바로잡기 독자 중 한 분인 Ole Begemann에 의하면,
URLResponse
와Error
가 동시에nil
이 아닌 경우는 존재할 수 있다고 합니다. 그 분이 작성하신 블로그 포스트가 있으니 자세한 내용은 여기서 읽어보시면 좋을 것 같습니다. 그리고 이 부분을 바로잡는 내용은 이 시리즈의 다음 에피소드인 대수적으로 풀어보는 타입 체계: 지수편에서 얘기할 예정입니다.
이 인터페이스를 그대로 사용하면 결과값이 필요한 경우엔 if let
을 사용하고, 반대로 결과값이 필요없다고 생각하는 경우엔 fatalError
를 두고 절대 호출되지 않기를 희망하는 수밖에 없습니다.
이번 에피소드에서 얻은 새로운 직관을 사용해서 우리에게 필요한 것을 표현해봅시다.
// Data * URLResponse + Error
타입을 사용하면 어떤 모양일까요?
Either<Pair<Data, URLResponse>, Error>
실제로 Swift 커뮤니티에서는 이미 이러한 종류의 타입인 Result
를 채택했습니다.
// Result<(Data, URLResponse), Error>
그리고 이 경우엔 Pair
대신 Data
와 URLResponse
로 이루어진 간단한 튜플을 사용하면 됩니다.
컴플리션 콜백에서 적절한 타입을 사용한다면 컴파일 시간에 허용됐지만 실제로 일어날 수 없는 상태를 제거할 수 있게 되고, 콜백의 로직도 아주 간단해질 것입니다.
Result
타입에 대해 조금 더 알아보겠습니다. 사용하고 있는 API가 Result
타입을 반환하지만 이 연산은 절대 실패하지 않는다면 어떨까요? 그런 경우 Result
의 에러 타입을 Never
로 만들 수 있을 것입니다!
// Result<A, Never>
이제 우리는 타입만 보고도 에러가 발생할 수 없는 API라는 것을 알게되었습니다.
그렇다면 도중에 취소가 가능한 비동기 API를 다룰 때는 어떨까요? 취소에 대한 경우를 Result
에 어떻게 포함할 수 있을까요?
// Result<A, Error>?
옵셔널로 만들어주면 됩니다!
대수적 직관은 코드의 복잡성을 해결하고 필요한 타입을 적절하게 사용하는 능력을 더 명확하게 만들어줍니다. 지금까지 보여드린 구조체나 열거형에 대한 예시는 우리가 매일 보는 그것들에 비하면 아주 간단한 편입니다.
튜플은 무서워하지 않는데 Either
는 무서우시다구요? 그 말은 곱셈은 무섭지 않은데 덧셈은 무섭다라는 말과 같고, “or”는 무서운데 “and”는 무섭지 않다와 같습니다. 개발할 때 곱셈(*
)과 “and”(&&
)만 사용하는 사람은 아무도 없습니다. 덧셈(+
)과 “or”(||
)도 사용해보세요. 그러면 합 타입, 더 나아가서 Either
가 편해질 것입니다!
오늘 알아본 내용은 대수적으로 타입을 알아보는 여행의 시작점일 뿐입니다. 오늘 알아본 덧셈과 곱셈을 제외하고도 타입 체계가 대수적인 개념을 어떻게 표현하는지 알아볼 내용이 아주 많습니다. 예를 들면 지수가 있겠네요! 타입의 제곱을 표현하는 방법엔 무엇이 있을까요? 더 말씀드리고 싶지만, 아쉽게도 오늘은 여기까지 입니다.
그럼 다음 에피소드에서 뵙겠습니다!
본문에 연습문제와 참고 자료가 있습니다. 여기에서 확인해주세요!