(Swift) Swift3의 커스텀 연산자 정의 변경점

Swift 3에 도입된 변화 중 하나는 연산자 정의 방식이 변경된 것이다. 이전버전까지는 다음과 같은 문법으로 새로운 연산자를 정의할 수 있었다.

infix operator <> { precedence 180 associativity left }

하지만 이 문법은 왠지 좀 엉성해보이는 구석이 있었다.

  • {...} 블럭내에서는 속성명과 속성값의 구분이 단순히 공백이다.
  • 속성명-속성값의 짝이 한 라인에 연결되어 선언될 수 있다.

특히나 연산자의 우선순위의 경우, 기본 연산자들의 우선순위가 드러나있지 않기 때문에 어떤 적절한 우선순위 값을 줄 것인지에 대한 부분이 모호하다. 또 (별로 좋은 상황은 아니지만) 정수값으로 우선순위를 할당한다면 연산자가 폭발적으로 늘어나야 하는 경우에 각자의 전후관계를 유지하기 위해서 기존 연산자들의 우선순위가 모조리 재정의되어야 하는 극단적 반례도 존재한다. 심지어 우선순위 값이 큰 것이 높은지 작은 것이 높은지도 모호하다!

이런 문제때문에 Swift3에서는 연산자 정의 방법이 바뀌었다. 제안문서에서는 크게 몇 가지 기본 룰을 제시한다.

  • 연산자 선언문은 더 이상 블럭본문을 가지지 않는다.
  • 연산자의 우선순위 속성은 우선순위 그룹(precedencegroup)으로 정의되며, 이는 마치 프로토콜처럼 연산자가 상속받는다.
  • 우선순위 속성은 더 이상 정수값으로 고정되지 않으며, 다른 연산자와의 선후관계로 정의한다.

따라서 Swift3의 연산자 정의는 대략 다음과 같은 모양을 취한다.

///: 연산자 선언 자체는 깔끔하게 처리한다.
prefix operator !
infix operator +

///: 우선순위 그룹은 선택적으로 기본 associativity를 정의할 수 있다. 
///: 이 때, 속성명: 값의 형태로 구분하며, 각 라인에 하나씩 정의한다.
precedencegroup Additive {
    associativity: left
}
infix operator + : Additive
infix operator - : Additive

///: 우선순위 상의 비교가 필요할 때, 타 우선순위와 `higherThan`, `lowerThan`을 사용하여 비교 순위만 지정한다. 

precedencegroup Additive {
    associativity: left
}

precedencegroup Multiplicative {
    associativity: left
    higherThan: Additivie
}

precedencegroup BitwiseAnd {
    associativity: left
}

infix operator + : Additive // `-` 역시 Additive로 정의할 수 있다.
infix operator * : Multiplicative
infix operator & : BitwiseAnd

위 정의에서 +, * 간의 관계는 정의되었으며, &과의 관계는 정의된 바 없다. 따라서 아래와 같이 우선순위 관계가 정의되지 않은 연산이 나란히 쓰이는 경우, 컴파일되지 못하고 에러가 발생한다.

1 + 2 * 3 // `*'의 우선순위가 높으므로 계산 가능하다.
1 + 2 & 3 // `&`는 `+`와의 관계가 정의되지 않았다. 따라서 계산할 수 없다.

우선순위 그룹의 관계는 한 번만 가능하며, 따라서 확장될 수 없다.

이를 통해 연산자 선언 문법이 다소 길어지는 감은 있지만, 대신에 전후관계를 명확히 설정할 수 있게 되어서 이 부분은 좋은 개선점으로 보인다.

Swift3의 커스텀 연산자와 관련하여 또 하나의 큰 개선사항이 있었는데, 바로 연산자를 항상 자유 함수로 구현해야만하던 제약이 사라졌다. 따라서 특정한 프로토콜에 대해 필요한 연산자는 프로토콜의 선언 및 구현부 본체에 쓸 수 있게 됐다.

protocol Equatable {
    static func ==(lhs: Self, rhs: Self) -> Bool
}

struct Foo: Equatable {
  let value: Int

  static func ==(lhs: Foo, rhs: Foo) -> Bool {
    return lhs.value == rhs.value
  }
}