Rust 에러 E0593: closure is expected to take a single 2-tuple as argument (인자 개수)
E0593은 클로저(또는 함수)를 map, sort_by, thread::spawn 같은 다른 곳에 넘겨줬는데, 그 매개변수 개수가 실제로 호출되는 방식과 맞지 않는다는 뜻이에요. 단연 가장 흔한 경우는 이거예요. 반복자는 클로저에 튜플 하나를 주는데, 내가 매개변수를 두 개 쓴 거죠. 대부분은 괄호 한 쌍이면 고쳐지고, rustc가 고친 코드를 직접 보여 주기도 해요. 반대로 내가 클로저를 호출하면서 값 개수를 틀렸다면, 그건 E0057이에요. 이 시리즈의 다른 에러는 Rust 컴파일러 에러 해설에서 볼 수 있어요.
에러 메시지
섹션 제목: “에러 메시지”error[E0593]: closure is expected to take a single 2-tuple as argument, but it takes 2 distinct arguments --> src/main.rs:6:10 |6 | .map(|i, name| format!("{}. {name}", i + 1)) // ❌ 매개변수가 두 개예요 | ^^^ --------- takes 2 distinct arguments | | | expected closure that takes a single 2-tuple as argument |help: change the closure to accept a tuple instead of individual arguments |6 - .map(|i, name| format!("{}. {name}", i + 1)) // ❌ 매개변수가 두 개예요6 + .map(|(i, name)| format!("{}. {name}", i + 1)) // ❌ 매개변수가 두 개예요 |“클로저는 2-튜플 하나를 인자로 받아야 하는데, 서로 다른 인자 2개를 받고 있다”는 뜻이에요. help는 “개별 인자 대신 튜플 하나를 받도록 클로저를 바꿔 보라”고 제안하고, 바뀐 줄을 -와 +로 보여 줘요.
이 에러를 내는 프로그램은 다음과 같아요.
fn main() { let names = vec!["Ferris", "Corro", "Rusty"]; let lines: Vec<String> = names .iter() .enumerate() .map(|i, name| format!("{}. {name}", i + 1)) // ❌ 매개변수가 두 개예요 .collect(); println!("{:?}", lines);}E0593은 첫 줄이 다른 모양으로 나오기도 해요. 모두 “매개변수 개수가 틀렸다”는 뜻이에요.
error[E0593]: closure is expected to take 2 arguments, but it takes 1 argumenterror[E0593]: closure is expected to take 0 arguments, but it takes 1 argumenterror[E0593]: function is expected to take 1 argument, but it takes 2 argumentserror[E0593]: closure is expected to take 2 distinct arguments, but it takes a single 2-tuple as argument위에서부터 “인자 2개를 받아야 하는데 1개를 받는다”, “0개를 받아야 하는데 1개를 받는다”, “함수가 1개를 받아야 하는데 2개를 받는다”, “서로 다른 인자 2개를 받아야 하는데 2-튜플 하나를 받는다”는 뜻이에요.
무슨 뜻인가요
섹션 제목: “무슨 뜻인가요”6번째 줄 아래의 두 표시를 보세요.
^^^는map을 가리켜요. 클로저를 호출하는 쪽이고, 값을 몇 개 넘길지도map이 정해요.---------는 매개변수 목록|i, name|을 가리켜요. 내가 받겠다고 약속한 거예요.
둘이 맞지 않아요. enumerate()는 아이템마다 (index, item) 튜플 하나로 바꾸고, map은 그 튜플을 하나의 인자로 클로저에 넘겨요. 그런데 클로저는 따로따로 두 개를 달라고 하는 거죠.
한 번에 봉투 하나만 들어가는 우편함 투입구를 떠올려 보세요. 봉투 안에는 편지 두 장(튜플)을 넣을 수 있지만, 낱장 편지 두 장은 투입구에 넣을 수 없어요. |(i, name)|은 “봉투째 주세요, 여기서 바로 열어 볼게요”라는 뜻이에요.
메시지에 나오는 “closure”(클로저)나 “function”(함수)이라는 단어는 내가 무엇을 넘겼는지 알려 줘요. 그 자리에서 쓴 클로저인지, .map(add)처럼 이름이 있는 fn인지요.
Rust가 이렇게 하는 이유
섹션 제목: “Rust가 이렇게 하는 이유”클로저를 받는 메서드는 모두 그 클로저를 어떻게 호출할지 정확히 밝혀 둬요. map은 FnMut(Self::Item) -> B 같은 걸 원해요. 인자 하나, 즉 아이템이에요. enumerate()에서 아이템은 (usize, &&str)이에요. 마침 튜플일 뿐인 값 하나죠. sort_by는 FnMut(&T, &T) -> Ordering, 즉 인자 두 개를 원해요. thread::spawn은 FnOnce() -> T, 인자가 아예 없어요.
Rust는 튜플을 알아서 여러 인자로 쪼개 주지도 않고, 여러 인자를 튜플로 묶어 주지도 않아요. 덕분에 규칙이 단순해요. 메서드 시그니처에 적힌 그대로 클로저가 받아요. 이런 시그니처를 읽을 수 있게 되면, 어떤 반복자 메서드가 무엇을 넘겨줄지 미리 알 수 있어요.
이 규칙 뒤에 있는 Fn 트레이트는 클로저 트레이트에서, 튜플은 튜플 구조 분해하기에서 더 읽을 수 있어요.
고치는 방법
섹션 제목: “고치는 방법”방법 1: 매개변수 목록 안에서 튜플 풀기
섹션 제목: “방법 1: 매개변수 목록 안에서 튜플 풀기”메시지에 “a single 2-tuple”(2-튜플 하나)이 있을 때 이 방법을 고르세요. 가장 자주 쓰게 될 방법이에요. 괄호만 추가하면 돼요. |(i, name)|은 튜플 하나를 받아서 두 부분에 각각 이름을 붙여요. enumerate, zip, 맵 순회에서 모두 통해요.
use std::collections::BTreeMap;
fn main() { let names = vec!["Ferris", "Corro", "Rusty"];
// enumerate는 아이템마다 (index, name) 튜플 하나를 줘요 let lines: Vec<String> = names .iter() .enumerate() .map(|(i, name)| format!("{}. {name}", i + 1)) .collect(); println!("{:?}", lines);
// 맵을 순회할 때도 아이템마다 (key, value) 튜플 하나를 줘요 let mut stock = BTreeMap::new(); stock.insert("apples", 3); stock.insert("pears", 0); stock.insert("plums", 5); let total: i32 = stock.iter().map(|(_name, count)| *count).sum(); println!("과일 {total}개");}실행 결과:
["1. Ferris", "2. Corro", "3. Rusty"]과일 8개_name 앞의 _는 Rust에게 “이건 안 쓰는 거 알고 있어요”라고 알려 줘요. 그냥 _만 써도 돼요. zip에 같은 방법을 쓰는 예는 E0057 페이지에 있어요.
방법 2: 메서드가 넘기는 개수만큼 매개변수 쓰기
섹션 제목: “방법 2: 메서드가 넘기는 개수만큼 매개변수 쓰기”메시지에 “expected to take N arguments”(인자 N개를 받아야 함)가 있을 때 이 방법을 고르세요. 메서드가 무엇을 넘기는지 확인하고, 정확히 그 개수만큼 매개변수를 쓰세요.
sort_by는 아이템 두 개를 비교해요:|a, b|.for_each,map,filter는 아이템 하나를 넘겨요:|item|, 필요 없으면|_|.thread::spawn은 아무것도 넘기지 않아요:||, 보통은move ||.
use std::thread;
fn main() { // sort_by는 아이템 두 개를 비교해요: 매개변수 두 개 let mut scores = vec![42, 7, 99, 15]; scores.sort_by(|a, b| b.cmp(a)); println!("{:?}", scores);
// for_each는 아이템 하나를 넘겨요: 매개변수 하나 (필요 없으면 _) let tasks = vec!["작성", "테스트", "배포"]; tasks.iter().for_each(|task| println!("할 일: {task}")); tasks.iter().for_each(|_| print!("*")); println!();
// thread::spawn은 아무것도 넘기지 않고 클로저를 호출해요: 매개변수 0개 let id = 1; let handle = thread::spawn(move || format!("작업자 {id} 완료")); println!("{}", handle.join().unwrap());}실행 결과:
[99, 42, 15, 7]할 일: 작성할 일: 테스트할 일: 배포***작업자 1 완료스레드에 데이터를 넘기고 싶을 때 매개변수를 추가하지는 마세요. 위의 id처럼 move로 캡처하면 돼요. 동시성과 정렬을 참고하세요.
방법 3: 이름 있는 함수를 감싸거나, 튜플을 받게 만들기
섹션 제목: “방법 3: 이름 있는 함수를 감싸거나, 튜플을 받게 만들기”메시지가 “function is expected to take…“로 시작할 때 이 방법을 고르세요. .map(add)처럼 함수를 이름으로 넘겼는데, 그 함수의 매개변수가 맞지 않는 거예요. 아래 예에서는 아이템이 쌍(pair)인데, add는 숫자 두 개를 받아요.
error[E0593]: function is expected to take a single 2-tuple as argument, but it takes 2 distinct arguments --> src/main.rs:7:48 |1 | fn add(a: i32, b: i32) -> i32 { | ----------------------------- takes 2 distinct arguments...7 | let sums: Vec<i32> = pairs.into_iter().map(add).collect(); | --- ^^^ expected function that takes a single 2-tuple as argument | | | required by a bound introduced by this call“함수는 2-튜플 하나를 인자로 받아야 하는데, 서로 다른 인자 2개를 받는다”는 뜻이에요. required by a bound introduced by this call은 “이 호출(map)이 건 바운드 때문에 필요하다”는 말이에요.
함수를 작은 클로저로 감싸서 튜플을 풀고 호출하거나, 튜플을 바로 받는 버전을 만들면 돼요.
fn add(a: i32, b: i32) -> i32 { a + b}
// 쌍을 튜플 하나로 받는 버전fn add_pair((a, b): (i32, i32)) -> i32 { a + b}
fn main() { let pairs = vec![(1, 2), (3, 4), (10, 20)];
// 감싸기: 튜플 하나를 받아서 인자 두 개로 넘겨요 let sums: Vec<i32> = pairs.iter().map(|&(a, b)| add(a, b)).collect(); println!("{:?}", sums);
// 또는 처음부터 튜플 하나를 받는 함수를 넘겨요 let sums2: Vec<i32> = pairs.into_iter().map(add_pair).collect(); println!("{:?}", sums2);}실행 결과:
[3, 7, 30][3, 7, 30]|&(a, b)|에 &가 있는 건 .iter()가 각 쌍을 &(i32, i32)로 빌려주기 때문이에요. 이 패턴이 두 숫자를 복사해서 꺼내 줘요. 보통은 감싸는 쪽이 더 좋은 선택이에요. add가 평범한 인자 두 개짜리 함수로 남아서, 코드의 다른 곳에서도 그대로 호출할 수 있거든요.
내가 만든 제네릭 함수가 Fn(i32) -> i32를 원하는데 인자 두 개짜리 add를 넘기면 “function is expected to take 1 argument, but it takes 2 arguments”가 나와요. 해결은 빠진 값을 채워 주는 감싸는 클로저예요. E0057의 방법 3에 예가 있어요.
자주 보게 되는 곳
섹션 제목: “자주 보게 되는 곳”HashMap::retain(이번엔 반대예요). 대부분의 맵 메서드는 튜플을 주지만, retain은 키와 값을 따로따로 인자 두 개로 넘겨요. 그래서 stock.retain(|(name, count)| *count > 0)은 “closure is expected to take 2 distinct arguments, but it takes a single 2-tuple as argument”(서로 다른 인자 2개를 받아야 하는데 튜플 하나를 받는다)를 내요. 해결: 괄호를 빼고 stock.retain(|_name, count| *count > 0)이라고 쓰세요. Vec::retain은 평소처럼 아이템 하나를 받아요.
매개변수를 빠뜨렸을 때. tasks.iter().for_each(|| println!("task"))는 “closure is expected to take 1 argument, but it takes 0 arguments”를 내고, rustc가 |_|를 제안해요. 해결: |_|, 아이템을 쓰고 싶다면 |task|를 쓰세요.
스레드. thread::spawn(|id| ...)는 “closure is expected to take 0 arguments, but it takes 1 argument”를 내요. 해결: thread::spawn(move || ...)로 쓰고 id는 바깥에서 캡처하세요.
함수 포인터 매개변수. 내 함수가 f: fn(i32) -> i32(제네릭 F: Fn이 아닌 평범한 함수 포인터)를 받는데 인자 두 개짜리 add를 넘기면, 이 에러 대신 E0308이 나와요. 라벨은 “incorrect number of function parameters”(함수 매개변수 개수가 틀림)예요. 해결은 같아요. |x| add(x, 10) 같은 감싸는 클로저를 쓰세요. 아무것도 캡처하지 않는 클로저는 fn 포인터로 넘길 수 있어요.
관련 에러
섹션 제목: “관련 에러”- E0057: closure takes N arguments but M were supplied: 내가 클로저를 직접 호출하면서 값 개수를 틀렸어요.
- E0631: type mismatch in closure arguments: 개수는 맞지만 매개변수 타입이 틀렸어요. 반복자는
&i32를 주는데i32라고 쓴 경우가 많아요. - E0061: function takes N arguments but M were supplied: 일반 함수를 인자 개수가 틀리게 호출했어요.
- E0308: mismatched types:
fn(...)포인터 매개변수일 때, 또는 아이템이 튜플이 아닌데|(a, b)|라고 썼을 때 나와요. - E0599: no method found: E0593 바로 뒤에 따라 나올 때가 있어요. 예를 들어 “the method
collectexists … but its trait bounds were not satisfied”(collect메서드는 있지만 트레이트 바운드를 만족하지 않음)예요. E0593을 먼저 고치면 사라져요. - 전체 컴파일러 에러: 에러 페이지 전체 목록이에요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. names.iter().enumerate().map(|i, name| ...)가 E0593을 내요. 어떻게 고칠까요?
2. scores.sort_by(...)에 넘기는 클로저에는 매개변수가 몇 개 필요할까요?
3. thread::spawn(|id| println!("{id}"))가 E0593을 내요. id를 스레드 안으로 어떻게 가져갈까요?
점수: 0 / 3
더 알아보기
섹션 제목: “더 알아보기”- E0593 — Rust error codes index (영어): 이 에러의 공식 설명이에요.
- 클로저 — The Rust Programming Language 한국어판: 클로저 문법, 매개변수 타입을 추론하는 방식,
Fn트레이트를 다뤄요. - As input parameters — Rust by Example (영어): 함수의
Fn바운드가 어떤 클로저를 받을지 정하는 방식을 보여 줘요. Iterator::enumerate— 표준 라이브러리 문서 (영어): 아이템 하나하나가(index, value)튜플 하나가 된다는 걸 보여 줘요.HashMap::retain— 표준 라이브러리 문서 (영어): 클로저가 키와 값을 인자 두 개로 받는 맵 메서드예요.