Type Driven Code 의 기본 원리
Fundamentals of type-driven code
타입 주도 코드의 기본 원리
타입 주도 코드의 기본 원리
- 타입 주도 코드에 대한 직관을 개발하려면 익숙한 것에서 시작해야 합니다.
- Swift 프로그래머로서 이 코드를 문제가 있다고 즉시 인식할 수 있을 것입니다.
func main(number: Int?) {
if number != nil {
print("double:", 2 * number!) // 요기서 강제 언래핑
}
}
- 이 코드의 문제는 무엇일까요? 맞습니다, 여기서는 if let 옵셔널 언래핑 구문을 사용해야 합니다. 하지만 그 전에 이 코드를 면밀히 검토해 봅시다.
- 이 코드가 정확히 왜 문제가 될까요? 옵셔널 값을 강제로 언래핑해야 했기 때문입니다.
- 하지만 정말 그렇게 큰 문제일까요?
- 이 구문은 Swift답지 않지만, 논리는 완벽히 타당하고 안전합니다.
- 왜냐하면
number가nil이 아님을 확인한 후에 접근하기 때문입니다. - 우리는 Objective-C에서도 오랫동안 비슷한 코드를 작성해 왔습니다!
- 문제는 강제 언래핑 자체가 아니라,
nil체크 후에도 강제 언래핑을 해야 하는 이유 입니다. - 기본 문제를 더 명확히 하기 위해 지역성을 깨고 옵셔널 인자를 받는 외부 함수를 소개해 봅시다:
// Main.swift
func main(number: Int?) {
if number != nil {
save(number)
}
}
// Save.swift
func save(_ number: Int?) {
// ⚠️ We don't know
// if number is nil or not,
// we must check ourselves!
if number != nil {
UserDefaults.standard.set(
number!,
foKey: "num"
)
} else {
print("Err: We don't want to store nils")
}
}
통찰 💡: 이 코드의 근본적인 문제는 정보 손실 입니다.
number가nil이 아님을 확인하고main의if문을 벗어나는 순간, 이 정보를 버리게 됩니다. 그리고 나중에save함수에서 다시 확인해야 합니다. 왜냐하면main이 이미 수행한 확인에 대한 정보가 없기 때문입니다.
- 우리는
number가nil이 아님을 나타내는 정보를 유지하고 이를 앞으로 전달하고자 합니다. - 그래서
number를 언랩하여 선택적이지 않은 값을 다음 함수에 전달함으로써 반복적인 확인의 필요성을 없앱니다.
func main(number: Int?) {
if let number = number {
save(number)
}
}
func save(_ num: Int) {
UserDefaults.standard.set(number, forKey: "num")
}
- 물론 이걸 보여드리는 건 선택적 언래핑(optional unwrapping ) 과 non-optional types을 상기시키기 위해서만은 아닙니다
- 이 코드는 모든 Swift 프로그래머가 즉시 인식하고 수정할 수 있는 정보 손실의 예입니다.
- 그러나 이는 우리가
if let구문을 선택적 값에 사용하도록 조건화되었기 때문입니다. - 동시에, 많은 Swift 프로그래머들은 화려한 구문 지원이 없는 타입을 다룰 때 다른 정보 손실의 사례를 인식하지 못합니다.
- 두 번째 예제를 고려해 봅시다.
func main(numbers: [Int]) {
guard !numbers.isEmpty else {
print("No numbers, sad")
return
}
print("Cool numbers:", numbers)
}
- 이 코드는 전혀 문제가 없고 Swift답습니다.
- 강제 언래핑 같은 의심스러운 것이 없으며, Swift 프로그래머의 머릿속에 경고를 울릴 만한 것도 없습니다.
- 그러나 이 코드는 이전 것과 동일한 종류의 정보 손실을 가지고 있습니다.
- 외부 함수의 도움으로 다시 이를 드러낼 수 있습니다
// Main.swift
func main(numbers: [Int]) {
if !numbers.isEmpty {
save(numbers)
} else {
print("No numbers, nothing to save...")
}
}
// Save.swift
func save(_ numbers: [Int]) {
// ⚠️ We don't know
// if numbers are empty or not,
// and must check ourselves!
if !numbers.isEmpty {
UserDefaults.standard.set(numbers, forKey: "numbers")
} else {
print("No numbers, nothing to save...")
}
}
- 배열이 비어 있지 않다는 것을 쉽게 기억할 수 있게 해주는 멋진 내장 구문이 없기 때문에,
- 프로그래머들이 이러한 불편함을 무시하고 저품질의 코드를 확산시키는 경우가 매우 흔합니다.
- 이 예에서 정보 손실을 방지하기 위해, 배열의 첫 번째 요소를 명시적으로 캡처하여 비어 있지 않다는 증거로 삼고 다음 함수로 전달해야 합니다
func main(numbers: [Int]) {
guard let first = numbers.first else {
print("No numbers, skipping...")
return
}
let remaining = Array(numbers.dropFirst())
save(first, remaining)
}
func save(
_ first: Int,
_ remaining: [Int]
) {
UserDefaults.standard.set(
CollectionOfOne(first) + remaining,
forKey: "numbers"
)
}
- 이제 isEmpty 검사를 반복할 필요가 없습니다.
- 물론, 이렇게 모든 곳에서 배열을 구조 분해하고 재구성하는 것은 완전한 번거로움이기 때문에,
- 우리는 이 과정을
NonEmptyArray타입 내에 캡슐화할 수 있습니다:
struct NonEmptyArray<Element> {
var first: Element
var remaining: [Element]
var arrayValue: [Element] {
CollectionOfOne(first) + remaining
}
init?(_ arrayValue: [Element]) {
guard let first = arrayValue.first else {
return nil
}
self.first = first
remaining = Array(arrayValue.dropFirst())
}
}
- NonEmptyArray 타입은 내부 구조를 활용해 비어있지 않다는 증거를 타입에 담습니다..
- 이 기술에 대한 자세한 내용은 제 기사 'Greater type safety with Structural Typing in Swift. 에서 배울 수 있습니다.
- 코드를 업데이트합시다:
func main(numbers: [Int]) {
guard let nonEmptyNumbers = NonEmptyArray(numbers) else {
print("No numbers, skipping...")
return
}
save(nonEmptyNumbers)
}
func save(_ numbers: NonEmptyArray<Int>) {
UserDefaults.standard.set(
numbers.arrayValue,
forKey: "numbers"
)
}
통찰💡: 정보의 보존과 전파를 위해 타입을 사용하세요. 예를 들어, 검증의 증거와 도메인 특화 불변성을 포함합니다
- Haskell, PureScript, Elm과 같은 언어는 표준 라이브러리에서
NonEmpty컬렉션 타입을 제공. - 이는 정말로 필수적이며, 이 시리즈를 다 읽고 나면 코드베이스 전반에서 비어 있지 않은 배열의 사용 사례를 발견하게 될 것입니다.
- Swift를 위해서는 Point-Free의 검증된 오픈 소스 라이브러리인 swift-nonempty. 를 추천합니다