Why is (t) different from (t,)?

When using the unstable fn_traits and unboxed_closures features, the following code doesn’t compile:

fn call_fn<F: FnOnce(usize) -> usize>(f: F) {
    f(0);
}

struct Identity;

impl FnOnce<(usize)> for Identity {
    type Output = usize;

    extern "rust-call" fn call_once(self, (n): (usize)) -> usize {
        n
    }
}

fn main() {
    call_fn(Identity);
}

The error message implies that the impl FnOnce isn’t implementing what we intend it to implement:

error[E0277]: expected a `std::ops::FnOnce<(usize,)>` closure, found `Identity`
  --> src/main.rs:19:5
   |
19 |     call_fn(Identity);
   |     ^^^^^^^ expected an `FnOnce<(usize,)>` closure, found `Identity`
   |
   = help: the trait `std::ops::FnOnce<(usize,)>` is not implemented for `Identity`
note: required by `call_fn`
  --> src/main.rs:4:1
   |
4  | fn call_fn<F: FnOnce(usize) -> usize>(f: F) {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

However, this code (where (usize) is replaced by (usize,) and similarly for the other tuples in the impl FnOnce block) does compile:

impl FnOnce<(usize,)> for Identity {
    type Output = usize;

    extern "rust-call" fn call_once(self, (n,): (usize,)) -> usize {
        n
    }
}

Why is that? Why does the trailing comma matter?

Because if it didn’t, you wouldn’t be able to parenthesise expressions or types without making them tuples.

3 Likes

yes, because scalars are an essentially different type from one-element tuples in Rust

and function arguments are represented as a tuple in the Fn traits

1 Like

I didn’t realize that was also true for types. But it definitely looks that way - e.g., fn foo(a: (usize)) is equivalent to fn foo(a: usize) (playground).