Recent

Author Topic: Const record parameters by default  (Read 29245 times)

Ryan J

  • Full Member
  • ***
  • Posts: 141
Const record parameters by default
« on: April 01, 2024, 04:17:56 am »
I'm seeing more and more code in Pascal and C++ riddled with "const" modifiers on basically all record parameters. It's becoming an eyesore but I don't blame programmers for wanting "const correctness" since there's a performance benefits and gives some peace of mind I suppose.

I wish I could profile this on some large codebase but it feels to me like 95% of record parameters are only read thus should be passed as const. Is it time to make "const" default in Pascal? I use Swift in my day job where const is by default and I very rarely write to a parameter, and in the case I do I simply make local copy first. The idea of writing to a parameter feels kind of wrong anyways since it's not really a local variable and intended for passing data between functions.

Curious what other people think about this.

ojz0r

  • Jr. Member
  • **
  • Posts: 72
Re: Const record parameters by default
« Reply #1 on: April 01, 2024, 08:21:48 am »
Verbosity <> Bad.
It self documents the intentions.

Also changeing the default behaviour would break alot of code in production.
I like big endians and i can not lie.

runewalsh

  • Full Member
  • ***
  • Posts: 115
Re: Const record parameters by default
« Reply #2 on: April 01, 2024, 08:31:40 am »
Among other obvious things, you will then need to invent a way to pass a record by value.

Ryan J

  • Full Member
  • ***
  • Posts: 141
Re: Const record parameters by default
« Reply #3 on: April 01, 2024, 08:36:05 am »
Verbosity <> Bad.
It self documents the intentions.

To flip the question around though, why does passing imply a copy? The copy is almost like an additional action performed and there's really no reason to assume this is needed. Ideally the compiler should be doing the least amount of work possible and that would imply not copy data for no reason unless the programmer requested it.

Ryan J

  • Full Member
  • ***
  • Posts: 141
Re: Const record parameters by default
« Reply #4 on: April 01, 2024, 08:39:25 am »
Among other obvious things, you will then need to invent a way to pass a record by value.

You'd need to add a "copy" or "mut" (mutable) modifier I guess. In Swift you just redeclare the parameter as a local which works too. Not sure if it helps the programmer to know that a function would copy the parameter, but if that is helpful information then a modifier would be best.

Ryan J

  • Full Member
  • ***
  • Posts: 141
Re: Const record parameters by default
« Reply #5 on: April 01, 2024, 09:35:23 am »
Another thing comes to mind with const. First off, when did const appear in Pascal? It feels to me like this is something which was kind of half baked and maybe taken from C++ or Delphi.

The problem I see when default const is that no effort is made by the compiler to know if a method mutates fields or not so you can easily circumvent the const parameter by calling a method. If all records were passed by const then you could write to the original value by accident by calling the wrong method.

Making a copy is almost like a protection for the value but this could be fixed if the compiler actually implemented "const" to mean read-only. I know Pascal has pointers so you can always trick the compiler but at least you can audit your code for suspect pointers operations.

Const as default aside, it would be real nice to see "const" on a class parameter and know that the object isn't going to be changed by the function which it's being passed in to. Again, seems half baked because was this problem never tackled?

lainz

  • Hero Member
  • *****
  • Posts: 4742
  • Web, Desktop & Android developer
    • https://lainz.github.io/
Re: Const record parameters by default
« Reply #6 on: April 01, 2024, 09:47:45 am »
In kotlin variables are passed as a constant. You can't edit them.

But for objects the rule is not the same. Objects follow the rule of var and val

https://pl.kotl.in/KTJOEqhOU

In that example change "val a: String" by "var a: String".

Val is constant. Var is variable.

Edit: you can do the same with Pascal classes. The object holds a private variable and you define a property that's read only.

So you can only initialize it with the constructor or with a method (and yes that method can be the setter wich raises an exception) that sets a flag of initialization...
« Last Edit: April 01, 2024, 09:54:31 am by lainz »

440bx

  • Hero Member
  • *****
  • Posts: 6383
Re: Const record parameters by default
« Reply #7 on: April 01, 2024, 10:43:19 am »
shaking my head here...

One of the most important aspects in programming, regardless of language, is the _fine_ and _explicit_ control of side effects.

