충돌 없이 대여와 참조 카운팅을 결합하려는 Ante의 접근법을 살펴봅니다.
대여와 참조 카운팅을 충돌 없이 섞기!
2026년 6월 28일—
Ante는 우리 모두가 불가능하다고 생각했던 것, 즉 런타임 충돌 없이 참조 카운팅과 대여 검사를 섞는 방향으로 첫걸음을 내디뎠습니다. 0
이건 매우 유망합니다. 언젠가 각 접근법이 적절한 곳에서 더 쉽게 각각을 사용할 수 있게 되면서도 충돌 위험은 피할 수 있다는 뜻이기 때문입니다. 그런 언어가 있다면, 저는 참조 카운팅으로 유연하게 게임을 프로토타이핑하고, 점차 더 빠른 대여-검사 코드로 옮겨갈 수 있을 것입니다. 1
많은 언어가 시도했지만, 주류 언어 중 어느 것도 이 둘을 매끄럽게 결합하는 방법을 알아내지 못했습니다.
Rust도 시도했지만, Rust의 참조 카운팅 Rc 타입을 쓰려 하면 종종 RefCell이 필요해집니다(예: Rc<RefCell<Spaceship>>). 그런데 이건 잘못 들고 있으면 런타임에 충돌할 수 있습니다. 23 Rc의 올바른 사용을 rustc가 컴파일 타임에 검사해 주면 좋겠다고 늘 생각합니다! 45
Swift도 새로운 대여 시스템으로 이를 시도했지만, 잘못 들고 있으면 런타임에 충돌하는 비싼런타임 검사가 있습니다.
알고 보니 참조 카운팅과 대여 검사를 섞는 일은 어렵습니다. 아래에서 그 이유를 보게 될 것입니다.
그럼 Ante에 대해 이야기해 봅시다!
Ante는 아직 진행 중인 작업입니다! 여기의 일부는 구현되어 있고, 일부는 아직 이론 단계이며, 설계도 계속 변하고 있습니다. 지금 이 순간에도 활발히 개발 중이니, Ante의 사이트와 discord에서 진행 상황을 따라가 보세요!
* [Ante](https://verdagon.dev/blog/ante-blending-borrowing-rc#ante)
* [형태 안정성](https://verdagon.dev/blog/ante-blending-borrowing-rc#shape-stability)
* [참조 카운팅](https://verdagon.dev/blog/ante-blending-borrowing-rc#reference-counting)
* [더 명시적인 문법](https://verdagon.dev/blog/ante-blending-borrowing-rc#more-explicit-syntax)
* [유니언](https://verdagon.dev/blog/ante-blending-borrowing-rc#unions)
* [references](https://verdagon.dev/blog/ante-blending-borrowing-rc#references)
* [유니언에 어떻게 도움이 되는가](https://verdagon.dev/blog/ante-blending-borrowing-rc#how--helps-with-unions)
* [uniq 변환](https://verdagon.dev/blog/ante-blending-borrowing-rc#uniq-conversion)
* [함수 호출을 가로질러](https://verdagon.dev/blog/ante-blending-borrowing-rc#across-function-calls)
* [값 반환하기](https://verdagon.dev/blog/ante-blending-borrowing-rc#returning-values)
* [저는 Ante가 여기서 뭔가를 해내고 있다고 생각합니다](https://verdagon.dev/blog/ante-blending-borrowing-rc#i-think-ante-is-onto-something-here)
* [더 큰 그림](https://verdagon.dev/blog/ante-blending-borrowing-rc#the-broader-picture)
* [결론](https://verdagon.dev/blog/ante-blending-borrowing-rc#conclusion)
* [부록: Rust의 Cell과의 비교](https://verdagon.dev/blog/ante-blending-borrowing-rc#appendix-comparison-to-rusts-cell)
0
더 정확히 말하면, Ante는 Rust의 RefCell이나 Swift의 배타성 검사에서 오는 런타임 패닉이나 오버헤드 없이, 가변 객체에 대해 참조 카운팅과 대여 검사를 섞는 방법을 찾아냈습니다.
1
이건 제가 Vale를 작업하는 이유와 거의 같습니다! 하지만 Vale는 세대 참조를 사용하고, 이것은 종종 바로 이런 종류의 프로그램 중단 위험을 가집니다.
2
예를 들어, 두 사람이 동시에 그것에 대한 유일한(읽기-쓰기) 참조를 얻으면 충돌할 수 있습니다.
3
panic을 일으키지 않는 try_borrow()와 try_borrow_mut() 메서드도 있지만, 그건 문제를 다른 곳으로 옮길 뿐입니다.
4
아쉽게도 GhostCell / QCell은 여기에 해당하지 않습니다. 다른 제약을 가져오고 RefCell의 드롭인 대체재도 아닙니다.
5
Cell도 있지만, 내부에 대한 참조를 얻을 수는 없습니다. 예를 들어 &Cell<(T, U)>에서 &Cell<U>로 갈 수 없습니다. Rust는 일반적으로 내부 가변성을 사용할 때 마찰이 많은 편이라, 많은 사용자가 이를 피하게 되기도 합니다.
Ante는 더 단순한 Rust를 목표로 합니다. 메모리 안전성과 스레드 안전성을 갖춘 시스템 프로그래밍 언어입니다. 단일 소유권과 대여 검사를 가지므로, 값은 인라인으로 존재합니다(스택 위이거나, 포함하는 struct/array 안).
그리고 사용자가 단순성을 우선하고 싶을 때는, 타입에 shared 키워드를 써서 참조 카운팅을 선택할 수 있습니다.
예를 들어, 이 코드는 red-black tree의 균형을 맞춥니다:
// Color can be either an R or a B
shared type Color = | R | B
// RbTree can either be an Empty or a Tree
shared type RbTree t =
| Empty
| Tree Color (RbTree t) t (RbTree t)
// A balance function, for RbTree of any element type t
balance (tree: RbTree t) {Copy t}: RbTree t =
match tree
| Tree B (Tree R (Tree R a x b) y c) z d
| Tree B (Tree R a x (Tree R b y c)) z d
| Tree B a x (Tree R (Tree R b y c) z d)
| Tree B a x (Tree R b y (Tree R c z d)) -> Tree R (Tree B a x b) y (Tree B c z d)
| other -> other
저는 보통 C 스타일 문법을 선호하지만, 이것만큼은 인정해야겠습니다. 정말 아름답습니다. 게다가 간결하기도 합니다. Python 동등 코드만큼 작고, C++ 동등 코드와 Rust 동등 코드보다 더 작습니다.
하지만 제게 가장 흥미로운 부분은 Ante가 메모리 안전성에서 하는 일입니다. Ante는 공유 가변성 초능력을 가지고 있습니다. 참조 카운팅된 데이터를 가변 대여하고 싶을 때, 런타임 오류 위험을 감수할 필요가 없습니다. 주류 언어 중에는 Rust도 Swift도 이것을 못 합니다.
이 공유 가변성 초능력에 대해 이야기하기 전에, 먼저 기초부터 시작해 봅시다. Ante가 어떻게 대여 검사를 하는지 보고, 그 다음 참조 카운팅을 섞어 보겠습니다.
Ante에는 형태 안정성이라는 개념이 있습니다. 이는 “형태가 안정적인 어떤 것에 대한 참조는, 다른 곳에서 어떤 변경이 일어나더라도 항상 유효하다6”는 뜻입니다.
이 때문에 Ante 코드는 동시에 같은 struct에 대한 여러 개의 가변 대여 참조를 안전하게 가질 수 있습니다.
상황을 설정하기 위해, 두 개의 Entity에 대한 가변 참조를 받는 heal 함수를 보겠습니다:
type Entity =
energy: I32
health: I32
heal (healer: mut Entity) (target: mut Entity) =
healer.energy -= 10
target.health += 10
Ante에서는 두 매개변수 모두에 같은 Entity를 넣어 heal을 호출할 수 있습니다. 예를 들어, 엔티티가 자기 자신을 치료하는 경우입니다:
self_heal (entity: mut Entity) =
heal entity entity
healer를 변경해도 Entity에 대한 공유 참조가 어떤 식으로든 무효화되지 않으므로, 7 컴파일러는 이 코드를 유효하다고 받아들입니다.
다시 말해, healer와 target이 같은 Entity를 가리킬 수도 있지만, 이건 메모리 안전합니다. 여기에는 Entity를 파괴할 수 있는 것이 없기 때문에, 두 참조 모두 유효하게 유지됩니다.
이제 조금 더 복잡하게 가 봅시다.
Ante 코드는 실제로 같은 struct에 대한 여러 개의 가변 대여 참조를 동시에 가질 수 있을 뿐 아니라, 그 struct의 어떤 필드나, 그 필드의 필드에 대해서도 동시에 여러 개의 가변 대여 참조를 가질 수 있습니다.
예를 들어, 여기서는 하나의 가변 대여 참조가 ship을 가리키고, 또 다른 가변 대여 참조가 동시에 ship의 engine을 가리킵니다.
type Engine =
fuel: I32
type Spaceship =
engine: Engine
name: String
refuel (ship: mut Spaceship) =
engine_alias: mut Engine = ship.engine
// Can still use original `ship`
ship.engine.fuel := 200
engine_alias.fuel := 100
Ante는 이게 완전히 메모리 안전하다는 것을 압니다. 이 함수가 실행되는 동안 아무도 ship을 파괴할 수 없고, 따라서 그 engine이나 fuel도 파괴할 수 없기 때문입니다.
즉, 이런 참조들은 형태 안정적인 데이터에 대한 참조입니다.
Rust와 Swift에 익숙한 분들은 이것에 놀랄 것입니다:
이제 여기에 참조 카운팅을 섞어 봅시다!
6
여기서 “유효하다”는 역참조 가능하다는 뜻입니다. 즉, 메모리 비안전성이나 정의되지 않은 동작 위험 없이 그 참조를 역참조할 수 있다는 의미입니다.
7
이는 Rust에서 &mut Spaceship으로 그 참조가 가리키는 Spaceship을 파괴할 수 없는 것과 비슷합니다. 심지어 Rust에서도 같은 Spaceship 지역 변수를 가리키는 여러 &mut Spaceship 참조가 있는 것은 완전히 안전할 것입니다.
8
Cell을 사용하면 때때로 비슷하게 접근할 수는 있지만, Ante가 왜 Rust의 Cell보다 더 나아가는지는 부록을 보세요.
위 능력은 아래에서 보겠지만, 참조 카운팅과 완벽하게 들어맞습니다.
Ante에서 타입 정의 앞에 shared를 붙이면, 그 타입은 자동으로 참조 카운팅됩니다. 9
그리고 shared mut 타입이 있으면, set_fuel이 Spaceship의 engine: Engine 필드를 다루는 것처럼, 락 없이 필드를 변경할 수 있습니다:
type Engine =
fuel: I32
shared mut type Spaceship =
engine: Engine
name: String
launch (var ship: Spaceship) =
set_fuel (mut ship.engine)
set_fuel (engine: mut Engine) =
engine.fuel := 100
이 코드 조각은 아래에서 더 설명하겠지만, 지금은 이렇게 생각하면 됩니다. 이것은 Rust 동등 코드와 Swift 동등 코드와 비슷하지만, Rust의 .borrow_mut()나 Swift의 자동 런타임 배타성 검사처럼 충돌 위험이 없습니다.
launch가 mut Engine 대여 참조를 만들 수 있는 이유는, launch가 포함하는 Spaceship을 살아 있게 유지하고 있으므로 engine도 살아 있을 것임을 알기 때문입니다.
좀 더 일반적으로 말하면: 공유된 shared mut 타입의 필드에 대해서는 언제나 mut 대여 참조를 만들 수 있습니다. 비록 그 타입이 공유되고 있더라도 말이죠. 10
9
적어도 현재는 그렇습니다. 결국에는 코드가 아니라 애플리케이션이 자신들의 메모리 관리 방식을 설정할 수 있게 될 것입니다. 예를 들어 RC, GC, 또는 사용자 정의 메커니즘을 통해서요.
10
다만, 나중에 보겠지만 그 필드 안에 있는 것들에 대해서는 언제나 mut 대여 참조를 취할 수 있는 것은 아닙니다. 그 부분에는 다른 메커니즘이 있습니다.
이제부터는 shared mut type이라는 문법 설탕 대신, 더 명시적인 Rc Spaceship 문법을 사용하겠습니다(왜인지는 나중에 알게 될 것입니다).
더 명시적인 Rc 문법으로 쓰면, 위 코드는 이렇게 됩니다: 11
type Engine =
fuel: I32
type Spaceship =
engine: Engine
name: String
launch (var ship: Rc Spaceship) =
set_fuel (mut ship.engine)
set_fuel (engine: mut Engine) =
engine.fuel := 100
차이점:
11
우리는 Rc의 내용물 필드에 접근할 수 있는 Ante 문법을 가정하고 있습니다. 그렇지 않다면 이것은 set_fuel (ship.as_mut ()).engine 같은 형태일 수도 있습니다.
유니언은 속도 면에서 정말 뛰어납니다.12 우리는 유니언을 좋아합니다.
안타깝게도 유니언은 악명 높게 unsafe하고, 메모리 안전 언어가 이를 잘 지원하기는 어렵습니다.
왜 그런지 보려면, 아래 예제를 보세요. 여기서 Engine은 유니언이고, 우리는 그것으로 unsafe한 장난을 치려고 하고 있습니다.
type Engine =
| StringTheoryEngine (str: String)
| ImpulseEngine (fuel: I32)
type Spaceship =
engine: Engine
name: String
launch (var ship: Rc Spaceship) (var other_ship: Rc Spaceship) =
match uniq ship.engine
| StringTheoryEngine str ->
other_ship.engine := ImpulseEngine 0x42
str.[0] := 'z'
| ImpulseEngine fuel ->
()
이 프로그램은 ship과 other_ship에 같은 Spaceship을 넘기면 실제로 segfault가 날 수 있습니다!
이 때문에 Ante는 위 코드를 컴파일하지 않도록 거부해야 합니다. Ante에는 이런 규칙이 있습니다:
유니언에 대한 mut 대여 참조가 있다면, 그 variant 중 하나에 대한 mut 대여 참조는 만들 수 없습니다.
이건 앞서 본 struct 규칙과는 반대입니다:
struct에 대한 mut 대여 참조가 있다면, 그 필드 중 하나에 대한 mut 대여 참조는 만들 수 있습니다.
이상적인 세상이라면, Ante는 위 프로그램을 다음과 같은 오류로 거부할 것입니다:
match uniq ship.engine
| StringTheoryEngine str ->
// error: Mutating `other_ship.engine` may cause `ship.engine` to be dropped while still in use
// note: `other_ship.engine` is an `Engine` which may alias with `ship.engine`
other_ship.engine := ImpulseEngine 0x42
str.[0] := 'z'
| ImpulseEngine fuel ->
()
하지만 Ante는 그걸 어떻게 알 수 있을까요? 그게 정말 가능할까요?
가능합니다! Ante가 이런 오류를 낼 수 있는 이유는 ship.engine이 임시 uniq 변환을 거쳤기 때문입니다.
그럼 그게 무슨 뜻이고, uniq는 또 무엇일까요?
12
유니언이 빠른 이유는 내용물을 인라인으로 보관하기 때문이고, 그 결과 캐시 미스(또는 “pointer chasing”)가 줄어듭니다. 예를 들어 C에서 이런 유니언이 있다면:
union Engine {
StringTheoryEngine ste;
ImpuseEngine ie;
};
그리고 그것이 Spaceship 안에 있다면:
struct Spaceship {
Engine engine;
};
StringTheoryEngine과 ImpulseEngine은 Spaceship 메모리 내부에 존재합니다.
Java 같은 다른 언어는 이렇게 하지 않습니다. 대신 Engine을 인터페이스로 만들고, Spaceship은 Engine 포인터를 담게 합니다.
포인터는 느립니다. 유니언은 빠릅니다. Ante는 여기서 C와 비슷합니다.
uniq는 “배타적인 가변 참조”를 뜻합니다.
어떤 변수가 uniq Spaceship을 담고 있다면, 그것은 그 Spaceship에 대한 유일하게 사용 가능한 참조입니다. 13
set_fuel (engine: uniq Engine) (other: mut Engine) =
// In here, we know nothing else points to `engine`
이것 자체로는 일반적으로 그리 유용하지 않지만, 유니언의 내용물 안쪽을 가리킬 수 있게 해 줍니다.
13
Ante의 uniq Spaceship은 Rust의 &mut Spaceship과 비슷합니다.
같은 데이터에 대한 다른 alias가 있을 수 있기 때문에, 유니언 필드의 임의 변경을 허용하는 것은 안전하지 않습니다.
예를 들어, 한 사용자가 Maybe String에서 String을 들고 있는 동안 다른 사용자가 그것을 None으로 바꿀 수 있습니다.
혹은 위 예제에서 본 것처럼, 누군가 String의 문자를 참조하는 동안 다른 누군가가 그 String의 컨테이너를 파괴할 수도 있습니다.
언어들은 일반적으로 이 문제를, String이 계속 살아 있도록 참조 카운팅을 하거나(Swift), 공유 가변성 자체를 금지하는 방식으로(Rust) 메웁니다.
하지만 우리는 위의 unsafe한 프로그램은 막고 싶으면서도, 안전하게 유니언 내용물 안쪽에 가변 참조를 가지는 프로그램은 허용하고 싶습니다:
type Engine =
| StringTheoryEngine (str: String)
| ImpulseEngine (fuel: I32)
type Spaceship =
engine: Engine
name: String
launch (var ship: Rc Spaceship) =
match uniq ship.engine // match statements require uniq references
| StringTheoryEngine str -> // Get uniq reference to StringTheoryEngine
str.[0] := 'z'
| ImpulseEngine fuel ->
()
예를 들어 Swift와 Rust의 동등한 프로그램들은, 안타깝게도 추가 런타임 오버헤드/검사를 더해서 이 안전한 프로그램을 허용합니다.
다행히 Ante는 추가 런타임 오버헤드/검사가 필요 없습니다. 이 함수 안에 있는 동안 아무도 ship.engine을 바꾸지 않는다는 것을 이해하기 때문입니다.
Ante가 그걸 아는 이유는 임시 uniq 변환 메커니즘 덕분입니다.
Ante의 통찰은, 어떤 것에 대해 임시로 uniq 참조를 얻을 수 있다는 것입니다. 단, 그 스코프 안에서는 그것을 참조할 수 있는 다른 어떤 것도 접근하지 않아야 합니다.
예를 들어, 여기 uniq 변환이 있는 프로그램이 있습니다. 주석은 그 스코프가 어디서 시작되고 끝나는지를 보여 줍니다:
match uniq ?
type Engine =
| StringTheoryEngine (str: String)
| ImpulseEngine (fuel: I32)
type Spaceship =
engine: Engine
name: String
launch (var ship: Rc Spaceship) =
// Start scope where nobody can access any other var that might contain a Spaceship
match uniq ship.engine
| StringTheoryEngine str ->
str.[0] := 'z'
| ImpulseEngine fuel ->
()
// End scope where nobody can access any other var that might contain a Spaceship
이 uniq 참조는 // Start scope와 // End scope 주석 사이의 스코프에 존재합니다(더 정확히는 ship의 선언과 마지막 사용 사이의 모든 부분입니다).
uniq 참조가 존재하는 동안, Ante는 우리에게 Spaceship을 간접적으로 담고 있을지도 모르는 다른 기존 변수를 사용하는 것을 막습니다.
Rust는 “다른 참조가 어딘가에 존재할지도 모른다”는 이유로 uniq가 존재하지 못하게 하는 반면, Ante는 이 참조들을 이 스코프 안에서 사용하지 않기만 하면 uniq를 만들 수 있다고 말합니다.
여기에는 중요한 뉘앙스가 있습니다. uniq Spaceship은 Spaceship에 대한 유일한 참조가 아니라, 유일한 사용 가능한 참조일 뿐입니다. 이는 어떤 객체에 대한 포인터가 여러 개 있을 수 있지만, 주어진 스코프 안에서는 restrict 포인터만 사용할 수 있다는 점에서 C와 비슷합니다. Ante는 이 뉘앙스를 활용합니다.
하지만 몇 가지 제약이 있습니다.
그 스코프 안에서 다른 지역 변수(예: 또 다른 Rc Spaceship)를 사용하고, 그것이 간접적으로 Spaceship을 참조할 가능성이 있다면 컴파일 오류가 납니다:
type Engine =
| StringTheoryEngine (str: String)
| ImpulseEngine (fuel: I32)
type Spaceship =
engine: Engine
name: String
launch (var ship: Rc Spaceship) (var other_ship: Rc Spaceship) =
// Start scope where nobody can access any other var that might contain a Spaceship
match uniq ship.engine
| StringTheoryEngine str ->
// error: Mutating `other_ship.engine` may cause `ship.engine` to be dropped while still in use
// note: `other_ship.engine` is an `Engine` which may alias with `ship.engine`
other_ship.engine := ImpulseEngine 0x42
str.[0] := 'z'
| ImpulseEngine fuel ->
()
// End scope where nobody can access any other var that might contain a Spaceship
이건 ship이 일시적으로 uniq Spaceship으로 변환되어 있는 동안 other_ship에 접근하고 있기 때문에 오류입니다.
Spaceship을 간접적으로라도 담고 있는 어떤 것도 사용할 수 없으므로, 14 다음도 거부됩니다:
type Engine =
| StringTheoryEngine (str: String)
| ImpulseEngine (fuel: I32)
type Spaceship =
engine: Engine
name: String
type HasAShip =
ship: Rc Spaceship
launch (var ship: Rc Spaceship) (var other: HasAShip) =
// Start scope where nobody can access any other var that might contain a Spaceship
match uniq ship.engine
| StringTheoryEngine str ->
// error: Mutating `other.ship.engine` may cause `ship.engine` to be dropped while still in use
// note: `other.ship.engine` is an `Engine` which may alias with `ship.engine`
other.ship.engine := ImpulseEngine 0x42
str.[0] := 'z'
| ImpulseEngine fuel ->
()
// End scope where nobody can access any other var that might contain a Spaceship
반대로, 다른 것들은 사용할 수 있습니다. 예를 들어 정수 new_fuel은 괜찮습니다:
type Engine =
| StringTheoryEngine (str: String)
| ImpulseEngine (fuel: I32)
type Spaceship =
engine: Engine
name: String
launch (var ship: Rc Spaceship) (new_fuel: I32) =
// Start scope where nobody can access any other var that might contain a Spaceship
match uniq ship.engine
| StringTheoryEngine str ->
str.[0] := 'z'
| ImpulseEngine fuel ->
fuel := new_fuel
// End scope where nobody can access any other var that might contain a Spaceship
new_fuel은 그저 I32일 뿐이라 Spaceship에 대한 참조를 담을 수 없기 때문에 사용할 수 있습니다. 그냥 정수입니다.
14
또한 Spaceship이 follow_ship: Rc Spaceship 같은 필드를 가지고 있다면, 그 경우도 거부됩니다. 그러면 그 uniq Spaceship에 그 필드를 통해서도 도달할 수 있기 때문입니다. 따라서 일반적으로 재귀 타입에는 mut->uniq 변환을 할 수 없습니다.
다음은 함수 호출의 일부로 mut->uniq 변환을 하는 예제입니다.
type Resonator =
resonance: I32
type Engine =
| ImpulseEngine (fuel: I32)
| WarpEngine (resonators: Vec Resonator)
type Spaceship =
engine: Engine
name: String
foo (var ship: Rc Spaceship) (new_res: Resonator) =
// Start scope where nobody can access any other var that might contain a Spaceship
maybe_use_resonator ship new_res // converts to `uniq` here
// End scope where nobody can access any other var that might contain a Spaceship
maybe_use_resonator (u_ship: uniq Spaceship) (new_res: Resonator) =
match u_ship.engine
| WarpEngine resonators ->
resonators.push new_res
| ImpulseEngine fuel ->
()
기억하세요. 규칙은 어떤 것에 대해 임시로 uniq 참조를 얻을 수 있다는 것입니다. 단, 그 스코프 안에서 그것을 참조할 수 있는 다른 어떤 것도 접근하지 않아야 합니다.
주석에서 볼 수 있듯, 컴파일러는 사실 maybe_use_resonator 호출 지점만 검사하면 됩니다. 인자들 중 어느 것도 Spaceship에 대한 참조를 담고 있지 않은지만 보면 됩니다. 그리고 다른 유일한 인자인 Resonator는 그렇지 않으니 괜찮습니다!
mut에서 uniq로의 변환에서 가장 큰 제약은, 변환된 uniq 참조를 함수에서 반환할 수 없다는 점입니다: 15
get_converted (foo: mut Foo): uniq Foo =
// Attempt to convert mut -> uniq
foo // error: `local uniq` refs cannot be returned as `uniq`
다행히도, 반환되는 참조가 단지 지역적으로만 유일하다고 명시하면 원하는 것을 얻을 수 있습니다:
get_converted (foo: mut Foo): local uniq Foo =
// Convert mut -> local uniq
foo
사실 내부적으로는 언제나 이런 일이 일어나고 있습니다. mut 참조에서 uniq 참조로 변환할 때마다, 실제로 얻는 것은 local uniq뿐입니다. 대부분의 경우 이것을 일반 uniq 참조처럼 쓸 수 있지만, 값을 반환할 때는 이를 명시적으로 적어야 합니다.
15
이것이 허용된다면, 반환된 uniq 참조는 변환된 참조와 alias일 수 있는 어떤 변수도 스코프 안에서 사용할 수 없도록 보장하는 컴파일러 검사 없이 사용될 수 있기 때문입니다.
Ante는 런타임 오류 없이 RC 참조(Rc Spaceship)를 유일한 대여 참조(uniq Spaceship)로 임시 전환할 수 있습니다. 이것은 엄청나게 큰 진전입니다. 16
물론 단점도 있습니다. Ante의 창시자인 Jake가 제게 강조했듯이, 이것은 실제로 어느 정도의 타입 분석을 요구합니다. “Engine에서 Spaceship에 도달할 수 있는가?”라는 질문에 답하려면, 컴파일러가 Engine과 그 안에 담긴 모든 것을 재귀적으로 살펴보아 아무것도 Spaceship을 담고 있지 않음을 확인해야 합니다. 이것은 꽤 취약할 수 있습니다. struct에 필드를 추가하는 것이 호환성을 깨는 API 변경이 될 수도 있습니다.
그래서 Jake는 이 보장을 유지하는 더 나은 방법을 찾고 있으며, 몇 가지 유망한 선택지가 있습니다:
* 거기서 더 나아가, 공유 타입을 변경할 때 Mutates ‘a 같은 effect를 추가하면 타입 분석 자체를 완전히 제거할 수 있습니다. 그러면 컴파일러는 각 Mutates ‘a effect가 같은 변수에서 나온 것인지만 보장하면 됩니다.
제 생각에는 다른 선택지도 있을 수 있습니다:
이런 가능성들이 있더라도, 어려운 부분은 이것을 인체공학적으로 만드는 일일 것입니다. Ante에서 Jake의 목표는 사용 가능하고, 읽기 쉽고, 단순한 무언가를 만드는 것입니다. 최대한의 유연성을 제공하면서 그 목표를 달성하는 일은 언제나 까다롭습니다.
16
이건 어떤 의미에서는 _컴파일 타임 RefCell_이고, C++ 사용자들이 아주 오래전부터 내면화해 온 것입니다. Jake는 이것을 메모리 안전성 접근법 안에 집어넣는 방법을 찾아냈습니다.
17
저와 몇몇 다른 사람들이 최근 이 방향을 탐구하고 있습니다.
자세히 들여다보면, 메모리 안전성 설계의 세계에서 매우 흥미로운 일이 벌어지고 있다는 것을 알 수 있습니다.
우리는 예전에 “공유 가변 대여(shared mutable borrowing)”가 불가능하다고 생각했습니다. 즉, 다른 이들이 자신들의 참조를 통해 그것을 변경할 수 있더라도, 우리는 어떤 것에 대한 대여 참조를 가질 수 없다고 여겼습니다. 사실 Rust는 거의 그 믿음 위에 세워졌다고 해도 됩니다.
하지만 이제는 예외가 아주 많이 쌓이기 시작하고 있습니다:
이건 단순한 요령 모음이 아닙니다. 우주가 우리에게 아주 강하게, 통합 원리가 바로 우리 눈앞에 있다고 말해 주는 것입니다.
그 통합 원리가 무엇인지는 지금은 신비롭게도 모호하게 남겨 두겠습니다. 제가 무슨 말을 하는지 안다면 댓글로 알려 주세요!
잠깐 선적인 관점으로 가 봅시다. 우리는 물과 같습니다. 산과 골짜기로 가득한 복잡한 지형 속에 있죠. 저항이 가장 적은 길을 따라 골짜기로 흘러드는 것은 쉽고, 그곳에서는 멀리 볼 수 없습니다. 산 정상까지 올라 더 멀리 보는 일은 어렵습니다.
그래서 바로 이런 글들이 존재합니다! 새로운 기법 하나하나는 우리가 산을 오르고 더 멀리 보도록 도와주는 또 하나의 도구입니다. 우리가 해야 할 일은 계속 도구를 더하는 것뿐이고, 그러면 우리는 필연적으로 메모리 안전성의 다음 큰 진보를 찾아내게 될 것입니다.
이런 글을 좋아하신다면 계속 지켜봐 주시고 RSS 피드, r/vale, twitter, bluesky를 구독해 주세요. Ante의 개발을 따라가려면 웹사이트와 discord를 확인해 보세요.
건배!
눈썰미 있는 Rust 사용자는 “이게 struct 안에 Cell을 넣는 것과 어떻게 다른가요?”라고 물을 것입니다.
질문을 설명하기 위해, 아래에 Ante 프로그램과 그 아래에 Cells를 사용한 동등한 Rust 프로그램을 보겠습니다:
type Spaceship =
fuel: I32
status: String
// Example: changes "Enterprise" to "Enterprise (refueling)"
add_to_name (var ship: Rc Spaceship) =
status_ref: mut String = (ship.as_mut ()).status // Get reference to the status String itself
status_ref += " (refueling)"
그리고 Rust 버전입니다:
struct Spaceship {
fuel: Cell<i32>,
status: Cell<String>,
}
// Example: changes "Enterprise" to "Enterprise (refueling)"
fn add_to_name(ship: Rc<Spaceship>) {
let status_ref = &ship.status; // Can't get the &String, but can get &Cell<String>
let mut status = status_ref.replace(String::new()); // Temporarily put something in its place
status += " (refueling)";
status_ref.replace(status); // Swap the modified name back into place
}
여기서 Rust에는 한계가 있습니다. Rc<Spaceship>이 주어졌을 때, 덧붙일 수 있는 &mut String을 얻을 방법이 없습니다. 그래서 Rust는 대신 기본값을 집어넣고, 나중에 다시 되돌려 놓는 것을 기억해야 합니다.
여기에는 몇 가지 단점이 있습니다:
마지막 위험은 익숙하게 들릴 것입니다. 이 Cell은 사실상 RefCell과 비슷합니다!
Ante는 이것을 다르게 접근합니다. status 문자열에 대한 참조를 임시로 얻을 수 있게 하고, 그동안 아무도 그것에 접근할 수 없도록 컴파일러가 강제합니다.