using 절

using 절 (Using Clauses)

함수형 프로그래밍은 대부분의 의존성을 단순한 함수 파라미터화로 표현하곤 해요. 깔끔하고 강력하지만, 긴 호출 체인에서 같은 값을 여러 함수에 반복해서 전달해야 해서 파라미터가 많은 함수가 생기기도 하죠. 컨텍스트 파라미터(context parameter)는 컴파일러가 반복적인 인자를 합성하게 해줘서 바로 이 지점을 도와줘요. 이 페이지에서는 using 절의 기본 사용법부터 명시적 using 절, summon까지 살펴볼게요.

출처: Scala 3 Reference

본문

함수형 프로그래밍은 대부분의 의존성을 단순한 함수 파라미터화로 표현하는 경향이 있어요. 깔끔하고 강력하지만, 때로는 많은 파라미터를 받는 함수를 만들면서 긴 호출 체인 속에서 많은 함수에 같은 값을 반복해서 전달하게 되기도 해요. 컨텍스트 파라미터가 여기서 도움이 돼요. 반복적인 인자를 프로그래머가 명시적으로 쓰는 대신 컴파일러가 합성할 수 있게 해주기 때문이죠.

예를 들어, 앞에서 정의한 given 인스턴스와 함께 정렬(ordering)이 존재하는 모든 인자에 대해 동작하는 max 함수를 이렇게 정의할 수 있어요.

def max[T](x: T, y: T)(using ord: Ord[T]): T =
  if ord.compare(x, y) < 0 then y else x

여기서 ord는 using 절로 도입된 컨텍스트 파라미터예요. max 함수는 다음과 같이 적용할 수 있어요.

max(2, 3)(using intOrd)

(using intOrd) 부분은 ord 파라미터의 인자로 intOrd를 전달해요. 하지만 컨텍스트 파라미터의 요점은 이 인자를 생략할 수도 있다는 거죠(그리고 보통은 생략해요). 그래서 다음 적용들도 똑같이 유효해요.

max(2, 3)
max(List(1, 2, 3), Nil)

익명 컨텍스트 파라미터 (Anonymous Context Parameters)

많은 상황에서 컨텍스트 파라미터의 이름은 전혀 언급할 필요가 없어요. 다른 컨텍스트 파라미터를 위한 합성 인자에서만 쓰이기 때문이죠. 그런 경우 파라미터 이름을 정의하지 않고 타입만 제공할 수 있어요. 예를 들어 볼게요.

def maximum[T](xs: List[T])(using Ord[T]): T =
  xs.reduceLeft(max)

maximum은 타입 Ord[T]의 컨텍스트 파라미터를, 추론된 인자로 max에 넘겨주기 위해서만 받아요. 파라미터의 이름은 생략돼요.

일반적으로 컨텍스트 파라미터는 전체 파라미터 목록 (p_1: T_1, ..., p_n: T_n)으로 정의하거나, 단순히 타입의 나열 T_1, ..., T_n로 정의할 수 있어요. 가변 인자(vararg) 파라미터는 using 절에서 지원되지 않아요.

클래스 컨텍스트 파라미터 (Class Context Parameters)

클래스 컨텍스트 파라미터를 클래스 본문 밖에서도 보이게 하려면, val이나 var 수식어를 추가해서 멤버로 만들 수 있어요.

class GivenIntBox(using val usingParameter: Int):
  def myInt = summon[Int]

val b = GivenIntBox(using 23)
import b.usingParameter
summon[Int]  // 23

이것은 명시적인 given 멤버를 만드는 것보다 선호돼요. 후자는 클래스 본문 안에서 모호성(ambiguity)을 만들기 때문이죠.

class GivenIntBox2(using usingParameter: Int):
  given givenMember: Int = usingParameter
  def n = summon[Int]  // ambiguous given instances: both usingParameter and givenMember match type Int

GivenIntBox의 외부에서 보면 usingParameter는 클래스 안에 given usingParameter: Int로 정의된 것처럼 보여요. 특히 given을 가져오는 방법 섹션에서 설명한 대로 임포트되어야 해요.

val b = GivenIntBox(using 23)
// Works:
import b.given
summon[Int]  // 23
usingParameter  // 23

// Fails:
import b.*
summon[Int]      // No given instance found
usingParameter   // Not found

복잡한 인자 추론하기 (Inferring Complex Arguments)

다음은 Ord[T] 타입의 컨텍스트 파라미터를 가진 메서드 두 개예요.

def descending[T](using asc: Ord[T]): Ord[T] = new Ord[T]:
  def compare(x: T, y: T) = asc.compare(y, x)

def minimum[T](xs: List[T])(using Ord[T]) =
  maximum(xs)(using descending)

minimum 메서드의 오른쪽은 descendingmaximum(xs)의 명시적 인자로 전달해요. 이 설정에서 다음 호출들은 모두 형식에 맞으며, 모두 마지막 호출로 정규화돼요.

