4.2. 매개변수
이 절에서는 함수의 매개변수 형식과 호출 지점에서 인자를 매칭하는 규칙을 정의합니다.
param ::= self_param | regular_param
self_param ::= "ref"? "self" (":" type_expr)?
regular_param ::= "ref"? identifier (":" type_expr)? ("=" const_expr)?
4.2.1. 기본 형태
fn f(x: i32, y: i32) -> i32 { x + y }
각 매개변수는 이름: 타입 형태로 적으며, 콤마로 구분합니다. 매개변수 이름은 식별자 의 형식을 따르고, 타입은 타입 표현식 의 규칙을 따릅니다.
4.2.2. 타입 생략
타입을 적지 않으면 제네릭으로 처리됩니다. 자세한 동작은 제네릭 의 단형화 규칙을 참고합니다. 트레잇 impl 에서 타입이 없는 메서드 매개변수는 암묵 타입 매개변수가 아니라, 트레잇이 그 자리에 선언한 타입 (트레잇 매개변수와 연관 타입을 대입한 것) 을 가집니다.
fn id(x) { x } // x 는 제네릭 - 호출 시 단형화
fn pair(x, y) { ... } // 매개변수 둘 다 제네릭
fn mixed(a: i32, b) { ... } // a 는 i32, b 는 제네릭
위의 id 는 다음과 같이 명시적으로 제네릭 을 표기한 형태와 동등합니다.
fn id<T>(x: T) -> T { x }
본문에서 사용한 트레잇은 시그니처에 자동으로 where 절로 합성되지 않습니다. 호출 사이트마다 단형화하면서 본문의 트레잇 사용을 그 자리에서 검증합니다.
fn compute(x, y) { x + y } // 시그니처: <T, U>(x: T, y: U)
// where 절 자동 합성 안 함
compute(1i32, 2i32); // OK - i32 + i32 dispatch
compute(1.0f64, 2.0f64); // OK - 별개 단형화 인스턴스
compute(MyType, 1i32); // 에러: 호출 사이트에서
// "MyType 에 Add<i32> 없음"
// (시그니처 자리가 아닌
// 호출한 곳에서 명확 표시)
이렇게 설계한 이유는 두 가지입니다. 첫째, 모듈 경계를 보존하기 위해서입니다. 본문 한 줄을 바꿔서 트레잇 사용이 늘어나더라도 시그니처가 바뀌지 않으므로 외부 호출자가 깨지지 않습니다. 둘째, 시그니처가 비대해지는 것을 피하기 위해서입니다. 자동으로 합성한 where 절이 본문의 모든 트레잇을 누적하면 시그니처가 비대해지고, 그 위에서 추론된 타입이 도구 표면에 거대한 형태로 노출되기 때문입니다.
매개변수가 동적 슬롯이 되기를 원한다면 명시적으로 any 를 적습니다 (any 슬롯 참고).
fn handle(x: any) { ... } // x 는 any 슬롯
4.2.3. 기본값
매개변수에 기본값을 줄 수 있습니다.
fn retry(n: i32 = 3i32, delay: f64 = 0.1f64) { ... }
retry(); // n=3, delay=0.1
retry(5i32); // n=5, delay=0.1
retry(5i32, 1.0f64); // n=5, delay=1.0
기본값에는 다음과 같은 규칙이 적용됩니다. 우선 const 표현식만 허용되며, 리터럴, const 변수, const 함수 호출이 여기에 해당합니다. 이는 struct 의 필드 기본값과 동일한 제약입니다. 호출 지점에서 생략된 매개변수만 평가되며, 매 호출마다 새로 평가됩니다. 또한 결정론적이어서 같은 const 표현식은 항상 같은 값을 만들어 냅니다. 현재 시각이나 난수와 같은 런타임 호출은 사용할 수 없으며, 필요하다면 factory 함수 패턴으로 우회합니다. 기본값 안의 이름은 호출자가 아니라 함수를 선언한 모듈의 스코프에서 해소합니다.
4.2.4. named 인자 호출
호출 지점에서 매개변수 이름으로 인자를 명시할 수 있습니다.
fn make_user(name: str, age: i32 = 0i32, email: str = "") -> User { ... }
make_user(name: "Alice", age: 30i32);
make_user(name: "Bob", email: "[email protected]"); // age 는 기본값
make_user("Charlie", age: 25i32); // 첫 인자만 위치, 나머지는 named
named 인자 호출에는 다음 규칙이 적용됩니다. 위치 인자가 먼저 오고 named 인자가 뒤에 와야 합니다. 한 호출에서 같은 매개변수에 위치와 named 를 둘 다 주면 에러가 발생합니다. 모든 위치 인자가 매칭된 뒤에는 남은 매개변수를 named 또는 기본값으로 채워야 합니다.
4.2.5. variadic: ...T
가변 개수의 인자를 받는 매개변수입니다.
fn sum(xs: ...i32) -> i32 {
let total = 0i32;
for x in xs { total += x; }
total
}
sum(); // xs: 빈 슬라이스
sum(1i32); // xs = [1]
sum(1i32, 2i32, 3i32); // xs = [1, 2, 3]
let ys: List<i32> = [4, 5];
sum(..ys); // xs = [4, 5] - 리스트 spread
variadic 매개변수에는 다음 규칙이 적용됩니다.
- 매개변수 중 마지막 한 개만 variadic 으로 둘 수 있습니다.
- 본체에서
xs: ...T는 원소 타입T의 읽기 전용 슬라이스 뷰 (Slice<T>, stdlib 2.9.7) 입니다. 호출의 남은 인자는 하나로 묶여 넘어가며, 길이는 호출마다 다를 수 있습니다. 그래서 인자 개수마다 함수를 따로 단형화하지 않습니다. - 개별 인자, 튜플 spread, 리스트 spread (
..xs) 를 섞어 넘길 수 있습니다 (4.2.6). - 본체에서 읽기, 인덱싱, 반복이 가능합니다. 변경이 필요하다면 라이브러리 메서드로 컬렉션을 새로 만듭니다.
이종 variadic: ...any
fn log_all(xs: ...any) { ... }
log_all(1i32, "hi", 3.14f64); // 다양한 타입 가능
매개변수 타입을 any 로 두면 이종 인자를 받는 variadic 이 됩니다.
4.2.6. spread: ..args
호출 지점에서 기존 컬렉션을 펼쳐 인자로 넘기는 형태입니다.
let args = (1i32, 2i32, 3i32);
foo(..args); // foo(1, 2, 3)
let xs: List<i32> = [1i32, 2i32, 3i32];
sum(..xs); // sum(1, 2, 3) - variadic 받음
spread 에는 다음 규칙이 적용됩니다.
튜플 spread 는 컴파일 타임에 길이와 타입을 검증합니다.
고정 매개변수로의
List/Slicespread 는 다음 인자 앞까지의 자리를 채우며, 길이는 런타임에 검사합니다. 배열 spread 는 그 길이만큼 채웁니다.Dict spread 는 named 인자로 변환됩니다. 각 키는 달리 채워지지 않은 매개변수의 이름이어야 하며 (런타임에 검사), 값은 그 매개변수의 타입이어야 합니다.
variadic 자리에는 튜플,
List,Slice, 배열 spread 를 일반 인자와 섞어 넘길 수 있습니다 (f(a, ..xs, b)). variadic 매개변수를 그대로 다른 variadic 에 넘길 수도 있습니다 (fn f(xs: ...i32) { g(..xs) }).컬렉션이 아닌 값을 spread 하면 에러입니다 ("needs a tuple, a List, a Slice or an array").
원소 타입
T가 제네릭으로 열려 있는 variadic 을 인자 없이 부르면T를 정할 수 없으므로 타입 표기가 필요합니다.List리터럴 안의..x는 튜플, 배열,List,Slice를 펼칩니다 ([0, ..xs, 9]). 그 밖이면 범위입니다.
// fn train(lr: f64, momentum: f64, epochs: i32 = 10)
let opts = {"lr": 0.01, "momentum": 0.9}; // Dict<str, f64>
train(..opts); // train(lr: 0.01, momentum: 0.9)
또한 한 호출 안에서 spread 와 일반 인자를 자유롭게 혼합할 수 있습니다.
foo(0, ..middle, 10);
foo(..a, ..b, ..c);
4.2.7. 폐기: _
호출 결과를 명시적으로 폐기할 때 쓰는 표기입니다.
_ = compute_with_side_effect();
여기서 _ 는 식별자 의 단일 밑줄입니다. 좌변 자리에서 값을 받지만 어디에도 바인딩하지 않으며, 사용자가 의도적으로 결과를 무시한다는 신호로 쓰입니다. let _ = e; 도 같으며, 같은 스코프에서 몇 번이고 쓸 수 있습니다 (_ 는 바인딩이 아니므로 재선언이 아님).
4.2.8. 매개변수 평가 순서
호출 시 인자 표현식은 적힌 순서 (좌→우) 로 평가됩니다. 메서드 호출은 수신자를 먼저 평가합니다.
foo(a(), b(), c()); // a() 먼저, b() 다음, c() 마지막
named 인자도 적힌 순서대로 평가됩니다.
make_user(name: f(), age: g()); // f() 먼저, g() 다음
이 순서 덕분에 부작용이 있는 인자가 예측 가능한 순서로 실행되도록 보장됩니다.
4.2.9. 매개변수 가시 범위
매개변수는 함수 본체의 최상위 스코프에 도입되며, 본체 안에서는 일반 변수처럼 사용하면 됩니다.
기본값 표현식은 매개변수가 없는 환경에서 평가됩니다. 따라서 같은 함수의 다른 매개변수를 참조할 수 없습니다.
fn f(a: i32, b: i32 = a + 1) { ... } // ERROR - 기본값에서 a 참조 불가
이 제약은 struct 의 필드 기본값 제약과 동일한 동기를 갖습니다. 결정론과 예측 가능한 비용이 그 이유입니다.
4.2.10. 정적 vs 점진의 매개변수 결정
| 적힌 형태 | 의미 |
|---|---|
x: i32 | 정적. 호출 지점에서 i32 만 받음 |
x | 제네릭. 호출 지점 타입으로 단형화 |
x: any | 동적 슬롯. 어떤 타입이든 런타임 태그로 받음 |
이 세 가지 형태는 한 함수 안에서 자유롭게 혼합할 수 있습니다.