Associated const fns can clean up abstraction layers

In mathematics or cryptography, we sometimes initialize many types by choosing among a limited rang of known implementation models. It works and maybe most ergonomic to have known implementation models in polymorphic fns and choose them using associated fn constants, ala

pub trait Foo {}
impl Foo for str {}
impl Foo for [u8] {}

pub fn go<F: Foo+?Sized>(_foo: &F) {}

pub trait Bar {
    const F: fn(&Self);
}

impl<F: Foo> Bar for F {
    const F: fn(&F) = go::<F>;
}

It's slightly more ergonomic if regular fn method worked like associated fn constants, ala

pub trait Baz {
    fn f(_: &Self);
}

impl<F: Foo> Baz for F {
    fn f = go::<F>;
}

I'm unsure exactly how self receivers should be handled here though, if at all.

Regardless..

It's likely this fn f = .. syntax breaks other past rejected proposals for fn(whatever) = .. syntax, so maybe this gives more reasons for not accepting those in future, or for ensuring compatibility. We should be careful with trait delegation in particular.

Is it very much less ergonomic to use a trait for the possible functions, and an associated type:

pub trait Foo {}
impl Foo for str {}
impl Foo for [u8] {}

pub trait Qux {
    fn f<F: Foo+?Sized>(_foo: &F);
}

pub struct Go;
impl Qux for Go {
    fn f<F: Foo+?Sized>(_foo: &F) {}
}

pub trait Bar {
    type F: Qux;
}

impl<F: Foo> Bar for F {
    type F = Go;
}

Using a strategy pattern like this can in fact be useful. In fact, with default provided methods (or an extension trait), you can forward Trait::f to Trait::Strategy::f so callers don't have to go through the strategy type indirection, just implementors. This even is extendable to methods with receivers, if you give the strategy trait a generic (or associated) type for the receiver.

However, I read OP as more "keep this in mind in future developments" than "a use case to solve."

I don't think this ultimately is a blocker on its own to fn f(args...) = expr; as a syntax; the differentiation would be

fn go() -> R { /* ... */ }

impl Struct {
    fn f() = go;
    // equals
    fn f() -> fn() -> R { go }

    // but
    fn f = go;
    // equals
    fn f() -> R { go() }
}

I don't think this really holds any more confusion potential than what already exists with distinguishing function items from function calls. This is also the worst case scenario with a nullary function; the difference is a lot more significant when actual arguments are specified. If we were to have both syntaxes, I would support a style lint pushing people to always use the applied form (e.g. fn f() = go()) for nullary functions.

But this also presupposes that both syntaxes are allowed anywhere and infer the return type. If we actually consider expression assignment functions, I expect they'll still require the full function signature to be listed, including return type. (Return type inference with -> _ might be allowable, but omitting it would be -> () like any other function.) Similarly, because function assignment doesn't specify a signature, I expect it'd be limited to use in trait impls which already specify an expected signature. (And then the behavior for handling self becomes obvious: it's just passed along as the first argument.)

Most delegation syntax I've seen has used use go as f rather than fn f = go; I'm not sure what the second-order effects of this would be.

Yes exactly. There is no suggestion here per se. We should just keep in mind several points around associated const fns:

First fn f = go::<Foo>; is extremely natural, so trait delegation should avoid conflicting syntax. I do not know if this conflicts with say fn f(self) = self.0.go(); but yeas as solves this mostly.

Also, it's broadly interesting if/how fn methods do/should differ from associated const fn types, or if/how they should be unified.

Associated const fns bring niche but interesting features, like being optional or static tricks. etc.

pub trait Bar {
    const F: Option<fn(&Self)>;
    static G: Once<fn(&Self)>;
    static G: Mutex<fn(&Self)>;
}

What do they lack? How do we express polymorphism fn types? Should associated const fns or fn types in general have self recievers? etc.

Also, do const fn types miss out on many optimizations? Would #[inline(always)] const F: fn(&self) work?

Imagine we unified them, so trait Foo { const F: fn(&Self); } and trait Foo { fn f(&self); } work exactly alike, except for capitalization. Could delegation work somewhat more like FRUs? In other words, we've some macro-like tool that produces the wrapped methods, but then we take whatever we did not specify.

pub type Wraper(Type)
impl Trait for Wrapper {
    fn some_method_we_change(&self) { .. }
    ..#[some_method_wrappng_proc_macro] Self::0
}