minimum(xs)
maximum(xs)(using descending)
maximum(xs)(using descending(using intOrd))

여러 개의 using 절 (Multiple using Clauses)

정의에는 여러 개의 using 절이 있을 수 있고, using 절은 일반 파라미터 절과 자유롭게 섞을 수 있어요. 예를 들어 볼게요.

def f(u: Universe)(using ctx: u.Context)(using s: ctx.Symbol, k: ctx.Kind) = ...

여러 using 절은 적용(application)에서 왼쪽에서 오른쪽으로 매칭돼요. 예를 들어 볼게요.

object global extends Universe { type Context = ... }
given ctx : global.Context { type Symbol = ...; type Kind = ... }
given sym : ctx.Symbol
given kind: ctx.Kind

그러면 다음 호출들이 모두 유효하고(마지막 호출로 정규화돼요)

f(global)
f(global)(using ctx)
f(global)(using ctx)(using sym, kind)

하지만 f(global)(using sym, kind)는 타입 에러를 줄 거예요.

명시적 using 절 (Explicit using Clauses)

적용(application)에서 빠진 인자는 암시적 탐색이나 기본 인자(default argument) 어느 쪽으로든 보충될 수 있어요.

명시적 적용(explicit application)에서는 — 명시적 파라미터든 using으로 명시적으로 공급된 implicit 파라미터든 — 빠진 인자가 기본값으로 채워져요. 추가로, 남은 implicit 파라미터는 내재적 값(implicit value)이 이용 가능하면 제공돼요.

컨텍스트 파라미터의 내재적 적용(implicit application)에서는 — 인자가 명시적으로 공급되지 않았다면 — 내재적 값이 이용 가능하면 제공되고, 빠진 것에는 기본 인자가 쓰여요.

다시 말해, 기본 인자를 가진 컨텍스트 파라미터에 대해 내재적 값이 제공될지 기본 인자가 쓰일지는, 적용에 명시적 using이 있는지에 달려 있어요.

예를 들어 설명할게요.

def f(using p: Boolean, i: Int, s: String = "hello, world") = s"p=$p, i=$i, s=$s"

given Boolean = true
given Int = 42
given String = "contextual"

f // all args are supplied implicitly from scope, since implicit values are available

f(using p = false, i = 27, s = "explicit") // all args are explicit

f(using s = "partial") // one arg is explicit, others are implicit

f(using i = 27, s = "partial") // only one arg is supplied implicitly

f(using p = false, i = 27) // for explicit using, the missing arg is supplied from the default arg

f(using p = false) // one default arg and one implicit value

마지막 두 경우처럼 기본 인자가 내재적 값을 가릴 때 컴파일러는 경고해요.

Argument for implicit parameter s was supplied using a default argument.

클래스 생성자도 같은 방식으로 처리돼요. 다만 생성자는 본질적으로 혼합형이에요. 클래스는 항상 명시적 파라미터 목록(비어 있을 수도 있음)을 가지기 때문이죠.

예를 들어 볼게요.

class C(using p: Boolean, i: Int, s: String = "hello, world"):
  override def toString = s"p=$p, i=$i, s=$s"

new C // same as new C(summon[Boolean], summon[Int], summon[String])()

명시적 파라미터 목록이 시그니처에 덧붙여진다는 점을 주목하세요.

C() // same as new C, where the implicit arguments are inserted before the explicit contructor proxy parens
C(using p = false, i = 27, s = "explicit") // explicit using, with empty parens supplied as for new C
C(using s = "partial")() // the trailing parens are required when args are previously inferred

(Scala 2의 implicit 파라미터 목록은 생성자에 대해서도 항상 맨 뒤에 와요.)

인스턴스 summon하기 (Summoning Instances)

Predef에 있는 summon 메서드는 특정 타입의 given을 반환해요. 예를 들어 Ord[List[Int]]의 given 인스턴스는 이렇게 만들어져요.

summon[Ord[List[Int]]]  // reduces to listOrd(using intOrd)

summon 메서드는 컨텍스트 파라미터에 대한 (넓히지 않는, non-widening) 항등 함수로 단순히 정의돼요.

def summon[T](using x: T): x.type = x

문법 (Syntax)

다음은 표준 Scala 3 문맥 자유 문법에서 파라미터와 인자의 새 문법을 델타(delta)로 본 것이에요. using은 소프트 키워드(soft keyword)라서 파라미터나 인자 목록의 시작에서만 인식돼요. 그 외의 곳에서는 일반 식별자로 쓸 수 있어요.

ClsParamClause      ::=  ... | UsingClsParamClause
DefParamClause      ::=  ... | UsingParamClause
UsingClsParamClause ::=  ‘(’ ‘using’ (ClsParams | Types) ‘)’
UsingParamClause    ::=  ‘(’ ‘using’ (DefTermParams | Types) ‘)’
ParArgumentExprs    ::=  ... | ‘(’ ‘using’ ExprsInParens ‘)’