확장 메서드
확장 메서드 (Extension Methods)
확장 메서드는 타입이 정의된 다음에 그 타입에 메서드를 추가할 수 있게 해 줘요. 예시를 볼게요.
case class Circle(x: Double, y: Double, radius: Double)
extension (c: Circle)
def circumference: Double = c.radius * math.Pi * 2
일반 메서드처럼 확장 메서드도 중위 . 표기로 호출할 수 있어요.
val circle = Circle(0, 0, 1)
circle.circumference
본문
확장 메서드의 변환 (Translation of Extension Methods)
확장 메서드는 앞에 붙는 파라미터 섹션을 첫 번째 인자 목록으로 받는, 특별히 레이블이 붙은 메서드로 변환돼요. 여기서 <extension>으로 표현한 레이블은 컴파일러 내부용이에요. 그래서 위 circumference 정의는 다음 메서드로 변환되고, 이렇게도 호출할 수 있어요.
<extension> def circumference(c: Circle): Double = c.radius * math.Pi * 2
assert(circle.circumference == circumference(circle))
연산자 (Operators)
확장 메서드 문법으로 연산자도 정의할 수 있어요. 예시를 볼게요.
extension (x: String)
def < (y: String): Boolean = ...
extension (x: Elem)
def +: (xs: Seq[Elem]): Seq[Elem] = ...
extension (x: Number)
infix def min (y: Number): Number = ...
"ab" < "c"
1 +: List(2, 3)
x min 3
위 세 정의는 다음으로 변환돼요.
<extension> def < (x: String)(y: String): Boolean = ...
<extension> def +: (xs: Seq[Elem])(x: Elem): Seq[Elem] = ...
<extension> infix def min(x: Number)(y: Number): Number = ...
오른쪽 결합 연산자 +:를 확장 메서드로 변환할 때 두 파라미터 x와 xs가 뒤바뀌는 점에 주의하세요. 이것은 오른쪽 결합 연산자를 일반 메서드로 구현하는 방식과 유사해요. 스칼라 컴파일러는 중위 연산 x +: xs를 xs.+:(x)로 전처리하기 때문에, 확장 메서드는 결국 첫 인자로 시퀀스를 받게 돼요. (즉 두 번의 뒤바뀜이 서로 상쇄되는 거예요.) 자세한 내용은 별도 문서를 참고하세요.
제네릭 확장 (Generic Extensions)
확장에 타입 파라미터를 추가해서 제네릭 타입을 확장할 수도 있어요. 예를 들면요.
extension [T](xs: List[T])
def second = xs.tail.head
extension [T: Numeric](x: T)
def + (y: T): T = summon[Numeric[T]].plus(x, y)
확장의 타입 파라미터는 메서드 자신의 타입 파라미터와도 결합할 수 있어요.
extension [T](xs: List[T])
def sumBy[U: Numeric](f: T => U): U = ...
메서드 타입 파라미터에 일치하는 타입 인자는 평소처럼 전달돼요.
List("a", "bb", "ccc").sumBy[Int](_.length)
반면 extension 다음의 타입 파라미터에 일치하는 타입 인자는 메서드를 확장 메서드가 아닌 것으로 참조할 때만 전달할 수 있어요.
sumBy[String](List("a", "bb", "ccc"))(_.length)
또는 두 타입 인자를 모두 전달할 때:
sumBy[String](List("a", "bb", "ccc"))[Int](_.length)
확장은 using 절도 가질 수 있어요. 예를 들어 위 + 확장은 using 절로도 동일하게 쓸 수 있어요.
extension [T](x: T)(using n: Numeric[T])
def + (y: T): T = n.plus(x, y)
집합 확장 (Collective Extensions)
가끔은 왼쪽 파라미터 타입을 공유하는 여러 확장 메서드를 정의하고 싶을 때가 있어요. 이때는 공통 파라미터를 하나의 extension으로 "끌어내고" 모든 메서드를 중괄호나 들여쓰기 영역으로 감쌀 수 있어요. 예시를 볼게요.
extension (ss: Seq[String])
def longestStrings: Seq[String] =
val maxLength = ss.map(_.length).max
ss.filter(_.length == maxLength)
def longestString: String = longestStrings.head
같은 것을 중괄호로도 쓸 수 있어요. (참고로 중괄호 안에서도 들여쓰기 영역을 쓸 수 있어요.)
extension (ss: Seq[String]) {
def longestStrings: Seq[String] = {
val maxLength = ss.map(_.length).max
ss.filter(_.length == maxLength)
}
def longestString: String = longestStrings.head
}
longestString의 오른쪽 변을 주목하세요. longestStrings를 직접 호출하면서 공통으로 확장된 값 ss를 리시버로 암묵적으로 가정하고 있어요.
이런 집합 확장은 각 메서드를 따로 정의한 개별 확장의 약칭이에요. 예를 들어 첫 번째 확장은 다음으로 확장돼요.
extension (ss: Seq[String])
def longestStrings: Seq[String] =
val maxLength = ss.map(_.length).max
ss.filter(_.length == maxLength)
extension (ss: Seq[String])
def longestString: String = ss.longestStrings.head
집합 확장은 타입 파라미터를 가질 수도 있고 using 절도 가질 수 있어요. 예시예요.
extension [T](xs: List[T])(using Ordering[T])
def smallest(n: Int): List[T] = xs.sorted.take(n)
def smallestIndices(n: Int): List[Int] =
val limit = smallest(n).max
xs.zipWithIndex.collect { case (x, i) if x <= limit => i }
확장 메서드 호출의 변환 (Translation of Calls to Extension Methods)
확장 메서드 참조를 변환하려면 컴파일러가 그 확장 메서드를 알아야 해요. 이런 경우 확장 메서드가 참조 지점에서 **적용 가능(applicable)**하다고 말해요. 확장 메서드가 적용 가능한 방법은 네 가지가 있어요.
- 확장 메서드가 참조를 둘러싼 스코프에서 정의되거나 상속되거나 import되어 단순 이름으로 보이는 경우.
- 확장 메서드가 참조 지점에서 보이는 어떤 given 인스턴스의 멤버인 경우.
- 참조가
r.m형태인데 확장 메서드가r의 타입의 암시적 스코프(implicit scope)에 정의된 경우. - 참조가
r.m형태인데 확장 메서드가r의 타입의 암시적 스코프에 있는 어떤 given 인스턴스에 정의된 경우.
첫 번째 규칙의 예시를 볼게요.
trait IntOps:
extension (i: Int) def isZero: Boolean = i == 0
extension (i: Int) def safeMod(x: Int): Option[Int] =
// extension method defined in same scope IntOps
if x.isZero then None
else Some(i % x)
object IntOpsEx extends IntOps:
extension (i: Int) def safeDiv(x: Int): Option[Int] =
// extension method brought into scope via inheritance from IntOps
if x.isZero then None
else Some(i / x)
trait SafeDiv:
import IntOpsEx.* // brings safeDiv and safeMod into scope
extension (i: Int) def divide(d: Int): Option[(Int, Int)] =
// extension methods imported and thus in scope
(i.safeDiv(d), i.safeMod(d)) match
case (Some(d), Some(r)) => Some((d, r))
case _ => None
두 번째 규칙에 따르면 확장 메서드를 담은 given 인스턴스를 정의해서 확장 메서드를 사용할 수 있게 만들 수 있어요. 이렇게요.
given ops1: IntOps() // brings safeMod into scope
1.safeMod(2)
세 번째와 네 번째 규칙에 따르면 확장 메서드는 리시버 타입의 암시적 스코프에 있거나, 그 스코프에 있는 given 인스턴스 안에 있으면 사용할 수 있어요. 예시를 볼게요.
class List[T]:
...
object List:
...
extension [T](xs: List[List[T]])
def flatten: List[T] = xs.foldLeft(List.empty[T])(_ ++ _)
given [T: Ordering] => Ordering[List[T]]:
extension (xs: List[T])
def < (ys: List[T]): Boolean = ...
end List
// extension method available since it is in the implicit scope
// of List[List[Int]]
List(List(1, 2), List(3, 4)).flatten
// extension method available since it is in the given Ordering[List[T]],
// which is itself in the implicit scope of List[Int]
List(1, 2) < List(3)
선택을 확장 메서드로 해석하는 정확한 규칙은 다음과 같아요.
e.m[Ts]라는 선택을 가정해 봅시다. 여기서 m은 e의 멤버가 아니고, 타입 인자 [Ts]는 선택적이며, T는 기대 타입이에요. 다음 두 다시 쓰기를 순서대로 시도해요.
-
선택을
m[Ts](e)로 다시 쓰고 타입 검사하는데, 이름 해석 규칙에 다음 사소한 수정을 적용해요.m이 모두 같은 중첩 레벨에 있는 여러 import로 import되었다면, 모호성 오류를 내는 대신 각 import를 확장 메서드로 시도해요. 오류 없이 타입 검사를 통과하는 확장으로 이어지는 import가 하나뿐이면 그 확장을 선택해요. 여러 그런 import가 있지만 와일드카드 import가 아닌 것이 하나뿐이면, 그 import의 확장을 선택해요. 그렇지 않으면 모호한 참조 오류를 보고해요.- 참고: import 규칙의 이 완화는 메서드
m이 확장 메서드로 쓰일 때만 적용돼요. 접두사 형태의 일반 메서드로 쓰이면 일반 import 규칙이 적용돼서, 여러 곳에서m을 import하면 모호성 오류가 날 수 있어요.
-
첫 번째 다시 쓰기가 기대 타입
T로 타입 검사를 통과하지 못하고, 어떤 적격한 객체o에 확장 메서드m이 있다면, 선택을o.m[Ts](e)로 다시 써요. 객체o는 다음 중 하나면 적격해요.o가T의 암시적 스코프의 일부이거나,o가 애플리케이션 지점에서 보이는 given 인스턴스이거나,o가T의 암시적 스코프에 있는 given 인스턴스이거나.
이 두 번째 다시 쓰기는 컴파일러가
T를m을 담은 타입으로의 암시적 변환도 시도하는 시점에 시도돼요. 다시 쓰는 방법이 둘 이상이면 모호성 오류가 나요.
확장 메서드는 앞에 식 없이 단순 식별자로도 참조할 수 있어요. 확장 메서드 f의 본문에 식별자 g가 나타나는데 g가 같은 집합 확장 안에 정의된 확장 메서드를 가리킨다면,
extension (x: T)
def f ... = ... g ...
def g ...
그 식별자는 x.g로 다시 쓰여요. f와 g가 같은 메서드여도 마찬가지예요. 예시를 볼게요.
extension (s: String)
def position(ch: Char, n: Int): Int =
if n < s.length && s(n) != ch then position(ch, n + 1)
else n
이 경우 재귀 호출 position(ch, n + 1)은 s.position(ch, n + 1)로 확장돼요. 전체 확장 메서드는 다음으로 다시 써져요.
def position(s: String)(ch: Char, n: Int): Int =
if n < s.length && s(n) != ch then position(s)(ch, n + 1)
else n
문법 (Syntax)
확장 메서드와 집합 확장에 대한, 현재 문법 기준의 문법 변경은 다음과 같아요.
BlockStat ::= ... | Extension
TemplateStat ::= ... | Extension
TopStat ::= ... | Extension
Extension ::= ‘extension’ [DefTypeParamClause] {UsingParamClause}
‘(’ DefParam ‘)’ {UsingParamClause} ExtMethods
ExtMethods ::= ExtMethod | [nl] <<< ExtMethod {semi ExtMethod} >>>
ExtMethod ::= {Annotation [nl]} {Modifier} ‘def’ DefDef
위에서 생성 규칙 ExtMethods의 표기 <<< ts >>>는 다음과 같이 정의돼요.
<<< ts >>> ::= ‘{’ ts ‘}’ | indent ts outdent
extension은 소프트 키워드(soft keyword)예요. 문장 시작에 나타나고 그 뒤에 [ 또는 (가 올 때만 키워드로 인식돼요. 그 외의 모든 경우에는 식별자로 취급돼요.