Why do function arguments need to be defined as generics?


#1

I’m coming from Scala, and I’m used to the following type signature working:

 fn verify_empty_write(mut write_fn: FnMut(&mut TBinaryOutputProtocol) -> ::Result<()>)

It didn’t - I got a compiler error about write_fn not being sized. Instead - after looking at the docs again - I had to rewrite it like this:

fn verify_empty_write<F: FnMut(&mut TBinaryOutputProtocol) -> ::Result<()>>(mut write_fn: F)

which seems…surprising. Why does the function have to be defined as a generic type? Also, why do I have to do mut write_fn when the function’s type is already FnMut?


#2

The issue is that matching closures you pass to this function all have different concrete types. But they all implement FnMut(&mut TBinaryOutputProtocol) so you can use this as the constraining trait. Useful to think about closures as structs that overload the call operator - these structs are generated for us at compile-time. So the function has to be generic to handle all of them.


#3

Can you also pass the FnMut by ref so it’s a trait object? That’s more analogous to Scala.


#4

And as for why “mut write_fn”, it’s because FnMut takes “&mut self”. That allows it to mutate some captured state internally (think a closure that captures some value and mutates it).


#5

While I’m no expert with regards to how the JVM works, I’m almost certain that the implementation in scala must ultimately boil down to some form of dynamic dispatch.

Not only that, I imagine that Java/Scala generics also perform dynamic dispatch (at least, when you do stuff like <T extends SomeClass> or <T implements SomeInterface> or whatever the syntax is). Hence, there is probably little disadvantage to letting FnMut work just like <T implements FnMut>. (again, though: don’t take it from me)

In contrast, Rust’s generics are statically dispatched, so there actually is something to be gained by encouraging their use.


One note: You can actually do dynamic dispatch in Rust.

You see, the reason the compiler is giving an error about an “unsized type” (rather than, say, an “invalid type”) is because there actually is a type called FnMut. In fact, every trait has a corresponding type. But similar to [T], they’re unsized, which means you can only pass around borrowed forms of the type (i.e. &FnMut or &mut FnMut).

I seldom ever use them, because (a) the standard library doesn’t, and (b) dynamically dispatched types are just not as powerful as generics (iirc you can have at most one non-standard-ibrary trait, so &(Clone + Eq + Foo) is okay but &(Foo + Bar) isn’t).


#6

This seemed a little surprising, but it’s true. If you come from the JVM, you have come to rely on interface-based polymorphism. (I do note that the text says ‘Rust does not currently support this’)

Let me quote error E0225:

"You attempted to use multiple types as bounds for a closure or trait object.
Rust does not currently support this. A simple example that causes this error:

fn main() {
    let _: Box<std::io::Read + std::io::Write>;
}

Builtin traits are an exception to this rule: it’s possible to have bounds of
one non-builtin type, plus any number of builtin types. For example, the
following compiles correctly:

fn main() {
    let _: Box<std::io::Read + Send + Sync>;
}

#7

Thanks guys - I figured it was static vs. dynamic dispatch. FWIW, it feels a little…limiting, because there are cases when you don’t know the caller in advance.

One case I stumbled on recently was - in simplified form:

let cmd_line_args = ...;
let protocol_type = cmd_line_args.extract("protocol").unwrap();
let channel_type = cmd_line_args.extract("channel").unwrap();

let protocol_creator_fn = protocol_type match {
  "protocol_1" => ...,
  "protocol_2" => ...,
}
let channel_creator_fn = channel_type match {
  "channel_1" => ...,
  "channel_2" => ...,
}

let server = BlockingThreadedServer(protocol_creator_fn, channel_creator_fn);

I understand the performance arguments, but I ended up having to “box all the things” ™.


#8

Yes, box all the things is the solution. Other languages also box all the things they just don’t give you a choice in the matter so it seems easy.