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:

  • 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.

 

8 thoughts on “Mutant records: on methods (and helpers)

  1. Thank you Eric for this in depth post, this brings a whole new set of issues that could be brought to the table when inheriting a project!

  2. Inside a delphi record you can already define fields as var or const, and if you don’t specify any, they implicitly are var.

    What would it mean to have a const type record with var fields in it?
    Or what if you define a const type record as a variable?

  3. @Wouter
    It’s about where you’re able to modify a field or not. A const record would be immutable, ie. once you’ve created it, you can’t change it’s values, and it’s methods don’t change the field values.
    Record methods are essentially functions with an implicit Self parameter, the question is wether that parameter is const or var. Currently it’s a magic var, ie. you can pass it constants, and the method will be able to change the constant. If you aren’t able to specify if Self is var or const, then there are only two alternatives: either it’s var and methods wouldn’t be able to change the Self fields, or it’s var, and you’re face with magic powers… Thus there is no clean solution if you can’t specify if a method is using const or var for the Self parameter.
    Const record and var record would be shortcuts to avoid repeating const/var for each method.
    The problem doesn’t exist for reference types (which are always pointers, and for which var/const is irrelevant to the ability to modify fields)

  4. I think there has never been a concept of strict immutable data types in Delphi/Pascal, like it exists for example in Objective-C.

    By defining
    const Olaf: TPerson =(Name : ‘Olaf’; Age : 43);

    “Olaf” becomes a constant as expected, which means that Olaf := Daniel would not compile as expected.
    Olaf.Age := 42 would still perfectly be fine – unless you defined “Age” as constant as well.

    In other words “const” never has a meaning for the contents of a container. It only influences the container. This means it is completely up to you, the developer, to decide how a method in a record would work. I would definitely object making “const” somehow influence a method’s behavior.

    If you look at Objective-C for example. then you will notice that immutable types wouldn’t expose any “set-like” methods at all, but have constructors for initializing values only – obviously for a reason. This is of course not really a language feature, but more a framework feature.

    Regards,
    Olaf

  5. Ok, I have to correct my self: a Record constant would not allow to assign values to it’s fields – unless compiler option “asignable typed constants” is activated. This still doesn’t invalidate my general opinion, that mutability of data structures is something your class/record design should handle and is not a matter of the language….

  6. @Olaf Monien
    The first issue you describe is that of missing compiler optimizations (there is no reason the compiler can’t use a field of a constant as a constant).

    And if you lessen the “const” with assignable constants and allow mutating methods, then you might as well drop the “const” keyword from the language, and use “var” everywhere, because that’s what you end up with…

    The language does support constants and immutability, and the compiler does take advantage of it for optimization… except when “records with methods” where introduced, they essentially broke that, and some of the optimizations the compiler *still* does with “const” are essentially invalid.

    The current situation is either a combination of oversights when adding features to the language or compiler bugs: you can’t have both the compiler optimize for “const” and have “const” be treated as “var” at the same time…
    Some parts of the compiler still properly honor “const”, others don’t.

    I don’t think it’s a desirable situation, as basically, you’ve got a half-supported language feature, where you could have a tool to leverage better code quality, and better compiler optimizations.

  7. Good spot. It does seem like a pretty big hole, doesn’t it?

    As a part time C++ hack, the var modifier seems redundant. As you say, you are bound to interpret
    procedure IncMe;
    as non-const; what does marking it explicitly so bring to the party?

  8. @Will Watts
    If you have a “const record”, then all methods would implicitly be “const”, so “var” would allow in that case to mark particular methods as mutating, and thus taking a “var” param.
    Such a “var” method would be restricted, and would error when invoked on a constant or property f.i.

Comments are closed.