(Includes Pre-RFC) Named and Optional Arguments are Awesome
이름 있는 인자와 선택적 인자는 훌륭합니다 — Rust용 pre-RFC 제안
Rust에 named argument와 optional argument를 추가하자는 pre-RFC 성격의 글입니다. Dart, C#, TypeScript의 문법과 Rust의 구조체·builder 패턴을 비교하고, 가독성·보일러플레이트·API 확장성 문제를 해결할 설계 후보와 반론을 함께 정리합니다.
- 주제
AI 요약
작성자는 자신이 불편하게 느끼는 Rust의 부분으로 named argument와 optional argument의 부재를 꼽습니다. 비교 대상으로 Rust, C#, TypeScript, Dart, Python을 제시하고, 먼저 다른 언어가 이름 있는 인자와 기본 인자를 어떻게 구현했는지 살펴본 뒤 Rust의 현재 우회 방식과 개선안을 검토합니다. 글의 후반부는 완성된 RFC라기보다 새로운 pre-RFC 초안에 해당합니다.
■ 다른 언어의 인자 문법
Dart는 GUI 구성 요소처럼 선택 가능한 설정이 많은 API를 염두에 두고 설계된 언어로 소개됩니다. 중괄호 안에 named parameter를 선언하고 기본값을 붙일 수 있습니다. 예를 들어 Text 함수에서 fontSize의 기본값을 16으로 지정한 뒤 호출부에서 fontSize: 24처럼 덮어쓸 수 있습니다. named parameter를 null로 기본 설정하거나 required로 지정하는 방식도 지원합니다. 대괄호를 사용하면 optional positional argument가 됩니다. say 함수는 세 번째 인자인 device를 생략하면 carrier pigeon을 사용하고, 호출부에서 smoke signal을 넘기면 그 값을 사용합니다.
C#에서는 모든 매개변수를 이름으로 지정할 수 있습니다. ExampleMethod(bar: ..., foo: ...)처럼 선언 순서와 다른 순서로 호출할 수 있고, 필수 매개변수 뒤에 기본값을 가진 매개변수를 배치하면 호출부에서 생략할 수 있습니다. 작성자는 C#의 방식이 단순하다는 점을 긍정적으로 평가합니다.
TypeScript에는 optional positional parameter가 있지만, named argument를 위한 전용 문법은 부족하다고 설명합니다. 그래서 객체 타입과 destructuring을 조합합니다. 이 방식은 타입 선언부와 함수 매개변수에서 필드 이름을 두 번 적어야 하고, 호출부에도 객체 중괄호가 추가됩니다. React 컴포넌트가 객체 하나를 인자로 받는 함수로 작성되는 경우가 많아 이 패턴이 반복된다는 지적도 나옵니다.
■ Rust에서 생기는 문제
Rust에서는 보통 함수 전용 구조체를 만들고 Default trait를 구현한 뒤, 구조체 초기화 마지막에 ..Default::default()를 붙이는 방식으로 이름 있는 인자와 비슷한 효과를 냅니다. 하지만 타입의 기본값과 실제 API의 기본값이 다르면 기본값을 채우는 별도 함수를 작성해야 합니다. 일부 필드만 선택적으로 만들기도 어렵고, 함수 하나만을 위해 이름 있는 구조체와 구현을 추가해야 하므로 TypeScript의 객체·destructuring 방식보다 장황하다고 평가합니다.
표준 라이브러리의 HashMap 생성자가 대표 사례로 제시됩니다. capacity, hasher, allocator라는 세 가지 선택 축을 조합하려면 new, with_capacity, new_in, with_capacity_in, with_hasher, with_capacity_and_hasher, with_hasher_in, with_capacity_and_hasher_in처럼 여덟 개의 함수를 둬야 합니다. Rust에는 함수 오버로딩이 없기 때문에 조합마다 다른 이름이 필요하고, 개발자와 코드 리뷰어가 인자 순서를 기억하거나 문서를 찾아야 합니다.
이름이 없는 인자는 같은 타입을 받는 값이 섞일 때 실수를 부릅니다. print(text, bold, italics, underline) 같은 함수에서 두 번째와 세 번째 bool의 순서를 잘못 기억하면 굵게 표시하려던 설정이 기울임으로 바뀔 수 있습니다. hard_link도 두 인자가 모두 경로 타입을 받으므로 호출부만 보고 어느 경로가 원본인지 알기 어렵습니다. 글에서는 hard_link(original: ..., link: ...)처럼 이름을 표시하면 의도가 드러난다고 설명합니다. 인자가 14개로 늘어나고 여러 값에 합리적인 기본값이 있다면 호출부에 모든 값을 적게 하는 방식도 읽기 어렵다고 지적합니다.
■ 기존 제안에 대한 검토
Nightly Rust에 이미 있는 default field values는 구조체의 Default 구현을 간단하게 만드는 기능입니다. Pet 구조체에서 age의 기본값을 42로 지정하고 name은 타입의 기본값을 사용하도록 만들 수 있습니다. 작성자는 구조체에는 잘 맞는 기능이라고 보지만, 함수 호출부에 이름 있는 인자와 선택적 인자를 직접 제공하지는 않는다고 설명합니다.
Structural records는 익명 구조체나 이름 있는 필드를 가진 tuple처럼 생각할 수 있는 제안입니다. do_stuff_with({ red: 255, green: 127, blue: 63 }) 같은 호출을 만들 수 있지만, 함수 선언부와 호출부에 필드 이름을 반복해야 합니다. 필드 하나만 가진 { x }가 블록 표현식인지 structural record인지 구분하기 어려워 추가 쉼표가 필요하고, 필드 이름마다 달라지는 형태에 일반적인 trait 구현을 제공하기도 어렵습니다. Python dictionary나 Dart map으로 오해할 가능성도 언급합니다. Rust language team이 비슷한 기능을 유용하다고 보면서도 구현 난도와 비용 때문에 추진하지 않았다는 설명도 포함됩니다.
C# 문법을 그대로 가져오는 방식은 이름 있는 인자를 원하는 순서로 작성하고, 기본값을 가진 인자를 생략하도록 합니다. 다만 함수 선언자가 인자 이름을 public API의 일부로 만들 의도를 명시하지 않아도 이름이 외부에 노출되는 문제가 생깁니다. Public arguments pre-RFC는 pub name: String처럼 호출부에서 사용할 이름과 함수 본문에서 사용할 이름을 함께 공개하고, to db처럼 호출 이름과 본문 변수 이름을 다르게 지정하는 문법을 제안했습니다. 이 제안은 overload, Fn trait, method overload, 문서화까지 폭넓게 다뤘지만, 기본 인자값을 포함하지 않았고 규모가 너무 크다는 비판을 받았습니다. named argument를 반드시 이름으로 써야 하는지, 위치만 맞으면 되는지도 분명하지 않았으며, extern function과 Fn trait의 처리도 복잡하다는 지적이 나왔습니다.
그 밖에 함수 호출을 구조체 초기화처럼 쓰는 방식, Dart의 중괄호 문법을 복사하는 방식, 새 edition에서만 named argument를 허용하는 방식, .at = ...처럼 점 접두사를 붙이는 방식도 검토합니다. 작성자는 구조체 초기화와 함수 호출이 지나치게 비슷해지는 문제, 기존 pattern 문법과의 혼동, 점 표기가 struct field를 연상시키는 문제를 각각 지적합니다.
■ 제안의 목적과 현재 API 비교
작성자의 제안은 positional argument와 named argument를 한 함수에서 함께 지원하고, named argument에 기본값을 지정해 호출부에서 생략하도록 하는 방식입니다. 가장 먼저 안전성과 가독성을 내세웁니다. solar_elevation(timestamp: ..., latitude: ..., longitude: ...)처럼 호출하면 숫자 세 개가 각각 무엇을 뜻하는지 즉시 드러납니다. 현재도 매개변수 구조체를 만들면 비슷한 효과를 얻지만, 함수 하나를 위해 구조체를 선언하는 비용 때문에 표준 라이브러리와 일반적인 private function에서는 잘 쓰이지 않는다고 설명합니다.
선택적 인자는 None을 반복해서 적는 보일러플레이트도 줄입니다. repository.checkout_index(None, None) 대신 필요한 값만 이름으로 전달하는 형태를 상정합니다. 구조체와 Default를 사용하면 일부 호출은 간결해지지만, 구조체 선언과 기본값 구현이 여전히 필요합니다. InstanceDescriptor 예시에서는 backends, flags, memory_budget_thresholds, backend_options, display를 모두 직접 채워야 합니다.
Builder pattern은 호출부에서 필요한 값만 설정하는 대안이지만, builder 타입과 메서드가 라이브러리의 public API에 추가됩니다. std::process::Command는 stdin, stdout, env_clear, envs 등을 연결하는 API를 제공하기 위해 약 60줄의 소스 코드와 더 많은 문서가 필요하다고 설명합니다. bon 같은 crate가 builder 선언을 줄여주지만, 여전히 전용 구조체와 호출 절차가 필요하고 컴파일 시간 증가 때문에 모든 라이브러리가 사용하지는 않는다고 덧붙입니다.
새 인자를 추가할 때의 API 확장성도 근거로 제시합니다. 현재 Rust에서는 기존 함수에 인자를 추가하면 breaking change가 됩니다. 기본값을 가진 새 인자를 추가하는 대신 비슷한 동작을 하는 새 함수를 만들게 되고, HashMap처럼 매개변수 조합이 늘수록 함수 수가 증가해 문서와 API 표면이 복잡해집니다. 작성자는 named argument와 optional argument가 이 반복을 줄이는 방향으로 작동한다고 봅니다.
■ <출처> 반응
- @u/Solumin — named argument와 optional argument를 두 개의 RFC로 나누는 편이 채택에 유리한지 궁금합니다. 둘은 밀접하지만 결국 별개의 개념입니다. named argument는 호출부를 읽을 때 중요하고, optional argument는 코드를 작성할 때 편리합니다. 인자가 세 개만 넘어가도 호출부가 복잡해지며, 같은 타입이거나
bool이면 더 심해집니다.hard_link예시가 그 점을 잘 보여줍니다.pub문법은 마음에 들지만, 모든 인자를 호출부에서 이름으로 쓸 수 있게 하는 편을 선호합니다. 일부 인자만 이름을 허용하면 작성자가 빠뜨린 경우라는 상태가 생길 뿐입니다. - @u/XtremeGoose — 전체적으로는 좋지만 Rust의 기존 same-name elision과 맞물리지 않는 점이 아쉽습니다.
Struct { field: field }를Struct { field }로 줄여 쓰는 방식과 달리, 이 제안에서는 콜론이 없으면 positional argument가 됩니다. 구조체 이름 생략, 구조체 선언부의 기본값,..Default::default()의 축약, 새 필드를 추가할 수 있는 non-exhaustive 구조체를 지원하는 편이 충분하다고 봅니다. 다만func({ x })가 이미 유효한 문법이라 별도 sigil이 필요할 수도 있습니다. - @u/guineawheek — 이런 글을 쓴다면 이 기능들이 좋은 생각이라는 전제에 대한 흔한 반론부터 답해야 합니다. Rustacean은 다른 언어에서 named argument와 optional argument가 나쁘게 사용된 사례를 보고 온 경우가 많아서, 사람들이 전제에 자동으로 동의한다고 가정하면 설득하기 어렵습니다.
- @u/Mercerenies — 눈에 잘 띄지 않고 backward compatibility를 해치지 않으며
Fn*trait를 더 복잡하게 만들지 않는다는 점이 좋습니다. 다만 optional parameter가fn이나Fn으로 coercion될 때의 규칙은 반대여야 할 수 있습니다. 대부분의 사용자가 관심을 두지 않는 선택적 인자를 가진 생성자는fn() -> MyStruct로 변환되고, 선택적 인자를 넘기는 호출은 막는 편을 직관적으로 생각합니다. 또 이 제안만으로는HashMap문제가 해결되지 않습니다. 생성자 수가 많은 이유에는 optional argument 부재뿐 아니라 allocator와 hasher에 대한 가정 차이도 있기 때문입니다. - @u/juhotuho10 — Rust가 좋은 이유는 많은 부분을 지나치게 명시적으로 만든다는 데 있습니다. optional argument가 그 성격을 너무 많이 약화할 수 있습니다. 다른 언어에서 기본값을 두면 안 되는 인자에 기본값을 넣거나, API를 깨지 않으려고 인자를 추가하면서 함수와 구현을 더 나쁘고 복잡하게 만드는 사례를 많이 봤습니다. 구조체의
Default는 나머지 인자에 관심이 없다는 사실을 명시적으로 표현하지만, optional argument는 기본값을 암묵적으로 삽입합니다.
원문: botahamec.dev / 번역·요약: Trawling