October 19, 2022

On Wednesday, 19 October 2022 at 17:46:15 UTC, Paul Backus wrote:

>

You have this completely backwards. A derived class may remove @mustuse, but can never add it.

look like I didn't get a good sleep last night :-)

As to the two propagation directions, if we analyzed it individually, H. S. Teoh's post has covered both the scenarios.

But I believe you got my point: we need global system propagation here as I showed in the previous post:

if you think about the global system analysis as a whole, it will work, and consistently. Yes, what I proposed is transitive closure:

/--------Base------\
|        |         |

Derived1 Derive2 Derive3
| | |
GrandDr1 DrandRr2 GradDr3
|
....

If the programmer only manually marked Derive3.remove() as @mustuse, everything else (Base, Derived1 Derive2 Derive3, GrandDr1 DrandRr2 GradDr3, ...
)'s .remove() method will become @mustuse (as seen by the compiler internally).

October 19, 2022
On Wed, Oct 19, 2022 at 05:40:00PM +0000, mw via Digitalmars-d wrote:
> Right now in this discussion thread there are two aspects:
> 1) the language logic, and
> 2) the implementation logic (and current status, including pre-built
> binaries).
> 
> I'm not interested in 2). We need to first make the language logic correct.
> 
> > If indeed @mustuse's purpose is to prevent accidentally discarding
> > important return values, i.e., something marked @mustuse must
> > *never* be silently dropped, then neither (1) nor (2) is acceptable.
> 
> That's because you still consider the @mustuse attribute on each class level individually, if you think about the global system analysis as a whole, it will work, and consistently. Yes, what I proposed is transitive closure:
> 
>     /--------Base------\
>     |        |         |
> Derived1   Derive2  Derive3
> 
> If the programmer only manually marked Derive3.remove() as @mustuse,
> everything else (Base, Derived1   Derive2  Derive3)'s .remove() method
> will become @mustuse (as seen by the compiler internally).

This means Derived1.remove and Derive2.remove are implicitly @mustuse. This is bad design; a programmer looking at the code for Derived1 cannot be expected to know that it is @mustuse.  It introduces a discrepancy between the surface source code vs. its semantics. Or, put another way, Derived1.remove has an "invisible" @mustuse that isn't represented in the source code, and that the programmer cannot possibly know about unless he looks at *all* instances of .remove in the entire class hierarchy.

The compiler must enforce that Derived1.remove and Derive2.remove are explicitly marked @mustuse.  IOW, it must be a compile error for some instances of .remove to be @mustuse and others not.


