@targetName 애너테이션
@targetName 애너테이션
@targetName 애너테이션은 정의에 대체 이름(alternate name)을 붙여주는 장치예요. 정의를 작성할 때 쓴 이름은 Scala 안에서 통하는 이름이고, 그 정의가 실제로 구현되는 이름은 따로 정해줄 수 있는 거죠. 아주 간단한 예시부터 볼게요.
본문
정의에 @targetName 애너테이션을 붙이면, 그 정의의 구현이 쓰는 이름을 다른 이름으로 정해줄 수 있어요. 예시를 보면 바로 감이 올 거예요.
import scala.annotation.targetName
object VecOps:
extension [T](xs: Vec[T])
@targetName("append")
def ++= [T] (ys: Vec[T]): Vec[T] = ...
여기서 ++= 연산은 (바이트코드나 네이티브 코드에서) append라는 이름으로 구현돼요. 이 구현 이름은 생성되는 코드에 영향을 주고, 다른 언어에서 이 메서드를 호출할 때 쓰는 이름이기도 해요. 예를 들어 Java에서는 ++=를 다음과 같이 호출할 수 있죠.
VecOps.append(vec1, vec2)
다만 @targetName은 Scala 안에서의 사용에는 아무 영향이 없어요. Scala에서는 이 메서드를 부를 때 반드시 ++=를 써야 하고, append를 쓰면 안 돼요.
세부 사항 (Details)
@targetName은 scala.annotation 패키지에 정의되어 있어요. String 타입의 인자를 하나 받는데, 그 문자열을 애너테이션이 붙은 정의의 외부 이름(external name)이라고 불러요.
@targetName 애너테이션은 최상위(top-level) 클래스, 트레이트, 오브젝트를 제외한 모든 종류의 정의에 붙일 수 있어요.
@targetName에 주는 이름은 호스트 플랫폼에서 정의된 엔티티의 이름으로서 합법적인 이름이어야 해요.
기호(symbolic) 이름을 가진 정의에는 @targetName을 붙이는 걸 권장해요. 검색하기 쉬운 대체 이름이 생기고, 런타임 진단 메시지에서 의미를 알기 어려운 인코딩이 나오는 일도 피할 수 있거든요.
백틱()으로 감싼 이름 중 호스트 플랫폼 이름으로서 합법적이지 않은 이름을 가진 정의도 @targetName`을 붙이는 게 좋아요.
오버라이딩과의 관계 (Relationship with Overriding)
@targetName 애너테이션은 두 메서드 정의가 서로 충돌하는지, 아니면 서로 오버라이드하는지 판단할 때 중요해요. 두 메서드 정의는 이름과 시그니처, 그리고 erasure된 이름(erased name)이 같을 때 서로 매치돼요. 여기서
- 정의의 시그니처는 모든 (값) 파라미터의 erasure된 타입 이름과 메서드의 결과 타입으로 이루어져요.
- 메서드 정의의 erasure된 이름은
@targetName이 주어졌다면 그 타깃 이름을, 그렇지 않다면 정의된 이름을 가리켜요. 즉@targetName을 쓰면 원래라면 충돌할 두 메서드 정의를 서로 구분할 수 있게 돼요. 예를 들어 볼게요.
def f(x: => String): Int = x.length
def f(x: => Int): Int = x + 1 // error: double definition
위 두 정의는 erasure된 파라미터 타입이 둘 다 Function0(이름-뒤 파라미터, 즉 by-name 파라미터가 번역된 타입)이라서 충돌해요. 그래서 이름도 시그니처도 같아지거든요. 하지만 둘 중 하나, 혹은 둘 모두에 @targetName을 붙이면 충돌을 피할 수 있어요.
@targetName("f_string")
def f(x: => String): Int = x.length
def f(x: => Int): Int = x + 1 // OK
이렇게 하면 생성된 코드에는 f_string과 f라는 메서드가 만들어져요.
다만 @targetName은 이름과 타입이 똑같은 두 정의 사이의 오버라이드 관계를 깨뜨리면 안 돼요. 그래서 다음 코드는 에러가 돼요.
import annotation.targetName
class A:
def f(): Int = 1
class B extends A:
@targetName("g") def f(): Int = 2
컴파일러는 여기서 이렇게 보고해요.
-- Error: test.scala:6:23 ------------------------------------------------------
6 | @targetName("g") def f(): Int = 2
| ^
|error overriding method f in class A of type (): Int;
| method f of type (): Int should not have a @targetName
| annotation since the overridden member hasn't one either
관련된 오버라이드 규칙을 정리하면 다음과 같아요.
- 두 멤버는 이름과 시그니처가 같고, erasure된 이름이 같거나 타입이 같다면 서로 오버라이드할 수 있어요.
- 두 멤버가 오버라이드한다면, erasure된 이름과 타입이 모두 같아야 해요. 늘 그렇듯이, 생성된 코드에 나타나는 어떤 오버라이드 관계든 원래 코드에도 있어야 해요. 그래서 다음 예시도 에러가 돼요.
import annotation.targetName
class A:
def f(): Int = 1
class B extends A:
@targetName("f") def g(): Int = 2
여기서 원래 메서드 g와 f는 이름이 달라서 서로 오버라이드하지 않아요. 그런데 타깃 이름으로 바꾸면 충돌이 생기고, 컴파일러가 이렇게 알려줘요.
-- [E120] Naming Error: test.scala:4:6 -----------------------------------------
4 |class B extends A:
| ^
| Name clash between defined and inherited member:
| def f(): Int in class A at line 3 and
| def g(): Int in class B at line 5
| have the same name and type after erasure.
1 error found