Thread overview
What to do when you want to add attributes to your class functions?
Sep 16, 2022
RazvanN
Sep 16, 2022
Kagamin
Sep 16, 2022
RazvanN
Sep 16, 2022
Kagamin
Sep 16, 2022
Kagamin
Sep 16, 2022
Adam D Ruppe
Sep 16, 2022
RazvanN
Sep 16, 2022
Adam D Ruppe
Sep 16, 2022
Dukc
Sep 17, 2022
H. S. Teoh
September 16, 2022

Let's say you have a library class that exposes some methods. At the beginning you did not focus on the safety, nogcness etc. of your methods, you just wanted to have something working. You publish your class, people start using it and now you are in a phase where you would like your methods to be callable from attributed contexts. So you go on and make your code safe, nothrow, nogc, pure. However, now you are faced with a large breaking change because suddenly you force your users to respect these attributes. This seems like a severe limitation to me. Morever, look at this code:

class Test
{
    void fun() @safe {}
}

class TestDerived : Test
{
    override void fun()
    {
        int a;
        int *b = &a;
    }
}

You get: (11): Error: cannot take address of local ain@safefunctionfun``. The override is not even marked as @safe and the compiler does not indicate that the function is @safe because it overrides a safe function. Anyway, I digress.

The point is, there is no way for a library owner to satisfy both people that want attributes and people that don't. If you do not put attributes on your code from the beginning, users of your class will not be able to call the super method from an attributed scope. If you put attributes on your methods, there is no way to bypass them in your overrides. I understand that if we would allow to relax the restrictions in overrides that would lead to the ability of calling unsafe code from safe contexts, but the current design makes it cumbersome to use classes with attributes. It just seems simpler to never use attributes at all (with classes).

Now, the entire point of this post is to ask what should we do in the case of: https://github.com/dlang/dmd/pull/14432. The story is: the methods of core.sync.condition can be made @nogc; the only reason why we did not do that is because we would throw some SyncErrors (which are allocated with the gc); the fix is trivial, we just use an abort function that does not throw anything, it just ends execution. However, now, if we mark the methods as @nogc we break downstream code (phobos as well) because suddenly those methods need to abide to new restrictions. How do we get out of this stale mate?

Cheers,
RazvanN

September 16, 2022

On Friday, 16 September 2022 at 09:25:58 UTC, RazvanN wrote:

>

Now, the entire point of this post is to ask what should we do in the case of: https://github.com/dlang/dmd/pull/14432. The story is: the methods of core.sync.condition can be made @nogc; the only reason why we did not do that is because we would throw some SyncErrors (which are allocated with the gc)

Are those really Errors? Then throw one statically allocated instance with generic message.

September 16, 2022

On Friday, 16 September 2022 at 11:33:13 UTC, Kagamin wrote:

>

On Friday, 16 September 2022 at 09:25:58 UTC, RazvanN wrote:

>

Now, the entire point of this post is to ask what should we do in the case of: https://github.com/dlang/dmd/pull/14432. The story is: the methods of core.sync.condition can be made @nogc; the only reason why we did not do that is because we would throw some SyncErrors (which are allocated with the gc)

Are those really Errors? Then throw one statically allocated instance with generic message.

That is not the problem. That has been taken care of. The issue is that users of Condition now have to abide by the rules of nogc.

September 16, 2022

Overridable functions there forward to templated functions:

void notify()
{
	notify!(typeof(this))(true);
}
void notify() shared
{
	notify!(typeof(this))(true);
}

So you make the templated function @nogc and add a @nogc method:

void notify()
{
	notify!(typeof(this))(true);
}
void notify() shared
{
	notify!(typeof(this))(true);
}
void notify2() shared @nogc
{
	notify!(typeof(this))(true);
}
September 16, 2022
On Friday, 16 September 2022 at 09:25:58 UTC, RazvanN wrote:
> However, now you are faced with a large breaking change because suddenly you force your users to respect these attributes. This seems like a severe limitation to me.

It is *absolutely* necessary to maintain the integrity of interfaces.

see http://dpldocs.info/this-week-in-d/Blog.Posted_2022_07_11.html#run-time-dispatch for some tangentially related discussion

> The override is not even marked as @safe and the compiler does not indicate that the function is @safe because it overrides a safe function.

The error message could perhaps point it out, but this behavior is also quite useful, letting implementations usually just work.


> The point is, there is no way for a library owner to satisfy both people that want attributes and people that don't.

You could have two branches of the interface. It is always allowed to add restrictions to child classes, so you could have

