Top-level self functions `fn f(self: T)`

Since inherent impls are not allowed from external crates, I find myself making traits just to be able to dot-off some object e.g. obj.f(), and falling back to fn f(this: T) when it's too much code for little gain because the function signature has to be duplicated.

I think being able to write top-level fn f(self: T) functions can lead to cleaner code while not overloading impl syntax just for this purpose.

3 Likes

How would this work as a scoped call? For example, say you make a crate moremethods and provide a method named blah. Do I have to use moremethods::blah to be able to do obj.blah()? Can I do obj.moremethods::blah()? What if I have two crates which both provide their own frobnicate() method-functions (or whatever we call these things) that I want to call within the same scope? There is "make a block and use inside of it", but that feels like a step-function from doing it once without ambiguity. With traits, you have <obj as Trait>::frobnicate() at least to inline it, but I'm not seeing an inline way without making path-as-method a thing (which sounds like a lot but maybe it isn't?).

Proceeds suggesting inherent impl with alternative syntax

sorry I can't help myself

2 Likes

I have seen this proposed here before. If a.foo(b) becomes A::foo(&a, b) or <A as Foo>::foo(&a, b), then why can't we call free functions as methods and get the same kind of deref ergonomics. Perhaps there is some merit to it not just using . to invoke that behavior though.

1 Like

I've hit this when splitting crates into two seperate parts, so I use wrapper types

Pattern 1. Borrow wrappers give you instant access, but they require frequent rewrapping, which always requires some explicit code.

pub struct APKToo<'a,'p,C: CurveGroup>(pub &'a AggregatedPublicKeys<'p,C>);

impl<'a,'p,C: CurveGroup> From<&'a AggregatedPublicKeys<'p,C>> for APKToo<'a,'p,C> { .. }

Pattern 2. Owned wrappers, unsafe { mem::transmute(..) }, and Derefs winds up much more ergonomic, especially if you can use the wrapper in your own types. I suppose the safe transmute work should removes the unsafe eventually.

#[derive(Clone,Debug)]
#[repr(transparent)]
pub struct APKToo<'p,C: CurveGroup>(pub AggregatedPublicKeys<'p,C>);

fn apk_local<'a,'p,C: CurveGroup>(apk: &'a AggregatedPublicKeys<'p,C>) -> &'a APKToo<'p,C> {
    unsafe { core::mem::transmute(apk) }
}

impl<'p,C: CurveGroup> core::ops::Deref for APKToo<'p,C> {
    type Target = AggregatedPublicKeys<'p,C>;
    fn deref(&self) -> &AggregatedPublicKeys<'p,C> { &self.0 }
}
impl<'p,C: CurveGroup> core::ops::DerefMut for APKToo<'p,C> {
    fn deref_mut(&mut self) -> &mut AggregatedPublicKeys<'p,C> { &mut self.0 }
}

We've many discussions around the orphan rules here, but yes I wonder if some fancier wrapper type declaration could improve this, but really not so sure.

pub fn f(self: T) { $body } would be functionally almost identical to:

pub trait f
where Self: Identity<Self = T>
{
    fn f(self: T) { $body }
}
impl f for T {}
pub fn f(__self: T) {
    <T as self::f>::f(__self)
}

This way use path::f gives both val.f() (method call syntax) and f(val) (function call syntax). The "desugared" trait item wouldn't be actually nameable, but it doesn't need to be β€” the function is available as just path::f.

The tricky and not immediately obvious questions, however are:

  • How do you distinguish between the different self receiver types? (Self, &Self, &mut Self, Rc<Self>, Pin<Receiver<Self>>, etc)
  • How do you determine which generics, if any, are placed on the trait item and which are placed on the method item?
  • Is generic Self allowed, and if so, how?

These questions have potential answers, but they all make "free method functions" behave less similarly to standard free functions.

I've tried writing an attribute for this a couple times (the self: T syntax is syntactically valid already!) but I don't think these hurdles are possible to solve nicely in just a syntactic macro. I vaguely recall seeing a crate that implements a simpler subset go by, but not well enough to find it.

