[Idea] Offering a stable way to impl `Fn`

Consider the following situation:

use ::core::ops::Add;

trait CurryAdd : Add + Sized {
    type Adder : FnOnce(Self) -> Self::Output;
    fn curry_add (lhs: Self) -> Self::Adder;
}

that is, the trait requires an explicitely named type implementing one of the Fn traits. Although existential types are trying to come up with a general solution for this very particular kind of case, I still think that it is a pity it is not possible to implement Fn traits on custom types.

Teaching-wise, it would be great to be able to show the closures-are-objects “equivalence”, while also allowing to show the unique case of Rust with three callable interfaces.

I understand that the precise internals of Fn traits must remain unstable at the moment, and when we think about it, it is plausible that they remain unstable forever (call operator and closures need to be able to be constantly improved).

Idea

But, since the current interface is already quite acceptable (I think most users would not complain), it could be nice to offer the current interface as a frozen stable “alternative”. This can be achieved with two things:

  • a stable version of the current fn_traits:

    //! names could obviously be different
    
    pub
    trait CallableByValue<Args> {
        type Output;
        
        fn call_by_value (self, _: Args) -> Self::Output;
    }
    
    pub
    trait CallableByMut<Args> : CallableByValue<Args> {
        fn call_by_mut (&'_ mut self, _: Args) -> Self::Output;
    }
    
    pub
    trait CallableByRef<Args> : CallableByMut<Args> {
        fn call_by_ref (&'_ self, _: Args) -> Self::Output;
    }
    
  • a wrapper that implements the unstable fn_traits when the wrappee implements the stable ones:

    #[repr(transparent)]
    struct FnMarker<F>(pub F);
    
    impl<Args, F> FnOnce<Args> for FnMarker<F>
    where
        F : CallableByValue<Args>,
    {
        type Output = F::Output;
        
        #[inline]
        extern "rust-call"
        fn call_once (self: Self, args: Args) -> Self::Output
        {
            self.0.call_by_value(args)
        }
    }
    
    impl<Args, F> FnMut<Args> for FnMarker<F>
    where
        F : CallableByMut<Args>,
    {
        #[inline]
        extern "rust-call"
        fn call_mut (self: &'_ mut Self, args: Args) -> Self::Output
        {
            self.0.call_by_mut(args)
        }
    }
    
    impl<Args, F> Fn<Args> for FnMarker<F>
    where
        F : CallableByRef<Args>,
    {
        #[inline]
        extern "rust-call"
        fn call (self: &'_ Self, args: Args) -> Self::Output
        {
            self.0.call_by_ref(args)
        }
    }
    
    • this would be the only part that would need to stay in sync with fn_traits as they evolve.

Usage

Basic

use ::core::ops::Add;

trait CurryAdd : Add + Sized {
    type Adder : FnOnce(Self) -> Self::Output;
    
    fn curry_add (lhs: Self) -> Self::Adder;
}

struct CurryAdder<T> (T);

impl<T : Add> callable::CallableByValue<(T,)> for CurryAdder<T> {
    type Output = T::Output;

    fn call_by_value (self, (rhs,): (T,)) -> Self::Output
    {
        self.0 + rhs
    }
}

impl<T : Add> CurryAdd for T {
    type Adder = callable::FnMarker<CurryAdder<T>>;
    
    fn curry_add (lhs: Self) -> Self::Adder
    {
        callable::FnMarker(CurryAdder(lhs))
    }
}


fn main ()
{
    dbg!(
        u8::curry_add(42)(27)
    );
    dbg!(
        i32::curry_add(42)(27)
    );
}

With some sugar (waiting for specialization)

use ::core::ops::Add;

trait CurryAdd : Add + Copy + Sized {
    type Adder : Fn(Self) -> Self::Output;
    
    fn curry_add (lhs: Self) -> Self::Adder;
}

struct CurryAdder<T> (T);

impl_Callable! {
for [T : Add + Copy + Sized]
    |self: &CurryAdder<T>, (rhs,) : (T,)| -> T::Output,
    {
        self.0 + rhs
    }
}

impl<T : Add + Copy + Sized> CurryAdd for T {
    type Adder = callable::FnMarker<CurryAdder<T>>;
    
    fn curry_add (lhs: Self) -> Self::Adder
    {
        callable::FnMarker(CurryAdder(lhs))
    }
}

Alternatives

  1. Existential types look like the best candidate, but it would not allow defining our own object-and-also-closures (e.g., inspecting the captured environment of a closure, or mutating it).

  2. If we were to be able to write, for instance, (&x).eq to mean |y| x.eq(y) , then a new kind of type could be defined to express the type of such a closure: (&'a T).eq (or x.eq : T.eq).

    For instance,

    • (&'a T).eq : 'a + Fn(&T) -> bool (closure |y: &T| x.eq(y))
    • T.eq : 't + Fn(&T) -> bool where T : 't (closure move |y: &T| x.eq(y))

This would avoid needing FnMarker<F>, since we would be able to use F.call_by_xxx. (for instance, CurryAdder<T>.call_by_value for the FnOnce example, or CurryAdder<T>.call_by_ref for the Fn : FnMut : FnOnce example)

2 Likes

Another way to do it would be to use the Fn* sugar in the impl

struct Foo;

impl FnOnce(i32) -> i32 for Foo {
    extern "rust-call" fn call(self, (args,): (i32,)) -> i32 {
        args + 1
    }
}

The problem with this is that it special cases Fn*, but it does seem consistent with the current syntax. It also locks us into the current representation of the Fn* traits, but we want to use variadic generics to describe Fn* because that would be better.

3 Likes

Could alternatively push the sugar further to avoid exposing "rust-call":

impl FnOnce(arg: i32) -> i32 for Foo {
    arg + 1
}

…or less drastic, keep the normal method syntax but just write it without "rust-call" or putting args in a tuple, relying on the sugar in the impl header to do the transformation:

impl FnOnce(i32) -> i32 for Foo {
    fn call(self, arg: i32) -> i32 {
        arg + 1
    }
}
6 Likes

I think I like the first variant more, that would be a super ergonomic way to expose the Fn* traits for stable implementation while keeping the actual representation of them hidden.

I mostly like this, but I’m not sure what to do with self. You may still need to access your own fields and methods, but it’s not very rustic to leave self implicit. Having it hidden is reminiscent of C++ this, which I thought we specifically avoided, even though that sort of happens anyway in closure captures.

2 Likes

Why not just stabilize Fn* as-is?

I kind of join @Yato in wishing for a proper language-level fix (variadic generics) instead of stabilizing an Fn-specific special-case.

It’s current representation is pretty bad and hacky, so we don’t want to stabilize it yet. (not a knock on the person who designed it, they did the best they could with the current state of Rust)

I think an implicit self argument might be fine in this case because there is no ambiguity about which variant of self you are using. I also think that if we use this syntax, then we should deprecate it as soon as we get variadic generics, as they provide a more consistent solution to the Fn* traits.

2 Likes