class Base { void foo(); }
class StrictBase : Base { void foo() @nogc pure etc; }

then people use those. Or, you can have

class Base {
   int foo_strict() @nogc pure etc {}
   final void foo() { }
}

Though that gets weird if you wanted to override the other one i still use final on purpose here cuz that's less weird than having both and needing to choose one.

> It just seems simpler to never use attributes at all (with classes).

Or never use the attributes at all with anything. They don't actually provide much value anyway.

> However, now, if we mark the methods as @nogc we break downstream code (phobos as well) because suddenly those methods need to abide to new restrictions.

In this case, either the overload or an outright breaking change is the right thing to do. I find it very unlikely these are actually overridden often. But the possible breaking of error handling needs to be considered regardless of attributes.
September 16, 2022
On Friday, 16 September 2022 at 12:42:40 UTC, Adam D Ruppe wrote:
> On Friday, 16 September 2022 at 09:25:58 UTC, RazvanN wrote:
>> However, now you are faced with a large breaking change because suddenly you force your users to respect these attributes. This seems like a severe limitation to me.
>
> It is *absolutely* necessary to maintain the integrity of interfaces.
>

I completely understand why that is the case, it's just that it makes it
difficult to use both features together.

> see http://dpldocs.info/this-week-in-d/Blog.Posted_2022_07_11.html#run-time-dispatch for some tangentially related discussion
>
>> The override is not even marked as @safe and the compiler does not indicate that the function is @safe because it overrides a safe function.
>
> The error message could perhaps point it out, but this behavior is also quite useful, letting implementations usually just work.
>
>
>> The point is, there is no way for a library owner to satisfy both people that want attributes and people that don't.
>
> You could have two branches of the interface. It is always allowed to add restrictions to child classes, so you could have
>
> class Base { void foo(); }
> class StrictBase : Base { void foo() @nogc pure etc; }
>
> then people use those. Or, you can have
>
> class Base {
>    int foo_strict() @nogc pure etc {}
>    final void foo() { }
> }
>

This works under the assumption that you have already created your
code with a thought to attributes. However, there is a ton of code
that is not attributed but could be.

> Though that gets weird if you wanted to override the other one i still use final on purpose here cuz that's less weird than having both and needing to choose one.
>
>> It just seems simpler to never use attributes at all (with classes).
>
> Or never use the attributes at all with anything. They don't actually provide much value anyway.

Reasonable people could disagree.

>
>> However, now, if we mark the methods as @nogc we break downstream code (phobos as well) because suddenly those methods need to abide to new restrictions.
>
> In this case, either the overload or an outright breaking change is the right thing to do. I find it very unlikely these are actually overridden often. But the possible breaking of error handling needs to be considered regardless of attributes.

The overload does not seem like a good solution to me. Right now I am making
the functions nogc, but what if in the future we could make them @safe (it's
just for illustrative purposes, I don't know if any of those functions could
be made @safe)? We create yet another overload?

September 16, 2022
On Friday, 16 September 2022 at 13:02:44 UTC, RazvanN wrote:
> what if in the future we could make them @safe
> We create yet another overload?

Yes. You'll probably want to think it through to try to get it right the first time (and not add new attributes to the language without good reason).

You are publishing an interface with certain restrictions. You need to decide what those restrictions are or be willing to make breaking changes.

In this case, I really do think the breaking change is the right thing to do. Quite unlikely the addition of `@nogc` here will *actually* break anything - a search of the dub code might be able to give stronger evidence for this too. But changing an exception to a different mechanism, including a different allocation, might break things in practice so i really expect that's where you should be focusing...
September 16, 2022

To reduce template bloat declare implementation nontemplated:

void notify()
{
	shared me=cast(shared)this;
	me.notifyImpl();
}
void notify() shared
{
	notifyImpl();
}
void notify2() shared @nogc
{
	notifyImpl();
}
private void notifyImpl() shared @nogc
{
	code...
}
September 16, 2022

On Friday, 16 September 2022 at 09:25:58 UTC, RazvanN wrote:

>

Let's say you have a library class that exposes some methods. At the beginning you did not focus on the safety, nogcness etc. of your methods, you just wanted to have something working. You publish your class, people start using it and now you are in a phase where you would like your methods to be callable from attributed contexts. So you go on and make your code safe, nothrow, nogc, pure. However, now you are faced with a large breaking change because suddenly you force your users to respect these attributes.

As with anything, it's about finding the most practical solution overall for everyone involved. Unless you're publishing low-level system code (not very likely with classes), the usual cost-benefit radio from best to worst is IMO @safe, pure, nothrow, @nogc.

