- DelphiTools - https://www.delphitools.info -

Mutant records: on methods (and helpers)

When “record with methods” were introduced, an important feature was overlooked: mutability.

This article discusses the problem, and introduces a possible syntax extension to solve it. Ideas & comments welcome!

The Problem

Effectively, “records with methods” treat all record “const” elements as “var“, even when they shouldn’t, and effectively ignores the compiler option “assignable constants”. Witness the following record:

type
   TRec = record
      Field : Integer;
      procedure IncMe;
   end;
...
procedure TRec.IncMe;
begin
   Inc(Field);
end;

Then the compiler will happily allow you to do:

const MyNullRec : TRec = (Field: 0);
...
MyNullRec.IncMe; // watch me, I'm magic!
WriteLn(MyNullRec.Field); // will write 1 !

…it will also accept…

procedure Myproc(const r : TRec);
begin
   r.IncMe; // modified whatever r as if it had been a var
end;

…and that as well…

type
   TMyClass = class
      private
         FRec : TRec;
         procedure SetRec(const aRec : TRec);
      public
         property RW : TRec read FRec write SetRec;
         property RO : TRec read FRec;
   end;
...
var myClass : TMyClass;
...
// everything's as it should be there
myClass.RO := aRec; // gives an error
myClass.RW := aRec; // uses SetRec
// but that compiles to...
myClass.RO.IncMe; // modifying a read-only property !
myClass.RW.IncMe; // modifies and bypasses SetRec !

It would “compile” even if the getter was a function returning a record… or just in about any case, really.

And of course you can cascade all the above in a real-world case, so you end up with weird side-effects and really hard-to-track down bugs.

Besides bug, that also raises the issue of usability of records like TPoint and other simple vector/matrix types, as properties of objects. As long as they did not have methods, you would have to use the setter to modify such a property, so the objects had guaranteed notification of a position change f.i., with methods being able to magically mutate the records, you no longer have that guarantee. And adding all the baggage so that a TPoint can be owned and notify its owner of a change would be over-the-top, wouldn’t it?

What’s Missing

Two syntax elements are missing:

Ideas for such a syntax, in explicit form, and reusing existing keywords:

type
   TRec = record
      Field : Integer;
      var procedure IncMe; // mutating
      const function ToString : String; // non-mutating
   end;

Modifiers in Pascal (except “class“) usually follow the declaration, but “var” and “const” can’t, as they could be ambiguous with the declaration of a following constant/variant. So if reusing those, rather than introducing new keywords, they have to be in front.

Also you could mark a record as explicitly mutable or immutable, by prefixing it with “var” or “const”

type
   TImmutableRec = const record
      Field : Integer;
      var procedure IncMe; // mark as explicitly mutating ?
      function ToString : String; // non-mutating by default
   end;
   TMutableRec = var record
      Field : Integer;
      procedure IncMe; // implicitly mutating
      const function ToString : String; // mark as explicitly non-mutating
   end;

For backward compatibility with existing records with methods, the implicit default would have to be “var record” as the records are currently all mutable.

With that extra information, the compiler could then perform proper checks and proper parameter passing, so the side-effects mentioned could happen only if you explicitly went for them, rather than by mistake as is currently the case.

Additionally, immutable records would implicitly have their fields as read-only everywhere outside the record constructor (which would then become truly useful as language syntax element). It might also be possible to extend the syntax to be able to declare explicitly immutable classes.

The case of helpers

When generalizing helpers (as in DWScript [1]), the above mutability issue also applies to helpers for value types, ie. records and static arrays, so a similar syntax would have to be introduced.

To minimize side-effects, helpers on record and static arrays currently pass their “Self” as const, this allows to use such helpers in all cases (including “const” parameters, function results, read-only properties…), but is restrictive, as it prevents mutation. So once the above syntax is decided upon, it could apply to them as well.

For helpers on reference types in DWScript (classes, interfaces, dynamic arrays), the issue doesn’t apply.

Note that in Delphi, helpers also have the design bug of being able to magically access private fields… but that’s another story.