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

d-bugmail at puremagic.com d-bugmail at puremagic.com
Thu Jun 22 07:07:31 UTC 2023


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

--- Comment #3 from RazvanN <razvan.nitu1305 at 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.

--


More information about the Digitalmars-d-bugs mailing list