Considering a library done from stratch, I'd say that most abstract functions should have first two of those attributes but not the last two. But if you need to consider backwards compatibility, one option to consider is going only with @safe. Less breakage than with @safe pure, and breaking code can cowboy it with @trusted // GREENWASHED: Not reviewed for safety to each of those. Or better yet, actually review those functions for safety.

>

This seems like a severe limitation to me.

It is a limitation, that's for sure. But I think it may be less severe than you think. Missing nothrow and @nogc attributes are far from the worst problems in a typical application code. Many would say that there they aren't even worth the visual clutter they are.

And if you don't mark your code pure and @safe well before you publish it for everyone to use in non-alpha state, it suggests that you don't consider even those attributes quite essential. It's arguable if that's a good attitude, but it's quite popular judging by this forum.

>

It just seems simpler to never use attributes at all (with classes).

That's close to a good advice with regards to nothrow and especially @nogc. I think that even if your abstract function should in principle be implemented without throwing or garbage collecting, it's often a good idea to omit those attributes just to make life easier for the child function implementor. Maybe even omit pure. But at least use @safe unless you have strong specific reasons not to (like heavy issues with backwards compatibility - I'd probably defer adding @safe to next major version of the library). Making an abstract function @system puts much more burden to anyone who uses the class and cares about safety, than it saves burden from those implementing the children functions.

>

Now, the entire point of this post is to ask what should we do in the case of: https://github.com/dlang/dmd/pull/14432.

Disclaimer: I had only a surface look on what Condition seems to do. It's pretty likely I'm not on map on how it works and how it's supposed to be used.

I would not add @nogc to the interface of this function. A reasonable implementation might use the heap to do some bookkeeping and might want to allocate on notify. An ideal implementation would not do so, but abstract classes should generally be easy to implement even from not-so-perfect code.

It can be questioned why the same class acts both as an abstract class for other implementations, and an implementation in itself. If there was a separate final class that implemented Condition, it's members could be @nogc. The downside would be that the class instance would be a bit bigger since D object grow with number of interfaces/classes they inherit - maybe that's why.

September 16, 2022
On Fri, Sep 16, 2022 at 10:55:46PM +0000, Dukc via Digitalmars-d wrote:
> On Friday, 16 September 2022 at 09:25:58 UTC, RazvanN wrote:
> > Let's say you have a library class that exposes some methods. At the beginning you did not focus on the safety, nogcness etc. of your methods, you just wanted to have something working. You publish your class, people start using it and now you are in a phase where you would like your methods to be callable from attributed contexts. So you go on and make your code safe, nothrow, nogc, pure. However, now you are faced with a large breaking change because suddenly you force your users to respect these attributes.

One way this could have worked is, if your library code was written with the most restrictive attributes that would still allow it to work.  Then code with less restrictives attributes would still be able to call your code (because your code would be covariant with the more relaxed attributes). E.g., if your code was written to be `pure` as much as possible, then it could be called from both pure and impure code. However, if you didn't pay attention to purity in the first place and a lot of your code is impure, then pure code would not be able to call your code.  Similarly, if your code was written to be @safe (whether or not you initially write the attribute explictly), then @system code could still call it.

However, this approach may run into problems when user code may inherit from your class.  Derived class overrides cannot expand the permissiveness of the overridden base class method's attributes; it can only restrict it -- e.g., if a base class method is pure, then it would be invalid to allow a derived class to override it with an impure method, because then pure code could invoke the impure code via the base class's pure interface. This implies that base class methods must be maximally permissive, in order to accomodate derived classes. But that means that it would not be callable from a more restrictive context. Thus this defeats our goal.

//

The other way to do it is to use templates and allow the compiler to do attribute inference. You'd write your code under maximal restrictions, and use unittests to enforce purity, nothrow, etc., but leave the actual template code unattributed to allow the compiler to infer attributes. So if the user code is pure, your code would not introduce impurity and it would work. If the user code is impure, the compiler just infers impure and it would still work.

However, template methods cannot be overridden by inheritance, so we haven't really solved the inheritance issue either.


[...]
> > Now, the entire point of this post is to ask what should we do in the case of: https://github.com/dlang/dmd/pull/14432.
[...]

Good question. :/  Given that in this case the base class API is already fixed, seems the best you could do is to add attributed overloads in the derived class. Which is an ugly solution. :-(


T

-- 
I think Debian's doing something wrong, `apt-get install pesticide', doesn't seem to remove the bugs on my system! -- Mike Dresser