Aller au contenu

Erreur Rust E0593 : closure is expected to take a single 2-tuple as argument

E0593 veut dire que vous avez transmis une closure (ou une fonction) à autre chose, comme map, sort_by ou thread::spawn, et que son nombre de paramètres ne correspond pas à la façon dont elle sera appelée. Le cas de loin le plus fréquent : un itérateur donne un seul tuple à votre closure, et vous avez écrit deux paramètres. La plupart du temps, la correction tient en deux caractères, et rustc vous l’écrit souvent lui-même. Si c’est vous qui appelez la closure avec le mauvais nombre de valeurs, c’est plutôt E0057. Pour les autres erreurs de cette série, consultez les erreurs du compilateur 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)) // ❌ two parameters
| ^^^ --------- 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)) // ❌ two parameters
6 + .map(|(i, name)| format!("{}. {name}", i + 1)) // ❌ two parameters
|

Le compilateur vous dit : « la closure devrait prendre un seul tuple de 2 éléments comme argument, mais elle prend 2 arguments distincts ». L’aide (help:) vous propose de faire accepter un tuple à la closure.

Voici le programme qui la provoque :

fn main() {
let names = vec!["Ferris", "Corro", "Rusty"];
let lines: Vec<String> = names
.iter()
.enumerate()
.map(|i, name| format!("{}. {name}", i + 1)) // ❌ two parameters
.collect();
println!("{:?}", lines);
}

(Le commentaire dit : deux paramètres.)

E0593 peut aussi commencer par d’autres premières lignes. Elles veulent toutes dire « mauvais nombre de paramètres » :

error[E0593]: closure is expected to take 2 arguments, but it takes 1 argument
error[E0593]: closure is expected to take 0 arguments, but it takes 1 argument
error[E0593]: function is expected to take 1 argument, but it takes 2 arguments
error[E0593]: closure is expected to take 2 distinct arguments, but it takes a single 2-tuple as argument

Dans l’ordre : la closure devrait prendre 2 arguments mais en prend 1 ; elle devrait en prendre 0 mais en prend 1 ; la fonction devrait en prendre 1 mais en prend 2 ; la closure devrait prendre 2 arguments distincts mais prend un seul tuple.

Regardez les deux marqueurs sous la ligne 6 :

  • ^^^ pointe vers map. C’est lui qui appelle votre closure, et c’est lui qui décide combien de valeurs il lui passe.
  • --------- pointe vers votre liste de paramètres |i, name|. C’est ce que vous avez promis d’accepter.

Les deux ne sont pas d’accord. enumerate() transforme chaque élément en un seul tuple, (index, élément), et map passe ce tuple à votre closure comme un seul argument. Votre closure, elle, en demande deux séparés.

Imaginez une fente de boîte aux lettres qui n’accepte qu’une enveloppe à la fois. Vous pouvez mettre deux lettres dans l’enveloppe (un tuple), mais la fente ne prendra pas deux lettres en vrac. Écrire |(i, name)|, c’est dire : « donnez-moi l’enveloppe, je l’ouvre tout de suite ».

Le mot « closure » ou « function » dans le message vous indique ce que vous avez passé : une closure écrite sur place, ou une fn nommée comme dans .map(add).

Chaque méthode qui prend une closure dit exactement comment elle va l’appeler. map attend quelque chose comme FnMut(Self::Item) -> B : un seul argument, l’élément. Avec enumerate(), l’élément est un (usize, &&str). C’est une seule valeur, qui se trouve être un tuple. sort_by attend FnMut(&T, &T) -> Ordering : deux arguments. thread::spawn attend FnOnce() -> T : aucun.

Rust ne découpe jamais un tuple en arguments séparés à votre place, et ne regroupe jamais des arguments séparés dans un tuple. La règle reste donc simple : ce que dit la signature de la méthode, c’est ce que reçoit votre closure. Une fois que vous savez lire ces signatures, vous pouvez prévoir ce que n’importe quelle méthode d’itérateur vous donnera.

Pour en savoir plus sur les traits Fn derrière tout ça, voir Les traits des closures, et sur les tuples, Déstructurer un tuple.

Solution 1 : décomposer le tuple dans la liste des paramètres

Section intitulée « Solution 1 : décomposer le tuple dans la liste des paramètres »

Choisissez-la quand le message parle d’« a single 2-tuple ». C’est la correction dont vous aurez besoin le plus souvent. Ajoutez des parenthèses : |(i, name)| prend un seul tuple et donne un nom à ses deux parties. Ça marche pour enumerate, pour zip et pour le parcours d’une map :

use std::collections::BTreeMap;
fn main() {
let names = vec!["Ferris", "Corro", "Rusty"];
// enumerate donne un tuple (index, nom) par élément
let lines: Vec<String> = names
.iter()
.enumerate()
.map(|(i, name)| format!("{}. {name}", i + 1))
.collect();
println!("{:?}", lines);
// parcourir une map donne aussi un tuple (clé, valeur) par élément
let mut stock = BTreeMap::new();
stock.insert("pommes", 3);
stock.insert("poires", 0);
stock.insert("prunes", 5);
let total: i32 = stock.iter().map(|(_name, count)| *count).sum();
println!("{total} fruits au total");
}

Sortie :

["1. Ferris", "2. Corro", "3. Rusty"]
8 fruits au total
▶ Essayez par vous-même

