23 트레이트 — 여러 타입이 지키는 한 가지 약속
먼저 알아야 할 것
struct 와 fieldrequires <트레이트> t . 는 타입 조건이다돌아보기
22장의 max_of 는 requires ordered t . 를 믿고 본문에서 무엇을 불렀는가? 그리고 조건을 갖추지 못한 plain 을 주면 어떻게 되었는가?
답. method a less b 를 불렀다. plain 을 주면 E-BOUND-UNSAT 으로 거절되었다. 조건은 약속이고, 약속을 넘어 실물을 만들면 받는 쪽의 계약이 거짓이 되기 때문이다. 이 장은 그 약속 — 트레이트 — 를 선언하고 갖추는 법 전체를 다룬다.
이 장의 필요성과 맥락
이 장이 끝나면
fn rect.area) method 로 부르는 법을 익힌다. 트레이트를 선언하고 satisfies 로 갖추며, op 을 여럿 가진 트레이트를 쓰는 법을 알게 된다. 트레이트 서명에 fn·proc 을 적지 않고 effects 줄이 갖추는 쪽을 정한다는 규칙, 갖추지 못했을 때의 진단들, via self 와 트레이트가 상속이 아니라는 점도 보게 된다.이 장에서 답할 질문
- 트레이트의 op 에 기본 구현을 줄 수 있는가?
23.1 무엇에 쓰나#
examples/ch23/why.low
module why .
rem run: demo
trait shape do
area input s self . output u64 .
end
struct rect do
satisfies shape .
w u64 .
h u64 .
end
struct square do
satisfies shape .
side u64 .
end
fn rect.area input s rect . output u64 .
do
return mul (field s w) (field s h) .
end
fn square.area input s square . output u64 .
do
return mul (field s side) (field s side) .
end
fn double_area input comptime t type . input s t . output u64 .
requires shape t .
do
return mul 2 (method s area) .
end
fn demo output u64 .
do
let r rect be make rect do w 2 . h 3 . end .
let q square be make square do side 4 . end .
return add (double_area rect r) (double_area square q) .
end
실행 결과
$ lowentc --run demo why.low
demo() = 44
trait shape do … end는 “area를 갖춘다” 는 약속이다. 서명의self는 이 약속을 갖출 타입 자신이다.rect와square는 몸 안에satisfies shape .를 적어 약속을 갖추겠다고 선언한다.fn rect.area와fn square.area가 실제로 갖춘다. 이름의rect.는 그 op 이rect에 붙었다는 뜻이다.double_area는 어떤 모양이든 받는다.requires shape t .가 “넓이를 알려 주는 타입만” 이라고 못 박는다.method s area는s의 타입에 붙은area를 부른다.rect면rect.area,square면square.area다.
트레이트는 자격 요건에 빗대면 쉽다. «넓이를 알려 줄 수 있음» 이라는 요건이 있고, 요건을 갖춘 타입만 그 요건을 요구하는 op 에 들어올 수 있다.
trait shape ── 요건: area input s self . output u64 .
▲ ▲
satisfies │ │ satisfies
┌─────────────┴─┐ ┌─┴─────────────┐
│ rect │ │ square │
│ rect.area │ │ square.area │ ← 요건을 실제로 갖춘 op
└───────────────┘ └───────────────┘
double_area 는 requires shape t . ── shape 를 갖춘 타입만 받는다
double_area rect r → method s area = rect.area
double_area square q → method s area = square.areadouble_area rect r 과 double_area square q 는 각각 전용 실물로 단형화되므로(22장), method 는 실행 중에 찾는 것이 아니라 번역할 때 정해진다. 가상 함수 표가 없다.
23.2 타입에 붙은 op 과 method#
트레이트와 상관없이도 op 을 타입에 붙일 수 있다. 이름 앞에 타입 이름과 점을 붙이면 되고, 첫 입력은 그 타입의 값이다. method <값> <이름> <인자…> 가 그 op 을 부른다. 수신자가 다른 폼의 결과여도 된다 — method (method r grow 1) area. 붙은 op 이 없으면 거절된다.
examples/ch23/method_undef.low
module method_undef .
rem expect: E-METHOD-UNDEF
struct point do
x u8 .
end
fn f input p point . output u8 .
do
return method p nosuch .
end
실행 결과
$ lowentc --check method_undef.low
method_undef.low:10:0 E-METHOD-UNDEF: no op of that name is associated with the receiver's type — declare it as `fn <type>.<name> input <recv> <type> . …` (RFC-0062)
없는 이름을 부르면 위로 찾아 올라가는 일이 없다. 붙은 op 은 이름 칸을 나누는 장치이지 상속이 아니다.
23.3 op 을 여럿 가진 트레이트#
examples/ch23/many.low
module many .
rem run: demo 3 4
rem trap: demo_empty
trait shape do
area input s self . output u64 .
perimeter input s self . output u64 .
grow input s self . input k u64 . output self .
checked_area input s self . output u64 . effects panic .
end
struct rect do
satisfies shape .
w u64 .
h u64 .
end
fn rect.area input s rect . output u64 .
do
return mul (field s w) (field s h) .
end
fn rect.perimeter input s rect . output u64 .
do
return mul 2 (add (field s w) (field s h)) .
end
fn rect.grow input s rect . input k u64 . output rect .
do
return make rect do w (add (field s w) k) . h (add (field s h) k) . end .
end
proc rect.checked_area input s rect . output u64 . effects panic .
do
if eq (field s w) 0 . do
panic "empty rect" .
end
return mul (field s w) (field s h) .
end
proc demo input w u64 . input h u64 . output u64 . effects panic .
requires le w 1000 .
requires le h 1000 .
do
let r rect be make rect do w w . h h . end .
let big rect be method r grow 1 .
return add (method big perimeter) (method big checked_area) .
end
proc demo_empty output u64 . effects panic .
do
let r rect be make rect do w 0 . h 5 . end .
return method r checked_area .
end
실행 결과
$ lowentc --run demo many.low 3 4
demo(3, 4) = 38
$ lowentc --run demo_empty many.low
== ir diagnostics (1) ==
0:0 E-VM-PANIC: the program called `panic` — this is an unrecoverable stop, and it is NOT a contract violation (the code chose to stop, it did not break a promise)
적는 법은 셋이다.
- 서명마다 한 줄이다. 줄은 op 의 이름으로 시작하고, op 머리와 같은 차례로 절을 적는다 — 입력, 출력, 효과. 다음 이름이 나오면 다음 서명이다. 몇 개든 적는다.
grow처럼 출력이self면 갖추는 쪽은 자기 타입(rect)을 돌려준다.- 갖추는 op 의 이름은
<타입>.<이름>이다.
demo_empty 는 폭이 0 인 사각형으로 checked_area 를 불러 멈춘다. 서명이 effects panic 이라고 약속했으므로 부르는 쪽은 멈출 수 있다는 것을 안다.
실제 사례. method 로 부른 효과가 번지지 않는 결함
--check 하면 demo 에 W-EFFECT-OVER(panic 을 선언했지만 하지 않는다)라는 경고가 붙는다. 틀린 경고다. demo_empty 는 실제로 멈춘다. 이 책을 쓰며 확인한 바로, 이 판의 처리기는 method 로 부른 op 의 효과를 부르는 쪽에 번지게 하지 않는다. 그래서 순수한 fn 이 method 로 panic 하는 op 을 불러도 번역이 통과한다. 같은 op 을 rect.checked_area r 로 직접 부르면 올바르게 거절된다. 명세(15장)가 요구하는 동작은 직접 부름 쪽이고, method 쪽은 결함이다. 고쳐지기 전까지는 효과가 있는 붙은 op 을 순수한 op 안에서 method 로 부르지 않는다.23.4 서명에는 fn·proc 을 적지 않는다#
트레이트 서명에 fn 이나 proc 을 적으면 거절된다.
examples/ch23/sig_kind.low
module sig_kind .
rem expect: E-TRAIT-SIG
trait shape do
fn area input s self . output u64 .
end
실행 결과
$ lowentc --check sig_kind.low
5:3 E-TRAIT-SIG: a trait signature does not say `fn` or `proc` — write `area input s self . output u64 .`. Its `effects` line says what the op may do (none = pure), and the implementation chooses `fn` or `proc` (§6.11.2)
그 op 이 무엇을 할 수 있는지는 서명의 effects 줄이 정한다.
서명의 effects | 갖추는 쪽 |
|---|---|
| 없음 | fn, 또는 effects 를 적은 proc |
effects panic 처럼 있음 | 그 효과나 그보다 적은 효과를 적은 proc. 효과를 안 쓰면 fn 도 된다 |
표 23.1 — 서명의 effects 와 갖추는 쪽
effects 줄이 없는 proc 은 좁히지 않은 것이라 무엇이든 할 수 있다고 읽힌다(5장). 그래서 효과 없는 서명을 그런 proc 으로 갖추면 거절된다.
examples/ch23/proc_noeff.low
module proc_noeff .
rem expect: E-TRAIT-EFFECT
trait shape do
area input s self . output u64 .
end
struct rect do
satisfies shape .
w u64 .
h u64 .
end
proc rect.area input s rect . output u64 .
do
return mul (field s w) (field s h) .
end
실행 결과
$ lowentc --check proc_noeff.low
9:0 E-TRAIT-EFFECT: the implementation is a `proc` with NO `effects` clause, so it may do anything — more than the trait declares. Write the same `effects` line the trait has (or fewer), or make it a `fn` if it is pure
서명보다 많은 효과를 적어도 거절된다.
examples/ch23/more_effect.low
module more_effect .
rem expect: E-TRAIT-EFFECT
trait shape do
checked_area input s self . output u64 . effects panic .
end
struct rect do
satisfies shape .
w u64 .
h u64 .
end
proc rect.checked_area input s rect . output u64 . effects panic wait .
do
if eq (field s w) 0 . do
panic "empty rect" .
end
return mul (field s w) (field s h) .
end
실행 결과
$ lowentc --check more_effect.low
9:0 E-TRAIT-EFFECT: the implementation has an EFFECT the trait does not declare. A caller reasons against the TRAIT's contract — if the implementation does more, that reasoning is a lie (this is exactly what makes dynamic dispatch safe or unsafe)
more_effect.low:15:1 W-EFFECT-OVER: this op DECLARES `wait` but never PERFORMS it. A declared effect is a cost the caller must budget for (a pure caller cannot call an `io` op). Drop it, or — if a future version will perform it — say so (RFC-0007)
부르는 쪽은 트레이트의 계약을 보고 판단한다. 갖춘 쪽이 계약에 없는 일을 하면 그 판단이 틀린다. 이름만 맞고 효과가 다르면 트레이트는 계약을 실어 나르지 못한다.
23.5 갖추지 못했을 때#
| 진단 | 뜻 |
|---|---|
E-TRAIT-UNDEF | satisfies 가 선언되지 않은 트레이트를 부른다 |
E-TRAIT-MISSING | 목록의 op 하나가 없다 |
E-TRAIT-SIG | 서명에 fn·proc 을 적었거나, op 은 있는데 매개변수 수가 다르다 |
E-TRAIT-EFFECT | 갖춘 op 이 서명보다 많은 효과를 가진다 |
E-TRAIT-RECV | 타입이 아니라 op 에 satisfies 를 적었다 |
표 23.2 — 트레이트를 갖추지 못한 자리
examples/ch23/missing.low
module missing .
rem expect: E-TRAIT-MISSING
trait shape do
area input s self . output u64 .
perimeter input s self . output u64 .
end
struct rect do
satisfies shape .
w u64 .
h u64 .
end
fn rect.area input s rect . output u64 .
do
return mul (field s w) (field s h) .
end
실행 결과
$ lowentc --check missing.low
10:0 E-TRAIT-MISSING: this type claims to satisfy a trait, but the trait REQUIRES an op that the type does not have. Declare it as `fn <Type>.<name> …` (RFC-0062). A claim that is not checked is the defect this language exists to remove
satisfies 는 주석이 아니다. 갖추겠다고 적은 순간부터 처리기가 목록 전체를 검사한다.
문. 트레이트의 op 에 기본 구현을 줄 수 있는가?
답. 줄 수 없다. 트레이트는 목록일 뿐 코드를 담지 않는다. 여러 타입이 같은 구현을 나누고 싶다면, 그 구현을 제네릭 op (double_area 처럼)으로 쓰고 타입들은 그 op 이 필요로 하는 최소한만 갖춘다. 기본 구현이 있으면 어떤 타입의 area 가 어디서 왔는지를 찾아 올라가야 하고, 그것이 상속이 만드는 엔트로피다.
23.6 via self — 할당 효과만 더 적을 수 있다#
할당기 트레이트처럼 구현마다 할당 효과가 다른 약속이 있다. 서명의 효과 줄에 via self 를 적으면, 갖추는 쪽은 할당 계열 효과(alloc·heap·lock·atomic)를 서명보다 더 적을 수 있다. 그 밖의 효과를 더 적는 것은 여전히 E-TRAIT-EFFECT 다.
export trait byte_allocator do
reserve input s self . input n u64 . output option mut slice u8 . effects state via self .
grow input s self . input old mut slice u8 . input newn u64 . output option mut slice u8 . effects state via self .
used input s self . output u64 . effects state .
end범프의 reserve 는 state 뿐이고, 힙에서 깎는 heap_bytes 의 reserve 는 heap state 다. 그 차이가 제네릭 op 의 via a 를 따라 부르는 쪽까지 올라간다(20장).
할당기가 아닌 약속에도 같은 장치를 쓴다. 번호표를 내주는 트레이트를 두 액터가 갖추는데, 하나는 수만 세고 하나는 번호표마다 고정 창에서 바이트를 깎는다.
examples/ch23/viaself.low
module viaself .
rem run: main
rem 번호표를 내주는 약속. 구현이 할당 계열 효과를 더 적어도 된다
trait ticketer do
issue input s self . output u64 . effects state via self .
end
rem 수를 세기만 한다 — 효과는 state 뿐
actor counter do
satisfies ticketer .
state do
next u64 .
end
proc issue output u64 . effects state . do
set next (add next 1) .
return next .
end
end
rem 번호표마다 고정 창에서 8 바이트를 깎는다 — alloc 을 더 적는다
actor carver do
satisfies ticketer .
state do
root cap allocator .
count u64 .
end
proc issue output u64 . effects alloc state . do
let g option mut slice u8 . . be alloc_bytes root capacity 8 .
guard is_some g . else return 0 .
set count (add count 1) .
return mul count 10 .
end
end
rem 어느 구현이든 받는다 — via t 가 그 구현의 효과를 여기로 나른다
proc issue_two
input comptime t type .
input who t .
output u64 .
effects state via t .
requires ticketer t .
do
let a u64 be send who issue .
let b u64 be send who issue .
return add a b .
end
proc main input al cap allocator . output u8 . effects alloc state .
do
var c counter be spawn actor counter .
var k carver be spawn actor carver .
let x u64 be issue_two counter c .
let y u64 be issue_two carver k .
return narrow u8 (add x y) .
end
실행 결과
$ lowentc --run main viaself.low
main() = 33
issue input s self . output u64 . effects state via self .이 서명이다.counter는effects state .로,carver는effects alloc state .로 갖춘다.alloc은 할당 계열이라via self가 허락한다.issue_two는effects state via t .라고 적었다.counter로 단형화한 인스턴스의 효과는state이고,carver로 단형화한 인스턴스의 효과는alloc state다. 효과가 인스턴스마다 정확하다.- 그래서
main은cap allocator를 받고alloc을 적어야 한다.carver로 부르는 줄이 없다면main에alloc이 필요 없다. - 답 33 은
counter의 1 + 2 와carver의 10 + 20 이다.
carver 의 상태에는 root cap allocator . 라는 권한 칸이 있다. 그 칸의 규칙은 25장에서 다룬다.
흔한 오해. 트레이트를 갖추면 그 트레이트의 무언가를 물려받는다
rect 와 square 는 shape 를 갖춘 뒤에도 서로 아무 관계가 없다. 이 언어에는 상속이 없다.23.7 흔한 실수#
반례. method 뒤에 이름을 먼저 적는다
examples/ch23/mistake_methodorder.low
module mistake_methodorder .
rem expect: E-METHOD-UNDEF
struct rect do
w u64 .
h u64 .
end
fn rect.area input s rect . output u64 .
do
return mul (field s w) (field s h) .
end
fn f output u64 .
do
let r rect be make rect do w 2 . h 3 . end .
rem ✘ `method` 뒤에는 값이 먼저, 이름이 다음이다
return method area r .
end
실행 결과
$ lowentc --check mistake_methodorder.low
mistake_methodorder.low:16:0 E-METHOD-UNDEF: no op of that name is associated with the receiver's type — declare it as `fn <type>.<name> input <recv> <type> . …` (RFC-0062)
객체 지향 언어의 r.area() 를 거꾸로 옮기면 method area r 이 되기 쉽다. method 는 값을 먼저 받는다 — 어느 타입에 붙은 op 을 찾을지 값의 타입이 정하기 때문이다. 이름을 먼저 적으면 도구는 area 를 값으로 읽고 그 타입에 붙은 r 을 찾다가 E-METHOD-UNDEF 를 낸다. method r area 로 적는다.
반례. 갖추는 op 의 이름에 타입 접두사를 빠뜨린다
examples/ch23/mistake_noprefix.low
module mistake_noprefix .
rem expect: E-TRAIT-MISSING
trait shape do
area input s self . output u64 .
end
struct rect do
satisfies shape .
w u64 .
h u64 .
end
rem ✘ 이름에 `rect.` 이 없다 --- 타입에 붙지 않은 보통 op 이다
fn area input s rect . output u64 .
do
return mul (field s w) (field s h) .
end
실행 결과
$ lowentc --check mistake_noprefix.low
8:0 E-TRAIT-MISSING: this type claims to satisfy a trait, but the trait REQUIRES an op that the type does not have. Declare it as `fn <Type>.<name> …` (RFC-0062). A claim that is not checked is the defect this language exists to remove
fn area input s rect . 는 rect 를 받는 보통 op 이지 rect 에 붙은 op 이 아니다. 트레이트는 rect.area 를 찾으므로 E-TRAIT-MISSING 이다. 받는 타입만 보고 붙여 주지 않는 까닭은, 같은 타입을 받는 보통 op 이 여럿일 때 어느 것이 약속을 갖춘 것인지 이름만 보고 알게 하려는 것이다. 이 진단에는 파일 이름이 빠져 있다. 줄 번호는 satisfies 를 적은 타입 선언을 가리킨다.
반례. op 만 있으면 트레이트를 갖춘 것으로 여긴다
examples/ch23/mistake_nosatisfies.low
module mistake_nosatisfies .
rem expect: E-BOUND-UNSAT
trait shape do
area input s self . output u64 .
end
rem ✘ `rect.area` 는 있지만 `satisfies shape .` 를 적지 않았다
struct rect do
w u64 .
h u64 .
end
fn rect.area input s rect . output u64 .
do
return mul (field s w) (field s h) .
end
fn double_area input comptime t type . input s t . output u64 .
requires shape t .
do
return mul 2 (method s area) .
end
fn f output u64 .
do
let r rect be make rect do w 2 . h 3 . end .
return double_area rect r .
end
실행 결과
$ lowentc --check mistake_nosatisfies.low
17:0 E-BOUND-UNSAT: this generic was instantiated with a type that does NOT satisfy the required trait. A bound is a PROMISE the callee relies on — instantiating past it would make the callee's contract a lie (RFC-0021 §6.3). Add `satisfies <trait> .` to the struct, and the required ops as `fn <Type>.<name>`
rect.area 가 있으니 “넓이를 알려 주는 타입” 인 것은 맞다. 그래도 satisfies shape . 를 적지 않으면 double_area rect r 은 E-BOUND-UNSAT 이다. 모양만 맞으면 통과시키는 방식(덕 타이핑)에서는 우연히 이름이 같은 op 이 약속을 갖춘 것으로 잘못 읽힌다. satisfies 는 “나는 이 계약을 지키겠다” 는 선언이고, 그 선언이 있어야 처리기가 목록 전체를 검사한다.
반례. 수신자를 첫 입력이 아닌 자리에 둔다
examples/ch23/mistake_recvlast.low
module mistake_recvlast .
rem expect: E-METHOD-RECV
struct rect do
w u64 .
h u64 .
end
rem ✘ 수신자 `s` 가 첫 입력이 아니다
fn rect.scaled input k u64 . input s rect . output u64 .
requires le k 100 .
do
return mul k (field s w) .
end
fn f output u64 .
do
let r rect be make rect do w 2 . h 3 . end .
return method r scaled 5 .
end
실행 결과
$ lowentc --check mistake_recvlast.low
mistake_recvlast.low:9:0 E-METHOD-RECV: an op attached to a type takes that type as its FIRST input — that is what `method <value> <name> …` passes. Here the receiver is not first, so a `method` call hands the value to the wrong parameter: it used to pass `--check` and stop at run time with `E-VM-TYPE`. Move the receiver to the first `input` clause
붙은 op 의 첫 입력은 수신자다. method r scaled 5 는 r 을 첫 자리에, 5 를 둘째 자리에 넣는다. scaled 는 첫 자리를 k, 둘째를 s 로 받았으므로 구조체에 곱셈을 하려다 멈춘다. 그런 머리는 E-METHOD-RECV 로 번역 때 거절한다. 붙은 op 은 input s rect . 를 언제나 맨 앞에 둔다.
흔한 오해. 타입에 붙은 op 은 method 로만 부를 수 있다
examples/ch23/direct_call.low
module direct_call .
rem run: f
struct rect do
w u64 .
h u64 .
end
fn rect.area input s rect . output u64 .
do
return mul (field s w) (field s h) .
end
fn f output u64 .
do
let r rect be make rect do w 2 . h 3 . end .
rem 같은 op 을 두 길로 부른다 --- 이름으로 곧장, 그리고 `method` 로
return add (rect.area r) (method r area) .
end
실행 결과
$ lowentc --run f direct_call.low
f() = 12
rect.area r 은 이름으로 곧장 부르고, method r area 는 값의 타입을 보고 같은 op 을 찾아 부른다. 둘은 같은 op 이고 결과도 6 과 6 이다. method 가 쓸모 있는 곳은 타입이 제네릭 매개변수여서 이름을 적을 수 없는 자리(double_area 의 method s area)다. 타입을 아는 자리에서는 직접 부르는 편이 효과도 올바르게 번진다(위의 결함 사례).
23.8 이 장의 문법 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
fn rect.area input s rect . … | 타입에 op 을 붙인다 — 첫 입력이 수신자 | 이름 칸을 나누는 장치이지 상속이 아니다 |
method r area · rect.area r | 값의 타입으로 찾아 부른다 · 이름으로 곧장 부른다 | 번역 때 정해진다 — 가상 함수 표가 없다 |
trait shape do area input s self . output u64 . end | 타입이 갖출 op 의 목록 | self 는 갖출 타입 자신 |
| 서명 줄: 이름 · 입력 · 출력 · 효과 | op 머리와 같은 차례, fn·proc 은 적지 않는다 | 효과 줄이 갖추는 쪽의 상한 |
struct rect do satisfies shape . … end | 이 타입이 약속을 갖춘다고 선언 | 선언이 있어야 목록 전체를 검사한다 |
requires shape t . | 제네릭 op 의 타입 조건 | 갖추지 못한 타입은 E-BOUND-UNSAT |
effects state via self .(서명) | 할당 계열 효과만 더 적어도 된다 | 할당기마다 효과가 다르다 |
E-TRAIT-MISSING · -SIG · -EFFECT · -RECV · -UNDEF | 갖추지 못한 자리마다의 진단 | satisfies 는 주석이 아니다 |
effects state via t .(제네릭 op) | 단형화한 타입이 via self 로 더 적은 효과를 물려받는다 | counter 로는 state, carver 로는 alloc state |
표 23.3 — 트레이트의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
fn <타입>.<이름> 은 타입에 op 을 붙이고 method 가 부른다. 트레이트는 타입이 갖출 op 의 목록이고, 타입은 몸 안의 satisfies 로 갖추겠다고 선언한다. 서명은 이름으로 시작해 입력·출력·효과 차례로 적고, fn·proc 은 적지 않는다. 서명의 effects 가 갖추는 쪽의 효과 상한이다. 어긋나면 E-TRAIT-* 로 거절되고, via self 는 할당 계열 효과만 더 적게 허락한다. 트레이트는 상속이 아니다.