5 Likes

I'm not sure what difference it makes if call f(v) or v.f(). The latter is even one character longer (which doesn't really matter).

Back when I coded C I was used to not having functions on types, and that worked fine. Then I did C++ for several years, which of course do have functions on structs. This also works fine.

The main reason to have self function in Rust is traits, since they allow static and dynamic dispatch based on the frist argument. (And Rust also has no function overloading except for what you do via trait trickery. Which seems like a good thing to me, I have seen too much abuse of function over loads in C++, I'd rather have unique searchable names).

So I don't really see the usefulness of this proposal. It doesn't seem to offer any new functionality over free functions. And the syntactic/ergonomic advantage seems to me like a wash.

My question then is: why complicate the language for no gain? Rust is already a fairly expansive language, let's make the new features we add count. Let's not end up like Perl did.

3 Likes

This is very much a place where opinions differ, and a large part of opinion is likely based on how often you use let to name immediate single use values. Rust chose postfix .await specifically because of the merit of left-to-right dataflow and . as the Rust pipeline operator.

Then there's also the "power of the dot" in IDEs. You take an expression and type . and you get a decent fuzzy search for all of the type's available methods, or rather what you can do with the type / what that type can do. If this kind of IDE assistance isn't in use and functionality is all either remembered or found via searching code or documentation, then the "power of the dot" is minimal.

So there are programming styles where the benefit of being able to extend the dot doesn't mean much. But in the most commonly used Rust style, it does have real potential benefit.

Also it matters much more for examples that aren't that minimal.

5 Likes

I think we should just allow inherent methods on foreign types so long as they're not exported (ie. not pub, but pub(crate) is fine). If there's a name conflict between a locally-defined method and an upstream one then the locally-defined one takes precedence and you get a warning.

4 Likes

I'll raise a parallel issue: Deref polymorphism is considered an anti-pattern. Yet, I'm not so sure this is really true though, beyond specific choice the language makes. It's more accurate to say Deref polymorphism raises some subtle points, so we shold've some little text on doing Deref polymorphism well, not necessarily prescriptive but certianly "Here are the troubles & bugs you might see unless you do blabla". A few specific questions:

First, should one tweak the variance when doing this? Above my APKToo would be covariant the paramaters, but maybe it should be invariant in some of them?

Second, you want to provide good documentation here, so you need some prominent html link from APKToo to the documentation on AggregatedPublicKeys. It's easy to do this, but also easy to forget.

Third, APKToo must reimplement any traits on AggregatedPublicKeys, so maybe one should've guide or even macros that help do this.

Fourth, should APKToo also be called AggregatedPublicKeys or not?

You can also look at this as a relaxation of impl Trait which I think is brought up before with its own drawbacks, but I think this is a useful scenario to cover regardless of the syntax chosen to express it.

Chaining is another advantage (see .await), but also a lot easier to type as @CAD97 mentioned. Generally I'd prefer to type out the code in the same order it is evaluated so you don't have to constantly move the cursor around.

I didn't mention any because there's not a narrow specific place that this is useful. For example, when you want to extract a function in the middle of the code to make it manageable but without changing the flow, or adding chainable functions to arbitrary types without making a new trait, and so forth.

My 8+ year-old version of this:

Basically, solve the "how is it exported?" by having it be sugar for a voldemort trait that you can't name so can't export.

(If you do want to export it, expand it into a real extension trait.)

3 Likes

It occurred to me that we wouldn't be having this conversation if we had a real pipeline operator (or Dlang-style UFCS, but that's less compatible with how traits work). A real shame that we don't.

The funny thing is that most of us know we want it, in the form of extension traits, and in the form of crates like tap. About the latter of which @josh said the following back in 2022:

[...] I could imagine adding that to the standard library if we can agree on the bikeshedding aspects of it.