Le _ au début de _name dit à Rust : « je sais que je ne m’en sers pas ». Vous pouvez aussi écrire simplement _. La même idée appliquée à zip est montrée sur la page E0057.

Solution 2 : donner autant de paramètres que la méthode en passe

Section intitulée « Solution 2 : donner autant de paramètres que la méthode en passe »

Choisissez-la quand le message dit « expected to take N arguments ». Regardez ce que la méthode passe, puis écrivez exactement ce nombre de paramètres :

  • sort_by compare deux éléments : |a, b|.
  • for_each, map et filter passent un élément : |item|, ou |_| si vous n’en avez pas besoin.
  • thread::spawn ne passe rien : ||, en général move ||.
use std::thread;
fn main() {
// sort_by compare deux éléments à la fois : deux paramètres
let mut scores = vec![42, 7, 99, 15];
scores.sort_by(|a, b| b.cmp(a));
println!("{:?}", scores);
// for_each donne un élément : un paramètre (_ si vous n'en avez pas besoin)
let tasks = vec!["écrire", "tester", "livrer"];
tasks.iter().for_each(|task| println!("à faire : {task}"));
tasks.iter().for_each(|_| print!("*"));
println!();
// thread::spawn appelle la closure sans rien : zéro paramètre
let id = 1;
let handle = thread::spawn(move || format!("thread {id} terminé"));
println!("{}", handle.join().unwrap());
}

Sortie :

[99, 42, 15, 7]
à faire : écrire
à faire : tester
à faire : livrer
***
thread 1 terminé
▶ Essayez par vous-même

Pour faire entrer des données dans un thread, n’ajoutez pas de paramètre. Capturez-les avec move, comme id ci-dessus. Voir La concurrence et Le tri.

Solution 3 : enrober une fonction nommée, ou lui faire prendre un tuple

Section intitulée « Solution 3 : enrober une fonction nommée, ou lui faire prendre un tuple »

Choisissez-la quand le message dit « function is expected to take… ». Vous avez passé une fonction par son nom, comme .map(add), et ses paramètres ne collent pas. Ici, les éléments sont des paires, mais add prend deux nombres :

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

Rustc vous dit que la fonction devrait prendre un seul tuple, alors que add prend deux arguments distincts. Vous pouvez enrober la fonction dans une petite closure qui décompose le tuple puis l’appelle, ou écrire une version qui prend directement le tuple :

fn add(a: i32, b: i32) -> i32 {
a + b
}
// une version qui prend la paire comme un seul tuple
fn add_pair((a, b): (i32, i32)) -> i32 {
a + b
}
fn main() {
let pairs = vec![(1, 2), (3, 4), (10, 20)];
// enrober : un tuple en entrée, deux arguments en sortie
let sums: Vec<i32> = pairs.iter().map(|&(a, b)| add(a, b)).collect();
println!("{:?}", sums);
// ou passer une fonction qui prend déjà un seul tuple
let sums2: Vec<i32> = pairs.into_iter().map(add_pair).collect();
println!("{:?}", sums2);
}

Sortie :

[3, 7, 30]
[3, 7, 30]
▶ Essayez par vous-même

Le & dans |&(a, b)| est là parce que .iter() prête chaque paire sous la forme &(i32, i32), et le motif en copie les deux nombres. La closure d’enrobage est en général le meilleur choix : add reste une fonction normale à deux arguments, que le reste de votre code peut appeler.

Si votre propre fonction générique attend Fn(i32) -> i32 et que vous lui passez add à deux arguments, vous obtenez « function is expected to take 1 argument, but it takes 2 arguments ». La correction est une closure d’enrobage qui fournit la valeur manquante, comme dans la solution 3 de E0057.

HashMap::retain (dans l’autre sens). La plupart des méthodes des maps vous donnent un tuple, mais retain passe la clé et la valeur comme deux arguments séparés. stock.retain(|(name, count)| *count > 0) donne donc « closure is expected to take 2 distinct arguments, but it takes a single 2-tuple as argument ». Correction : retirez les parenthèses, stock.retain(|_name, count| *count > 0). Vec::retain, lui, prend un seul élément, comme d’habitude.

Oublier le paramètre. tasks.iter().for_each(|| println!("task")) donne « closure is expected to take 1 argument, but it takes 0 arguments », et rustc suggère |_|. Correction : |_|, ou |task| si vous voulez vous en servir.

Les threads. thread::spawn(|id| ...) donne « closure is expected to take 0 arguments, but it takes 1 argument ». Correction : thread::spawn(move || ...), en capturant id depuis l’extérieur.

Un paramètre pointeur de fonction. Si votre fonction prend f: fn(i32) -> i32 (un simple pointeur de fonction, pas un générique F: Fn), passer add à deux arguments donne plutôt E0308, avec l’étiquette « incorrect number of function parameters » (nombre incorrect de paramètres). Même correction : une closure d’enrobage comme |x| add(x, 10). Les closures qui ne capturent rien peuvent être passées comme pointeurs fn.

Petit quiz

1. names.iter().enumerate().map(|i, name| ...) donne E0593. Quelle est la correction ?

2. Combien de paramètres faut-il à la closure de scores.sort_by(...) ?

3. thread::spawn(|id| println!("{id}")) donne E0593. Comment faire entrer id dans le thread ?

Score : 0 / 3