상태 유지 역량
상태 유지 역량 (Stateful Capabilities)
소개
효과의 중요한 한 종류는 변경 가능한 변수와 변경 가능한 데이터 구조에 대한 접근을 나타내요. 이것은 프로그램 상태(program state)라는 개념과 밀접하게 연결돼 있어요. caps.Stateful을 확장하는 타입은 프로그램 상태를 조회하고 변경할 수 있는 값을 모델링해요. 그런 타입이 또한 caps.ExclusiveCapability나 그 분류자 중 하나를 확장한다면, 그 값은 상태 유지 역량(stateful capability)이 돼요.
이 챕터는 captureChecking을 가정해요. Arrays와 Untracked Vars에 대한 절들은 분리 검사(separation checking)에도 의존해요.
우리는 두 종류의 접근을 구분해요: 상태 변경을 허용하는 전체 접근(full access)과 상태를 관찰할 수 있지만 변경할 수는 없는 읽기 전용 접근(read-only access). 이것은 이런 효과를 허용하는 역량의 종류로 반영돼요. x가 상태 유지 역량이라면, 전체 접근에 대해 x를, 읽기 전용 접근에 대해 x.rd를 써요.
이것은 스코프된 역량 모델의 정제로 볼 수 있는데, 이 두 접근 모드를 구분하도록 확장된 거예요.
상태 유지 역량의 흔한 종류는 읽고 쓸 수 있는 변경 가능한 변수나 데이터 구조예요. 이런 변경 가능한 데이터 구조는 마커 트레이트 caps.Mutable로 표현돼요. 예를 들어 간단한 참조 셀(reference cell)을 생각해볼게요.
본문
import language.experimental.captureChecking
import caps.Mutable
class Ref[T](init: T) extends Mutable:
var fld: T = init
val r: Ref[Int]^ = Ref(22)
val f: () ->{r} Unit = () => r.fld += 1
val g: () ->{r.rd} Int = () => r.fld
여기서 f는 r의 필드 fld에 쓰므로 r을 캡처해요. 반면 g는 r의 내용만 읽으므로 r.rd를 캡처해요.
사실 Mutable 인스턴스는 각각 별도의 트레이트로 표현되는 여러 속성을 결합해요. 다음에서 이 트레이트들과 연관된 역량을 소개할게요.
역량 종류
역량은 다음과 같이 불러요.
SharedCapability로 분류되면 공유(shared),- 그 외에는 배타(exclusive).
Stateful 트레이트
scala.caps 객체에서 새 트레이트를 정의해요.
trait Stateful
이것은 전역 프로그램 상태를 조회하고 변경할 수 있는 클래스들을 위한 마커 트레이트로 쓰여요. 이런 클래스들은 전형적으로 변경 가능한 변수와/또는 update 메서드를 포함해요. Stateful 자체는 타입을 역량으로 분류하지 않아요. Mutable 같은 트레이트는 Stateful을 역량 분류자와 결합해요.
또한 맨몸의 상태 유지 타입과 ^가 붙은 같은 타입을 구분하는 것도 중요해요. Ref 같은 많은 흔한 상태 유지 타입에서 Ref[Int] 같은 맨몸 타입은 읽기 접근만 주고, Ref[Int]^는 전체 접근을 주어요. 이 내용은 아래 Read-only Capabilities에서 더 설명할게요.
Update 메서드
Update 메서드는 새 소프트 수식자 update로 선언돼요. 직관적으로 update는 상태 변경을 수행할 수 있는 메서드를 표시해요.
예시:
import language.experimental.captureChecking
import caps.*
class Counter extends Stateful:
private var count = 0
def value: Int = count
update def incr(x: Int): Unit = count = x
update는 Stateful을 확장하는 클래스나 객체에서만 사용할 수 있어요. Stateful 클래스에서 보통 메서드는 수신자(receiver)에 대해 읽기 전용으로 검사돼요: 그들은 그 상태를 관찰할 수 있지만, 변경하거나 그것의 update 메서드를 호출할 수는 없어요. update 메서드는 수신자에 대한 그 제한을 풀어줘요. 다른 상태 유지 값은 여전히 보통 메서드에서 갱신될 수 있는데, 단 메서드가 그들에게 전체 접근을 주는 참조를 가져야 해요. 예를 들어 파라미터나 지역 정의에서요.
클래스 Counter에서 incr는 count에 할당하여 수신자 상태를 변경하므로 update 수식자가 필요해요.
예를 들어 보통 메서드는 전체 접근 참조를 명시적으로 받는다면 다른 카운터를 갱신할 수 있어요.
import language.experimental.captureChecking
import caps.*
class Counter extends Stateful:
private var count = 0
def value: Int = count
update def incr(x: Int): Unit = count = x
class CounterOps:
def copyValue(from: Counter^, to: Counter^): Unit =
to.incr(from.value)
약간 더 정교한 예시는 생성자 파라미터에 그 레지스트리에 대한 참조를 저장함으로써 감싸는 레지스트리 객체의 변수를 갱신하는 거예요.
import language.experimental.captureChecking
import caps.*
object Registry extends Stateful:
var sharedCount = 0
class CounterX(reg: Registry.type):
def next: Int =
reg.sharedCount += 1
reg.sharedCount
Stateful 클래스의 보통 메서드는 update 메서드를 호출할 수 없어요. 그렇지 않으면 피호출자가 수행하는 배타 접근이 호출자에게도 부과되어, 호출자가 더 이상 읽기 전용이 아니게 되기 때문이에요.
이 구분은 오버라이딩에도 영향을 줘요. update 메서드는 보통 메서드를 구현하거나 오버라이드할 수 없지만, 보통 메서드는 update 메서드를 구현하거나 오버라이드할 수 있어요. toString이나 == 같은 Object에서 상속된 메서드가 보통 메서드이므로, 이 메서드들은 어느 것도 update 메서드로 구현될 수 없어요.
함수 타입의 apply 메서드도 보통 메서드이므로, Stateful 클래스는 update 메서드를 apply 메서드로 사용해 함수 타입을 구현할 수 없어요.
Stateful 타입
타입이 Stateful을 확장하고 비-private 멤버로 변경 가능한 변수나 update 메서드를 가지면 상태 유지(stateful)라 불러요.
제약: 비-상태 유지 타입은 패턴 매치로 상태 유지 타입으로 다운캐스트될 수 없어요. (참고: 현재 이것은 강제되지 않아요.)
정의: 다음 조건들이 충족되면 부모 클래스 생성자는 읽기 전용이에요.
- 클래스가 환경으로부터 어떤 배타 역량도 보존하지 않는다.
- 생성자가 배타 역량을 보존하는 인자를 받지 않는다.
- 클래스가 배타 보편 역량을 보존하는 필드를 가지지 않는다.
제약: 클래스나 트레이트가 Stateful을 확장하면, 그 모든 부모 클래스나 트레이트는 Stateful을 확장하거나 읽기 전용이어야 해요.
아이디어는 이래요. Stateful을 확장하는 타입에 대한 참조를 Stateful을 확장하지 않는 타입으로 업캐스트할 때, 그로 인해 배타 역량을 사용할 수 있는 메서드에 접근권을 얻어서는 안 된다는 거예요. 앞선 제약에 따르면 그런 클래스는 읽기 전용이어야 하는데, 이는 그것이 구현하는 어떤 코드도 스스로 배타 역량에 접근할 수 없음을 뜻해요. 또한 그런 메서드를 수행하는 메서드로 어떤 메서드도 오버라이드할 수 없는데, 그런 메서드는 update 메서드여야 하고 update 메서드는 보통 메서드를 오버라이드할 수 없기 때문이에요.
예시:
표준 라이브러리의 트레이트 IterableOnce와 Iterator를 생각해볼게요. 상태와 변경 추적을 명시적으로 만들려면, 이들을 이렇게 모델링할 수 있어요.
import language.experimental.captureChecking
import caps.*
trait IterableOnce[+T] extends Stateful:
def iterator: Iterator[T]^{this}
update def foreach(op: T => Unit): Unit
update def exists(op: T => Boolean): Boolean
trait Iterator[+T] extends IterableOnce[T]:
def hasNext: Boolean
update def next(): T
update def foreach(op: T => Unit): Unit =
while hasNext do op(next())
update def exists(op: T => Boolean): Boolean /* = ... next() ... */
트레이트 IterableOnce는 많은 update 메서드를 가진 상태 유지 타입이에요. 그중에는 foreach와 exists가 있어요. 이들은 update로 분류되어야 하는데, 그들의 하위 트레이트 Iterator에서의 구현이 update 메서드 next를 사용하기 때문이에요.
하지만 상태 유지 타입이 아닌 IterableOnce의 다른 구현들도 있어요(간접적으로 Stateful 트레이트를 확장하긴 하지만요). 특히 컬렉션 클래스들은 필요할 때마다 새 iterator를 만들어 IterableOnce를 구현해요. 그러면 next()를 통한 변경은 그 이터레이터의 상태로 제한되고, 기저 컬렉션은 영향받지 않아요. 이런 구현들은 IterableOnce의 각 update 메서드를 update 수식자 없는 보통 메서드로 구현해요.
import language.experimental.captureChecking
import caps.*
trait IterableOnce[+T] extends Stateful:
def iterator: Iterator[T]^{this}
update def foreach(op: T => Unit): Unit
update def exists(op: T => Boolean): Boolean
trait Iterator[+T] extends IterableOnce[T]:
def hasNext: Boolean
update def next(): T
update def foreach(op: T => Unit): Unit =
while hasNext do op(next())
update def exists(op: T => Boolean): Boolean /* = ... next() ... */
trait Iterable[+T] extends IterableOnce[T]:
def iterator: Iterator[T]^{this} /* = new Iterator[T] { ... } */
def foreach(op: T => Unit) = iterator.foreach(op)
def exists(op: T => Boolean) = iterator.exists(op)
여기서 Iterable은 멤버로 update 메서드를 가지지 않으므로 상태 유지 타입이 아니에요. 상속된 모든 update 메서드가 보통 메서드로 (재)구현돼요.
참고: 어떤 경우든 상태 유지 타입은 무엇을 확장하는지가 아니라 update 메서드의 존재로 정의되므로 기저 트레이트 Stateful이 필요 없다고 생각할 수도 있어요. 사실 Stateful의 중요성은 다른 메서드들을, 배타 역량에 접근할 수 없는 읽기 전용 메서드로 정의한다는 데 있어요. Stateful을 확장하지 않는 타입에는 그렇지 않아요. 예를 들어 함수 타입의 apply 메서드는 update 메서드가 아니고 타입 자체도 Stateful을 확장하지 않아요. 하지만 apply는 배타 역량에 접근하는 메서드로 구현될 수 있어요.
import language.experimental.captureChecking
import caps.*
class Counter extends Stateful:
private var count = 0
def value: Int = count
update def incr(x: Int): Unit = count = x
def tick(c: Counter^): () ->{c} Int =
() =>
c.incr(c.value + 1)
c.value
참조 셀, 변경 가능한 배열이나 행렬 같은 변경 가능한 클래스는 분명히 상태 유지 타입이에요. 하지만 그것은 다음에서 설명하는 두 가지 다른 측면도 가져요: 그것은 전형적으로 Unscoped로 분류되고, 이 흔한 조합은 트레이트 Mutable로 표현돼요.
Unscoped 분류자
보통 역량은 제한된 수명을 가져요. 예를 들어 다시 withFile 메서드를 생각해볼게요.
class File:
def read(): String = ...
def close(): Unit = ...
def withFile[T](op: (f: File^) => T): T =
op(new File)
여기서 우리는 op의 반환 타입이 File 파라미터 f를 캡처할 수 없도록 강제해야 해요. 이것은 op가 withFile 호출 밖에서 알려지지 않은 새 역량을 반환하지 못하게 함으로써 이뤄져요. 하지만 이 기법은 너무 제한적일 수 있어요. 예를 들어 파일의 내용을 Ref 셀 같은 Stateful 역량으로 읽고 싶을 수 있어요. 그 Ref 셀은 파일을 붙잡고 있지는 않아도, 셀 자체가 역량이므로 순수 타입도 아니에요.
Ref 셀을 Unscoped로 선언하면 이걸 컴파일할 수 있어요. Unscoped로 분류된 역량은 환경을 벗어날(escape) 수 있어요. 예를 들어 다음은 허용돼요.
import language.experimental.captureChecking
import caps.*
class File:
def read(): String = ""
def close(): Unit = ()
def withFile[T](op: (f: File^) => T): T =
op(new File)
class Ref[T](init: T) extends Stateful, Unscoped
def readIntoRef(): Ref[String]^ =
withFile: f =>
val r: Ref[String]^ = Ref(f.read())
r
여기서 r은 withFile의 스코프를 벗어나는 Ref[String] 타입의 새 참조예요. Ref가 Unscoped로 분류되므로 그런 것이 OK예요. Unscoped는 분류자이므로 Ref가 f를 캡처할 수 없다는 뜻인데, f는 File로서 Unscoped로 분류되지 않거든요. 따라서 withFile에서 Ref를 반환해도 f의 수명에는 영향을 주지 않아요.
Mutable 클래스
참조 셀, 배열, 행렬 같은 클래스는 상태 유지(stateful)이고 unscoped예요. 이 흔한 조합은 scala.caps 객체의 Mutable 트레이트로 표현돼요.
import language.experimental.captureChecking
import caps.*
trait Mutable extends Stateful, Unscoped
예시:
import language.experimental.captureChecking
import caps.*
class Ref[T](init: T) extends Mutable:
private var current = init
def get: T = current
update def set(x: T): Unit = current = x
import language.experimental.captureChecking
import language.experimental.separationChecking
import caps.*
import scala.reflect.ClassTag
class Arr[T: ClassTag](n: Int) extends Mutable:
private val elems: Array[T]^ = new Array[T](n)
def apply(i: Int): T = elems(i)
update def update(i: Int, x: T): Unit = elems(i) = x
Arrays
클래스 scala.Array는 문자 그대로 Mutable을 확장하지는 않아요. 하지만 분리 검사가 켜져 있으면 캡처 검사는 그것이 확장한 것처럼 취급해요. 대략 이렇게 생각할 수 있어요.
class Array[T] extends Mutable:
def length: Int
def apply(i: Int): T
update def update(i: Int, x: T): Unit
이것은 내장된 처리예요. 실제 Array 클래스는 JVM이 지원하는 것 너머로 Mutable이나 다른 새 트레이트를 확장할 수 없어요.
반대로 Scala 표준 라이브러리의 변경 가능한 컬렉션 중 어느 것도 현재 Stateful이나 Mutable을 확장하지 않아요. 그래서 변경 가능한 컬렉션을 실험하려면 대안 클래스 라이브러리를 써야 해요.
읽기 전용 역량 (Read-only Capabilities)
x가 상태 유지 값에 전체 접근을 준다면, x.rd가 그에 대응하는 읽기 전용 역량이에요.
암시적으로 추가되는 캡처 집합
Mutable처럼 ExclusiveCapability와 Stateful 두 트레이트를 모두 확장하는 타입에 대한 참조는, 명시적 캡처 집합이 주어지지 않으면 암시적 캡처 집합 {any.rd}를 얻어요. 이것은 암시적으로 {any}를 더하는 다른 역량 트레이트들과는 달라요.
예를 들어 다음을 생각해볼게요.
import language.experimental.captureChecking
import caps.*
class Ref[T](init: T) extends Mutable:
private var current = init
def get: T = current
update def set(x: T): Unit = current = x
def addContents(from: Ref[Int], to: Ref[Int]^): Unit =
to.set(to.get + from.get)
여기서 from은 암시적으로 읽기 전용이고, to의 타입은 캡처 집합 any를 가져요. 즉 명시적 캡처 집합으로 쓰면 이렇게 읽혀요.
def addContents(from: Ref[Int]^{any.rd}, to: Ref[Int]^{any}): Unit
다시 말해 명시적인 ^는 상태 변경이 일어날 수 있는 곳을 나타내요.
읽기 전용 접근 (Read-Only Accesses)
update 메서드나 클래스 m에 대한 접근 p.m은 접두사 p가 상태 유지 값에 전체 접근을 줄 때만 허용돼요. 접두사가 순수하거나 읽기 전용이면 접근이 허용되지 않아요.
읽기 전용 접근은 다음 조건 중 하나가 성립하는 상태 유지 값에 대한 접근이에요.
- 참조가
this이고 접근이this의 클래스의update메서드에서 온 것이 아니다. 예를 들어:import language.experimental.captureChecking import caps.* class Ref[T](init: T) extends Mutable: var current: T = init def get: T = this.current // read-only access to `this` - 참조가 경로(path)인데, 그 경로 자체 또는 그 경로의 접두사가 읽기 전용 캡처 집합을 가진다. 예를 들어:
또 다른 예시:import language.experimental.captureChecking import caps.* class Ref[T](init: T) extends Mutable: private var current = init def get: T = current update def set(x: T): Unit = current = x val r: Ref[Int]^{any.rd} = Ref(22) def get: Int = r.get // read-only access to `r`
마지막 예시에서import language.experimental.captureChecking import caps.* class Ref[T](init: T) extends Mutable: private var current = init def get: T = current update def set(x: T): Unit = current = x class RefContainer extends Mutable: val r: Ref[Int]^ = Ref(22) val c: RefContainer = new RefContainer() def get: Int = c.r.get // read-only access to `c.r`c.r은 접두사c가 읽기 전용 참조이므로 읽기 전용 접근이에요.RefContainer가Mutable을 확장하므로(따라서Stateful과ExclusiveCapability둘 다)^{any.rd}가 암시적으로c: RefContainer에 추가됐다는 점을 주목하세요. - 참조의 기대 타입이 상태 유지 타입이 아닌 값 타입이다. 예를 들어:
val r: Ref[Int]^ = Ref(22) val x: Object = r // read-only access to `r` - 참조 뒤에 보통 메서드나 클래스(
update메서드나 클래스가 아닌)인 멤버를 가진 선택이 즉시 따른다. 예를 들어:import language.experimental.captureChecking import caps.* class Ref[T](init: T) extends Mutable: private var current = init def get: T = current update def set(x: T): Unit = current = x val r: Ref[Int]^ = Ref(22) r.get // read-only access to `r`
처음 두 조건은 안전 조건을 나타내요: 접근의 문맥이 갱신을 허용하지 않으므로 접근을 읽기 전용으로 선언해야 해요. 마지막 두 조건은 기회주의적이에요: 접근의 문맥이 전체 역량을 요구하지 않으므로 접근을 읽기 전용으로 선언하는 것이 허용돼요.
읽기 전용 접근은 읽기 전용 역량 x.rd를 그 환경에 부과해요. 다른 접근은 전체 역량 x를 부과해요.
예시:
참조 x와 두 클로저 f, g를 생각해볼게요.
import language.experimental.captureChecking
import caps.*
class Ref[T](init: T) extends Mutable:
private var current = init
def get: T = current
update def set(x: T): Unit = current = x
def classifyClosures(): Unit =
val x = Ref(1)
val f = () => x.get // f: () ->{x.rd} Unit
val g = () => x.set(1) // g: () ->{x} Unit
f는 보통 메서드에 접근하므로 그 환경에 x.rd만 부과하고, 그것이 캡처 집합에 나타나요. 반면 g는 x의 update 메서드에 접근하므로 그 캡처 집합은 {x}예요.
배타 캡처 집합을 가진 상태 유지 타입에 대한 참조는 읽기 전용 집합을 가진 참조로 넓힐 수 있어요. 예를 들어 다음은 OK예요.
import language.experimental.captureChecking
import caps.*
class Ref[T](init: T) extends Mutable:
private var current = init
def get: T = current
update def set(x: T): Unit = current = x
val a: Ref[Int]^ = Ref(1)
val b1: Ref[Int]^{a.rd} = a
val b2: Ref[Int]^{any.rd} = a
지연 val과 읽기 전용 제한
Stateful 클래스의 지연 val 초기화자는 보통 메서드와 유사한 읽기 전용 제한을 받아요. 구체적으로 Stateful 클래스의 지연 val 초기화자는 update 메서드를 호출하거나 비-지역 배타 역량, 즉 지연 val의 스코프 밖에서 정의된 역량을 참조할 수 없어요.
예를 들어 지연 val이 지역 메서드의 스코프에 선언되면, 그 초기화자는 감싸는 환경의 역량을 자유롭게 사용할 수 있어요.
import language.experimental.captureChecking
import caps.*
class Ref[T](init: T) extends Mutable:
private var current = init
def get: T = current
update def set(x: T): Unit = current = x
def example(r: Ref[Int]^) =
lazy val goodInit: () ->{r.rd} Int =
val i = r.get // ok: read-only access
r.set(100 * i) // ok: can call update method
() => r.get + i
하지만 Stateful 클래스 안에서는 지연 val 선언이 비-지역 배타 역량에 대해 읽기 접근만 가져요.
import language.experimental.captureChecking
import caps.*
class Ref[T](init: T) extends Mutable:
private var current = init
def get: T = current
update def set(x: T): Unit = current = x
class Wrapper(val r: Ref[Int]^) extends Stateful:
lazy val badInit: () ->{r} Int =
r.set(100) // error: call to update method
() => { r.set(r.get * 2); r.get } // error: call to update method
lazy val goodInit: () ->{r.rd} Int =
val i = r.get // ok
() => r.get * i // ok
badInit의 초기화자는 비-지역 배타 역량 r에 대한 update 메서드인 r.set(100)을 호출하려 해요. 이것은 초기화자가 외부 상태에 변경을 수행해서는 안 되므로 거부돼요.
지역 역량 (Local Capabilities)
제한은 비-지역 역량에만 적용돼요. 지연 val은 초기화자 안에서 지역적으로 생성하는 역량에 대한 update 메서드를 자유롭게 호출할 수 있어요.
import language.experimental.captureChecking
import caps.*
class Ref[T](init: T) extends Mutable:
private var current = init
def get: T = current
update def set(x: T): Unit = current = x
class Example extends Stateful, ExclusiveCapability:
lazy val localMutation: () => Int =
val local: Ref[Int]^ = Ref(10) // created in initializer
local.set(100) // ok: local capability
() => local.get
여기서 local은 지연 val의 초기화자 안에서 생성되므로 지역 역량으로 간주돼요. 초기화자는 그것에 대한 update 메서드를 호출할 수 있어요.
이것은 지연 val이 Stateful 클래스의 보통 메서드처럼 동작하게 해요: 그들은 환경에서 읽을 수 있지만, 명시적으로 표시되지 않는 한 갱신할 수는 없어요. 메서드와 달리 Stateful 클래스의 지연 val에는 현재 update 수식자가 없으므로, 그 초기화는 비-지역 역량에 대해 항상 읽기 전용이에요. 설득력 있는 사용 사례와 충분한 커뮤니티 수요가 있다면, 미래 버전의 캡처 검사가 update lazy val을 지원할 수도 있어요.
Update 제한
역량 r이 읽기 전용 접근이라면, r을 사용해 r의 update 메서드를 호출하거나 r의 필드에 할당할 수 없어요. 예를 들어 r.set(22)와 r.current = 22는 둘 다 허용되지 않아요.
예시:
import language.experimental.captureChecking
import caps.*
class Ref[T](init: T) extends Mutable:
var current = init
update def set(x: T) =
current = x // ok, set is an update method
def badSet(x: T) =
current = x // error: `this` is read-only access
val r: Ref[Int]^ = Ref(0)
r.set(22) // ok, `r` is exclusive capability.
val ro: Ref[Int] = r
ro.set(22) // error: `ro` is read-only access
Untracked Vars
분리 검사 아래에서 변경 가능한 필드는 Stateful 클래스에만 선언될 수 있어요. 이 필드들에 대한 갱신은 그런 클래스의 update 메서드에서만 일어날 수 있어요.
하지만 때로는 보통 메서드에서 변경 가능한 필드에 대한 할당을 금지하는 것이 너무 제한적일 수 있어요. 예를 들어:
import language.experimental.captureChecking
import language.experimental.separationChecking
import caps.unsafe.untrackedCaptures
class Cache[T](eval: () -> T):
@untrackedCaptures private var x: T = compiletime.uninitialized
@untrackedCaptures private var known = false
def force: T =
if !known then
x = eval()
known = true
x
Cache가 변경 가능한 필드를 가짐에도 Stateful 클래스로 선언되지 않았다는 점을 주목하세요. 이 경우 변경 가능한 필드 x는 순수 함수 eval의 결과를 저장하는데 쓰이고, 필드 known은 eval이 호출됐는지 반영해요. 이것은 eval()을 직접 호출하는 것과 동등하지만, 캐시된 값이 기껏해야 한 번 평가되므로 더 효율적일 수 있어요. 따라서 의미론적 관점에서 force가 실제로 x에 할당하더라도, Cache를 force가 update 메서드인 Stateful 클래스로 만드는 것이 필요하지 않아야 해요.
변경 가능한 필드에 @untrackedCaptures를 어노테이션하면 상태 유지 클래스와 update 메서드의 필요를 피할 수 있어요. 그러면 untracked 변경 가능한 필드에 대한 할당은 읽기 전용 제한에 대해 검사되지 않아요. @untrackedCaptures 어노테이션은 scala.caps.unsafe 객체에서 import할 수 있어요. @untrackedCaptures가 변경 가능한 상태의 보이는 부작용을 숨기지 않도록 책임감 있게 사용하는 것은 개발자에게 달려 있어요.
어떤 클래스의 필드가 아닌 지역 변경 가능한 변수에 대한 할당에는 제한이 없다는 점을 주목하세요. 그래서 @untrackedCaptures는 그런 지역 변수에는 허용되지 않아요.
untrackedCaptures 어노테이션은 변경 가능한 변수와 무관한 다른 몇 가지 문맥에서도 사용될 수 있어요. 그것들은 그 doc comment에 설명돼 있어요.
Read-Only Capsets
하위 타입과 하위 캡처를 고려하면 모순처럼 보이는 것을 관찰해요: x.rd는 제한된 역량으로 보이므로 {x.rd}는 {x}를 하위 캡처해야 해요. 그런데 위 예시에서 때로는 반대 방향으로 간다는 것을 봤어요: a의 캡처 집합은 {a} 또는 {any}인데도, a를 사용해 각각 캡처 집합 {a.rd}와 {any.rd}를 가진 b1과 b2를 정의할 수 있어요.
이 모순은 캡처 집합을 두 가지 다른 역할로 사용한다는 점을 지적함으로써 설명할 수 있어요.
첫째, 그리고 항상 그래왔듯, 캡처 집합은 값이 사용하거나 사용하지 않을 수 있는 보존된 역량을 정의해요. 역량이 더 많을수록 더 큰 타입이 되고, 빈 캡처 집합이 그 순서에 따르면 가장 작은 집합이에요. 이것은 말이 돼요. map 같은 고차 함수가 임의의 효과를 가질 수 있는 함수 A => B를 받아들일 의향이 있다면, 순수 함수 타입 A -> B를 그것에 전달하는 것은 확실히 OK예요.
하지만 변경에 대해서는 캡처 집합을 두 번째 역할로 사용하는데, 그 역할에서는 접근 권한의 집합을 정의해요. Ref[T]^가 있으면 그 모든 메서드에 접근할 수 있지만, Ref[T]^{any.rd}가 있으면 보통 메서드에만 접근할 수 있고 update 메서드에는 접근할 수 없어요. 그 관점에서 배타 역량을 가진 상태 유지 타입은 읽기 전용 역량만 가진 상태 유지 타입보다 더 많은 것을 할 수 있게 해줘요. 따라서 Liskov 치환 원칙에 따라, 배타 역량을 가진 집합은 읽기 전용 역량만 가진 집합을 하위 캡처해요.
이 모순은 이 두 역할을 구분함으로써 풀 수 있어요. 접근 권한에 대해, 읽기 전용 집합을 추가 한정자 reader로 표현해요. 그 한정자는 형식 이론과 구현에서만 사용되고, 현재 소스에서는 표현할 수 없어요. 우리는 공유 역량이나 읽기 전용 역량만으로 이루어진 상태 유지 타입의 모든 캡처 집합에 암시적 읽기 전용 한정자 reader를 추가해요. 그래서 우리가
val b1: Ref[A]^{a.rd} = a
라고 쓸 때, 우리는 실제로
val b1: Ref[A]^{a.rd}.reader = a
를 의미해요.
현재 구현은 -Ycc-verbose 설정에서 암시적 reader 한정자를 보여줘요.
집합에 대한 하위 캡처 이론은 그 후 이전과 같되, 다음 추가 규칙이 있어요.
C <: C.readerC₁ <: C₂라면C₁.reader <: C₂.reader{x, ...}.reader = {x.rd, ...}.reader{x.rd, ...} <: {x, ...}