컨텍스트 바운드
컨텍스트 바운드 (Context Bounds)
컨텍스트 바운드는 타입 파라미터에 의존하는 컨텍스트 파라미터의 흔한 패턴을 나타내는 약칭이에요. 이런 패턴은 스칼라에서 타입 클래스를 모델링할 때 아주 흔하게 등장해요. 컨텍스트 바운드를 쓰면, 지난 섹션의 maximum 함수를 이렇게 쓸 수 있어요.
def maximum[T: Ord](xs: List[T]): T = xs.reduceLeft(max)
메서드나 클래스의 타입 파라미터 T에 붙은 : Ord 같은 바운드는 Ord[T]를 쓰는 컨텍스트 파라미터를 나타내며, 둘러싼 메서드의 시그니처에 추가돼요. 이렇게 생성된 파라미터를 컨텍스트 바운드의 **증인(witness)**이라고 불러요.
예를 들어 위 maximum 메서드는 다음처럼 확장돼요.
def maximum[T](xs: List[T])(using Ord[T]): T = ...
컨텍스트 바운드는 하위 타입 바운드와 결합할 수 있어요. 둘 다 있으면 하위 타입 바운드가 먼저 와요. 예를 들면 이런 형태예요.
def f[T <: B : C](x: T): R = ...
본문
이름 있는 컨텍스트 바운드 (Named Context Bounds)
컨텍스트 바운드는 as 절로 이름을 붙일 수 있어요. 예를 들어 다음 트레이트 정의를 생각해 볼게요.
trait SemiGroup[A]:
extension (x: A) def combine(y: A): A
trait Monoid[A] extends SemiGroup[A]:
def unit: A
모노이드 인스턴스 리스트에 대한 reduce 함수를 이렇게 쓸 수 있어요.
def reduce[A: Monoid as m](xs: List[A]): A =
xs.foldLeft(m.unit)(_ `combine` _)
컨텍스트 바운드의 타입 뒤에 as x를 써서 인스턴스를 x에 바인딩해요. 이것은 import 리네이밍과 비슷한데, 거기서도 앞에 오는 무언가에 새 이름을 도입하죠.
이름 절이 있는 컨텍스트 바운드에서는 증인 파라미터가 그 이름을 갖게 돼요. 예를 들어 reduce의 확장된 시그니처는 다음처럼 돼요.
def reduce[A](xs: List[A])(using m: Monoid[A]): A
컨텍스트 파라미터에 이제 이름이 있으니 reduce 본문에서 참조할 수 있어요. 위 정의에서 m.unit 참조가 그 예시예요.
컨텍스트 바운드에 as 절이 없으면 생성된 증인 파라미터는 컴파일러가 합성한 이름을 갖게 돼요. 다만 현재 실험적인 언어 확장에서는 이 경우 컨텍스트 파라미터에 바운드된 타입 파라미터와 같은 이름을 주기도 해요.
이름 있는 컨텍스트 바운드는 스칼라 3.6에서 도입됐어요.
집계 컨텍스트 바운드 (Aggregate Context Bounds)
타입 파라미터는 여러 컨텍스트 바운드를 가질 수 있어요. 바운드가 여러 개면 중괄호 {...} 안에 써요. 예시예요.
trait:
def showMax[X : {Ord, Show}](x: X, y: X): String
class B extends A:
def showMax[X : {Ord as ordering, Show as show}](x: X, y: X): String =
show.asString(ordering.max(x, y))
이 문법은 스칼라 3.6부터 유효해요. 이전 문법은 [X : Ord : Show]처럼 : 절을 연쇄하는 방식이었어요. 이 문법도 여전히 쓸 수 있지만 시간이 지나 deprecated되고 제거될 거예요.
생성된 컨텍스트 파라미터의 배치 (Placement of Generated Context Parameters)
컨텍스트 바운드에서 생성된 증인 컨텍스트 파라미터는 다음과 같이 추가돼요.
- 바운드 중 하나가 이후 파라미터 절에서 이름으로 참조되면, 컨텍스트 바운드는 그 첫 번째 파라미터 절 바로 앞에 오는
using절로 매핑돼요. - 그렇지 않고 마지막 파라미터 절이
using(또는implicit) 절이면, 컨텍스트 바운드에서 생긴 모든 파라미터를 그 절 앞에 병합해 하나의using절을 만들어요. - 그렇지 않으면 컨텍스트 바운드에서 생긴 파라미터들이 끝에 새
using절을 이뤄요.
규칙 2와 3은 스칼라 2의 규칙과 일치해요. 규칙 1은 새로운 것이지만, 지금까지 컨텍스트 바운드는 참조될 수 없었으므로 기존 코드에는 적용되지 않아요. 따라서 스칼라 2 및 이전 스칼라 3 버전과의 바이너리 호환성이 유지돼요.
예시를 볼게요.
def f[T: {C1, C2}](x: T): R
다음으로 확장되고,
def f[T](x: T)(using C1, C2): R
규칙 3에 따라 마찬가지로,
def f[T: {C1 as c1, C2 as c2}](x: T): R
다음으로 확장돼요.
def f[T](x: T)(using c1: C1, c2: C2): R
def f[T: {C1, C2}, U: C3](x: T)(using y: U, z: V): R
다음으로 확장돼요.
def f[T, U](x: T)(using _: C1[T], _: C2[T], _: C3[U], y: U, z: V): R
y와 z가 스칼라 2 스타일의 implicit 파라미터여도 같은 확장이 일어나요.
trait Parser[P]:
type Input
type Result
여기 필요한 타입의 입력에 파서를 돌리는 메서드 run이 있어요.
def run[P : Parser as p](in: p.Input): p.Result
규칙 1에 따라 이 메서드 정의는 다음과 같이 확장돼요.
def run[P](using p: Parser[P]](in: p.Input): p.Result
타입 p.Result가 컨텍스트 파라미터 p를 합법적으로 참조할 수 있도록 using 절이 명시적 파라미터 절(in: p.Result) 앞에 놓인다는 점을 주의하세요.
마이그레이션 (Migration)
마이그레이션을 쉽게 하기 위해, 스칼라 3.0–3.5의 컨텍스트 바운드는 옛 스타일의 implicit 파라미터로 매핑되며, 인자는 (using ...) 절로든 일반 애플리케이션으로든 전달할 수 있어요. 스칼라 3.6부터는 위에서 설명한 대로 컨텍스트 파라미터로 매핑돼요.
소스 버전이 3.6-migration이면, 컨텍스트 바운드에서 생긴 증거 컨텍스트 파라미터를 일반 인자와 짝지으면 마이그레이션 경고가 나요. 그 경고는 (using ...) 절이 필요하다는 뜻이에요. 이 다시 쓰기는 -rewrite 아래에서 자동으로 할 수 있어요.
다형성 함수를 위한 컨텍스트 바운드 (Context Bounds for Polymorphic Functions)
스칼라 3.6부터 컨텍스트 바운드는 다형성 함수 타입과 다형성 함수 리터럴에서도 쓸 수 있어요.
type Comparer = [X: Ord] => (x: X, y: X) => Boolean
val less: Comparer = [X: Ord as ord] => (x: X, y: X) =>
ord.compare(x, y) < 0
이런 컨텍스트 바운드의 확장은 메서드 타입에서의 확장과 비슷한데, 메서드에서 using 절을 추가하는 대신 컨텍스트 함수 타입을 삽입한다는 차이가 있어요.
예를 들어 위 type과 val 정의는 다음으로 확장돼요.
type Comparer = [X] => (x: X, y: X) => Ord[X] ?=> Boolean
val less: Comparer = [X] => (x: X, y: X) => (ord: Ord[X]) ?=>
ord.compare(x, y) < 0
using 절의 확장은 별칭 타입 안을 들여다봐요. 예를 들어 이전 예시의 변형으로 파라미터화된 타입 별칭을 쓰는 경우를 볼게요.
type Cmp[X] = (x: X, y: X) => Boolean
type Comparer2 = [X: Ord] => Cmp[X]
Comparer2의 오른쪽 변 확장은 Cmp[X] 별칭을 확장한 다음, Comparer에서와 같은 자리에 컨텍스트 함수를 삽입해요.
[X] => (x: X, y: X) => Ord[X] ?=> Boolean
타입 멤버를 위한 컨텍스트 바운드 (Context Bounds for Type Members)
스칼라 3.6부터 컨텍스트 바운드는 타입 파라미터뿐 아니라 추상 타입 멤버에도 쓸 수 있어요.
예시예요.
class Collection:
type Element: Ord
이런 컨텍스트 바운드는 생성된 증인을 담을 파라미터 목록이 없기 때문에 타입 파라미터를 위한 컨텍스트 바운드와 다르게 확장해야 해요. 대신 추상 타입을 위한 컨텍스트 바운드는 지연 given(deferred givens)으로 매핑돼요.
예를 들어 위 Collection 클래스는 다음으로 확장돼요.
class Collection:
type Element
given Ord[Element] = deferred
지연 given에 대한 섹션에서 설명하듯, deferred는 scala.compiletime 패키지에 정의된 특별한 이름이에요.
문법 (Syntax)
컨텍스트 바운드의 새 문법은 다음과 같아요.
TypeParamBounds ::= TypeAndCtxBounds
TypeAndCtxBounds ::= TypeBounds [‘:’ ContextBounds]
ContextBounds ::= ContextBound
| '{' ContextBound {',' ContextBound} '}'
ContextBound ::= Type ['as' id]
함수 타입과 함수 리터럴의 문법은 제네릭 타입 파라미터에 대한 컨텍스트 바운드를 허용하도록 다음과 같이 일반화돼요.
FunType ::= FunTypeArgs ('=>' | '?=>') Type
| DefTypeParamClause '=>' Type
FunExpr ::= FunParams ('=>' | '?=>') Expr
| DefTypeParamClause '=>' Expr
추상 타입 멤버의 문법은 컨텍스트 바운드를 허용하도록 다음과 같이 일반화돼요.
TypeDef ::= id [TypeParamClause] TypeAndCtxBounds