Postfix functions

Rust seems to have two kinds of functions. I can write either f64::sin(x) or x.sin(). However, if I define a freestanding function fn f(x: f64, ...) I cannot invoke f with x.f(...). On the other hand, different types can have methods with the same name, but I cannot overload freestanding functions (and have no desire to). I can sort of make new postfix functions that by creating a trait and implementing it for f64 (or whatever the type of the first argument is) and this seems to be a well-known pattern, especially to make more methods on iterators or Result objects.

I am not proposing an alternative but I'd like to understand why traits are the only way to make new postfix functions. Are there technical reasons why (like something would break) or perhaps taste? (The fact that you have to jump through hoops to make postfix functions suggests that you should do it sparingly.)

I don't know the answers to your questions, but a relevant term, for a hypothetical ability to use any function as postfix, is "UMCS", which stands for "universal/uniform/unified method call syntax".

It formerly was called "UFCS" ("F" for "function"), but it was realized that this properly means the opposite: the ability to call a method without method syntax (e.g., Into::into(x) rather than x.into()).

They're not.

If you mean for foreign types, it's similar reasoning as the orphan rules: If downstream crates could define inherent functions, the owning crate defining inherent functions or methods is much more likely to be a significantly breaking change, especially if such extensions could be utilized by even further downstream crates.

Or alternatively, coherence becomes even more complicated, and ambiguous when two upstream crates define the same inherent method on a diamond grandparent type, say.

(In case you didn't know, an inherent method takes precedence over a trait method with the same signature. And there's nothing wrong with creating your own trait and defining it for foreign types.)

3 Likes

Yes, I was talking about foreign types. I'm not talking about new inherent functions, just functions that could be called with the syntax x.f(...) as an alternative to f(x, ...). As I see it, the difference is that importing a type brings all the inherent methods into scope as well.

Say I have a crate that implements the pnorm function from R in Rust and I want users to be able to call it with the same notation Rust uses for f64 methods. To be able to write x.pnorm() users would have to import a trait with this function that included this as a method. How is this less breaking than users having to import a function named pnorm?

I understand how to implement a trait for foreign types. It is just a lot of typing, so I'd like to understand if there's some deep reason why an alternative is a bad idea.

Seems to me that what you want then is to wrap (a raw ptr to) each foreign type in a newtype, and then write inherent methods for the newtype that delegate to the fns defined on the foreign type. When you do that, you immediately get the method calling syntax for free.

You'll want to use newtypes anyway to create a safe Rust API to the foreign code, since calling foreign fns in Rust in unsafe and thus unergonomic on its own.

I'm not sure exactly what scenario you're talking about, but I meant the ability of the owner of the type to add methods to their own type (and to have that take precedence over trait methods).

Without another level of precedence, if you could define an inherent method, and then core did too, nothing using your crate could compile anymore (or coherence would be broken).

Even with another level of precedence (type owner before others), if you and Pat define an inherent method with the same name in your respective crates, it's impossible for anyone downstream to use both your crate and Pat's crate (or coherence would be broken).


In contrast, with traits the analogous situations may result in an ambiguity error,[1] but not an actual conflict (broken coherence). The ambiguity can be worked around by specifying which trait you meant, for example.

let r = <f32 as pats_crate::F32Ext>::rpath(value);

One way to think of inherent methods is as an implementation of an inherent trait on the type. One could also imagine giving downstream the ability to do the same, except the inherent traits differ per crate.

However, this would be something new to the language, and would also need some new way to disambiguate between the implementations of different crates...

// Spitballing
let r = <f32 in pats_crate>::rpath(value);

...and I'm not sure how open the wider community would be to allowing inherent methods to be ambiguous in the first place.

Also to be determined, how would these be imported? Searching your entire dep graph whenever an f32 is encountered isn't feasible. So there's probably no real benefit over importing a trait on the consuming side, and just less typing on the method implementation side.


  1. or the inherent method will take precedence ↩ī¸Ž

3 Likes

Okay, thanks for explaining this so thoroughly.

2 Likes

There are various crates (e.g. ext-trait) dedicated to allowing you to define a lightweight extension trait with less overhead. ext-trait and all other helper crates I've seen use an impl block to mimic inherent impls and define a named trait to import, but it's possible to instead define a free function and trait with the same name, so importing the item imports both and makes both free function and method usable. (Doing so is generally unexpected and likely to surprise and/or confuse your downstream users, though.)

As quinedot mentioned, it likely won't ever be possible to add inherent methods directly to an upstream type without using an extension which needs to be imported. However, "UMCS" where it's possible to call free functions with method syntax (still requiring an import) or a lighter weight opt-in version of extension functions (e.g. fn pnorm(self: f32)) may still happen someday.

2 Likes

Do you think defining more postfix functions with traits is a good idea? Is the fact that you have to do extra work a nudge from the language suggesting not to do it?

Imo it's more a push to consider the additional cognitive cost than just not to do it. (Rust heavily prefers method syntax, what with autoref etc.) If someone has a value of some type, it can usually be assumed they have some familiarity with its inherent methods. If method syntax isn't one of those, though, it could potentially come from any trait in scope. Additionally, if the upstream type adds a method with that name in the future, it'll shadow your method.

It's more heavyweight to define an extension not because it's discouraged, but because it's a more interesting thing to do. There's more to consider when defining a trait than a function. And a trait can do a lot more than a single extension method for a single impl header.

Also, if you don't seal your trait somehow, downstream could implement it for their types as well, making adding new required methods breaking... builtin #[sealed] attribute when :point_right::point_left:

2 Likes

Unironically as soon as I get around to rebasing a PR. It's already RFC accepted and implemented on my fork :slight_smile:

4 Likes

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