export 절
export 절 (Export Clauses)
**export 절(export clause)**은 객체의 선택된 멤버들을 위한 **별칭(alias)**을 정의해요. 예를 들어 볼게요:
class BitMap
class InkJet
class Printer:
type PrinterType
def print(bits: BitMap): Unit = ???
def status: List[String] = ???
class Scanner:
def scan(): BitMap = ???
def status: List[String] = ???
class Copier:
private val printUnit = new Printer { type PrinterType = InkJet }
private val scanUnit = new Scanner
export scanUnit.scan
export printUnit.{status as _, *}
def status: List[String] = printUnit.status ++ scanUnit.status
본문
두 export 절은 Copier 클래스에 다음 export 별칭들을 정의해요:
final def scan(): BitMap = scanUnit.scan()
final def print(bits: BitMap): Unit = printUnit.print(bits)
final type PrinterType = printUnit.PrinterType
실험적인 modularity 언어 import를 쓰면, export된 메서드와 값만 final이 되고, 생성된 PrinterType은 단순한 타입 별칭이 돼요:
type PrinterType = printUnit.PrinterType
이 별칭들은 Copier 내부에서도, 외부에서도 접근할 수 있어요:
val copier = new Copier
copier.print(copier.scan())
형태와 셀렉터
export 절은 import 절과 같은 형태를 가져요. 일반적인 형태는 다음과 같아요:
export path . { sel_1, ..., sel_n }
한정자 표현식 path로 시작하는데, 이는 **안정적 식별자(stable identifier)**여야 해요. 그다음 별칭을 만들 대상들을 식별하는 하나 이상의 셀렉터 sel_i가 따라와요. 셀렉터는 다음 형태 중 하나예요:
- 단순 셀렉터
x는x라는 이름을 가진path의 적격(eligible) 멤버 전부에 대한 별칭을 만들어요. - 이름 바꾸기 셀렉터
x as y는x라는 이름을 가진path의 적격 멤버 전부에 대한 별칭을 만들되, 별칭 이름이x대신y예요. - 생략 셀렉터
x as _는x가 이후의 와일드카드 셀렉터에 의해 별칭이 만들어지는 것을 막아요. - given 셀렉터
given x는 선택적인 타입 범위x를 가져요.x에 부합하거나(Any라면 생략)x가 생략되면Any에 부합하는 적격 given 인스턴스 전부에 대한 별칭을 만들어요. 단, 이전의 단순·이름 바꾸기·생략 셀렉터에 의해 이름이 지정된 멤버들은 제외해요. - 와일드카드 셀렉터
*는 given 인스턴스, 컴파일러가 생성한 합성 멤버, 그리고 이전의 단순·이름 바꾸기·생략 셀렉터에 의해 이름이 지정된 멤버를 제외한path의 적격 멤버 전부에 대한 별칭을 만들어요.
참고 사항:
- 적격 **생성자 프록시(constructor proxy)**도 합성 멤버임에도 포함돼요.
- export로 만들어진 멤버도 포함돼요. 이 멤버들은 컴파일러가 만들지만 합성으로 간주되지는 않아요.
다음이 모두 성립하면 멤버는 **적격(eligible)**이에요:
- 그 소유자(owner)가 export 절을 포함하는 클래스(*)의 기본 클래스(base class)가 아니고,
- 그 멤버가, export 절을 포함하는 클래스의 기본 클래스를 소유자로 하는 구체 정의를 오버라이드하지 않으며,
- export 절에서 접근 가능하고,
- 생성자도, 객체의 (합성) 클래스 부분도 아니며,
given으로 선언된 given 인스턴스인 것과 export가 given 셀렉터에서 오는 것이 서로 동치일 때.
단순 또는 이름 바꾸기 셀렉터가 어떤 적격 멤버도 식별하지 못하면 컴파일 타임 오류예요.
타입 멤버와 용어 멤버의 별칭
타입 멤버는 타입 정의로, 용어 멤버는 메서드 정의로 별칭이 만들어져요. 예시:
object O:
class C(val x: Int)
def m(c: C): Int = c.x + 1
export O.*
// generates
// type C = O.C
// def m(c: O.C): Int = O.m(c)
export 별칭의 성질
export 별칭은 참조하는 멤버들의 타입 파라미터와 값 파라미터를 복사해요. 용어 멤버의 export 별칭은 **항상 final**이에요. given 인스턴스의 별칭은 다시 given으로 정의되고(옛 스타일 implicits의 별칭은 implicit), 확장(extension)의 별칭은 다시 확장으로, inline 메서드나 값의 별칭은 다시 inline으로 정의돼요. 별칭에 줄 수 있는 다른 수식어는 없어요. 이는 오버라이딩에 몇 가지 결과를 가져와요:
- 메서드나 필드의 export 별칭은
final이라서 오버라이드할 수 없어요. - export 별칭은
override로 표시되지 않으므로 기본 클래스의 구체 멤버를 오버라이드할 수 없어요. - 하지만 export 별칭은 기본 클래스의 지연(deferred) 멤버를 구현할 수 있어요.
- export 타입 별칭도 보통은
final이지만, 실험적인 언어 import인 modularity가 있으면 예외예요. 타입 별칭에 대한 일반 규칙은 어쨌든 한 클래스에 타입 별칭이 여럿 있으면 오른쪽 변이 서로 일치해야 하도록 보장해요. 그렇지 않으면 클래스를 인스턴스화할 수 없으니까요. 그래서 export 타입 별칭에서final을 떼어내는 것은 안전해요.
한정자 경로에서 비공개 값을 참조하지 않고 접근하는 공개 값 정의에 대한 export 별칭은 컴파일러가 **"안정적(stable)"**으로 표시하고, 그 결과 타입은 별칭 대상 정의들의 싱글턴 타입이 돼요. 이는 기술적으로는 메서드임에도 안정적 식별자 경로의 일부로 쓸 수 있다는 뜻이에요. 예를 들어 다음은 유효해요:
class C { type T }
object O { val c: C = ... }
export O.c
def f: c.T = ...
제약 사항 (Restrictions)
- export 절은 클래스에 나타나거나 최상위(top-level)에 나타날 수 있어요. export 절은 블록 안의 문(statement)으로는 나타날 수 없어요.
- export 절에 와일드카드나 given 셀렉터가 들어 있으면, 그 한정자 경로가 패키지를 가리키는 것은 금지돼요. 패키지에 대한 와일드카드 의존성을 증분 컴파일을 위해 안전하게 추적하는 방법이 아직 알려지지 않았기 때문이에요.
- export 이름 바꾸기는 대상 이름과 일치하는 이름을 바꾸지 않은 export를 숨겨요. 예를 들어 다음 절은
B가A as B이름 바꾸기에 의해 숨겨지므로 유효하지 않아요:
export {A as B, B} // error: B is hidden
- export 절의 이름 바꾸기는 대상 이름이 쌍마다 서로 달라야 해요. 예를 들어 다음 절은 유효하지 않아요:
export {A as C, B as C} // error: duplicate renaming
- 다음과 같은 단순 이름 바꾸기 export는 아직 지원되지 않아요:
export status as stat
이 형태는 "export된 것은 export를 포함하는 객체의 이미 멤버일 수 없다"는 제약에 걸리게 돼요. 이 제약은 미래에 완화될 수도 있어요.
- 무인자(nullary) Java 메서드의 export 별칭은 (빈 괄호 유무에 관계없이 Scala에서 호출될 수 있는데) Scala에서 무인자 메서드이고, 적용할 때 괄호가 필요해요. 반면 멤버 타입이 보존되므로, Java 메서드에 유연 타입(flexible type)이 추론됐다면 export 별칭도 유연 타입을 가지게 돼요.
(*) 명시되지 않은 한, 이 논의에서 "클래스"라는 용어는 객체와 트레이트 정의도 포함해요.
동기 (Motivation)
상속보다 컴포지션을 선호하라는 것은 표준적인 권고 사항이에요. 이는 실제로 최소 권한의 원칙(principle of least power)을 적용한 거예요. 컴포지션은 컴포넌트를 블랙박스로 취급하는 반면, 상속은 오버라이딩을 통해 컴포넌트의 내부 동작에 영향을 줄 수 있거든요. 때로는 상속이 암시하는 긴밀한 결합이 문제에 가장 좋은 해결책일 수 있지만, 그럴 필요가 없을 때는 더 느슨한 결합인 컴포지션이 더 좋아요.
지금까지는 객체지향 언어들( Scala 포함)이 상속을 컴포지션보다 훨씬 쉽게 사용하게 만들어 왔어요. 상속은 extends 절만 있으면 되지만, 컴포지션은 일련의 포워더(forwarder)를 장황하게 나열해야 했죠. 그런 의미에서 객체지향 언어들은 프로그래머를 종종 너무 강력한 해법으로 밀어붙이고 있는 셈이에요. export 절은 이 균형을 바로잡아 줘요. 컴포지션 관계를 상속 관계만큼 간결하고 쉽게 표현할 수 있게 만들어 주죠. export 절은 멤버를 이름 바꾸거나 생략할 수 있으므로 extends 절보다 더 유연하기도 해요.
export 절은 또한 패키지 객체에서 최상위 정의로의 전환으로 생긴 공백도 메워 줘요. 이 전환에서 사라지는 유용한 관용구 중 하나는, 어떤 클래스에서 상속받는 패키지 객체예요. 이 관용구는 파사드(facade) 같은 패턴에서 자주 쓰여서, 내부 컴포지션의 멤버들을 패키지 사용자들이 쓸 수 있게 해 줍니다. 최상위 정의는 사용자 정의 객체로 감싸이지 않으므로 아무것도 상속할 수 없어요. 하지만 최상위 정의는 export 절이 될 수 있고, 이는 파사드 디자인 패턴을 더 안전하고 유연한 방식으로 지원해 줘요.
확장 안의 export 절 (Export Clauses in Extensions)
export 절은 확장(extension)에도 나타날 수 있어요.
예시:
class StringOps(x: String):
def *(n: Int): String = ...
def capitalize: String = ...
extension (x: String)
def take(n: Int): String = x.substring(0, n)
def drop(n: Int): String = x.substring(n)
private def moreOps = new StringOps(x)
export moreOps.*
이 경우 한정자 표현식은 같은 확장 절 안의 단 하나의 무인자 확장 메서드를 가리키는 식별자여야 해요. export는 한정자 경로의 결과에 있는 모든 접근 가능한 용어 멤버에 대한 확장 메서드를 만들어요. 예를 들어 위 확장은 다음처럼 확장돼요:
extension (x: String)
def take(n: Int): String = x.substring(0, n)
def drop(n: Int): String = x.substring(n)
private def moreOps = StringOps(x)
def *(n: Int): String = moreOps.*(n)
def capitalize: String = moreOps.capitalize
문법 변화 (Syntax changes)
TemplateStat ::= ...
| Export
TopStat ::= ...
| Export
ExtMethod ::= ...
| Export
Export ::= ‘export’ ImportExpr {‘,’ ImportExpr}
ImportExpr ::= SimpleRef {‘.’ id} ‘.’ ImportSpec
ImportSpec ::= NamedSelector
| WildcardSelector
| ‘{’ ImportSelectors) ‘}’
NamedSelector ::= id [‘as’ (id | ‘_’)]
WildCardSelector ::= ‘*’ | ‘given’ [InfixType]
ImportSelectors ::= NamedSelector [‘,’ ImportSelectors]
| WildCardSelector {‘,’ WildCardSelector}
export 절의 정교화 (Elaboration of Export Clauses)
export 절은 타입 검사 중 **정교화 순서(elaboration order)**에 대한 질문을 제기해요. 다음 예시를 생각해 볼게요:
class B { val c: Int }
object A { val b = new B }
export A.*
export b.*
export b.* 절이 합법적일까요? 합법적이라면 무엇을 export하는 걸까요? export A.b.*와 동일할까요? 마지막 두 절을 서로 바꾸면 어떨까요?
export b.*
export A.*
이런 까다로운 질문을 피하기 위해 export의 정교화 순서를 다음과 같이 고정해요.
export 절은 감싸는 객체나 클래스의 타입 정보가 **완성(completed)**될 때 처리돼요. 지금까지 완성은 다음 단계들로 이루어져 있었어요:
- 클래스의 애너테이션들을 정교화한다.
- 클래스의 파라미터들을 정교화한다.
- 클래스의 셀프 타입이 주어지면 이를 정교화한다.
- 클래스의 모든 정의를 클래스 멤버로 등록한다(타입은 요구 시 완성).
- 클래스의 모든 부모들의 타입을 결정한다.
export 절이 있으면 다음 단계들이 추가돼요:
- export 절 안의 모든 경로들의 타입을 계산한다.
- export 절 안의 모든 경로들의 적격 멤버에 대한 export 별칭을 등록한다.