Is UFCS possible in Rust?

Would it be impossible to have UFCS in Rust?

Like this works

Trait::method(obj);

but this doesn’t

method(obj);

which is really annoying, in particular since this works

obj.method();

butt this doesn’t:

obj.Trait::method();

This asymmetries trigger my OCD, hard. Would this be purusable or would it break backwards compatibility and cannot be pursued?

(EDIT: This also triggers my OCD when replacing Trait with Type in all the cases above).

Trait::method(obj)

? :slight_smile:

Er, I misread a bit, but the equivalent to Trait::method(obj) is Type::method(obj), not just method(obj).

Yes, my OCD is also raised by the asymmetry between Type::method(obj) and obj.method() (You can replace Trait with Type in my OP). I’ve added a note at the end of the OP to reflect this.

What is exactly requested here? Is it about something similar to Java static imports?

Seems like this is orthogonal to UFCS which is (in Rust) about desugaring the method invoke syntax-sugar back to its actual function invoke syntax.

Basically, if a trait is in scope, I shouldn’t need to write Trait::method(obj) but method(obj) should be enough. In particular, given that obj.method() just works (and something like obj.Trait::method() is not required).

The only argument I’ve heard against this is that “OMG but then I don’t know if method(obj) is invoking a free function or not!” but to me it somehow seems that nobody complains about obj.method() with “OMG Rust is so confusing! I never know whether obj.method() is invoking a type’s method or a trait method!”.

If that were made to work, one thing one could do afterwards is allowing free functions to be called with method syntax, which would make Rust have UFCS. But I don’t care about this that much because Trait/type methods are much more powerful than free functions in Rust.

Let’s separate the two issues at hand.

The first I agree with, Rust could potentially relax the current restriction on “static” paths akin to what Java provides as explained in the link above. This is a minor usability improvement IMO and as such low priority.

The second, UFCS as in D, is an anti-pattern which I strongly disagree with. A free-function that was not designed to be used as a method must not be allowed to be called as such. Rust already has UFCS, the correct kind. C# extension methods are a disciplined example of that: the argument to be used as the “this” reference is explicitly marked as such. Rust has this inherently in the Trait system which is already has open-world semantics, thus doesn’t require to add more special purpose syntax like C# does.

Rust’s current syntax is very symmetric (more generally, “normal”), and I’m pretty confused by what you think the rules should be. I don’t know what obj.Trait::method() would mean.

We don’t allow you to use std::iter::Iterator::*; to get the map() in scope, but maybe we should.

3 Likes

@withoutboats What I meant is that if I have to qualify method in Trait::method(obj) because Trait cannot be deduced, I would expect to have to qualify it when I do obj.method() as well, obj.Trait::method() was just a strawman syntax for that.

However, since obj.method() "works" I really don't understand why method(obj) does not. In both cases the traits needs to be in scope so... it doesn't feel symmetric to me at all.


@yigal100

A free-function that was not designed to be used as a method must not be allowed to be called as such.

why?

Your mental model of how this works / should work is quite different from how Rust works.

Methods are a type directed syntax for name resolution; they look at the inherent impl of the type, as well as any trait in scope, in order to find the method. In addition, methods will perform automatic referencing and dereferencing in order to find the item. Once this type-directed resoution is done, a method is essentially translated into <$TYPE as $TRAIT>::method(receiver).

Free functions just do a simple scoped name resolution. Methods are about introducing a type-based resolution step prior to that name resolution.

The only reason you have to qualify trait functions is that you cannot import trait items directly into scope (map is not in scope, Iterator is in scope). I don’t know if there’s a reason we could never support importing the items from a trait, but today we don’t support it.

Do you happen to know why this is this way? It seems to me that the resolution performed by methods is more powerful. Would it break everything is free functions would use this as well?

Anyhow I would like Trait methods and type methods to be callable as free functions without qualification.

I find Trait methods in particular way more powerful than free-functions (and even more with specialization), and having to write a "boiler-plate" free function requiring the trait just to be able to call them without qualification seems unnecessary.

It would be very odd behavior IMO because the ref/deref dispatch we do only on the reciever, which has the keyword self. This is why self doesn't take a type and is a keyword in Rust and not a convention (like it is in Python).

The point you make about specialization is especially strong, sometimes we talk about specialization using free functions as examples, but free functions can't actually be specialized (well they can if you dispatch them to trait functions, but that's not a great solution).

From a high level, it certainly seems to me like this should work (whether or not you think its good style):

// import the items associated with Iterator into scope
use std::iter::Iterator::*;

fn foo(vec: Vec<i32>) -> Vec<i32> {
    collect(map(vec, |x| x * 2))
}

I don't know why that doesn't work today, I suspect one of two things: a) its just a legacy bug that it doesn't work, b) there are gnarly edge cases that we don't know how to resolve, so its an active choice not to make it work.

@scott brought up the other day that it would be nice to have zip as a free function in the prelude, so cases like this would be nicer:

for (x, y) in zip(xs, ys) {
   ...
}
3 Likes

Unfortunately it wouldn't work quite so directly for iterators. For example, map(vec, ...) fails because vec does not implement Iterator, only IntoIterator. In fact, a large part of the motivation for zip(a, b) is to change the first argument from Iterator to IntoIterator.

The idea of importing trait (or inherent impl?) methods as bare functions in general is an interesting one, though. I've been pondering the inconvenience involved with specializing a top-level function lately, too.

5 Likes

A free-function that was not designed to be used as a method must not be allowed to be called as such.

Why?

Just as a data point, there are languages where it is totally normal to perform method dispatch on the first argument to (what looks like) a free function. R’s “S3” and “S4” object systems are probably the most prominent examples. (From the user-of-object-libraries-someone-else-wrote perspective, the main difference is that in S4 you can have multiple dispatch on arbitrary combinations of arguments.)

It should be said that R is a functional language at heart and these “object systems” might be better described as “dynamically typed, runtime dispatched generic function systems”. But Rust is also much more about the generic functions than the class hierarchies, so maybe the shoe fits?

Methods are a special case of functions as they have a designated main receiver. Which is the main receiver in a symmetric binary function? IMO, it simply is incorrect to allow the main.method(secondary_arguments) syntax for all functions. For instance, a.max(b) doesn’t make sense to me and shouldn’t be allowed.

Before anyone else mentions, yes, I’m aware of Rust’s design of binary Ops and some iterator combinators that already violate this. I still find it weird that Rust has a.zip(b). Btw, there is a proposal to add the functional form to the prelude.

Other behaviors that method syntax allows such as implicit autoderef are IMO a separate concern. We can debate separately if we want to extend that somehow to regular arguments and other places that might benefit from it.

Only if you assign them this meaning. For example I implement some functions as trait methods just because trait methods allows me to specialize them, or some functions as type methods because that allows me to overload them. Nothing more, nothing less.

We also treat the receiver differently in a number of ways - autoreferencing & dereferencing, lifetime elision, etc. It doesn’t make sense to me for us to allow method syntax for a function in which these properties don’t hold for the ‘psuedo-receiver.’

1 Like

Sure. I can see why it could be a bad idea to treat the first argument of a free function this way, but:

  • why can't all arguments be treated this way?

  • why can't the first argument of a free function be treated this way when using with method syntax only?

Anyhow, I do not think being able to call free functions as methods in rust would really add that much to the language since type/trait methods are more powerful, but being able to call trait methods as free functions when they are in scope would be nice (and if both a trait method and a free function that match are in scope I just expect an ambiguity error and having to manually qualify what i mean).