May 14, 2022
https://issues.dlang.org/show_bug.cgi?id=20670

--- Comment #10 from Paul Backus <snarwin+bugzilla@gmail.com> ---
> But `immutable struct` is not actually a qualifier, it's an
> "immutable declaration." This is currently, apparently, implemented as
> an implicit qualifier, which is what this bug is about, and at any rate
> is not what the spec says.

Here's the relevant part of the spec:

https://dlang.org/spec/attribute.html#immutable

It says immutable works "the same was as const does", so let's look at the section on const:

> The const attribute changes the type of the declared symbol from T to
> const(T), where T is the type specified (or inferred) for the introduced
> symbol in the absence of const.

https://dlang.org/spec/attribute.html#const

This paragraph implicitly assumes that the declared symbol has a type. But in the case of `immutable struct S`, S does not *have* a type, it *is* a type. So the spec does not give us a clear answer one way or the other to the question of what `immutable struct S` ought to mean.

--
May 14, 2022
https://issues.dlang.org/show_bug.cgi?id=20670

--- Comment #11 from FeepingCreature <default_357-line@yahoo.de> ---
The problem is that it seems like Unqual has conflicting purposes - and this is, I think, ultimately about Unqual; about the ability to take a type - any type - and gain a mutable version.

The problem is that immutable, on one hand, says "it acts like every field has been marked immutable." But the ability to mark a field immutable is already enough to completely obsolete Unqual as a concept, since a declaration of Unqual!T will not be a mutable value. Okay, so from this we take that it's just the case that we have mutable types, and immutable types, and Unqual is a specialty tool that's useless in the general case. (Hence our own librebindable.) But then the ability to strip immutable from a type is a problem. Because now you have to differentiate between "a type marked as immutable" and "a type declaration marked as immutable". Because, since you've already gained, necessarily, the ability to deal with types that cannot be turned into mutable declarations by any means, there is no point to stripping immutable from `immutable struct S` anymore. What does it buy you? The bubble of types you can create a mutable declaration of gets a bit bigger. It still doesn't cover an appreciable fraction of the D type landscape - including the types you get if you *just replace immutable struct with the thing it says it's a shortcut for!*

So from that basis, I'd drawn a distinction between 'immutable struct S' and 'struct S, immutable S' - as "in one case, the user may want to see a mutable S, especially if used as an inner field in a constructed type; ie. we want to move immutable as far outward as possible, as in `immutable Nullable!S`, and in the other case, the user asserts that they never ever ever want to have to deal with a mutable S for any reason."

Which makes sense, because that gives us pure value types, with working invariants. Domain types, where we don't need to hide every field behind an accessor - because *the only way* to set a field is via the constructor. As it should be for domain types. So I've always thought that this was what `immutable struct S` was for.

Because it's not like it's good for anything else.

--
May 14, 2022
https://issues.dlang.org/show_bug.cgi?id=20670

--- Comment #12 from Paul Backus <snarwin+bugzilla@gmail.com> ---
> Because, since you've already gained, necessarily, the ability to deal with types that cannot be turned into mutable declarations by any means, there is no point to stripping immutable from `immutable struct S` anymore. What does it buy you?

As Dennis said: fewer special cases in the type system.

Forget about Unqual. It was just meant to be an example (evidently not a very good one). Avoiding special cases in the type system is the point.

--
May 14, 2022
https://issues.dlang.org/show_bug.cgi?id=20670

--- Comment #13 from FeepingCreature <default_357-line@yahoo.de> ---
To clarify where I'm coming from, please follow our (Funkwerk's) evolution of domain types, using a fictionalized example.

First, consider a type:

class PassengerWagon;
class CargoWagon;

struct Wagon {
  PassengerWagon passengerWagon;
  CargoWagon cargoWagon;
}

Now this is trying to approximate a sum type, but one issue is that a wagon can, for instance, be both a passenger and a cargo wagon. Wagons cannot actually do this; it is a modelling error.

We solve it with an invariant:

struct Wagon {
  PassengerWagon passengerWagon;
  CargoWagon cargoWagon;
  invariant((passengerWagon is null) != (cargoWagon is null));
}

Now of course this is a very dangerous type! Because if you

auto wagon = Wagon(passengerWagon, null);
wagon.passengerWagon = null;

and the invariant is silently violated, and as the bad value gets carried deeper into the program, it may cause mischief far away. Also, the implicit struct constructor doesn't check the invariant, so we can do angry stuff like Wagon().

So we secure it!

