www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Lazily Initialized Variables that are Compatible with const/immutable

reply Vijay Nayar <madric gmail.com> writes:
An uncommon, but not rare, situation that I run into from time to 
time, is that there are variables used either globally or as a 
member of a class that are expensive in their resource usage 
(time to compute, or memory size), but which are only sometimes 
used, e.g. only when certain functions are called, which depends 
on the runtime usage of the program.

The concept of [lazy 
initialization](https://en.wikipedia.org/wiki/Lazy_initialization) is used in
these situations to avoid paying this cost up front, and deferring it until the
value is used.  E.g. if your object has a member named `value`, then
`obj.value` may refer to a method that returns a cached value, and if the cache
is empty, produces the value and caches it.

However, in D, such usage runs into problems when the 
object/method is either `const` or `immutable`. Making such 
changes requires modifying a variable that technically existed at 
the time `obj` was created.

While working on a project, I encountered a solution in Dart 
which I think could be carried over into D:

https://dart.dev/guides/language/language-tour#late-variables

```
When you mark a variable as late but initialize it at its 
declaration, then the initializer runs the first time the 
variable is used. This lazy initialization is handy in a couple 
of cases:

     The variable might not be needed, and initializing it is 
costly.
     You’re initializing an instance variable, and its initializer 
needs access to this.

In the following example, if the temperature variable is never 
used, then the expensive readThermometer() function is never 
called:

     // This is the program's only call to readThermometer().
     late String temperature = readThermometer(); // Lazily 
initialized.
```

The already existing keyword `lazy` could be used for this 
purpose in D, and then it would be possible to write code such as:

```
class Thing {
   lazy immutable String piDigits = computePiDigits();

   String computePiDigits() { ... }

   double getTemperature() const {
     // If only this function is used, piDigits is never computed.
   }

   double getCircumferance() const {
     // Some silly usage of piDigits, which causes 
computePiDigits() to be run.
     return piDigits.to!double * radius * 2.0;
   }
}
```

Is there another way to perform lazy initialization that is 
compatible with const/immutable, or is this needed as a language 
feature?
Nov 17 2022
next sibling parent reply MorteFeuille123 <MorteFeuille123 mrt.rt> writes:
On Thursday, 17 November 2022 at 10:57:55 UTC, Vijay Nayar wrote:
 An uncommon, but not rare, situation that I run into from time 
 to time, is that there are variables used either globally or as 
 a member of a class that are expensive in their resource usage 
 (time to compute, or memory size), but which are only sometimes 
 used, e.g. only when certain functions are called, which 
 depends on the runtime usage of the program.

 The concept of [lazy 
 initialization](https://en.wikipedia.org/wiki/Lazy_initialization) is used in
these situations to avoid paying this cost up front, and deferring it until the
value is used.  E.g. if your object has a member named `value`, then
`obj.value` may refer to a method that returns a cached value, and if the cache
is empty, produces the value and caches it.

 However, in D, such usage runs into problems when the 
 object/method is either `const` or `immutable`. Making such 
 changes requires modifying a variable that technically existed 
 at the time `obj` was created.

 [...]

 Is there another way to perform lazy initialization that is 
 compatible with const/immutable, or is this needed as a 
 language feature?
No, this is needed as a bultin feature I'd say. I sometime think that since we can have local static variablbes, we could have local member variables. Similarly hiddden, only accessible through a getter, which replaces the need for a const member.
Nov 17 2022
parent reply razyk <user home.org> writes:
On 17.11.22 13:04, MorteFeuille123 wrote:
 Is there another way to perform lazy initialization that is compatible 
 with const/immutable, or is this needed as a language feature?
No, this is needed as a bultin feature I'd say. I sometime think that since we can have local static variablbes, we could have local member variables. Similarly hiddden, only accessible through a getter, which replaces the need for a const member.
module2.d ``` import std.stdio; private string _lazy1; string compute(){ writeln("computing..."); return "_lazy1 val"; } string lazy1(){ if (_lazy1 == null) _lazy1 = compute(); return _lazy1; } ``` module1.d ``` import std.stdio; import module2; void main() { writeln(lazy1); writeln(lazy1); } ```
Nov 17 2022
next sibling parent reply Vijay Nayar <madric gmail.com> writes:
On Thursday, 17 November 2022 at 15:33:47 UTC, razyk wrote:
 On 17.11.22 13:04, MorteFeuille123 wrote:

While the solutions from razyk and Ali take care of lazy initialization, with Ali's solution also having the benefit of being thread-safe, none of these methods are actually compatible with `const` or `immutable`, meaning that in any case where lazy initialization is needed, you cannot have a const class, you have to pick one or the other in the current setup.
Nov 17 2022
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 17.11.22 22:49, Vijay Nayar wrote:
 On Thursday, 17 November 2022 at 15:33:47 UTC, razyk wrote:
 On 17.11.22 13:04, MorteFeuille123 wrote:

While the solutions from razyk and Ali take care of lazy initialization, with Ali's solution also having the benefit of being thread-safe, none of these methods are actually compatible with `const` or `immutable`, meaning that in any case where lazy initialization is needed, you cannot have a const class, you have to pick one or the other in the current setup.
Well, that's what `const` and `immutable` mean. If you need to restrict mutation in some other way, classic encapsulation works. Note that it is not all that often appropriate to use those qualifiers when writing traditional OO code.
Nov 17 2022
parent reply Vijay Nayar <madric gmail.com> writes:
On Thursday, 17 November 2022 at 22:01:21 UTC, Timon Gehr wrote:
 On 17.11.22 22:49, Vijay Nayar wrote:
 On Thursday, 17 November 2022 at 15:33:47 UTC, razyk wrote:
 On 17.11.22 13:04, MorteFeuille123 wrote:

While the solutions from razyk and Ali take care of lazy initialization, with Ali's solution also having the benefit of being thread-safe, none of these methods are actually compatible with `const` or `immutable`, meaning that in any case where lazy initialization is needed, you cannot have a const class, you have to pick one or the other in the current setup.
Well, that's what `const` and `immutable` mean. If you need to restrict mutation in some other way, classic encapsulation works. Note that it is not all that often appropriate to use those qualifiers when writing traditional OO code.
In response to this argument, I would cite the Dart language's implementation of `late` variables: https://dart.dev/guides/language/language-tour#late-variables To its core, Dart is fully object oriented, and it makes frequent use of immutable and const values which is used to optimize performance (in Dart, `const` is closer to D's `immutable`). The mechanism presented in the link above demonstrates the concept of lazy evaluation while also being compatible with const. The variable is still const, however, the computation of its initial and only value is deferred until its first usage.
Nov 18 2022
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 18.11.22 10:00, Vijay Nayar wrote:
 On Thursday, 17 November 2022 at 22:01:21 UTC, Timon Gehr wrote:
 On 17.11.22 22:49, Vijay Nayar wrote:
 On Thursday, 17 November 2022 at 15:33:47 UTC, razyk wrote:
 On 17.11.22 13:04, MorteFeuille123 wrote:

While the solutions from razyk and Ali take care of lazy initialization, with Ali's solution also having the benefit of being thread-safe, none of these methods are actually compatible with `const` or `immutable`, meaning that in any case where lazy initialization is needed, you cannot have a const class, you have to pick one or the other in the current setup.
Well, that's what `const` and `immutable` mean. If you need to restrict mutation in some other way, classic encapsulation works. Note that it is not all that often appropriate to use those qualifiers when writing traditional OO code.
In response to this argument, I would cite the Dart language's implementation of `late` variables: https://dart.dev/guides/language/language-tour#late-variables To its core, Dart is fully object oriented, and it makes frequent use of immutable and const values
I am not sure what the point is. Dart does not support D const/immutable and D does not support Dart final/const. They are different. D's built-in qualifiers really don't mix well with OO. One reason is the embarrassing limitation that there is no way to tail-qualify a class reference. Another reason is that "absolutely no mutation" is often incompatible with encapsulation as you may want to cache results or lazily initialize class fields. It is not surprising that with Dart being OO first, they have designed their superficially similar features to fit OO use cases.
 which is used to optimize performance (in 
 Dart, `const` is closer to D's `immutable`).
Actually, it is closer to `static immutable`. Dart `const` means "compile-time constant". There is also `final`, but it is not transitive.
 The mechanism presented in 
 the link above demonstrates the concept of lazy evaluation while also 
 being compatible with const. The variable is still const, however, the 
 computation of its initial and only value is deferred until its first 
 usage.
I don't understand why you'd want to do that. A const variable has a const initializer and it is final. What do you gain from late initializing such a variable? Also, Dart designers agree with me to the point where you actually can't even do that: "Members can't be declared to be both 'const' and 'late'" (Note that in D you are actually less restricted and can initialize immutable globals in a `shared static this` module constructor at program startup.) You can do "late final", but the meaning of that is equivalent to doing encapsulation manually with runtime checks like I suggested. Maybe create a mixin/mixin template or struct that does the things for you that Dart has built-in if you need those. The built-in qualifiers are probably not what you want if you need late initialization. I guess an alternative would be to create a DIP that adds Dart-like features, but I don't know how well that would be received.
Nov 18 2022
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 18.11.22 11:59, Timon Gehr wrote:
 
 
 which is used to optimize performance (in Dart, `const` is closer to 
 D's `immutable`).
Actually, it is closer to `static immutable`.
(But even that analogy has significant limitations, e.g., transitivity of Dart `const` is often checked at runtime, even if it is completely obvious during type checking that an assignment is not going to work.)
Nov 18 2022
parent reply Vijay Nayar <madric gmail.com> writes:
On Friday, 18 November 2022 at 11:06:44 UTC, Timon Gehr wrote:
 On 18.11.22 11:59, Timon Gehr wrote:
 
 
 which is used to optimize performance (in Dart, `const` is 
 closer to D's `immutable`).
Actually, it is closer to `static immutable`.
(But even that analogy has significant limitations, e.g., transitivity of Dart `const` is often checked at runtime, even if it is completely obvious during type checking that an assignment is not going to work.)
It's not as complicated as you are making it out to be. The purpose of Dart's late variables is as they describe: ``` When you mark a variable as late but initialize it at its declaration, then the initializer runs the first time the variable is used. This lazy initialization is handy in a couple of cases: * The variable might not be needed, and initializing it is costly. * You’re initializing an instance variable, and its initializer needs access to this. ``` The same use-cases would apply in D, these situations are programming-language agnostic. For example, you are building a complicated geometric library object (as is the case in the S2 Geometric library), and it contains a complex index data structure which will be used to organize the data before it is written to disk. When data is being written to disk, you want this variable to be computed only once, it will not change once used, and you would still like to use these objects in const contexts that will not change the values. However, if you are not calling the methods that write to disk, then computing the index isn't necessary and can be avoided. There are many cases where lazy initialization can be used to avoid costly setup, but currently in D, if you use lazy initialization, you must immediately rewrite all your code and avoid ever using `const`, because it might trigger initialization of lazy variables. This is especially silly for functions like "print" or "writeToDisk", which absolutely do not modify the classes at hand.
Nov 18 2022
parent Timon Gehr <timon.gehr gmx.ch> writes:
On 18.11.22 18:22, Vijay Nayar wrote:
 On Friday, 18 November 2022 at 11:06:44 UTC, Timon Gehr wrote:
 On 18.11.22 11:59, Timon Gehr wrote:
 which is used to optimize performance (in Dart, `const` is closer to 
 D's `immutable`).
Actually, it is closer to `static immutable`.
(But even that analogy has significant limitations, e.g., transitivity of Dart `const` is often checked at runtime, even if it is completely obvious during type checking that an assignment is not going to work.)
It's not as complicated as you are making it out to be.
I don't understand what you think is complicated. Everything I wrote was true. The issue was that in your post you were conflating concepts in D and Dart that are only superficially similar. Anyway, by all means, go ahead and create a DIP for lazy initialization. I have proposed this exact thing (with lazy keyword and all) in the past, but it never really caught on. (And frankly, it _would_ be confusing if for fields "lazy" actually meant lazy while for function parameters it still means "by name"...)
 The purpose of Dart's late variables is as they describe:
 
I am very much aware of the purpose of lazy initialization. It's one reason for my recommendation to avoid D's mutability qualifiers if any kind of abstraction is involved. The mutability qualifiers break abstractions. Anyway, Dart is not actually providing anything that you can't have in D. It does not actually have transitive `const` and `immutable` qualifiers. Those are the issue. If you don't use them you can use the same patterns as in Dart, just with different syntax.
 ...
 
 There are many cases where lazy initialization can be used to avoid 
 costly setup, but currently in D, if you use lazy initialization, you 
 must immediately rewrite all your code
Not if you avoided qualifiers. (As I do.)
 and avoid ever using `const`, 
Yes, that was my point. Lazy initialization is not the only common pattern that the qualifiers prevent. Don't use them unless you know exactly what you are doing and will want to do in the future. (So very infrequently or at least in very special circumstances.)
 because it might trigger initialization of lazy variables. This is 
 especially silly for functions like "print" or "writeToDisk", which 
 absolutely do not modify the classes at hand.
That's a weird way to put it because they do actually modify memory during late initialization and `const` prevents you from doing it. Don't use `const` if that's not what you want. Probably you want what's sometimes called "logical const", but that's not a feature in D. In your OP, you asked the question "Is there another way to perform lazy initialization that is compatible with const/immutable, or is this needed as a language feature?" You'd need to change the language to do what you want, but just adding built-in support for lazy initialization would not actually fix the more general problem. Mutation of memory is often an implementation detail, not just for lazy initialization. You'd probably just run into some other roadblock further down the line. And any hack you do in your own code to work around the strictness of qualifiers leads to undefined behavior. It's just not worth the hassle.
Nov 18 2022
prev sibling parent MorteFeuille123 <MorteFeuille123 ert.rt> writes:
On Thursday, 17 November 2022 at 15:33:47 UTC, razyk wrote:
 On 17.11.22 13:04, MorteFeuille123 wrote:
 Is there another way to perform lazy initialization that is 
 compatible with const/immutable, or is this needed as a 
 language feature?
No, this is needed as a bultin feature I'd say. I sometime think that since we can have local static variablbes, we could have local member variables. Similarly hiddden, only accessible through a getter, which replaces the need for a const member.
module2.d ``` import std.stdio; private string _lazy1; string compute(){ writeln("computing..."); return "_lazy1 val"; } string lazy1(){ if (_lazy1 == null) _lazy1 = compute(); return _lazy1; } ``` module1.d ``` import std.stdio; import module2; void main() { writeln(lazy1); writeln(lazy1); } ```
No, I tought to this pattern: auto getLazyGlobal() { static int result; // global but only accessible in getLazyGlobal static bool done; // ditto scope(exit) done = true; return done ? result : (result = compute()) } but applied to member variables. That would require a new syntax or a new storage class struct S { auto getLazyMember() { int S.result; bool S.done; scope(exit) done = true; return done ? result : (result = compute()) } } so that `result` and `done` are well `S` members but only accessible in `getLazyMember()`.
Nov 18 2022
prev sibling next sibling parent reply Siarhei Siamashka <siarhei.siamashka gmail.com> writes:
On Thursday, 17 November 2022 at 10:57:55 UTC, Vijay Nayar wrote:
 Is there another way to perform lazy initialization that is 
 compatible with const/immutable, or is this needed as a 
 language feature?
Wouldn't precomputing this const/immutable data at compile time via CTFE be a better idea in general? Unless it's some sort of a sparse data structure with a huge memory footprint and the goal is to postpone memory allocation. But then you would probably also want to free memory as soon as possible after you are done using it and const/immutable would be a hindrance again.
Nov 17 2022
parent Vijay Nayar <madric gmail.com> writes:
On Thursday, 17 November 2022 at 12:16:41 UTC, Siarhei Siamashka 
wrote:
 On Thursday, 17 November 2022 at 10:57:55 UTC, Vijay Nayar 
 wrote:
 Is there another way to perform lazy initialization that is 
 compatible with const/immutable, or is this needed as a 
 language feature?
Wouldn't precomputing this const/immutable data at compile time via CTFE be a better idea in general? Unless it's some sort of a sparse data structure with a huge memory footprint and the goal is to postpone memory allocation. But then you would probably also want to free memory as soon as possible after you are done using it and const/immutable would be a hindrance again.
CTFE would indeed have better performance for data that is known at compile time, but often, the objects created which need lazy evaluation, are specific to the instance and runtime data. A few examples include: * Initializing costly hardware connections/setup. * Creating large objects used to organize or index data that are only needed sometimes, e.g. when serializing for writing to disk. * Retrieving information from a network, e.g. a JWT or other authorization information to be used for a session.
Nov 17 2022
prev sibling parent =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 11/17/22 02:57, Vijay Nayar wrote:

 Is there another way to perform lazy initialization that is compatible
 with const/immutable, or is this needed as a language feature?
If the variable needs to be shared, there is a library solution: https://dlang.org/phobos/std_concurrency.html#.initOnce And here is a user of initOnce(): https://github.com/dlang/phobos/blob/master/std/parallelism.d#L3569 Ali
Nov 17 2022