I'd argue this is an overly simplified take. Deref from Wrapper to T is reasonable when:

  • &Wrapper should always be accepted wherever &T is expected without any change in behavior or expectation, and
  • Wrapper provides no methods with incompatible semantics to those provided by T now and forever. (This requires that either you control T's methods or that Wrapper has no[1] methods.)

Deref is an anti-pattern if you want to restrict the usage of T in any way. Notably this can be argued to include unit-of-measure style newtypes, since two different units should be added together differently than unitless numbers. It's a fine pattern if you only extend the provided behavior.

And an extra subtle one is that if T is itself Deref, &*t becomes &**wrapper, which breaks our desired property that &T and &Wrapper behave identically, so this veers into anti-pattern as well. Thus it becoming known as an anti-pattern, imho: people (especially early Rust users) reach towards using it when it isn't appropriate much much more than when it is.

Or IOW, Deref can be used for transparent-plus newtypes but should not for transparent-except newtypes. In OO terms, maybe the reasonable applications are comparable to a non-virtual base class.

Then there are the limitations and restrictions of using Deref like this, of course.

&Wrapper should have the same variance as &T.

Rustdoc shows a prominent section for "methods from Deref<Target=T>," which seems sufficient for most cases. Although in many cases I could see the additional encouragement to see T's item docs as well being useful.

This is difficult, and solutions quickly run into almost the exact same issue that coherence addresses β€” if T adds an impl for a trait you're implementing in a later version, what should happen? Giving a name to the extended type resolves the "hashmap problem," but you still risk diverging behavior between &APKToo and &AggregatedPublicKeys.

Not if documentation/code will ever be seeing both types, as that's a recipe for running into by-value and reverse coercions not existing with an expected Foo but found Foo error.


  1. Implementing a trait that has methods is typically seen as an accepted exception, but should still be done cautiously, and ideally the impl should be the "obvious" one for the wrapped type. β†©οΈŽ

3 Likes

A macro could help automate the construction of a trait and implementation block. As a result, given a system for automatically generating trait names the following derive trait syntax might suffice to provide all the expedient syntax of a trait definition:

#[impl_block(TypeA)]
fn process(&mut self, ... ) {...}

becomes...

trait TypeA__ {
   fn process(&mut self, ...);
}
impl TypeA__ for TypeA {
   fn process(&mut self, ... ) {...}
}

That sounds like a feature request for your IDE. Nothing technically prevents it from including functions with the given type as the first parameter into search suggestions. They either didn't think about implementing it, or don't think it's a good idea.

The IDE can already do more complex searches. E.g. RustRover can suggest method calls which require inserting .await, ? or into_iter/iter/iter_mut before the function call (which actually annoys me to no end, I don't need search results for my type polluted with a hundred Iterator methods).

It can also do stuff like postfix template, which allow you to write expr.let and have it converted into let <placeholder> = expr;. It can certainly wrap the expression in a function call.

Anyway, if you think that's a good idea, there is no reason to complicated the language with a new obscure feature which will be rarely used and still won't cover many cases of free functions which don't use that feature. Just ask your IDE vendor to add search based on the first parameter. It shouldn't even be hard to add, since it would rely entirely on an already existing feature for inherent & trait methods. The only questions could be about UX and value add.

2 Likes

Yes @gdennie I'd love some concise syntax for extension traits that have only one implementation, and can infer the trait definition from that implementation. You do want some trait name for imports, maybe some control over trait params and bounds too. You often need multiple methods under one extensio trait too.

pub trait MyExtension<..trait_params..>
where ..bounds.. 
match;  // Infer the trait body from the only impl present in this crate

impl<..params..> MyExtension<..trait_params..> for ForeignType<..params..>
where ..bounds..
{ ..impl_body.. }

The trait body is inferred from the impl_body, probably everything is final of course.

You could've some more complex syntax that avoided repeating the trait_params or their bounds, but maybe that's unecessarily confusing, especially since such traits would rarely have any trait_params.

It's possible that inferring the trait body could become tricky here, given the different overlapping params, which maybe improved by some merged sysntax.