Thread overview
[Issue 24004] UFCS is not uniform/universal (enough)
Jun 21, 2023
Bolpat
Jun 21, 2023
RazvanN
Jun 21, 2023
Bolpat
Jun 22, 2023
RazvanN
Jun 23, 2023
Walter Bright
Jun 23, 2023
Dennis
June 21, 2023
https://issues.dlang.org/show_bug.cgi?id=24004

Bolpat <qs.il.paperinik@gmail.com> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
                 CC|                            |qs.il.paperinik@gmail.com

--
June 21, 2023
https://issues.dlang.org/show_bug.cgi?id=24004

RazvanN <razvan.nitu1305@gmail.com> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
                 CC|                            |razvan.nitu1305@gmail.com

--- Comment #1 from RazvanN <razvan.nitu1305@gmail.com> ---
I can see the principle of this, however, this comes in stark contradiction on how module visibility works. 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. Of course, this can be solved by adding some extra priority rules, but this is just one example. 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; simply importing whatever function you want to use is a much cleaner approach and keeps you on the safe side given that people rarely encapsulate thinks 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; someone might end up moving the function to another file and then you get confusing error messages.

--
June 21, 2023
https://issues.dlang.org/show_bug.cgi?id=24004

--- Comment #2 from Bolpat <qs.il.paperinik@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.

--
June 22, 2023
https://issues.dlang.org/show_bug.cgi?id=24004

--- Comment #3 from RazvanN <razvan.nitu1305@gmail.com> ---
(In reply to Bolpat from comment #2)


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

Actually it does, since some functions become visible whereas in the past they weren't.

> 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).
> 

Note that this is not true: if a local symbol matches, the imports are no longer searched, irrespective if the call is correct or not.

Anyway, I was referring to the situation where the symbol in the same module does not match, but the one in the imported module does. Right now, if you have module a that defines mem and import module b in its entirety and b also defines mem, assuming you are calling mem so that b.mem matches but a.mem does not you would get an error saying that a.mem cannot be called. With your proposition, if you import a symbol from b, b.mem would become available via UFCS.

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

No, as pointed out by my previous point they don't. You would have to define the priority or at least see if the overload sets are merged or not.

> 
> > 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.
> 

What do you mean? UFCS can already be used with templates:

```
void fun(T)(T a) {}


void main()
{
    2.fun();
}
```

I'm probably misunderstanding your point, but as I see it, the proposal just wants to make some symbols visible if those are used via UFCS, I don't see how templates are affected by this. If you are using a type and a function that works on a type, just import them both. If you forget to import the function that is used on the type, ideally a template constraint will catch that:

```
void fun(T)(T a)
if (__traits(compiles, a.mem()))
{
    a.mem();
}
```

It's up to the user of the templated function to provide whatever context is needed.

> > 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.
> 

Yes, but I would argue that this is bad design. The implementer of the template should make sure that the context is provided via template constraints. The user of the template should provide the context.

> > 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.
> 

I understand, I just don't see the benefit of implementing this when you can simply import the symbol if you need it. I don't see it as a convenience feature, rather than a special case of UFCS that you need to explain to newcomers. Right now, the explanation is trivial: "If you use a selective import, you essentially import the designated symbol". With this proposal, this becomes: "If you selectively import a symbol, you have access to that symbol and to any other function (or just to functions that take a parameter of type typeof(symbol) as the first parameter?) provided that the function is called via UFCS". To me, that's justs special casing that uglifies the language for a benefit that is easily obtainable with the current semantics.

> 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.
> 

My opinion is that templates should not have to import modules to be able to call non-member functions on types. It's the burden of the caller to make sure the context is right.


> > 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.

I don't see the benefit of adding this to the language, but if you manage to convince Walter & Atila then maybe this will have a chance at being implemented.

--
June 23, 2023
https://issues.dlang.org/show_bug.cgi?id=24004

Walter Bright <bugzilla@digitalmars.com> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
                 CC|                            |bugzilla@digitalmars.com

--- Comment #4 from Walter Bright <bugzilla@digitalmars.com> ---
Thank you for the interesting proposal. It does have some merit, but I have reservations.

In particular, what symbols get found and do not get found in a lookup has been the topic of several very heated debates. People have very different ideas about what is "intuitive" behavior and what isn't. For example, when and how local imports are searched. Everybody thinks their scheme is obvious and the others are demented.

Then, we went and implemented "alias this" without thoroughly understanding what it means. And now we're stuck with some odd behaviors when different lookup schemes intersect. We can't fix it because too much existing code has settled into relying on its current behavior.

People do not have a clear idea how things are looked up, much like in C++ nobody can explain how the overload rules work. Not even me, and I implemented them correctly. What people do is try random things until the overload they wanted is selected.

We are already well down that path.

This makes me very trepidatious about adding new overloading behaviors. Note that the proposed behavior may break existing code, and since people likely already just tried things until it worked, they may be quite baffled as to how to fix their code.

So I'm reluctantly going to say no, unless some really strong, very compelling use case comes to the fore.

I do appreciate your efforts, though. Proposals like yours are what makes developing new programming paradigms fun! Thank you.

--
June 23, 2023
https://issues.dlang.org/show_bug.cgi?id=24004

Dennis <dkorpel@live.nl> changed:

           What    |Removed                     |Added
----------------------------------------------------------------------------
                 CC|                            |dkorpel@live.nl

--- Comment #5 from Dennis <dkorpel@live.nl> ---
This reminds me of openmethods: https://github.com/jll63/openmethods.d

Though I'm not sure it solves the import problem, it has been a while since I saw the "Open Methods for D" Dconf 2018 talk.

--