[Issue 20670] immutable template specialization pattern matches immutable struct, strips immutable

d-bugmail at puremagic.com d-bugmail at puremagic.com
Sat May 14 18:24:39 UTC 2022


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

--- Comment #13 from FeepingCreature <default_357-line at 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.

--


More information about the Digitalmars-d-bugs mailing list