runtimeChecked 메서드

runtimeChecked 메서드 (The runtimeChecked method)

컴파일러의 정적 검사가 지나치게 보수적일 때, "이 패턴은 런타임에 항상 매치돼"라고 컴파일러에게 알려주고 싶은 경우가 있어요. 예전에는 : @unchecked 타입 어스크립션을 썼지만 문법이 어색했죠. Scala 3.3의 runtimeChecked 확장 메서드는 표현식 끝에 붙이기만 하면, 런타임에 건전하게 수행할 수 없는 정적 검사만 골라서 끄는 깔끔한 방법이에요.

출처: Scala 3 Reference

본문

runtimeChecked 메서드는 scala.Predef에 정의된 확장 메서드예요. 어떤 표현식에서든 호출할 수 있습니다. .runtimeChecked로 끝나는 표현식은 컴파일러의 특정 정적 검사, 예를 들어 패턴 매치 완전성(exhaustivity) 검사에서 면제돼요. 이 관용구는 이런 경우에 : @unchecked 타입 어스크립션을 대체하려는 의도입니다.

예시 (Example)

runtimeChecked의 흔한 사용 사례는, 편의를 위해 또는 타입이 표현할 수 없는 알려진 불변식(invariant) 때문에, 패턴이 항상 매치될 것을 단언하는 거예요.

예를 들어 동적으로 로드된 사전류 구조에서 기대하는 항목을 찾는 경우:

// example 1
trait AppConfig:
  def get(key: String): Option[String]

val config: AppConfig = ???

val Some(appVersion) = config.get("appVersion").runtimeChecked

또는 값이 어떤 특정 패턴에만 매치될 수 있다고 단언하는 경우:

// example 2
enum Day:
  case Mon, Tue, Wed, Thu, Fri, Sat, Sun

val weekDay: Option[Day] = ???

weekDay.runtimeChecked match
  case Some(Mon | Tue | Wed | Thu | Fri) => println("got weekday")
// case Some(Sat | Sun) => // weekend should not appear
  case None =>

두 경우 모두 runtimeChecked가 없으면 (예시 1에서는) 오류가 나거나 (예시 2에서는) 경고가 나요. 정적으로 컴파일러가 런타임에 다른 경우가 있을 수 있다는 것을 알기 때문에 프로그래머에게 경고하는 게 옳기 때문이죠.

// warning in example 2 when we don't add `.runtimeChecked`.
-- [E029] Pattern Match Exhaustivity Warning: ----------------------------------
6 |weekDay match
  |^^^^^^^
  |match may not be exhaustive.
  |
  |It would fail on pattern case: Some(Sat), Some(Sun)

안전성 (Safety)

runtimeChecked 메서드는 런타임에 건전하게 수행될 수 있는 정적 검사만 끄요. 그래서 확인되지 않은(unchecked) 타입 테스트가 있는 패턴은 여전히 경고를 만들어 냅니다. 예를 들어:

scala> val xs = List(1: Any)
     | xs.runtimeChecked match {
     |   case is: ::[Int] => is.head
     | }
1 warning found
-- Unchecked Warning: ---------------------------------------
3 |  case is: ::[Int] => is.head
  |       ^
  |the type test for ::[Int] cannot be checked at runtime
  |because its type arguments can't be determined from List[Any]
val res0: Int = 1

경고가 암시하듯, ::[Int] 타입은 List[Any] 타입 값을 런타임에 테스트할 수 없어요. 그래서 runtimeChecked를 써도 검증할 수 없는 단언으로부터 사용자를 계속 보호해 줍니다.

이전 Scala 버전처럼 경고를 완전히 피하려면, 타입 인자에 @unchecked를 붙여야 해요:

scala> xs.runtimeChecked match {
     |   case is: ::[Int @unchecked] => is.head
     | }
val res1: Int = 1

명세 (Specification)

표준 Scala 3 라이브러리의 일부로 새 어노테이션 scala.internal.RuntimeChecked를 추가해요. 프로그래머가 이 어노테이션을 직접 사용할 것으로 기대되진 않습니다.

package scala.annotation.internal

final class RuntimeChecked extends Annotation

패턴 매치의 스크루티니(scrutinee)이고 RuntimeChecked로 어노테이션된 타입을 가진 어떤 항(term)도 패턴 매치 완전성 검사에서 면제돼요.

사용자 지향 API는 어떤 값에 대해서도 자격을 갖는 새 확장 메서드 scala.Predef.runtimeChecked로 보강됩니다:

package scala

import scala.annotation.internal.RuntimeChecked

object Predef:
  ...
  extension [T](x: T)
    inline def runtimeChecked: x.type @RuntimeChecked =
      x: @RuntimeChecked

runtimeChecked 메서드는 인자를 돌려주면서, 그 타입을 RuntimeChecked 어노테이션으로 리파인드해요.

동기 (Motivation)

Pattern Bindings에서 설명했듯이, -source:future 아래에서는 패턴 정의가 refutable(반증 가능)이면 오류예요. 예를 들어 다음을 봅시다:

def xs: List[Any] = ???
val y :: ys = xs

이것은 3.0에서는 경고 없이 컴파일됐고, 3.2에서 경고가 됐으며, 미래 3.x 버전에서 기본으로 오류로 만들고 싶어요. 3.2에서의 탈출구로 : @unchecked의 타입 어스크립션을 권장했어요:

-- Warning: ../../new/test.scala:6:16 -----------------------
6 |  val y :: ys = xs
  |                ^^
  |pattern's type ::[Any] is more specialized than the right
  |hand side expression's type List[Any]
  |
  |If the narrowing is intentional, this can be communicated
  |by adding `: @unchecked` after the expression,
  |which may result in a MatchError at runtime.

하지만 : @unchecked는 문법적으로 어색하고, 게다가 잘못된 이름이에요. 사실 이 경우 패턴은 완전히 검사되지만, 필요한 검사가 런타임에 일어날 뿐이니까요. runtimeChecked 메서드는 이런 용도로 @unchecked를 대체하려는 의도입니다.

@unchecked 어노테이션은 건전하지 않은 타입 테스트에 대한 경고를 조용히 만들기 위해 여전히 유지됩니다.

runtimeChecked로 Scala 2.13 의미론 복원하기 (Restoring Scala 2.13 semantics with runtimeChecked)

Scala 3에서 : @unchecked 타입 어스크립션은 매치 스크루티니에 대한 모든 패턴-매치 경고를 끄는 효과가 있어요. 이는 패턴 완전성 검사만 엄격히 끄는 2.13과 다르죠. runtimeChecked는 Scala 2.13의 의미론을 복원합니다.