Uncontrolled side effects are highly undesirable because they make understanding and maintaining a program exponentially more difficult as the number of side effects increases.  This is one of the many reasons global variables are very, very bad.  Because they can be altered by just about any function/procedure (that's what makes them global) it puts a heavy burden on the programmer to be aware of every function/procedure were they are altered, not to mention that the order in which they are altered/modified is also, quite often important.

With the above preliminary out of the way...

The reason the default is to pass parameters by value, i.e, make a copy of the value, is to avoid side effects.  Effectively, if the programmer wants a side effect then the programmer should make it _explicit_ that changes to the value must be persistent, i.e, pass by reference, e.g, "var".

The reason for "const" isn't because of performance (though it usually has a positive effect on performance), the reason is to inform the programmer that the parameter is simply information for the function/procedure to use in the process of performing whatever task it's supposed to perform.  It's also a good defensive programming habit because if the programmer wants (or needs) to modify the value then the programmer can make a copy of the value into a local variable and modify that to their hearts content.  The making of that local copy reveals that modifying the value is needed and localizes the side effect to that function/procedure.

The bottom line is always the same: 1. _minimize_ the number of side effects 2. exert fine control over where and when side effects occur.

"const" is great.  The one thing that is unfortunate is that Pascal does not make as much and extensive use of it as it should (C does!)

Lastly, "const" doesn't have to mean "read-only", it must mean unmutable no matter where the value resides, read-write memory or otherwise.  "const" is used by the compiler to keep the programmer honest/"on/his her toes", i.e, if something is declared "const" then the programmer cannot modify it (the compiler does the best it can to flag code that attempts to modify a "const".)



FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

Ryan J

  • Full Member
  • ***
  • Posts: 141
Re: Const record parameters by default
« Reply #8 on: April 01, 2024, 11:07:57 am »
@440bx, this makes little sense to me. Isn't making passing records by const default limiting side effects? I don't hear the case for why copy should be default instead of const, because in my experience const is what you want 95% of the time.


Ryan J

  • Full Member
  • ***
  • Posts: 141
Re: Const record parameters by default
« Reply #9 on: April 01, 2024, 11:21:53 am »
"const" is great.  The one thing that is unfortunate is that Pascal does not make as much and extensive use of it as it should (C does!)

Lastly, "const" doesn't have to mean "read-only", it must mean unmutable no matter where the value resides, read-write memory or otherwise.  "const" is used by the compiler to keep the programmer honest/"on/his her toes", i.e, if something is declared "const" then the programmer cannot modify it (the compiler does the best it can to flag code that attempts to modify a "const".)


I say const is half baked because of this.  How is this "unmutable no matter where the value resides"?

Code: Pascal  [Select][+][-]
  1. type
  2.   TMyClass = class
  3.     x: Integer;
  4.  
  5.     procedure Mutate;
  6.   end;
  7.  
  8. procedure TMyClass.Mutate;
  9. begin
  10.   x := 100;
  11. end;
  12.  
  13. procedure PassClass(const r: TMyClass);
  14. begin
  15.   r.Mutate;
  16. end;
  17.  

If you do the same thing was const records it will modify the original value, providing the record is large enough. So making const by default may be the best policy for the programmer but it would require the compiler knowing which methods mutate and forcing you to mark them as "mutating" like swift does.

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12303
  • Debugger - SynEdit - and more
    • wiki
Re: Const record parameters by default
« Reply #10 on: April 01, 2024, 11:23:59 am »
To elaborate on 440bx's statement

The below program is erroneous.

"ChangeFoo" make changes to Foo, while "OptimizeMe" expects Foo to be unchanged.

Currently, it will most likely work (with any version and settings of Fpc). But that is not guaranteed.
If/Once the optimizer in Fpc gets better, it could very well be, that the below Program
- writes "2" to the console twice.
- does other unexpected things

Code: Pascal  [Select][+][-]
  1. program Test;
  2. type TFoo = record a,b: Integer; end;
  3. var  Foo: TFoo;
  4.  
  5. procedure ChangeFoo;
  6. begin
  7.   Foo.a := 1;
  8. end;
  9.  
  10. procedure OptimizeMe(const x: TFoo);
  11. begin
  12.   writeln(x.a); // writes 2
  13.   ChangeFoo;
  14.   writeln(x.a); // in future, maybe writes "2" again, or "1" at random...
  15. end;
  16.  
  17. begin
  18.   Foo.a := 2;
  19.   Foo.b := 3;
  20.   OptimizeMe(Foo);
  21. end.
  22.  

https://www.freepascal.org/docs-html/ref/refsu67.html
Quote
Note that specifying const is a contract between the programmer and the compiler. It is the programmer who tells the compiler that the contents of the const parameter will not be changed when the routine is executed
"ChangeFoo" is executed when (while) "OptimizeMe" still runs.

The programmer who wrote the "const" promised, they would not change "Foo" during this time. And yet, they do.

The compiler would have been in its right to cache the value "2" and use it



Mind, that while currently it accidentality works for records (at least as far as I tested), it does already break for other types (ansistrings, dyn-array, any refcounted, ...).

And there have been real life apps that crashed due to the above programmers mistake.



So change everything to "const" => and many existing apps will fail.

Mind, btw that in many cases (depends on OS/CPU) a record as the above (up to 2 pointers in size) may be passed in registers. And that means, even though it is a copy, it may be of the same speed (in terms of optimizations).


Ryan J

  • Full Member
  • ***
  • Posts: 141
Re: Const record parameters by default
« Reply #11 on: April 01, 2024, 11:32:24 am »
Martin_fr, yes I see your point. It sounds like const is just not fully implemented, or doesn't try very hard at least so making it default would be too dangerous.

In my experience default const is what the programmer wants and would generate the best code (we forget to add const or get lazy) but the compiler is not good enough to ensure we don't really change the value so it can not be trusted to add const for us.

In my tests if you modify a const record via a method (like your example) and the record is large enough it will write to the original value, maybe it could corrupt memory even, I don't know.

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12303
  • Debugger - SynEdit - and more
    • wiki
Re: Const record parameters by default
« Reply #12 on: April 01, 2024, 11:35:23 am »
Also "pointer to record" has existed since the dawn of time....

Whenever a record should not be passed by value, then the correct way is usually to explicitly define a pointer to the record type, and pass a pointer.
Using "const" (or "constref") is not the preferred way for this.

And that also means: records themself fulfil the existing need to pass data by value (as copy). In the same way that "i: integer" is passed by value (as copy). This is a documented and useful bit of functionality. It should not be scrapped.

Syntax for both features (by value / by ref) exist and can be used.

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 12303
  • Debugger - SynEdit - and more
    • wiki
Re: Const record parameters by default
« Reply #13 on: April 01, 2024, 11:42:25 am »
There are 2 separate things here...

1) Making amends for either
- the programmers forgetfulness
- the programmers laziness

