검사된 예외

검사된 예외 (Checked Exceptions)

소개

Scala는 언어 import를 통해 검사된 예외(checked exceptions)를 켤 수 있어요. 아래는 safer exceptions 페이지에서 가져온 예시로, 2021 Scala Symposium에서 발표된 논문에서도 설명돼 있어요.

출처: Scala 3 Reference

본문

import language.experimental.saferExceptions
import language.experimental.captureChecking
import caps.*
import language.experimental.saferExceptions

class LimitExceeded extends Exception

val limit = 10e+10
def f(x: Double): Double throws LimitExceeded =
  if x < limit then x * x else throw LimitExceeded()

throws 절은 CanThrow 역량을 제공하는 암시적 파라미터로 확장돼요. 따라서 함수 f는 다음과 같이 동등하게 쓸 수 있어요.

import language.experimental.saferExceptions
import language.experimental.captureChecking
import caps.*
import language.experimental.saferExceptions

class LimitExceeded extends Exception

val limit = 10e+10
def f(x: Double): Double throws LimitExceeded =
  if x < limit then x * x else throw LimitExceeded()
object Desugared:
  def f(x: Double)(using CanThrow[LimitExceeded]): Double =
    if x < limit then x * x else throw LimitExceeded()

암시적 파라미터가 없으면 오류가 보고돼요. 예를 들어 함수 정의

import language.experimental.saferExceptions
import language.experimental.captureChecking
import caps.*
import language.experimental.saferExceptions

class LimitExceeded extends Exception

val limit = 10e+10
def f(x: Double): Double throws LimitExceeded =
  if x < limit then x * x else throw LimitExceeded()
def g(x: Double): Double =
  if x < limit then x * x else throw LimitExceeded() // error: missing CanThrow[LimitExceeded]

는 다음 오류 메시지와 함께 거부돼요.

  |  if x < limit then x * x else throw LimitExceeded()
  |                               ^^^^^^^^^^^^^^^^^^^^^
  |The capability to throw exception LimitExceeded is missing.
  |The capability can be provided by one of the following:
  | - Adding a using clause `(using CanThrow[LimitExceeded])` to the definition of the enclosing method
  | - Adding `throws LimitExceeded` clause after the result type of the enclosing method
  | - Wrapping this piece of code with a `try` block that catches LimitExceeded

CanThrow 역량은 throw 표현식이 요구하고 try 표현식이 만들어요. 예를 들어 표현식

import language.experimental.saferExceptions
import language.experimental.captureChecking
import caps.*
import language.experimental.saferExceptions

class LimitExceeded extends Exception

val limit = 10e+10
def f(x: Double): Double throws LimitExceeded =
  if x < limit then x * x else throw LimitExceeded()
def sumAll(xs: Double*): Double =
try xs.map(f).sum
catch case ex: LimitExceeded => -1

은 컴파일러가 대략 다음과 같이 확장해요.

try
  erased given ctl: CanThrow[LimitExceeded] = compiletime.erasedValue
  xs.map(f).sum
catch case ex: LimitExceeded => -1

(ctl 역량은 타입 검사에만 쓰이고 생성된 코드에는 나타날 필요가 없으므로 erased로 선언할 수 있어요.)

다른 역량 기반 기법들과 마찬가지로, 결과에 캡처된 역량을 경계해야 해요. 예를 들어 여기 문제가 되는 사용 사례가 있어요.

import language.experimental.saferExceptions
import language.experimental.captureChecking
import caps.*
import language.experimental.saferExceptions

class LimitExceeded extends Exception

val limit = 10e+10
def f(x: Double): Double throws LimitExceeded =
  if x < limit then x * x else throw LimitExceeded()
def escaped(xs: Double*): (() => Double) throws LimitExceeded =
  try () => xs.map(f).sum
  catch case ex: LimitExceeded => () => -1
val crasher = escaped(1, 2, 10e+11) // error: CanThrow escapes into returned closure
crasher()

이 코드는 거부되어야 해요. 그렇지 않으면 crasher() 호출이 처리되지 않은 LimitExceeded 예외를 던지게 되기 때문이에요.

language.experimental.captureChecking 언어 import 아래에서는 이 코드가 실제로 거부돼요.

예외 검사와 캡처 검사를 통합하려면 두 가지만 바꾸면 돼요.

  • CanThrowControl을 상속하는 클래스로 선언되어, 모든 CanThrow 인스턴스에 대한 참조가 추적돼요.
  • 도피 검사(escape checking)가 try 표현식으로 확장돼요. try의 결과 타입은 try 본문에서 정의된 역량을 캡처하는 것이 허용되지 않아요.