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!
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?
Two syntax elements are missing:
- the ability to specify if a method is mutating the record or not, ie. if it should get “Self” as a “var” or as a “const“, so that you can make the above behavior explicit, and the compiler can emit errors when it’s not appropriate
- the ability to mark a record as mutable/immutable, to further remove the ambiguities
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), 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.