[Issue 24004] UFCS is not uniform/universal (enough)

d-bugmail at puremagic.com d-bugmail at puremagic.com
Wed Jun 21 14:24:41 UTC 2023


https://issues.dlang.org/show_bug.cgi?id=24004

--- Comment #2 from Bolpat <qs.il.paperinik at gmail.com> ---
> I can see the principle of this, however, this comes in stark
> contradiction on how module visibility works.

It has nothing to do with visibility. `private` non-members won’t work, same as
private member functions don’t work.

> I can think of a few situations where implementing such a thing
> will lead to ambiguities, such as if `mem` is also defined in
> the importer module.

This is already solved: The same problem exists when importing a module (as a
whole) and having a local symbol with the same name. It’s two different
overload sets (when there’s a match in both of them, it’s an error).

> Of course, this can be solved by adding some extra priority rules,
> but this is just one example.

The rules already exist.

> There are probably other cases and each of these require extra logic
> to treat them. Bottom line, I don't think that the extra complexity
> is worth it to add such a feature for which I don't see any big benefit;

The benefit is that UFCS can be used in templates. This *is* a big benefit.

> simply importing whatever function you want to use is a much
> cleaner approach

I argued why that cannot be done “simply” in templates. You need some
non-trivial metaprogramming to extract the module of a type to then import it.

> and keeps you on the safe side given that people rarely encapsulate
> things at the module level in a structured manner.
> I mean, it would be really weird if you had a function that is not
> specifically imported but used via UFCS because some other
> functionality is imported in the module;

I don’t really get it. A UFCS call is syntactically indistinguishable to a
member function call; I have no idea how one looks at `obj.mem` and think
“Where did `mem` come from?” because it could be a member function of the type.
If it happens to be a non-member of the same module the type comes from, what’s
the deal?

I’m not suggesting that because an expression of type `T` is somewhere in your
function that is equivalent to importing the module `T` is in. I’m only
suggesting that `obj.mem` should be able to resolve to a non-member function
inside the module `typeof(obj)` is in.

Via template type parameters (or alias parameters), you can have access to a
type without importing anything. If you think of modules as the units of
implementation, you now have partial access to the implementation.
Because of UFCS, the question whether `obj.mem` calls a member function or a
non-member function of `typeof(obj)`’s module is an implementation detail when
you did `import mod;`, but it needlessly matters when you have access to the
type by other means, including, but not exclusive to, `import mod : SomeType;`.

The primary use-case is not `import mod : SomeType;`. In this case, you’re 100%
right in that one can simply use `import mod;` or `import mod : SomeType,
nonmem;`. There’s nothing simple for a template. Even if we had a primitive,
say `module(symbol)` that returns the module of a symbol, so that one could
write `import module(T);` for a type parameter `T`, that is something one
should not have to remember to do when writing a template. It doesn’t work for
sub-expressions etc.; to be on the safe side, you’d have to `import
module(typeof(…));` a lot. And it’s not even right because that actually does
bring all the members of all the modules in scope at once.

> someone might end up moving the function to another file and then
> you get confusing error messages.

Move something out of a module (without public-importing it back) leads to
breakage.

---

There’s things only member functions can do. (In the case of classes, this is
totally obvious, just take `virtual` and inheritance.) Specifically for a
`struct`, a member function takes `this` as a reference even if it’s an rvalue.
As of now (without -preview=rvaluerefparam), a non-member cannot do that.

There’s things only non-member functions can do:
  - Take the argument by pointer.
  - Be a template w.r.t. the type of the first parameter
  - Infer the value category of the first parameter and forward.
  - Overload based on the value category of the first parameter.
  - Only allow lvalues for the first parameter.
  - Only allow rvalues for the first parameter.
  - Have the first parameter be `in` or `out`.
  - Be defined in antoher module and be publically imported.
(The list is probably incomplete.)

The Rationale in [Uniform Function Call Syntax
(UFCS)](https://dlang.org/spec/function.html#pseudo-member) says:
> This provides a way to add external functions to a class as if they
> were public final member functions. This enables minimizing the
> number of functions in a class to only the essentials that are needed
> to take care of the object's private state, without the temptation
> to add a kitchen-sink's worth of member functions.

This can be read as advertising what C# calls extension methods, or it can be
read as encouraging to put functionality of classes not in member functions but
in non-member functions if it makes sense.

--


More information about the Digitalmars-d-bugs mailing list