We Have Named Arguments at Home
Rust에는 이미 이름 있는 인자가 있습니다
Rust에 이름 있는 인자, 기본 인자, 오버로딩이 없어도 구조체와 Option, Default, 트레이트, 열거형, 반복자로 비슷한 API를 만들 수 있다는 글입니다. 저자는 호출 규칙을 늘리는 대신 기존 타입 시스템을 조합하자고 주장하며, Reddit에서는 이 방식의 명료함과 보일러플레이트 부담을 두고 논쟁이 이어집니다.
- 주제
AI 요약
Rust에 이름 있는 인자(named arguments), 선택적 인자(optional arguments), 기본 인자(default arguments), 함수 오버로딩(function overloading)이 없어 불편하다는 의견이 꾸준히 나옵니다. 글쓴이는 새 문법을 추가하지 않아도 Rust의 기존 기능을 조합하면 필요한 사용성을 상당 부분 얻는다고 설명합니다. 완전히 같은 대안은 아니지만, 함수 호출 규칙을 복잡하게 만들지 않는다는 점이 장점이라고 봅니다.
여러 스칼라 인자는 구조체로 묶습니다
이미지 자르기 함수가 좌표와 크기 값을 네 개의 연속된 u32로 받으면, 호출 위치만 보고 각 값의 의미를 알아보기 어렵습니다. 글쓴이는 x, y, width, height 필드를 둔 Crop 구조체를 인자로 받게 하자고 제안합니다. 구조체 리터럴은 필드 이름을 드러내고 순서도 바꿀 수 있으며, 오타 검사와 자동완성, 필드별 문서화도 지원합니다. 값에 불변 조건을 둘 수 있고 다른 함수에 전달하기도 쉽습니다.
필드 이름을 함수 호출 규약이 아니라 타입에 둔다는 점도 강조합니다. 함수 포인터는 매개변수 이름을 보존하지 않으므로, 이름 있는 인자를 도입하면 함수 포인터나 클로저에서 어떤 이름을 써야 하는지 문제가 생깁니다. 반면 Size 같은 구조체를 받는 함수는 fn(Size)라는 타입으로 표현하면 됩니다. 구조체 필드 초기화는 작성한 위치에서 식을 평가하므로, 인자 평가 순서를 새로 정할 필요도 없습니다.
다만 모든 인자를 구조체로 바꾸자는 뜻은 아닙니다. Vec::push에 값 하나를 넣으려고 별도 구조체를 만드는 식은 과합니다. 여러 값이 하나의 개념이나 설정 묶음을 이룰 때 구조체를 정의하는 편이 자연스럽습니다. 예를 들어 선을 그릴 때 좌표 네 개와 굵기, 투명도를 나열하기보다 Line과 Point로 표현하고, 연결 설정은 ConnectionOptions로 묶습니다. 글쓴이는 이런 설계가 빠진 도메인 개념을 드러낸다고 봅니다.
선택값과 기본값은 타입으로 표현합니다
선택적 인자 하나는 Option<Duration>으로 나타낼 수 있습니다. 호출하는 쪽은 None이나 Some(...)을 명시하고, 함수 타입에도 선택성이 드러납니다. 선택적 인자가 여럿이면 None을 줄줄이 전달하는 방식은 읽기 어려워집니다. 이때 RequestOptions 같은 설정 구조체를 만들면 설정값을 미리 계산해 둔 뒤 함수에 넘길 수도 있습니다.
구조체에 Default를 구현하고 구조체 갱신 문법을 쓰면, 바꿀 값만 적고 나머지는 기본값으로 채울 수 있습니다. 라이브러리 API에서는 직접 Default를 구현해 기본 시간 제한이나 리디렉션 설정을 문서화할 수도 있습니다. 글쓴이는 ..Default::default()가 이름 있는 기본 인자보다 장황하다는 점을 인정합니다. 대신 생략할 수 있는 인자와 호출 규칙을 따로 정의하지 않아도 되고, 기본값을 함수 호출 밖에서도 독립적으로 사용할 수 있다고 설명합니다.
구조체 생성 과정에 검증이나 변환이 필요하면 빌더(builder)를 쓸 수 있습니다. 빌더 메서드는 일반 메서드 호출이므로 설정에 따라 조건부로 값을 추가하기 쉽습니다. 다만 빌더가 작은 언어처럼 커질 수 있어 기본 선택지로 삼지는 말아야 한다고 덧붙입니다.
오버로딩과 가변 인자는 별도 추상화로 다룹니다
같은 이름의 함수 여러 개를 두는 대신 connect와 connect_with_timeout처럼 이름을 나누면 호출자가 오버로드 해석을 할 필요가 없습니다. 입력 타입을 다양하게 받으려면 Into, AsRef, Borrow 같은 트레이트를 활용합니다. 서로 다른 종류의 대상을 받는 연산은 트레이트로 다형성을 표현하거나, Redirect 열거형의 각 변형으로 나타낼 수 있습니다. 옵션 해시처럼 동적으로 값의 종류를 결정하는 방식 대신, 필드와 변형을 정적 타입으로 드러내는 접근입니다.
같은 타입의 인자를 여러 개 받는 함수는 슬라이스나 IntoIterator를 받을 수 있습니다. 호출자는 배열이나 벡터를 그대로 넘기면 되므로 기존 컬렉션과도 자연스럽게 조합됩니다. 이질적인 인자 여럿이 꼭 필요할 때는 매크로를 쓸 수 있지만, 단지 가변 인자 문법을 흉내 내기 위해 매크로를 쓰지는 않겠다고 합니다. 필드 초기화 축약 문법도 이름 반복을 줄입니다. timeout: timeout 대신 timeout으로 적어 필드 이름은 남기고 중복만 없앨 수 있습니다.
글의 결론은 Rust에 새 기능을 절대 추가하지 말자는 주장이 아닙니다. 이름 있는 인자나 기본 인자는 호출부를 간결하게 만들 수 있지만, 패턴, 함수 포인터, 트레이트, 평가 순서, 호환성 등 언어 차원의 설계 문제도 함께 해결해야 합니다. 구조체, Option, Default, 트레이트, 열거형, 반복자 같은 기능은 인자 전달 외에도 쓰이는 독립적인 추상화입니다. 글쓴이는 기존 기능을 조합해 복잡한 API를 표현하는 편이 Rust의 설계 방향에 맞으며, 새 호출 규칙을 추가할 시급성은 크지 않다고 봅니다.
Reddit 반응
- @u/garagedragon — 함수 포인터와 클로저에서 생기는 문제, 시그니처에 패턴을 쓸 때 생길 비슷한 문제 때문에 일반적인 방식의 이름 있는 인자에는 마음이 멀어졌습니다. 대신 타입이 하나로 결정되는 위치에서 구조체 초기화나 패턴을 쓸 때 타입 이름을 생략하게 해주면 좋겠습니다. 글의 예시도 덜 장황해집니다.
- @u/AnUnshavedYak — 이 아이디어가 좋습니다. 다른 초기화에도 적용되면 더 좋겠습니다. Go의 익명 구조체가 종종 그립습니다. 중첩 구조체를 초기화할 때
Foo { bar: { baz: { ..Default::default() }, } }처럼 쓸 수 있으면 좋겠습니다. - @u/Nabushika — Rust에 이름 있는 인자는 필요하지 않다고 생각하지만, 구조체를 대신 쓸 때
RequestOptions를 매번 적지 않도록 Zig식.{timeout, ..}문법을 넣는 데 반대하지는 않겠습니다. 구조체 이름을 쓰는 점이 사실상 유일하게 불편합니다.
- @u/AnUnshavedYak — 이 아이디어가 좋습니다. 다른 초기화에도 적용되면 더 좋겠습니다. Go의 익명 구조체가 종종 그립습니다. 중첩 구조체를 초기화할 때
- @u/coderstephen — 글의 큰 방향에는 대체로 동의하지만 보일러플레이트가 적지 않습니다. 사람들이 대안을 찾는 이유도 거기에 있습니다. 구조적 레코드(structural records)를 언어에 추가하는 방안을 더 선호합니다. 타입을 따로 선언하고 호출할 때도 타입 이름을 적는 것보다, 인라인 레코드로 시그니처와 호출을 표현하는 편이 덜 장황합니다.
- @u/Recatek — 이 주제에 진지한 의견이 있는 사람이라면 현재 대안은 이미 알고 있을 겁니다. 글은 보일러플레이트를 대수롭지 않게 넘기지만, 그 부담이 논쟁의 핵심입니다.
- @u/bleachisback — 보일러플레이트뿐 아니라 글 앞에 나온
..Default::default()도 부담입니다.
- @u/Tastaturtaste — 제시된 대안은 이미 알고 있었지만, 이 글은 이름 있는 인자나 선택적 인자의 효용을 다시 의심하게 만들 정도로 구체적인 예를 들었습니다.
- @u/nicoburns — 함수마다 필요하지도 않았을 수십 줄의 코드가 생기고, 호출 위치에도 여러 줄이 더 붙습니다.
- @u/WormRabbit — 줄 수뿐 아니라 인지 부담도 있습니다. 공개 API의 구조체를 문서화해야 하고, 필요한 트레이트 구현과 메서드도 고민해야 합니다. 설정을 전달하고 싶을 뿐인데 복잡성이 여러 곳으로 퍼집니다.
- @u/tjallingt — 구조체 타입을 추론할 수 있다면 정말 좋겠습니다.
hello(_ { firstname: "tom", lastname: "hanks" })처럼 쓸 수 있으면 좋겠습니다. - @u/cbarrick — 빌더 타입을 생성하는 매크로가 이름 있는 인자를 구현하는 방법이라고 봅니다. 전용 언어 기능은 필요하지 않습니다. Java에서도 흔히 쓰는 방식입니다.
- @u/nicoburns — 컴파일 시간 비용이 정당화되지 않아 여러 라이브러리에서 매크로를 제거했습니다.
- @u/WormRabbit — 매크로와
syn의존성의 컴파일 비용이 추가되고, 생성된 보일러플레이트도 API 문서와 자동완성에 그대로 보입니다. 트레이트 메서드처럼 적용하기 어려운 경우도 있습니다.
- @u/epage — 보일러플레이트를 줄이려면
..만으로 기본값을 채우거나, 안정화된_를 구조체 초기화 타입 추론에 쓰는 방안을 생각해볼 수 있습니다. - @u/facetious_guardian — 타입이 명확한 방식을 지지합니다. 선택적 인자는 대개 기본값으로 대체할 수 있으니, 많은 경우
Option대신Default::default()를 쓰는 편이 낫다고 봅니다. - @u/SnooCalculations7417 —
X(u32),Y(u32),Width(u32),Height(u32)처럼 각 인자에 새 타입을 만들면 타입 시스템을 활용한 이름 있는 인자를 얻을 수 있습니다.- @u/buwlerman — 추가 타입을 정의하는 보일러플레이트가 남고, 인자 순서도 정해진다는 문제가 있습니다.
- @u/NotDuckie — 선택적 인자와 이름 있는 인자, 함수 오버로딩은 다른 언어에 익숙해져서 지지하는 개념일 뿐이라고 봅니다. Rust에서 하는 방식을 익히면 필요하지 않습니다.
- @u/AlexanderMomchilov — 이름 붙은 구조체가 유용한 건 맞지만
connect(ConnectionOptions { ... })는 우스꽝스럽습니다...Default::default()를 사소한 불편이라고 하는 것도 솔직하지 않습니다.
원문: corrode.dev / 번역·요약: Trawling