오늘도 뜬금없는 글입니다. 이번에는 custom operator에 특히 switch case문에서 사용하는 custom expression pattern에 대해 써보겠습니다.
개발을 하다보면 쉽게 마주치는 if 문이 있습니다.
if 100 > number {
print("smaller than 100")
} else if 100 <= number {
print("equal or bigger than 100")
}
switch case의 풍부한 문법에 눈뜨기 시작하신 스위프트 유저라면 switch case문을 사랑하기 시작했을 겁니다.
저도 마찬가지라 저 코드를 어떻게 하면 switch case로 옮길 수 있을까 고민을 했습니다.
시작과 끝의 범위가 명확하다면
case 0...100:
print("blah blah")
라는 형식도 가능합니다만, 두 번째 else if 문을 switch case 문으로 하려면
case 100..<Int.max:
print("blah blah")
처럼 끝을 지정해야합니다. 아니 난 그냥 100보다 큰 숫자만 체크하면 되는건데, 뭔 맥시멈 밸류까지 끌어와야 하나, 라는 생각에 커스텀 패턴을 만들어보자는 생각을 했습니다.
코드는 다음과 같습니다.
//괜히 만들어봤습니다. 좀더 프로토콜 중심으로 구성하고 싶었는데, 제 제너릭에 대한 습득이 아직 부족하여서.. associatedtype으로는 문제가 많아 struct를 중심으로 작성합니다.
protocol PatternComparable {}
// 비교 operator와 비교값을 담을 수 있도록 struct를 작성합니다.
// op - compare operator
// to - target comparable value
// Comparable을 제너릭으로 사용하여 Comparable을 상속하는 모든 타입에서 이 코드를 사용가능 하도록 했습니다.
struct Pattern<C: Comparable>: PatternComparable {
let op: (lhs: C, rhs: C) -> Bool
let to: C
func compare(from: C) -> Bool {
return op(lhs: from, rhs: to)
}
}
// Pattern struct를 편하게 생성할 수 있도록 만든 함수입니다.
func patternize<C: Comparable>(op: (lhs: C, rhs: C) -> Bool, _ to: C) -> Pattern<C> {
return Pattern<C>(op: op, to: to)
}
// ~=는 pattern matching operator입니다.
// pattern - case 문에 들어가는 predicate입니다.
// value - switch 문에 들어가는 비교값입니다.
func ~=<C: Comparable>(pattern: Pattern<C>, value: C) -> Bool {
return pattern.compare(value)
}
// 직접 tuple값으로 (예: (op: <, to: 100)) 패턴을 생성해보려 했으나 실패했습니다. tuple패턴은 switch에도 역시 tuple이 와야하더군요. swift3에서 튜플 타입에 변화가 생기면 사용가능할까 하여 놔뒀습니다.
func ~=<C: Comparable>(pattern: (op:(lhs: C, rhs: C)->Bool, to:C), value: C) -> Bool {
return pattern.0(lhs:value, rhs: pattern.1)
}
// Comparable에서 직접 Pattern struct를 생성하도록 만든 프로토콜입니다. 다만 문법상으로 순서가 애매합니다. 비교연산자의 우측에 가야할 값을 좌측으로 보내야해서 좀 헷갈린다고나 할까요. 그냥 이런것도 가능하구나 하고 생각해주시면 될 거 같습니다. 더 좋은 방식이 있다면 함께 의논해보면 좋겠습니다.
// 예) 100보다 큰 숫자를 체크할 경우
// case 100.patterned(>) - 이상하죠?
protocol Patternable {}
extension Int: Patternable {}
extension Int8: Patternable {}
extension Int16: Patternable {}
extension Int32: Patternable {}
extension Int64: Patternable {}
extension UInt: Patternable {}
extension UInt8: Patternable {}
extension UInt16: Patternable {}
extension UInt32: Patternable {}
extension UInt64: Patternable {}
extension Double: Patternable {}
extension Float: Patternable {}
extension CGFloat: Patternable {}
extension Patternable where Self: Comparable {
func patterned<E where E: Comparable>(op: (lhs: E, rhs: E) -> Bool) -> Pattern<E> {
return Pattern<E>(op: op, to: self as! E)
}
}
코드는 아래와 같은 방식으로 테스트해 볼 수 있습니다:
let value = "test"
switch value {
case patternize(<=, "test1"):
print("smaller than test1")
case patternize(>, "tes"):
print("bigger than tes")
default:
print("default")
}
let value1 = 100.1
switch value1 {
case 100.0.patterned(<=):
print("smaller than 100.0")
case patternize(>, 100.0):
print("bigger than 100.0")
default:
print("default")
}
결과:
smaller than test1 bigger than 100.0
Comparable을 제너릭의 상속값으로 사용했기 때문에, 문자열같이 Comparable을 extension으로 가지는 모든 타입에서 사용이 가능하다는 장점이 있습니다. 그래도 if 문보다 손이 많이 가는건 어쩔 수가 없네요. 더 좋은 방법을 함께 토론할 수 있다면 좋겠습니다.
좋은 하루 되세요.
댓글