2) Optimize

----
"1" should not modify the language. Passing by value, instead of using a pointer type is my fault. It may slow down my app. It may change the behaviour of my app... The same happens if I forget "if sender <> nil then", that can change the behaviour of my app (by crashing it).

"2" should happen without syntax interference... (yes we have "inline" which is a syntax for optimization.... but that should be the exception.

I don't know what happened to the work on "pure functions". But that would be a possibility. If the compiler can truly tell, that there won't be side effects.... (though the record by pointer optimization would take much more than just establishing "no side effects").

But in general, write pointers, if you want pointers.

440bx

  • Hero Member
  • *****
  • Posts: 6383
Re: Const record parameters by default
« Reply #14 on: April 01, 2024, 11:42:53 am »
@440bx, this makes little sense to me. Isn't making passing records by const default limiting side effects? I don't hear the case for why copy should be default instead of const, because in my experience const is what you want 95% of the time.
The reason copy is the default is because it's pretty much forced to be that way.  Let me explain...

Function/procedure parameters are either stored in registers or on the stack.  The important thing is, if you have a variable somewhere, for that variable to become a parameter to a function/procedure, it needs to be copied into a register or onto the stack.  THAT is about 95% of the cases.  The remaining cases, that is when a copy need _not_ be made is when the variable may be modified.  In that case, a pointer to the variable can be placed in a register or on the stack.

The "const" part is just an additional "nice thing" which can be used to optimize the passing of a parameter but doesn't always result in an optimization.  For instance, if the parameter is an ordinal type then a _copy_ of the variable (that becomes the parameter) is made regardless of whether it is const or not.  IOW, in the case of ordinal types, "const" has no effect whatsoever on how the parameter is passed.  In the case of a record (that doesn't fit in a register), it does only because the compiler takes advantage of knowing it is "const" to save itself the trouble (and the time) of making a copy. Note that, that's a compiler optimization, the compiler could still make a copy on the stack and enforce that it not be written to, i.e, treat it as "const" since it is "const".  When the compiler knows the record parameter is const it can pass the record address (i.e, a reference) without making a copy because it will not allow writing to the record (effectively treating it as const in spite of it being passed by address.)

I see that one of the problems you have is distinguishing between a pointer being constant and what the pointer points to being constant.  Those are two very different things and unfortunately Pascal does not provide fine control over cases like that.

Consider a case like this.... a large record
Code: Pascal  [Select][+][-]
  1. type
  2.   PLARGE_RECORD = ^TLARGE_RECORD;
  3.   TLARGE_RECORD
  4.     field : packed array[0..1000] of char;
  5.   end;
  6.  
  7. < some more stuff here... whatever you want>
  8.  
  9. procedure DoSomething(const L : PLARGE_RECORD { notice it is pointer });
  10. begin
  11.   L^.field[0] := 'a';  { that's legal and perfectly sensible }
  12. end;
  13.  
In the above, L is a pointer and L is declared const and nowhere in DoSomething is the value of L changed.  Therefore the "const" attribute is honored as it should.   Now, what L points to is a different story.  Nowhere has the compiler been told that what L points to is constant, therefore L^.field[0] := 'a'; is perfectly valid.

The above is important because classes are pointers just like L above.
FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

 

TinyPortal © 2005-2018