To start, this idea isn’t fully formed, and I’m not sure if it would be tractable to implement in practice. But I’m not sure it wouldn’t be, so I figured I’d throw it out there, in case others can improve upon it.
I thought of this in a conversation in #rust on April 29, 2018, but didn’t write it up properly until now.
Motivation
For a long time, it was impossible to return a closure by value, because its type can’t be named. This was recently solved by impl Trait:
fn foo()->impl Fn()->i32 + Clone {
|| 5
}
This is good if what you want is an existential type. But that isn’t exactly the same problem as type nameability. And either way, this return value is still an unnameable type, which can be inconvenient for the caller. For instance, I can’t annotate the types of these variables, no matter how much I want to:
let closure = foo();
let clone = closure.clone();
let iter = std::iter::repeat (closure.clone()).take (3);
Also, some conceptual return types are inherently impossible to express through the impl Trait concept (Not my example; suggested by someone in the #rust channel, although unfortunately I don’t have a record of who it was):
fn bar()->impl Into <impl Debug> + Into <impl Debug>
Here, we have a return type that can be converted into either of 2 different Debug types. But the caller could never use the into() implementations of the returned value, because it would be ambiguous which one you were trying to call. In order to call either of them, the caller would have to name one of the types, like so:
let value = bar();
let debug: UnnameableType = value.into();
or
let debug = Into::<UnnameableType>::into(value);
This situation would be resolved by having a way to name all of the unnameable types in question. But how to do that?
Here’s one way that wouldn’t fully solve the problem: C++ partially deals with type naming through the decltype keyword. However, this wouldn’t solve the second problem, and often wouldn’t be an ergonomic approach even if it did work:
fn baz()->decltype(|| {…very long and complex closure…}) {
|| {…very long and complex closure…}
}
Idea
My idea has 2 components: first, a way to assign a name to the type of any value. (This could be used to name the types of closures.) Second, a way to return a type from a function to its caller.
Idea 1: To name the type of a value
Currently, there is no legal type annotation for a variable with an unnameable type. So, I thought, what if we made the type annotation be a way to assign a name to the type?
let closure: MyClosure = || 5; // this line *defines* the name MyClosure to refer to the type of that closure
let clone: MyClosure = closure.clone();
let iter: Take<Repeat<MyClosure>> = std::iter::repeat (closure.clone()).take (3);
If you annotate multiple variables with the same identifier, they would be required to be the same type, much the way match arms have to be the same type even if you don’t specify the type explicitly.
The biggest open question is, what would be the scope in which the identifier MyClosure would have its meaning? My starting assumption was that it would extend to the boundaries of the function it was defined in, but I’m not exactly sure of my reason for assuming that.
Idea 2: To “return” one or more types from a function
In the case of the return value with 2 Into implementations, we’d like to communicate 3 types back to the caller. That’s more than Idea 1 can take care of. So the function needs to have a list of types associated with it.
A function can already have a list of types associated with it: its generic parameters. So, to borrow the syntax from that:
fn foo<returntype T: Fn()->i32 + Clone>()->T {
let result: T = || 5;
result
}
That’s explicitly using the syntax from Idea 1. But this implicit form would also work:
fn foo<returntype T: Fn()->i32 + Clone>()->T {
|| 5
}
Any parameter labeled with the keyword returntypewould be determined by the function definition, rather than by the caller. But it could be named by the caller as an (otherwise-undefined) identifier in order to receive the type, as follows:
let closure = foo::<MyClosure>();
which would have the same effect as line from the example from Idea 1. To deal with the 2 Into implementations, you would do this:
fn bar<returntype T: Into <U> + Into <V>, returntype U: Debug, returntype V: Debug>()->T {
…
}
…
let value = bar::<_, Debug1, Debug2>();
let debug1: Debug1 = value.into();
or
let debug2 = Into::<Debug2>::into(value);
Notes/complications
In the examples I wrote above, I wrote specific trait bounds for the returntypes. On one hand, it seems like it isn’t necessary because they wouldn’t have to be existential types – the caller could be permitted to rely on the concrete type, whatever it is. On the other hand, that means the function signature isn’t very clear about what it’s returning – you’d have to read the body of the function in order to figure out what you were permitted to do with the return values. So it might be clearer to only allow the returntypes to function as existential types. On the third hand, it does seem like it would sometimes be useful to allow the caller to rely on the concrete type. Maybe the pragmatic compromise would be to allow that only within the same module or the same crate.
In the case of a function with no ordinary generic parameters, the returntypes are actually fixed regardless of the inputs. So in that case, it might be possible for the type name created by Idea 1 to exist as an item in the same module as the function. Then it could potentially be used more easily, and it wouldn’t require Idea 2 to communicate it to the caller.
On the other hand, for functions with generic parameters, the function essentially has a mapping from generic parameters to returntypes. The syntax I suggested might not be ideal, because it means there’s no way to refer to that mapping WITHOUT calling the function. (I haven’t thought of a scenario where that would be desirable, but it could theoretically happen.)
In conclusion, there’s a lot about these ideas that I’m unsure about. But I did some web searches for previous discussions about type nameability, and I didn’t find anything along these lines, so I’d be interested to hear what people think.