Dada에서 클래스 필드 변경, 기본 읽기 권한, `!`를 통한 가변 참조, `given`/`.give`로 표현하는 명시적 이동, 그리고 Rust와의 비교를 살펴본다.
URL: https://smallcultfollowing.com/babysteps/blog/2026/02/10/dada-moves-and-mutation/
Title: moves and mutation · baby steps
Dada를 계속 살펴보자. 이전 글에서는 약간의 문자열 조작을 소개했다. 이제 권한(permissions)에 대해 이야기해보자. 여기서부터 Dada가 Rust를 조금 더 닮기 시작한다.
Dada의 클래스(classes) 는 새로운 타입을 선언하는 기본적인 방법 중 하나다(열거형(enum)도 있는데, 그건 나중에 다룰 것이다).
클래스를 선언하는 가장 편리한 방법은 괄호 안에 필드를 적는 것이다. 그러면 동시에 생성자(constructor)도 암묵적으로 선언된다:
class Point(x: u32, y: u32) {}
이는 사실 Rust에 더 가까운 형태의 문법 설탕(sugar)이다:
class Point {
x: u32
y: u32
fn new() -> Point {
Point { x, y }
}
}
그리고 생성자를 호출해서 클래스 인스턴스를 만들 수 있다:
let p = Point(22, 44) // Point.new(22, 44)의 설탕 문법
예상하듯이 p의 필드를 변경(mutate)할 수 있다:
p.x += 1
p.x = p.y
Dada에서는 매개변수(parameter)를 선언할 때 기본값이 읽기 전용 접근(read-only access)이다:
fn print_point(p: Point) {
print("The point is {p.x}, {p.y}")
}
let p = Point(22, 44)
print_point(p)
매개변수의 필드를 변경하려고 하면 오류가 난다:
fn print_point(p: Point) {
p.x += 1 # <-- ERROR!
}
! 사용매개변수를 !로 선언하면, 호출자(caller)로부터 클래스 인스턴스에 대한 가변 참조(mutable reference)를 받게 된다:
fn translate_point(point!: Point, x: u32, y: u32) {
point.x += x
point.y += y
}
Rust로 치면 point: &mut Point 같은 것이다. translate_point를 호출할 때도 가변 참조를 _전달_한다는 뜻으로 !를 붙인다:
let p = Point(22, 44) # 점 생성
print_point(p) # 22, 44 출력
translate_point(p!, 2, 2) # 점 변경
print_point(p) # 24, 46 출력
보이는 것처럼 translate_point가 p.x를 수정하면, 그 변화는 p에 그 자리에서(in place) 반영된다.
Rust에 익숙하다면 방금 예제가 조금 놀라울 수 있다. Rust에서는 print_point(p) 같은 호출이 p를 이동(move) 시켜 소유권(ownership)을 넘겨버린다. 그 뒤에 다시 사용하려 하면 오류가 난다. Dada에서 기본값은 Rust의 &x처럼 읽기 전용 참조를 넘겨주는 것이기 때문이다(이것은 올바른 _직관_을 주지만, 동시에 오해의 소지도 있다. 이후 글에서 Dada의 _참조_가 Rust와 한 가지 매우 중요한 점에서 다르다는 것을 보게 될 것이다).
어떤 함수가 매개변수의 소유권이 필요하다면 given으로 선언한다:
fn take_point(p: given Point) {
// ...
}
그리고 호출자 쪽에서는 그런 함수를 .give로 호출한다:
let p = Point(22, 44)
take_point(p.give)
take_point(p.give) # <-- Error! 두 번 give 할 수 없다.
Rust 코드와 Dada 코드를 나란히 비교해보면 흥미롭다:
| Rust | Dada |
|---|---|
vec.len() | vec.len() |
map.get(&key) | map.get(key) |
vec.push(element) | vec!.push(element.give) |
vec.append(&mut other) | vec!.append(other!) |
message.send_to(&channel) | message.give.send_to(channel) |
가장 편리한 것들이 가장 짧고 가장 자주 쓰인다. 그래서 읽기(read)를 기본값으로 둔다.
Rust에서 . 연산자는 호출되는 메서드에 따라 아주 다양한 일을 할 수 있다. 변경(mutate)할 수도, 이동(move)할 수도, 임시값(temporary)을 만들 수도 등등. Dada에서는 이런 것들이 호출 지점(callsite)에서 모두 보이도록 만들었다—하지만 거슬리지 않게(unobtrusive).
이는 사실 Dada의 “점진적 프로그래밍(gradual programming)” 시절부터 내려온 것이다. 결국 메서드에 타입 주석(type annotation)이 없다면 foo.bar()가 foo를 공유(shared)로 빌리는지 가변(mutable)로 빌리는지 결정할 수 없다. 그래서 호출 지점에서 모든 것이 보이고 명시적인 표기법이 필요했다.
Dada는 &mut 같은 전위(prefix) 연산자를 가능한 한 피하려고 한다. 그런 것들은 . 표기법과 잘 조합되지 않기 때문이다.