연산자 규칙
연산자 규칙 (Rules for Operators)
중위 연산자(infix operator) 규칙은 몇몇 부분이 바뀌었어요. 첫째, 영숫자(alphanumeric) 메서드는 정의에 infix 수식어가 붙어야만 중위 연산자로 쓸 수 있어요. 둘째, 기호(symbolic) 연산자의 정의에는 @targetName 애너테이션을 붙이는 걸 권장해요(강제는 아니에요). 마지막으로, 문법 변경 덕분에 중위 연산자를 여러 줄 식의 왼쪽에 쓸 수 있게 됐어요.
본문
infix 수식어
메서드 정의에 붙는 infix 수식어는 그 메서드를 중위 연산(infix operation)으로 사용할 수 있게 해줘요. 예를 들어볼게요.
import scala.annotation.targetName
trait MultiSet[T]:
infix def union(other: MultiSet[T]): MultiSet[T]
def difference(other: MultiSet[T]): MultiSet[T]
@targetName("intersection")
def *(other: MultiSet[T]): MultiSet[T]
end MultiSet
val s1, s2: MultiSet[Int]
s1 union s2 // OK
s1 `union` s2 // also OK but unusual
s1.union(s2) // also OK
s1.difference(s2) // OK
s1 `difference` s2 // OK
s1 difference s2 // gives a deprecation warning
s1 * s2 // OK
s1 `*` s2 // also OK, but unusual
s1.*(s2) // also OK, but unusual
영숫자 연산자를 포함하는 중위 연산은 다음 조건 중 하나가 성립하지 않는 한 폐기(deprecate)돼요.
- 연산자 정의에
infix수식어가 붙어 있거나, - 연산자가 Scala 2로 컴파일되었거나,
- 연산자 뒤에 여는 중괄호(
{)가 오는 경우.
영숫자 연산자(alphanumeric operator)란 전적으로 문자, 숫자, $, _ 문자로 이루어진 연산자, 또는 java.lang.Character.isIdentifierPart(c)가 true를 반환하는 임의의 유니코드 문자 c로 이루어진 연산자를 말해요.
기호 연산자를 포함하는 중위 연산은 항상 허용돼요. 그래서 기호 이름을 가진 메서드에는 infix가 중복이에요.
infix 수식어는 타입에도 붙일 수 있어요.
infix type or[X, Y]
val x: String or Int = ...
동기 (Motivation)
infix 수식어의 목적은 코드 베이스 전체에서 메서드나 타입을 적용하는 방식의 일관성을 확보하는 거예요. 메서드를 중위 연산자로 적용할지, 보통의 적용으로 쓸지 메서드 작성자가 결정하고, 사용처에서 그 결정을 일관되게 따르자는 아이디어죠.
세부 사항 (Details)
infix는 소프트 수식어(soft modifier)예요. 수식어 위치가 아닌 곳에서는 보통의 식별자로 취급돼요.- 메서드가 다른 메서드를 오버라이드하면, 둘의
infix애너테이션은 일치해야 해요. 둘 다infix로 애너테이션되거나, 둘 다 아니어야 해요. infix수식어는 메서드 정의에 붙일 수 있어요. infix 메서드의 첫 번째 비-수신자(non-receiver) 매개변수 목록은 정확히 하나의 매개변수를 정의해야 해요. 예시를 볼게요.
infix def op1(x: S): R // ok
infix def op2[T](x: T)(y: S): R // ok
infix def op3[T](x: T, y: S): R // error: two parameters
extension (x: A)
infix def op4(y: B): R // ok
infix def op5(y1: B, y2: B): R // error: two parameters
infix수식어는 정확히 두 개의 타입 매개변수를 가진 타입, 트레이트, 클래스 정의에도 붙일 수 있어요.
infix type op[X, Y]
이런 infix 타입은 중위 문법, 즉 A op B로 적용할 수 있어요.
- Scala 3.0으로의 마이그레이션을 매끄럽게 하기 위해, 영숫자 연산자는 Scala 3.1부터(또는 Dotty/Scala 3에서
-source future옵션이 주어졌을 때) 폐기되기 시작해요.
@targetName 애너테이션
기호 연산자의 정의에는 연산자를 영숫자 이름으로 인코딩한 @targetName 애너테이션을 붙이는 걸 권장해요. 여기에는 여러 장점이 있어요.
- Scala와 다른 언어 사이의 상호 운용성에 도움이 돼요. 다른 언어에서 Scala로 정의된 기호 연산자를 target name으로 호출할 수 있어서, 기호 이름의 저수준 인코딩을 기억하지 않아도 돼요.
- 스택트레이스(stacktrace)나 다른 런타임 진단의 가독성에 도움이 돼요. 저수준 인코딩 대신 사용자가 정의한 영숫자 이름이 보이기 때문이에요.
- 문서화 도구 역할을 해요. 기호 연산자의 별칭으로서 대안이 되는 보통 이름을 제공하니까요. 이렇게 하면 정의를 검색에서 찾기도 쉬워져요.
문법 변경 (Syntax Change)
중위 연산자는 이제 여러 줄 식의 줄 시작 부분에 올 수 있어요. 예시를 볼게요.
val str = "hello"
++ " world"
++ "!"
def condition =
x > 0
||
xs.exists(_ > 0)
|| xs.isEmpty
이전에는 이런 식이 거부되었어요. 컴파일러의 세미콜론 추론이 연속 줄인 ++ " world"나 || xs.isEmpty를 별개의 문장으로 취급했기 때문이죠.
이 문법이 동작하게 하려고, 규칙을 수정해서 선행 중위 연산자(leading infix operator) 앞에서는 세미콜론을 추론하지 않도록 했어요. 선행 중위 연산자는 다음을 만족하는 것을 말해요.
+나approx_==같은 기호 식별자, 또는 백틱 안의 식별자인데,- 새 줄을 시작하고,
- 빈 줄 뒤에 오지 않으며,
- 뒤에 공백 문자 하나 이상과, 식을 시작할 수 있는 토큰이 따라온다.
또한 연산자가 자체 줄에 나타나면, 다음 줄은 연산자와 같은 들여쓰기 폭 이상이어야 해요.
예시를 볼게요.
freezing
| boiling
이건 하나의 중위 연산으로 인식돼요. 다음과 비교해 보세요.
freezing
!boiling
이건 freezing과 !boiling 두 문장으로 보여요. 차이는 첫 번째 예시의 연산자만 뒤에 공백이 따라온다는 점이에요.
또 다른 예시를 볼게요.
println("hello")
???
??? match { case 0 => 1 }
이 코드는 세 개의 서로 다른 문장으로 인식돼요. ???는 문법적으로 기호 식별자이지만, 어느 것도 뒤에 공백과 식을 시작할 수 있는 토큰이 따라오지 않아요.
단항 연산자 (Unary Operators)
단항 연산자(unary operator)는 비어 있더라도 명시적인 매개변수 목록을 가져선 안 돼요. 단항 연산자는 unary_op라는 이름의 메서드인데, 여기서 op는 +, -, !, ~ 중 하나예요.