역량 분류자

역량 분류자 (Capability Classifiers)

소개

역량(capability)은 극도로 다재다능해요. 많은 서로 다른 영역의 개념을 표현할 수 있어요. 예외, 연속(continuation), I/O, 변경(mutation), 정보 흐름, 보안 권한——이것들은 그저 몇 가지 예시일 뿐이고, 목록은 계속돼요.

때로는 어떤 역량이 문맥에서 기대되거나 반환되는지를 제한하거나, 다시 말해 분류하는 것이 중요해요. 예를 들어 CanThrowboundary Label 같은 제어(control) 역량만 허용하고 다른 역량은 허용하지 않길 원할 수 있어요. 또는 변경은 허용하되 다른 부작용은 허용하지 않길 원할 수 있어요. 이것은 역량 클래스가 분류자(classifier)를 상속하게 함으로써 이뤄져요.

출처: Scala 3 Reference

본문

예를 들어 scala.caps 패키지는 Control이라는 분류자 트레이트를 이렇게 정의해요.

trait Control extends SharedCapability, Classifier

Gears 라이브러리는 그러면 Control을 상속하는 역량 클래스 Async를 정의해요.

import language.experimental.captureChecking
import caps.*
trait Async extends Control

일반 상속과 달리, 분류자는 역량의 캡처 집합도 제한해요. 예를 들어 함수가 있다고 해볼게요.

import language.experimental.captureChecking
import caps.*
trait Async extends Control
def f(using async: Async^) = ()

(Async가 역량 트레이트이므로 여기서 ^는 평소처럼 중복돼요.) 그러면 어떤 실제 async 인자도 Control을 확장하는 타입을 가진 역량만 캡처할 수 있다는 보장을 얻게 돼요. 변경이나 I/O 같은 다른 역량은 허용되지 않아요.

클래스나 트레이트는 마커 트레이트 caps.Classifier를 직접 확장함으로써 분류자가 돼요. 위 정의에서 Control은 분류자 트레이트지만 Async는 아닌데, AsyncControl을 통해 Classifier를 간접적으로만 확장하기 때문이에요.

분류자는 유일해요: 클래스는 서로 무관한 두 분류자 트레이트를 동시에 직접 또는 전이적으로 확장할 수 없어요. 그래서 클래스가 두 분류자 트레이트 C1C2를 전이적으로 확장한다면, 둘 중 하나는 다른 하나의 하위 트레이트여야 해요.

사전 정의된 분류자

caps 객체는 Classifier 트레이트 자체와 그것을 확장하는 몇 가지 트레이트를 정의해요.

trait Classifier

sealed trait Capability

trait SharedCapability extends Capability, Classifier
trait Control extends SharedCapability, Classifier

trait ExclusiveCapability extends Capability
trait Unscoped extends ExclusiveCapability, Classifier

사전 정의된 역량 트레이트의 계층을 보여주는 그래프가 있어요. 분류자 트레이트에는 밑줄이 그어져 있어요.

              Capability
              /        \
             /          \
            /            \
           /              \
 SharedCapability     ExclusiveCapability
 ----------------            |
        |                    |
        |                    |
     Control              Unscoped
     -------              --------

계층의 꼭대기에서 우리는 공유(shared) 역량과 배타(exclusive) 역량을 두 트레이트 SharedCapabilityExclusiveCapability로 구분해요. 지금까지 본 모든 역량 클래스는 공유예요. ExclusiveCapability는 분리 검사(separation checking)의 규칙이 적용되는 별칭-방지(anti-aliasing) 제한에 대해 검사되는 역량을 위한 기저 트레이트예요. 분리 검사는 현재 캡처 검사의 선택적 확장으로, 다른 언어 import로 켜요. Capability가 sealed 트레이트이므로 모든 역량 클래스는 공유이거나 배타예요. SharedCapability는 분류자이지만 ExclusiveCapability는 아니에요. 따라서 배타 역량은 그 캡처 집합에 공유 역량을 가질 수 있지만 그 반대는 불가능해요.

Control 역량은 공유예요. 이것은 그것들이 변경 가능한 상태에 대한 접근을 제어하는 역량 같은 배타 역량을 직접 또는 간접적으로 캡처할 수 없음을 뜻해요. 전형적인 Control 역량은:

  • boundary에서 돌아올 수 있게 하는 Label들,
  • 예외를 던질 수 있게 하는 CanThrow 역량, 또는
  • 중단(suspend)을 허용하는 Async 역량.

이것들은 모두 그 역량 클래스가 Control을 확장함으로써 표현돼요.

분류자 제한 (Classifier Restriction)

다음 문제를 생각해볼게요. Try.apply 메서드는 body 파라미터로 계산을 받아, 예외나 boundary.break 중단을 잡으면서 실행해요. 예외나 break는 Try 객체의 get 메서드를 호출할 때 다시 발생해요. 역량-인지 Try의 시그니처는 어떠해야 할까요?

Try.apply에 전달되는 body는 임의의 효과를 가질 수 있으므로 임의의 역량을 보존할 수 있어요. 하지만 결과 Try 객체는 body의 역량 중 Control로 분류된 것만 보존해요. 그래서 Try.apply의 시그니처는 이렇게 보여야 해요.

import language.experimental.captureChecking
import caps.*
class Try[+T]
object Try:
  def apply[T](body: => T): Try[T]^{body.only[Control]} = ???

결과의 캡처 집합에 새로운 형태의 역량 body.only[Control]이 있음을 주목하세요. 이것은 제한된 역량(restricted capability)이라고 불러요. 제한된 역량의 일반적인 형태는 c.only[A]인데, 여기서

  • c는 일반적이고 제한되지 않은 역량,
  • A는 분류자 트레이트예요.

기저 역량에 대해 캡처 집합을 대입할 때, 분류자와 무관한 것으로 알려진 모든 역량을 버려요. 예를 들어, 캡처 집합 {io, async}를 사용하는 실제 body { expr }가 있다고 해볼게요. 여기서

  • ioIO 역량인데, IOSharedCapability를 확장하되 Control은 확장하지 않는다고 가정해요.
  • asyncControl 역량이에요.

그러면 Try { expr }의 결과는 Try^{async} 타입을 가져요. io의 타입이 Control을 확장하지 않는 Capability 클래스이므로 io를 버려요.

expr이 추가 역량 proc: () => Unit을 사용한다면, proc도 결과 캡처 집합에 나타나요. proc은 완전히 효과-다형적이므로 그것이 Control 역량을 보존하지 않는다고 배제할 수 없기 때문에, 제한된 캡처 집합에 유지해야 해요. 이 요소들은 다음 예시에서 함께 보여져요.

class IO extends caps.SharedCapability
class Async extends caps.Control

def test(io: IO, async: Async, proc: () => Unit) =
  val r = Try:
    // code accessing `io`, `async`, and `proc` and returning an `Int.
  val _: Try[Int]^{async, proc} = r