Reopen -- `FnOnce`: why is `Output` an associated type?

Long ago, we have rfcs/0587-fn-return-should-be-an-associated-type.md at master · rust-lang/rfcs · GitHub

impl<R,F> Foo for F : FnMut() -> R { ... }

This impl is currently illegal because the parameter R is not constrained.

I don't agree with this view.

The most simple Default example in which there are mentioned.

This means that our fn FnOnce FnMut Fn can not be abstracted:

  • transmute
  • for<'a> Vec::<&'a i32>::new

Because we have hr_lifetime_in_assoc_type future-compatibility warnings · Issue #33685 · rust-lang/rust (github.com) This is not a problem because associate the type with a unique implementation of the trait.

But function is not satisfy this, we should not think the function itself is illegal, but should think to the return value is the generic type.

These functions do exist and reasonable, because the output is associated with type, we can abstract it, this is a design error, because of its illegal judgment is given.


The last discussion was in 2020

Can you elaborate on this?

Add has an Output associated type, but seems to be used and abstracted fine.

What's different with Fn? Is it just that the paren sugar currently keeps things from being written on stable right now?

When you write this function, you will get a compiler error.

fn a<F: for<'s> Fn() -> Vec<&'s i32>>(f: F) 
error[E0582]: binding for associated type `Output` references lifetime `'s`, which does not appear in the trait input types

He requires that the higher-order lifecycle cannot only appear in the associated type, but must appear in the generic parameter. (Calling generics as input types is confusing, but let's ignore this little annoyance now)

This is reasonable, because the association type of the feature is unique to the implementation, so the user must not give a type that meets this condition. It is reasonable to expose the problem as early as possible.

However, because the function output is of association type, we will get this E0582.

It is worth mentioning that the emergence of 'E0582' is not to prompt as early as possible that no type can meet the constraint, but to repair a serious error with extended life cycle Unsound projection when late-bound-region appears only in return type · Issue #32330 · rust-lang/rust · GitHub —— Fixing '# 32330' in this way hides the fact that the output as an association type leads to a type that meets the constraint.

I would argue that being able to assume the output type of a Fn type is constrained is much more useful than being able to abstract over those functions. How often do you even want to abstract over them?


That said I think it's a bit too late to change this. There's a lot of code that assumes that the return type of FnOnce is an associated type, and thus constrained, and changing this would break all that code.

It's a breaking change. If the output can vary, the R in the implementation below would have multiple possibilities. Supporting extensions like this was one of the motivations of the RFC.

trait FnExt<A>: Fn(A) -> <Self as FnExt<A>>::Out {
    type Out;
}

impl<A, R, F: Fn(A) -> R> FnExt<A> for F {
    type Out = R;
}

Further thoughts that go vaguely in the direction of "if we get a second set of closure traits, they should probably use something GAT-like and not input type parameters"

It would probably wreak havoc on various types of inference (particularly around Fn trait extensions). Based on some experiences with them -- I'll refer back to this later.

Side note: Your Vec::new example is effectively covered by Vec::<'&'static i32>::new due to variance (but other examples wouldn't be). [1]


Here's one motivation that reveals a downside.

You can't have closures that capture things and return values to them; you can't have custom Fn-like implementers that do something like this.

struct S(String);
impl FnMut<()> for S {
    fn call_mut(&mut self, _: ()) -> &str {
        self.0.push_str(".");
        &self.0
    }
}

But there's probably no acceptable solution without whole-new traits, whether it's GATs or making the output type a trait input parameter, because the pattern can't be supported by FnOnce (so you'd have to break the supertrait connection).

struct S(String);
impl<'a> FnMut<(), &'a str> for S {
// Output type     ^^^^^^^
    fn call_mut(&mut self, _: ()) -> &'a str {
        self.0.push_str(".");
        &self.0
    }
}