IIRC, Rust in the Linux kernel is using a related but distinct trick. (Unfortunately I don't recall how to find a link to give an actual example.) Specifically, they have a system to do something like

#[vtable]
pub trait Ops {
    fn spam(&self) -> Result { Err(Unsupported) }
    fn eggs(&self) -> Result { Err(Unsupported) }
}

#[vtable]
impl Ops for Foo {
    fn spam(&self) -> Result { /* ... */ }
}

expanding to

pub trait Ops {
    const USE_VTABLE_ATTR: ();

    const HAS_SPAM: bool = false;
    fn spam(&self) -> Result { Err(Unsupported) }

    const HAS_EGGS: bool = false;
    fn eggs(&self) -> Result { Err(Unsupported) }
}

impl Ops for Foo {
    const USE_VTABLE_ATTR: () = ();

    const HAS_SPAM: bool = true;
    fn spam(&self) -> Result { /* ... */ }
}

intended for use with FFI vtables such as (roughly, shorthand)

#[repr(C)]
#[derive(Default)]
pub struct OpsVtable {
    pub spam: Option<unsafe fn(*const c_void) -> Result>;
    pub eggs: Option<unsafe fn(*const c_void) -> Result>;
}

impl OpsVtable::new<T: Sized>() -> Self
where T: Ops {
    Self {
        spam: if !T::HAS_SPAM { None }
            else { Some(|this| (&*this.cast::<T>()).spam()) },
        eggs: if !T::HAS_EGGS { None }
            else { Some(|this| (&*this.cast::<T>()).eggs()) },
    }
}

where null/None is used as a marker for methods which aren't provided and should get the default behavior (as opposed to every provider setting the vtable field to the default behavior if necessary).


Optional trait methods which are optional as in Option rather than just defaulted have interested me for a while. Utilizing fn pointers is probably sufficient since LLVM can devirtualize an indirect call to constant function pointer relatively easily, but it does necessitate the use of function call syntax rather than method call syntax unless combined with some sort of fnptr supporting UMCS (receiver.(spam)()?) or much more involved flow typing feature for whether the method is tested to exist.

They can't ever be exactly alike, because the Foo::const F is a function pointer (pointer sized/aligned) whereas the Foo::fn f is a function item (zero sized/aligned). It's also unlikely that the const will ever be able to be invoked with method syntax because of the difference between fn(this: &Self) and fn(self: &Self).

But the general concept of treating fn items more like const items certainly is interesting, especially w.r.t. how it gives applying FRU syntax an obvious meaning, even if that obvious meaning doesn't actually quite work (because of the deliberate limits on type-adjusting FRU) without applying further adjustments.

The "full send" version of running with the idea gives

pub trait Ops {
    fn spam(&self);
    fn eggs(&self);
}

semantics that could roughly be explained as

pub trait Ops {
    type [fn spam]: FnItem(&Self);
    type [fn eggs]: FnItem(&Self);

    const impl: struct {
        #[method] spam: [fn spam],
        #[method] eggs: [fn eggs],
    };
}

trait FnItem(Args...) -> Ret
    = (Fn(Args...) -> Ret) + Default + Sized
    + CoerceInto<fn(Args...) -> Ret>
    + const { size_of::<Self>() == 0 };

pub trait dyn Ops = Ops<
    [fn spam] = fn(&Self),
    [fn eggs] = fn(&Self),
>; // despite fn(): !FnItem()
1 Like

One thing that fn f = ... syntax would allow is making traits like Copy less magic. If we could specify associated functions in trait bounds the same way we do with associated types/consts then we could write this:

trait Clone {
    fn clone(&self) -> Self;
}

trait Copy: Clone<fn clone = core::ptr::read::<Self>> {
}

This code says that, to implement Copy, your Clone implementation needs to set fn clone to be aliased to ptr::read. ie. it guarantees that the Clone implementation is just copying bytes, without the language needing a magic, ad-hoc check that if you derive(Copy) you also derive(Clone).

This does have the problem though that ptr::read::<T> and <T as Clone>::clone don't have identical signatures since one is unsafe and takes a raw pointer but the other is safe and takes a reference. The signatures are clearly compatible though (for some definition of "compatible") and so I don't think it should matter if the user wants to express a trait bound like this. To write a Clone impl like this though you'd at least need to have an unsafe keyword somewhere. Maybe just:

impl Clone for Foo {
    fn clone = unsafe ptr::read::<Foo>;
}
3 Likes

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