Data Types and Algebra
Swift의 데이터 타입과 대수학(Algebra)의 관계에 대해서 알아보도록 하겠습니다.
데이터 타입과 대수학이 관계가 깊다는 얘기를 우연히 접한적이 있습니다. 데이터 타입은 제가 String, Int, Bool, Long, Short, unsigned Int 등 문자열, 실수 정수 등 여러 종류의 데이터를 나타내기 위한 것이다라는 생각을 바로 할 수 있었는데요
대수학은 뭘까요??…. 나무위키에 따르면 초중등교육에서는 미지수에 변수를 대입하는 기술이라고 하는데.... 그 이상의 개념은 저는 잘 모르겠어서 암튼 여기까지 하고 일단 넘어가 보겠습니다.
구조체와 대수학
먼저 아래와 같은 구조체를 정의해보겠습니다.
struct Some<T, U> {
let first: T
let second: U
}
위 구조체는 제네릭 타입으로 T, U를 받고 first와 second라는 2개의 필드에 각각 T, U의 타입을 가지고 있습니다.
만약 T와 U가 Boolean값이라면 Some이라는 구조체가 가질 수 있는 경우의 수는 총 4가지 입니다.
(true, true), (true, false), (false, true), (false, false)
만약 Boolean을 T에 3가지의 경우를 가질 수 있는 열거형을 U타입에 넘기면 어떻게 될까요?
enum SomeEnum {
case a
case b
case c
}
Some<Bool, SomeEnum>(first: true, second: .a)
Some<Bool, SomeEnum>(first: true, second: .b)
Some<Bool, SomeEnum>(first: true, second: .c)
Some<Bool, SomeEnum>(first: false, second: .a)
Some<Bool, SomeEnum>(first: false, second: .b)
Some<Bool, SomeEnum>(first: false, second: .c)
이렇게 총 6가지의 경우가 나옵니다. 2*3이죠
만약 U의 위치에 Void를 넣으면 어떻게 될까요?? Void는 경우의 수가 () 한 개뿐이므로
2*1의 2가지 경우가 나오죠.
Void는 수학적으로 1을 의미한다고 해석하면 될 것 같기도 합니다?!
그렇다면 0은 어떻게 표현할까요??
간단합니다. Never라는 키워드를 Result타입의 Failure타입에서 보신적이 있으실 거에요.
Failure타입에 있을 경우 발생하지 않는다는 뜻을 나타내게 되는데요. Never의 실제 구현은 아래와 같습니다.
@frozen enum Never { }
케이스가 없는 열거형이네요.
실제로 아래와 같이 U에 Never를 넘길 경우 아예 구조체 인스턴스를 만들 수 조차 없습니다.
Some<Bool, Never>(first: true, second: ...???)
자 잠깐 정리하면
Bool은 2, Void는 1, Never는 0 이네요.
그리고 구조체에서는 T와 U의 경우의 수의 곱만큼 실제 구조체가 가질 수 있는 경우의 수가 존재하게 됩니다.
<Bool, Bool> = 2*2
<Bool, Void> = 2*1
<Void, Void> = 1*1
<Bool, Never> = 2*0
열거형과 대수학
열거형에서는 어떻게 될까요
enum ExampleEnum<T, U> {
case a(T)
case b(U)
}
위와 같은 열거형이 있다고 해봅시다. 그리고 구조체에서와 같이 아래의 총 4개의 열거형을 정의해보겠습니다.
ExampleEnum<Bool, Bool>
ExampleEnum<Bool, Void>
ExampleEnum<Void, Void>
ExampleEnum<Bool, Never>
그러면 열거형이 가질 수 있는 경우의 수는 아래와 같습니다
ExampleEnum<Bool, Bool>.a(true)
ExampleEnum<Bool, Bool>.a(false)
ExampleEnum<Bool, Bool>.b(true)
ExampleEnum<Bool, Bool>.b(false)
// 2+2
ExampleEnum<Bool, Void>.a(true)
ExampleEnum<Bool, Void>.a(false)
ExampleEnum<Bool, Void>.b(())
// 2+1
ExampleEnum<Void, Void>.a(())
ExampleEnum<Void, Void>.b(())
// 1+1
ExampleEnum<Bool, Never>.a(true)
ExampleEnum<Bool, Never>.a(false)
// 2+0
구조체와 다르게 곱연산이 아니라 합연산만큼 경우의 수가 있는 것을 볼 수 있습니다.
Optional을 예로 들어보겠습니다.
enum Optional<A> {
case none
case some<A>
}
여기서 none은 none(Void)로 볼 수 있을 것 같습니다. Optional.none(())와 Optional.none은 언제나 한가지의 경우만 존재하니까요.
그래서 Optional<A>의 경우의 수는 1 + A의 경우의 수만큼 존재할 수 있음을 알 수 있고 이를
Optional<A> = A? = 1 + A = Void + A
이렇게 볼 수 있겠네요
이걸 아는게 왜 중요할까요???
결론부터 얘기하면 실제 서비스를 만들기 위해 데이터를 모델링할 때 존재하지 않을 케이스를 만들지 않아서 불필요한 작업을 사전에 방지하는데 도움이 되기 때문입니다.
유저 정보를 API호출로 받아오는데 거기에는 이름과 나이 전화번호를 준다고 해봅시다.
또 전화번호는 국가코드와 국가코드 번호 그리고 숫자 번호가 있다고 해봅시다. 그리고 전화번호는 유저가 회원가입 시 입력할 수도 있고 안 할수도 있습니다.
이를 모델링 해보겠습니다.
첫 번째 방법
struct UserInfo {
let name: String
let age: Int
let phoneNationalCode: String?
let phoneNationalCodeNumber: String?
let phoneNumber: String?
}
두 번째 방법
struct UserInfo {
let name: String
let age: Int
var phoneNumber: PhoneNumber?
}
struct PhoneNumber {
let nationalCode: String
let nationalCodeNumber: String
let number: String
}
제 생각에는 2 번째 방법이 보다 잘 도메인을 잘 표현했다고 생각이 듭니다. 단순히 optional unwrapping의 횟수를 3번에서 1번으로 줄여서가 아니라 개념적으로 동시에 존재해야만 존재할 수 있는 3가지 개념들을 제대로 표현해낸 방법이 2번째이기 때문입니다.
다른 예를 또 들어보겠습니다.
대기열 기능을 만드려고 합니다
대기열에는 다음 3가지 상태가 있습니다.
- 들어가기 전 ready상태
- 대기열에 들어가서 기다리는 waiting 상태로 이 때는 내 앞에 남은 사람의 숫자 n을 같이 보내줍니다
- 들어갈 수 있는 상태 finish
서버측에서 API Response 형상 예시를 보내왔습니다. 아래처럼 말이죠
{
"status": "ready"
}
{
"status": "waiting",
"remaining_count": 6,
}
{
"status": "finish"
}
어떻게 모델링하면 좋을까요??
첫 번째 방법
enum WaitingStatus {
case ready
case waiting
case finish
}
struct Wating {
case status: WaitingStatus
case remainingCount: Int?
}
두 번째 방법
enum WaitingStatus {
case ready
case waiting(Int)
case finish
}
당연히 2번째 방법이 좋습니다. 첫 번째 방법의 경우는 ready인데 9명이 기다리고 있다라는 상태를 가질 수 있는 가능성을 모델링에서 막아주고 있지 않기 때문입니다.
오랜만에 하는 포스팅이기 때문에 가벼운 글부터 시작해 보았는데요. 도움이 되셨을 지 모르겠네요ㅎㅎ
참고