[Ergonomics] Implicit wrappers for coercing argument types

In the thread Ergonomics initiative discussion: Allowing owned values where references are expected, @shepmaster raised an interesting point. They noted that a common source of confusion and annoyance has to do with the need to sometimes make “small adjustments” to function arguments. Their example was as follows:

struct Thing;

impl Thing {
    fn foo(&self) { ... }
}

// works:
vec![Thing].into_iter().map(|t| t.foo())

// does not work:
vec![Thing].into_iter().map(Thing::foo)

In both cases, the iterator is yielding up values of type Thing. However, in the first case, the t.foo() expression will autoref as part of the . operator. In the second case, we get an error, because Thing::foo is a fn that implements FnMut(&Thing), and we need something that implements FnMut(Thing). (In the context of the original thread, we were talking about introducing coercions that would allow e.g. Thing::foo(t) to work as well, though it would still consume t; see the original thread for details.)

I can certaintly testify that this distinction causes me periodic annoyance. I presume it also causes confusion for new users from time to time.

(As a historic aside, back in the Ye Olde Days of Ruste, we used to have “modes”, which were kind of a primitive forebearer of the current borrowings system; in those days, we would get a “mode mismatch” in cases like these, and I distinctly remember discussing with @brson as we walked to lunch how annoying it was, and how this same problem would still arise in the new system, but instead as a kind of type mismatch. So this annoyance has been with us a long time.)

So @shepmaster proposed that we might want to fix this by “lifting” coercions that we apply to individual values (e.g., from T to &T ) to functions, so that they can be applied to function arguments. I think the way this would work is that we would coerce a function (or any value?) that implements FnMut(T) to one that implements FnMut(U) where U can be coerced to T. This would be equivalent to introducing a wrapper function, like this:

// original:
vec![Thing].into_iter().map(Thing::foo)

// would become:
vec![Thing].into_iter().map(wrap_Thing_foo)
fn wrap_Thing_foo(t: Thing) { Thing::foo(&t) }

Anyway, I wanted to spin off this thread to discuss the idea in more depth. Leaving aside for a moment the challenge of how to modify the type checker to know when to introduce these wrappers, I’m curious what other purposes we might use these wrapper functions for. One thing that came to mind was a vague proposal that I saw floated by @eddyb (or perhaps @retep998?) that we could do ABI conversion in an automatic way. The idea would be that if you require a extern "C" fn(&Thing), you wouldn’t need to declare Thing::foo as extern "C", instead we could generate a similar wrapper to convert the minor ABI details:

extern "C" fn wrap_Thing_foo(t: &Thing) { Thing::foo(t) }

(Naturally these might be combined into one wrapper that both coerces its arguments and converts the ABI.)

Finally, it seems obvious that we might permit coercions on the return type as well.

Thoughts?

13 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.