impl<'a> FnOnce<(), &'a str> for S {
// Output type      ^^^^^^^
    fn call_once(&self, _:()) -> &'a str {
        // What here? :-(
    }
}

Arguably this is only a problem if the traits are stabilized, but presumably that's still a goal.


Here's another downside. It's not uncommon for people to want to generalize over owned-or-borrowing closures; there's a question along those lines every month or so on URLO. My first playground link above is such an example. Here's another:

impl S {
    fn choose_randomly<It, F: FnMut(&str) -> It>(&self, chooser: F) -> It {
        // Roll of dice ha ha
        chooser(&self.0[4])
    }
}

// Fails because the return of `trim` is not a singular type, so it can't
// resolve to `It`
s.choose_randomly(str::trim)

There are workarounds... albeit they can have inference issues, like in the first playground.

But with the traits in their current form you can write this particular example like so:

impl S {
    fn choose_randomly<F>(&self, mut chooser: F)
        -> <F as FnOnce<(&str,)>>::Output
    where
        F: for<'a> FnMut<(&'a str,)>

This works because you don't have to mention the associated type as a type parameter, thus making it resolve to a single type. GATs for FnMut and Fn-like traits could preserve that, but input type parameters won't (unless we get generic type constructors I guess, impl<F<*>, ...>).


  1. The playground shows one side of the variance; the other side is that anything that can meet your non-compiling bound has to support the 'static case. ↩︎

2 Likes

It is important to ensure correctness. If a thing may be incorrect, at least we should try to make efforts first. If we notice the difference and can't help it, at least mention it in the document.

Yes, this is my fault, but there are always types of life cycle unchanged.

That's why I came here,

one type is more general than the other

fn adopts a similar structure to FnOnce, so this is one of the sequelae caused by the added restrictions in the repair of #32330.

Essentially, the change is that each time you reference foo (directly!), there is now a single lifetime assigned for its parameter 'a , no matter how many times you call it. In cases like bar above, we need to assign distinct references to each call, so we have to reference foo twice.

Don't be so pessimistic. Changing an association type back to a generic type has essentially relaxed the restrictions, and it is not difficult to be compatible with it.


 the type parameter `R` is not constrained by the impl trait, self type, or predicates

As long as the output is generic, this error will be suppressed.


The first repair way is to give a new traits.

trait NewFnOnce<Args: Tuple, Output> {
    fn call_once(self, args: Args) -> Output;
}

impl<Args: Tuple, Output, F: NewFnOnce<Args, Output>> FnOnce<Args> for F {
    type Output = Output;

    extern "rust-call" fn call_once(self, args: Args) -> Self::Output {
        F::call_once(self, args)
    }
}

Then the default implementation is changed to a new trait.

This work will happen when the trait changes, which is senseless.

I seem to put it too simple...

(Maybe this is what you meant by your edits, but) it can't be suppressed because this scenario is possible.

impl<A> MyFn<A, String> for S {}
impl<A> MyFn<A, i32> for S {}

impl<A, R, F: MyFn<A, R>> MyFnExt<A> for F {
    type Out = R; // Is `R` `String` or `i32` for `F = S`?
}

You are right. This is really a breaking change. For the new function trait, the previous assumption that the return depends on the input is invalid. The code based on this will no longer work.

But now, I just want to let us regain the complete abstraction of the function. If it is not compatible with the old function characteristics, implementing new NewFnOnce trait for closures and function pointers independently may solve this problem.

The function pointer does not have this ::Output usage, and its modification should be harmless.

Let's first confirm whether my view is correct:

Perhaps the supreme mystery: 'Leave it' is feasible :rofl:

This problem cannot be avoided if and only if:

  • The return type does invariant for the lifecycle
  • We need to express the function with its construction process

In fact, almost the only meet this occasion is constructed from the type of self-reference life cycle, this in itself is not worth promoting.

The reason why I encountered this problem is that I tried to express a type that is self-referential.

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