struct Wagon {
  private PassengerWagon passengerWagon_;
  private CargoWagon cargoWagon_;
  invariant((passengerWagon_ is null) != (cargoWagon_ is null));
  @disable this();
  this(PassengerWagon passengerWagon, CargoWagon cargoWagon) {
    this.passengerWagon_ = passengerWagon;
    this.cargoWagon_ = cargoWagon;
  }
  PassengerWagon passengerWagon() {
    return passengerWagon_;
  }
  CargoWagon cargoWagon() {
    return cargoWagon_;
  }
}

You may note that this simple type is starting to become... annoying. Which is to say, terrible. Terrible to read, terrible to write, and  rife with potential for typos.

We had a *lot* of structs like this. With a lot of fields. The amount of programmer-hours spent on this was unreasonably high.

I introduced boilerplate to fix it:

struct Wagon {
  @ConstRead
  private PassengerWagon passengerWagon_;
  @ConstRead
  private CargoWagon cargoWagon_;
  invariant((passengerWagon_ is null) != (cargoWagon_ is null));
  // also generate the constructor, while we're at it
  mixin(GenerateThis);
  mixin(GenerateFieldAccessors);
}

Now it's shorter, but it's still kind of annoying, and also for some reason all our machines run out of memory when we trigger builds. Could the thousand lines of templates in boilerplate have something to do with that? We may never know.

But - wasn't the thing we actually wanted just to enforce that all fields were only set through the constructor?

And wasn't there already a D language feature that ensured that fields couldn't change?

immutable struct Wagon {
  PassengerWagon passengerWagon;
  CargoWagon cargoWagon;
  invariant((passengerWagon_ is null) != (cargoWagon_ is null));
  mixin(GenerateThis);
}

Nice and simple, no accessors, basically no templateness, no unintended mutations bypassing the invariants.

Of course, :points at the large list of bugs he filed with regard to immutable.:

And also, :points at his Turducken technique post and librebindable just to get *what Unqual was supposed to be from the start.*:

And by the way, we have an internal implementation of hashmaps! We don't have that for performance, but *just for dealing with immutable types.*

It's just that that simple struct, with the simple annotation, and zero template overhead, is *so damn tempting.* This is how we want every struct in the domain layer to look. No mutation, pure data. If immutable is not for that, fine, but something should be, because this is a key component of writing safe, trustable code.

--
May 14, 2022
https://issues.dlang.org/show_bug.cgi?id=20670

--- Comment #14 from Paul Backus <snarwin+bugzilla@gmail.com> ---
> But - wasn't the thing we actually wanted just to enforce that all fields were only set through the constructor?

If that's all you want, then the current behavior of `immutable struct` should be sufficient--because, as I pointed out in comment 3, stripping immutable from the type does not strip it from the members. In fact, even the behavior Dennis proposes in comment 4 would be sufficient.

However, going by the original bug report, it seems like in addition to this you *also* want to be able to distinguish between `immutable struct S` and `struct S { immutable: }` in template specializations. And in order to do this, you would like a new special case to be introduced to the type system.

Am I missing anything?

--
May 14, 2022
https://issues.dlang.org/show_bug.cgi?id=20670

--- Comment #15 from Dennis <dkorpel@live.nl> ---
(In reply to FeepingCreature from comment #13)
> This is how we want every struct in the domain layer to look.

Would this work?
```
struct S {
    immutable:
    // fields...
}
```

--
May 14, 2022
https://issues.dlang.org/show_bug.cgi?id=20670

--- Comment #16 from FeepingCreature <default_357-line@yahoo.de> ---
Huh. Yeah...

Does that work?

edit: Also, I hadn't quite realized that struct S was still, actually, immutable on every field. Now that you point it out, that obviates the worries I'd had about it. But why is there even an immutable tag on it then? Why isn't `immutable struct S` just exactly the same as `struct S { immutable: }`?

--
May 15, 2022
https://issues.dlang.org/show_bug.cgi?id=20670

--- Comment #17 from Paul Backus <snarwin+bugzilla@gmail.com> ---
Introduced by the fix for issue 17582:

https://github.com/dlang/dmd/pull/6958

So, it seems like the "implicit qualifier" behavior has always been intended, but was inadvertently disabled for a 5-year period, starting from version 2.056 and ending in version 2.076.

--
December 13
https://issues.dlang.org/show_bug.cgi?id=20670

--- Comment #18 from dlangBugzillaToGithub <robert.schadek@posteo.de> ---
THIS ISSUE HAS BEEN MOVED TO GITHUB

https://github.com/dlang/dmd/issues/19676

DO NOT COMMENT HERE ANYMORE, NOBODY WILL SEE IT, THIS ISSUE HAS BEEN MOVED TO GITHUB

--
1 2
Next ›   Last »