[...]
> > > That is why I say:
> > > 
> > > if a method is marked as @mustuse anywhere in the inheritance tree, that method in all the class levels in the whole inheritance tree became @mustuse!
> > 
> > This is correct.  But it cannot be implicit; if you attribute a method @mustuse somewhere in the inheritance tree, then you must also write @mustuse in every other class in the hierarchy. Otherwise it's not enforceable.
> 
> I'm glad you agree with my global analysis as a whole view here.
> 
> (I want the compiler to propagate and inject such attribute is only to save the programmers manual work, by no means it means it's implicit.

Of course it does.  How is it not implicit when Derived1.remove is *not* marked @mustuse in the source code, but is treated by the compiler as @mustuse?  The fact that what's in the source code doesn't correspond to the compiler's internal representation, is the definition of "implicit".

It saves a few keystrokes but introduces long-distance implicit relationships between distant pieces of code (i.e., Derive3.remove being marked @mustuse implicitly causes Derived1.remove to be @mustuse, but the latter is not represented in the source code). This makes the code hard to understand and reduces maintainability.

Rather, the correct approach should be that all instances of .remove should be *explicitly* marked @mustuse in the source code. It should be a compile error for some instances of .remove to be marked @mustuse and others not marked.


T

-- 
Computerese Irregular Verb Conjugation: I have preferences.  You have biases.  He/She has prejudices. -- Gene Wirchenko
October 19, 2022
On Wednesday, 19 October 2022 at 17:40:00 UTC, mw wrote:
> That's because you still consider the @mustuse attribute on each class level individually, if you think about the global system analysis as a whole, it will work, and consistently. Yes, what I proposed is transitive closure:
>
>     /--------Base------\
>     |        |         |
> Derived1   Derive2  Derive3
>
> If the programmer only manually marked Derive3.remove() as @mustuse, everything else (Base, Derived1   Derive2  Derive3)'s .remove() method will become @mustuse (as seen by the compiler internally).

The fundamental problem with this on a conceptual level (leaving aside implementation issues) is that it completely breaks modularity.

Suppose we have the following module layout:

--- base.d
module base;

class Base {
    int fun() {...}
}

void doStuff(Base b)
{
    import std.stdio;

    writeln("Calling b.fun");
    b.fun(); // ok - Base.fun is not @mustuse
}

--- derived.d
module derived;

import base;

class Derived : Base {
    int fun() {...}
}
---

If I now decide to add @mustuse to Derived.fun, in the "derived" module, and we apply your proposed global analysis, this will cause a compilation error in the "doStuff" function in the "base" module!

Note that the "base" module does not have any explicit dependency on the "derived" module. It does not import it, or otherwise refer to it in any way. In the real world, these two modules might even be in separate dub packages.
October 19, 2022
On Wednesday, 19 October 2022 at 18:04:54 UTC, Paul Backus wrote:
> On Wednesday, 19 October 2022 at 17:40:00 UTC, mw wrote:
>> That's because you still consider the @mustuse attribute on each class level individually, if you think about the global system analysis as a whole, it will work, and consistently. Yes, what I proposed is transitive closure:
>>
>>     /--------Base------\
>>     |        |         |
>> Derived1   Derive2  Derive3
>>
>> If the programmer only manually marked Derive3.remove() as @mustuse, everything else (Base, Derived1   Derive2  Derive3)'s .remove() method will become @mustuse (as seen by the compiler internally).
>
> The fundamental problem with this on a conceptual level (leaving aside implementation issues) is that it completely breaks modularity.
>
> Suppose we have the following module layout:
>
> --- base.d
> module base;
>
> class Base {
>     int fun() {...}
> }
>
> void doStuff(Base b)
> {
>     import std.stdio;
>
>     writeln("Calling b.fun");
>     b.fun(); // ok - Base.fun is not @mustuse
> }
>
> --- derived.d
> module derived;
>
> import base;
>
> class Derived : Base {
>     int fun() {...}
> }
> ---
>
> If I now decide to add @mustuse to Derived.fun, in the "derived" module, and we apply your proposed global analysis, this will cause a compilation error in the "doStuff" function in the "base" module!
>
> Note that the "base" module does not have any explicit dependency on the "derived" module. It does not import it, or otherwise refer to it in any way. In the real world, these two modules might even be in separate dub packages.

Yes, I know that. But this in my view is still a compiler implementation issue: even in separate dub packages, as long as the compiler visit that package, it need to propagate the attribute to that package's relevant class.


October 19, 2022
On Wednesday, 19 October 2022 at 18:01:42 UTC, H. S. Teoh wrote:
> On Wed, Oct 19, 2022 at 05:40:00PM +0000, mw via Digitalmars-d wrote:
> It saves a few keystrokes but introduces long-distance implicit relationships between distant pieces of code (i.e., Derive3.remove being marked @mustuse implicitly causes Derived1.remove to be @mustuse, but the latter is not represented in the source code). This makes the code hard to understand and reduces maintainability.
>
> Rather, the correct approach should be that all instances of .remove should be *explicitly* marked @mustuse in the source code. It should be a compile error for some instances of .remove to be marked @mustuse and others not marked.

Conceptually I agree  about the *explicitly* part. My only concern is that as in Paul Backus' example the classes are in separate dub packages that the programmer has *no* control with, e.g. the programmer decided to derived from one of the unmarked std.lib class, and added @mustuse mark to a method in his derived class, s/he will not be able to manually change the std.lib class source code to add @mustuse explicitly.

But for the @mustuse logic to be correct, the compiler need to internally propagate to the base std.lib classes.

And yes, I know this global analysis view will have conflict with the current compilation & build process.

October 19, 2022
On Wednesday, 19 October 2022 at 18:16:39 UTC, mw wrote:
> Yes, I know that. But this in my view is still a compiler implementation issue: even in separate dub packages, as long as the compiler visit that package, it need to propagate the attribute to that package's relevant class.

My point is that, even if we assume it is possible to implement, it is still a bad idea, because it would introduce implicit coupling between modules where no coupling (explicit or implicit) previously existed.

I hope I do not have to explain to you why implicit coupling is a bad thing in software development.
October 19, 2022
On Wed, Oct 19, 2022 at 06:25:54PM +0000, Paul Backus via Digitalmars-d wrote:
> On Wednesday, 19 October 2022 at 18:16:39 UTC, mw wrote:
> > Yes, I know that. But this in my view is still a compiler implementation issue: even in separate dub packages, as long as the compiler visit that package, it need to propagate the attribute to that package's relevant class.
> 
> My point is that, even if we assume it is possible to implement, it is still a bad idea, because it would introduce implicit coupling between modules where no coupling (explicit or implicit) previously existed.
> 
> I hope I do not have to explain to you why implicit coupling is a bad thing in software development.

If we really wanted to enforce @mustuse across dub packages, the solution is to include it in the mangled type. That will force a link error when methods don't match up.

Of course, changing mangling will also cause breakage of existing code. :-D


T

-- 
Век живи - век учись. А дураком помрёшь.
October 19, 2022
On Wednesday, 19 October 2022 at 18:25:54 UTC, Paul Backus wrote:
> On Wednesday, 19 October 2022 at 18:16:39 UTC, mw wrote:
>> Yes, I know that. But this in my view is still a compiler implementation issue: even in separate dub packages, as long as the compiler visit that package, it need to propagate the attribute to that package's relevant class.
>
> My point is that, even if we assume it is possible to implement, it is still a bad idea, because it would introduce implicit coupling between modules where no coupling (explicit or implicit) previously existed.

That's because now we want to add @mustuse half-way, and it requires global system analysis. If D started from scratch as Eiffel did, and enforces all query methods are @mustuse from the very beginning, such problem and remedy headache would never existed.

So now we need to balance either we want prevent accidentally (but fatal in most cases) discarding function returns, or we want prevent more coupling between modules.

For me, in this particular situation, I prefer the former which I think will make the code more robust.


October 19, 2022
On Wednesday, 19 October 2022 at 18:25:54 UTC, Paul Backus wrote:
> because it would introduce implicit coupling between modules where no coupling (explicit or implicit) previously existed.

How is this any different than inheritance of attributes like @safe, which exists now?

(i would argue the difference is that applies to the parameter and this applies to the return value, and you can normally strengthen params and weaken return contracts so it is kinda backward. but that's separate from the coupling thing... kinda, i guess the other difference is safe and friends are of most restriction to the method implementor, so if it is inherited the author of the class just deals with it where mustUse would be of most restriction to the method caller. but i gotta run to a meeting and haven't thought it all through)
October 19, 2022
On Wednesday, 19 October 2022 at 18:45:40 UTC, Adam D Ruppe wrote:
> On Wednesday, 19 October 2022 at 18:25:54 UTC, Paul Backus wrote:
>> because it would introduce implicit coupling between modules where no coupling (explicit or implicit) previously existed.
>
> How is this any different than inheritance of attributes like @safe, which exists now?

The difference between @mustuse and @safe is that adding @safe imposes additional restrictions on the *function*, but adding @mustuse imposes additional restrictions on the *calling code*.

Another way to think of it is: @safe is like an "out" contract, and @mustuse is like an "in" contract.

Derived classes are allowed to weaken in contracts and strengthen out contracts, but not the reverse. By the same logic, derived classes are allowed to remove @mustuse and add @safe, but not the reverse.