digitalmars.D - const or immutable?
- =?UTF-8?Q?Ali_=c3=87ehreli?= (78/78) Sep 22 2021 Please excuse this thread here, which I know belongs more to the Learn
- Adam D Ruppe (11/15) Sep 22 2021 well i often ignore it but `const` (or often better yet, `inout`
- =?UTF-8?Q?Ali_=c3=87ehreli?= (19/34) Sep 22 2021 'inout' has been useful in cases like you mention. For example, a=20
- Timon Gehr (5/13) Sep 22 2021 No. I think `const` and especially `immutable` are great for plain old
- =?UTF-8?Q?Ali_=c3=87ehreli?= (8/12) Sep 23 2021 You mean, providing "const access" to an encapsulated entity through an
- Kagamin (6/6) Sep 22 2021 My guideline is that most stuff is mutable, so there's little
- =?UTF-8?Q?Ali_=c3=87ehreli?= (25/31) Sep 23 2021 I've been putting const as much as possible:
- Mathias LANG (72/92) Sep 22 2021 Short answer: `immutable` is a special case that is way too
- =?UTF-8?Q?Ali_=c3=87ehreli?= (9/11) Sep 22 2021 2.094 perhaps?
- Temtaime (3/16) Sep 23 2021 Surely, i'm using 'in' anywhere i need const. I'm avoiding using
- =?UTF-8?Q?Ali_=c3=87ehreli?= (3/5) Sep 23 2021 (You meant for parameters.) Makes sense! :)
- =?UTF-8?Q?Ali_=c3=87ehreli?= (48/54) Sep 23 2021 In addition to what you say, complexity in a tool is detrimental for
- Alexandru Ermicioi (8/9) Sep 23 2021 imho they can be interpreted like this for parameters:
- H. S. Teoh (120/210) Sep 24 2021 Nope. Probably should, but I don't because the transitivity of
- =?UTF-8?Q?Ali_=c3=87ehreli?= (93/165) Sep 24 2021 I am going there as well. However, you seem to have some guidelines,
- Kagamin (4/14) Sep 28 2021 I write parsers as splitters: the function takes a string, splits
Please excuse this thread here, which I know belongs more to the Learn forum but I am interested in the opinions of experts here who do not frequent that forum. tl;dr Do you use 'const' or 'immutable' by-default for parameters and for local data? Long version: It was simpler in C++: We would make everything 'const' as much as possible. Unfortunately, that doesn't work in D for at least two reasons: 1) There is 'immutable' as well 2) There is no head-const (i.e. no 'const' pointer to mutable data; i.e. "turtles all the way down") Further complications: - Reference semantics versus copy semantics; by type (e.g. slices), by the 'ref' keyword, by a member of a struct that has reference semanticts (struct is by-copy; but a member may not be), etc. - It is said that 'immutable' is a stronger type of const, which at first sounds great because if 'const' is good, 'immutable' should be even better, right? Unfortunately, we can't make everything 'immutable' because 'const' and 'immutable' have very different meanings at least in parameter lists. - Parameters versus local data. I am seeking simple guidelines like C++'s "make everything const." Let's start with what I like as descriptions about parameters: 1) 'const' parameter is "welcoming" because it can work with mutable, 'const', and 'immutable'. It (sometimes) means "I am not going to mutate your data." 2) 'immutable' parameter is "selective" because it can work only with 'immutable'. It means "I require immutable data." But it's not that simple because a 'const' parameter may be a copy of the argument, in which case, it means "I will not mutate *my* data." This is actually weird because we are leaking an implementation detail here: Why would the caller care whether we mutate our paramener or not? // Silly 'const': void foo(const int i) { // ... } But of course it matters if the type has indirections (e.g. a member is a reference to some other data). Aside: If 'const' is welcoming, why do we type 'string' for string parameters when we don't actually *require* immutable: // Unenecassary 'immutable' but I do this everywhere. void printFirstChar(string s) { write(s.front); } It should have better been const: void printFirstChar(const char[] s) { write(s.front); } But wait! It works above only because 'front' happened to work there. The problem is, 's' is not an input range; and that may matter elsewhere: static assert(isInputRange!(typeof(s))); // Fails. :( So only the elements of the string should be 'const': void printFirstChar(const(char)[] s) { write(s.front); static assert(isInputRange!(typeof(s))); // Passes. } (Granted, why is it 'const' anyway? Shouldn't printFirstChar be a function template? Yes, it should.) So, what are your guidelines here? More important to me: How do you define your local data that should not be mutated? const c = SomeStruct(); immutable i = SomeStruct(); In this case, 'const' is not "welcoming" nor 'immutable' is "selective" because these are not parameters; so, the keywords have a different meaning here: With local data, they both mean "do not mutate". Is 'immutable' better here because we may pass that data to an immutable-requiring function? Perhaps we should learn from string and make it really 'immutable'? But it's not the same because here 'immutable' applies to the whole struct whereas 'string's immutability is only with its elements. There! Not simple! :) Or, should 'immutable' be preferable here because it's implicitly shared, which may be useful in the future? Are there simple guidelines around this topic? Please? :) Personally, I generally ignore 'immutable' (except, implicitly in 'string') both for parameters and local data. Thank you, Ali
Sep 22 2021
On Wednesday, 22 September 2021 at 20:06:59 UTC, Ali Çehreli wrote:tl;dr Do you use 'const' or 'immutable' by-default for parameters and for local data?well i often ignore it but `const` (or often better yet, `inout` if there's any relation to a member of yours or if you return it) is better on function arguments, and `immutable` is better on internal members. The exception is when you are storing something from someone else, where immutable params might be more important. But if you are looking and not touching, const or `in` is good.Aside: If 'const' is welcoming, why do we type 'string' for string parameters when we don't actually *require* immutable:Unfortunate naming here... the convenient name encourages its use but it is indeed often suboptimal.
Sep 22 2021
On 9/22/21 1:17 PM, Adam D Ruppe wrote:On Wednesday, 22 September 2021 at 20:06:59 UTC, Ali =C3=87ehreli wrot=e:tl;dr Do you use 'const' or 'immutable' by-default for parameters and='inout' has been useful in cases like you mention. For example, a=20 slice's elements should reflect the mutability of 'this'. I am mostly=20 interested in revising a beginner-oriented chapter, where 'inout'=20 doesn't exist yet.for local data?well i often ignore it but `const` (or often better yet, `inout` if there's any relation to a member of yours or if you return it)is better on function arguments, and `immutable` is better on internal members. The exception is when you are storing something from someone else, whe=reimmutable params might be more important.I noticed that as well: For example, if a file name constructor=20 parameter is string, I don't need to copy the file name to a member.=20 (This actually appears later in the book where I realize constructor=20 arguments might better be 'immutable'.)But if you are looking and not touching, const or `in` is good.'in' has been another question: It appears a lot in my book but I almost = never use it myself. Perhaps that's the answer? I would like that.=20 However, the proposed change of meaning of 'in' is also worrying but I=20 think it will strenghthen the case for it's use I guess.itAside: If 'const' is welcoming, why do we type 'string' for string parameters when we don't actually *require* immutable:Unfortunate naming here... the convenient name encourages its use but =is indeed often suboptimal.I agree. However, ironically and worryingly :), it just works most of=20 the time. Ali
Sep 22 2021
On 22.09.21 22:06, Ali Çehreli wrote:Please excuse this thread here, which I know belongs more to the Learn forum but I am interested in the opinions of experts here who do not frequent that forum. tl;dr Do you use 'const' or 'immutable' by-default for parameters and for local data? ...No. I think `const` and especially `immutable` are great for plain old data types, especially data that has multiple references to it. For non-trivial user-defined data types, I think just using proper encapsulation is usually sufficient and more flexible.
Sep 22 2021
On 9/22/21 1:48 PM, Timon Gehr wrote:No. I think `const` and especially `immutable` are great for plain old data types, especially data that has multiple references to it. For non-trivial user-defined data types, I think just using proper encapsulation is usually sufficient and more flexible.You mean, providing "const access" to an encapsulated entity through an accessor. Sure... Still, do you make 'const' local objects of such types or are they auto? const a = MyStruct(); immutable b = MyStruct(); auto c = MyStruct(); // This one? Ali
Sep 23 2021
My guideline is that most stuff is mutable, so there's little reason to try to make it const. For most simple types like scalars and arrays const is relatively cheap and useful, but its usefulness decreases with complexity of the type, the more complex is the type the more likely it's mutable. Strings often benefit from immutability.
Sep 22 2021
On 9/22/21 2:34 PM, Kagamin wrote:My guideline is that most stuff is mutable, so there's little reason to try to make it const.I've been putting const as much as possible: const arr = foo(); However, as soon as I decide to mutate the result, I change it to auto: auto arr = foo(); arr[0] += 42; So, defensive programming prescribes 'const' and it seems like I did protect myself from accidental mutation. But how common are these accidental mutations? Is it so common to worth the neural activity and needing to change the code when it's not an accidental mutation? As I hinted in some of my DConf presentations, I started to question the value of some programming constructs. For example, I don't use 'private' unless a type gets complicated enough to protect its invariants. I agree that 'private' is useful for API types to change implementations freely but I still question: If I already documented how to use the API, even with examples; but the users went ahead and reached inside and used members of my API structs... I am in the opinion that if I still have the right to change the implementation. I don't think it's the end of the world if the users have to change their code.For most simple types like scalars and arrays const is relatively cheap and useful, but its usefulness decreases with complexity of the type, the more complex is the type the more likely it's mutable. Strings often benefit from immutability.There are cases where I remove a 'const' just to make the code compile instead of fighting the type system and it just works. The fact that I am embarrassed to write these must be telling: Either I am becomming more and more mediocre as a programmer (as I gain more experience!), or maybe there is something else here. :) Ali
Sep 23 2021
On Wednesday, 22 September 2021 at 20:06:59 UTC, Ali Çehreli wrote:tl;dr Do you use 'const' or 'immutable' by-default for parameters and for local data?Short answer: `immutable` is a special case that is way too prominent in D. `const` is my go-to *when I need it*.- It is said that 'immutable' is a stronger type of const, which at first sounds great because if 'const' is good, 'immutable' should be even better, right? Unfortunately, we can't make everything 'immutable' because 'const' and 'immutable' have very different meanings at least in parameter lists.I think we should not fall for the fallacy of "more attributes == good". More attributes just means more restrictions. Why do we need more restrictions ? To make our life easier when writing, reading, or changing code. The less a piece of code can do, the easier it is to understand. I think that fallacy started with ` safe`: It is good to make everything ` safe`, right ? Can't go wrong with memory safety. And `pure` also, because not using global is good, right ? Except it starts to be very impractical for `pure`, so we have hacks like save-set-reset `errno` to accommodate for it, because we *want* things to be `pure`, although they mutate global state. And then enters `nothrow`, and ` nogc`, and those categorically shouldn't be treated the same as ` safe`. Not everything needs to be `nothrow` / ` nogc`. And not everything needs to be constified.I am seeking simple guidelines like C++'s "make everything const."I don't think it's [that simple](https://youtu.be/qx22oxlQmKc?t=923).Aside: If 'const' is welcoming, why do we type 'string' for string parameters when we don't actually *require* immutable:This is IMO "D's greatest mistake". When Sociomantic did the D1 -> D2 transition, we came up with three aliases: ```D alias mstring = char[]; alias cstring = const(char)[]; alias istring = immutable(char)[]; ``` Our guidelines were simple: - Template Parameter ? => `istring`; - Mutable buffer => `mstring` (usually `ref` too as we used `assumeSafeAppend` a lot); - Otherwise, it's most likely `cstring`; The few exceptions I can remember were some constructors / setters for things that we knew would be constructed once per application lifetime (e.g. command-line parser). Obviously, `cstring` ended up being used in most places. It's now one of the first alias I define whenever I start a project that will deal with strings.But wait! It works above only because 'front' happened to work there. The problem is, 's' is not an input range; and that may matter elsewhere:IMO that's another problem. Ranges generally don't work (well) with `const` (or `immutable`) qualified elements, nor do they work well with non-copyable data.Or, should 'immutable' be preferable here because it's implicitly shared, which may be useful in the future? Are there simple guidelines around this topic? Please? :) Personally, I generally ignore 'immutable' (except, implicitly in 'string') both for parameters and local data.If we go back to those keywords definition: - `const`: Cannot be modified through this instance; - `immutable`: Cannot be modified through *any* instance; Now there's a very annoying (not so) corner case with the second definition: It binds the lifetime of the memory to the lifetime of *any* instance. Because if the data is destroyed, it means an instance can modify it, which contradicts `immutable`. Luckily, D has a GC, meaning we get away pretty easily with this... Until we want every part to function independently and then we don't. This was an intractable problem before `scope` had any effect, because you could stack-allocate an `immutable` array and they slice it, and boom! Now the only problem is that we need to combine immutable and smart pointers, so that we can properly free the memory whenever the last reference goes out of scope (remember AfixAllocator and Andrei's talk?). ------ But I am digressing from your original question. Want a simple set of guidelines ? Here's what we do: - Enable `-preview=in` (with DMD >= v2.095) and pass parameters by `in` for functions (even for types without indirections); - Avoid `in` for parameters which are stored (setters, ctors...); - Use `const` for parameter with indirections if possible; - Use `immutable` if needed (e.g. need to forward to a function that uses `immutable`); - Use mutable parameters otherwise; - For local data, don't make it `const` or `immutable` unless needed: If a function is so long that it doesn't fit on one's screen, break it down; It's simple, it works, it looks good.
Sep 22 2021
On 9/22/21 6:29 PM, Mathias LANG wrote:alias cstring = const(char)[];I've been thinking about that myself. Good to see it being used.- Enable `-preview=in` (with DMD >= v2.095) and pass parameters by `in`2.094 perhaps? https://dlang.org/changelog/2.094.0.html#preview-in Yeah, this meaning of 'in' makes a lot of sense to me. In fact, Adam mentioned 'in' as well. I can make it "standard" by recommending it in the book. >:) Ali P.S. I will respond to most other comments after digesting them a bit. :)
Sep 22 2021
On Thursday, 23 September 2021 at 03:31:01 UTC, Ali Çehreli wrote:On 9/22/21 6:29 PM, Mathias LANG wrote:Surely, i'm using 'in' anywhere i need const. I'm avoiding using explicit const/immutable.alias cstring = const(char)[];I've been thinking about that myself. Good to see it being used.- Enable `-preview=in` (with DMD >= v2.095) and pass parameters by `in`2.094 perhaps? https://dlang.org/changelog/2.094.0.html#preview-in Yeah, this meaning of 'in' makes a lot of sense to me. In fact, Adam mentioned 'in' as well. I can make it "standard" by recommending it in the book. >:) Ali P.S. I will respond to most other comments after digesting them a bit. :)
Sep 23 2021
On 9/23/21 2:41 AM, Temtaime wrote:Surely, i'm using 'in' anywhere i need const. I'm avoiding using explicit const/immutable.(You meant for parameters.) Makes sense! :) Ali
Sep 23 2021
On 9/22/21 6:29 PM, Mathias LANG wrote:I think we should not fall for the fallacy of "more attributes == good".In addition to what you say, complexity in a tool is detrimental for adoption, usability, undestandability, maintenance, etc.I don't think it's [that simple](https://youtu.be/qx22oxlQmKc?t=923).Excellent presentation. I have always respected Herb Sutter, am jealous of his methodical approach to problems, and happy to have met him at SD 2000 to chat briefly while having his book signed. Although most of what he presents is either already in D or discussed for D, he (and many other great minds) are still working for improving C++. I don't approve his ignoring D in the context of that talk; he is ever finds its way into C++, it will be again "other languages are taking from us but they never acknowledge it" (what I remember from past a Bjarne Stroustrup keynote speech). It takes a different kind of personality to realize that all improvements that one is thinking for C++ is already in D and to see it reason enough to put efforts in. (I know Herb Sutter knows about D because he has been rubbing shoulders with Andrei. He was a co-author of a proposal to add D's 'static if' for C++, where D was referenced.) I like how he says "I can't teach it. It's too complicated." Exactly! That's why I am here on this thread. Some of what he presented relates to D: - in, out, and ref - Some of Walter's efforts for live (when proposed improved C++ compilers analyzing flow graphs) - In a D constructor, initialization is "first assignment". - " = void"Our guidelines were simple: - Template Parameter ? => `istring`;Of course, manifest constants (e.g. enum) as well.- `const`: Cannot be modified through this instance; - `immutable`: Cannot be modified through *any* instance;Allow me to stay on an unnecessary detail: Those are very common definitions that are repeated elsewhere in this thread. However, partly encouraged by Herb Sutter's stressing "intents", I will explain my different take on it: Parameters are parts of a contract between the function and the caller; they must be described from the function's point of view: - When a function parameter is 'const', it is the function's telling to the caller "I promise I will not mutate it". (And of course the compiler enforces it.) - When a function parameter is 'immutable', it is the function's telling to the caller "I require immutable (with the literal meaning here)". Perhaps not an important difference but I think such details are important for teachability. And there is a difference between parameters and variables because the latter is not a requirement like it is for functions. TO me, an 'immutable' variable carries a meaning for it to be required to be so; 'const' does not have that meaning for a variable. That's why I like 'const' for variables like the following: const a = 42; // Simple immutable b = 42; // Brings more meaning and supposition Ali
Sep 23 2021
On Wednesday, 22 September 2021 at 20:06:59 UTC, Ali Çehreli wrote:....imho they can be interpreted like this for parameters: const: I won't change it. immutable: I won't change it, and expect no one else to change it too. Regards, Alexandru.
Sep 23 2021
On Wed, Sep 22, 2021 at 01:06:59PM -0700, Ali Çehreli via Digitalmars-d wrote:Please excuse this thread here, which I know belongs more to the Learn forum but I am interested in the opinions of experts here who do not frequent that forum. tl;dr Do you use 'const' or 'immutable' by-default for parameters and for local data?Nope. Probably should, but I don't because the transitivity of const/immutable makes it very difficult to use correctly in complex data structures. I *have* tried this before in various cases, but found that it usually requires a lot of busywork (oops compiler says can't pass const here, can't accept const there, OK let's fix this function, oops, the function it calls also needs const, oops, now it can't accept mutable, must use inout instead, oops, inout is a pain to get right, forget this, I give up), and the benefits, at least so far, are minimal -- it has maybe found 1 or 2 bugs in my code, but only after many hours of refactoring just to make everything const-correct. So IMHO, not worth the effort. Where I've had most success with const/immutable is with leaf-node modules implementing simple data structures, i.e., at the bottom of the dependency chain, where other code depends on it but it doesn't depend on other code. And perhaps one or two levels above that. But beyond that, it quickly becomes cumbersome and with benefits not proportional to the amount of effort required to make it work.Long version: It was simpler in C++: We would make everything 'const' as much as possible. Unfortunately, that doesn't work in D for at least two reasons: 1) There is 'immutable' as wellIt's very simple. Use const when the data could be either immutable or mutable (usually this applies to function parameters), immutable everywhere else. But since POD types make copies anyway, in practice I find const/immutable not worth the complexity with PODs. Strings are perhaps the most notable exception to this IME.2) There is no head-const (i.e. no 'const' pointer to mutable data; i.e. "turtles all the way down")TBH, I see this as an advantage. However, there *are* certainly cases where you really want head-const, but there's no in-language solution (and Phobos' Rebindable isn't a 100% solution).Further complications: - Reference semantics versus copy semantics; by type (e.g. slices), by the 'ref' keyword, by a member of a struct that has reference semanticts (struct is by-copy; but a member may not be), etc.Yeah, once you get beyond the most trivial data structures, you start running into problems with const/immutable due to the complicated interactions with everything else.- It is said that 'immutable' is a stronger type of const, which at first sounds great because if 'const' is good, 'immutable' should be even better, right? Unfortunately, we can't make everything 'immutable' because 'const' and 'immutable' have very different meanings at least in parameter lists.I think it's fallacious to try to put const/immutable on a scale of "better" or "worse". They serve different purposes: const is to guarantee the recipient of a reference cannot modify the referent; immutable is to guarantee the referent itself can never be modified. The former is like lending your data to somebody that you don't trust -- you yourself can still touch the data but the recipient is only allowed to look at it. The latter is when the data itself cannot ever change, not even by yourself.- Parameters versus local data. I am seeking simple guidelines like C++'s "make everything const."Parameters are where the const/immutable are most differentiated. Local data -- it depends. If you got it from another function call, it may already come as const or immutable, so you just have to follow what you receive (or weaken immutable to const if you wish, though I don't see the point unless you plan to rebind it to mutable later on). If it's pure POD locally-initialized, just use immutable.Let's start with what I like as descriptions about parameters: 1) 'const' parameter is "welcoming" because it can work with mutable, 'const', and 'immutable'. It (sometimes) means "I am not going to mutate your data." 2) 'immutable' parameter is "selective" because it can work only with 'immutable'. It means "I require immutable data."Const is like a 3rd party contract to ensure that they don't damage your data. They cannot change it, but someone who has write access (i.e., a mutable reference) still may. Immutable is like data made of steel: you cannot change it even if you wanted to. I.e., *nobody* has write access to it.But it's not that simple because a 'const' parameter may be a copy of the argument, in which case, it means "I will not mutate *my* data." This is actually weird because we are leaking an implementation detail here: Why would the caller care whether we mutate our paramener or not?In this case, I'd use `in` instead: this tells the caller "this parameter is an input; its value will not change afterwards". For PODs, it already doesn't change, but `in` reads nicer than `const`. :-D// Silly 'const': void foo(const int i) { // ... }This reads less silly: void foo(in int i) { ... } :-) [...]Aside: If 'const' is welcoming, why do we type 'string' for string parameters when we don't actually *require* immutable: // Unenecassary 'immutable' but I do this everywhere. void printFirstChar(string s) { write(s.front); } It should have better been const: void printFirstChar(const char[] s) { write(s.front); } But wait! It works above only because 'front' happened to work there. The problem is, 's' is not an input range; and that may matter elsewhere: static assert(isInputRange!(typeof(s))); // Fails. :(It makes me cringe everytime someone writes const/immutable without parentheses, because it's AMBIGUOUS, and that's precisely the problem here. What you *really* meant to write is const(char)[], but by omitting the parentheses you accidentally defaulted to const(char[]), which no longer works as a range. Also, const(char)[] works as long as you don't need to rebind. But if you do, e.g., in a parsing function that advances the range based on what was consumed, you run into trouble: // N.B.: ref because on exit, we update input to after the // token. void consumeOneToken(ref const(char)[] input) { ... } string input = "..."; consumeOneToken(input); // Oops, compile error On the surface, this seems like a bug, because input can be rebound without violating immutable (e.g., input = input[1..$] is valid). But on closer inspection, this is not always true: void evilFunction(ref const(char)[] input) { char[] immaMutable; input = immaMutable; // muahaha } string input = "..."; evilFunction(input); // oops, we just bound mutable to immutable This is why the compiler does not allow binding string (immutable(char)[]) to ref const(char)[]. But that also means you don't want to use const(char)[] when ref is involved. Instead, you want string: void consumeOneToken(ref string input) {...} // this works But then, this also means you can't pass in mutable char[]! So ultimately, one solution is, rebind string to const(char)[] in the *caller*, then pass it to the function: void consumeOneToken(ref const(char)[] input) { ... } string origInput = "..."; const(char)[] input = origInput; // ugly ugly consumeOneToken(input); // but at least this works nowSo only the elements of the string should be 'const': void printFirstChar(const(char)[] s) { write(s.front); static assert(isInputRange!(typeof(s))); // Passes. } (Granted, why is it 'const' anyway? Shouldn't printFirstChar be a function template? Yes, it should.)Why should it be? Using a template here generates bloat for no good reason. Using const(char)[] makes it work for either case with just one function in the executable.So, what are your guidelines here? More important to me: How do you define your local data that should not be mutated? const c = SomeStruct(); immutable i = SomeStruct(); In this case, 'const' is not "welcoming" nor 'immutable' is "selective" because these are not parameters; so, the keywords have a different meaning here: With local data, they both mean "do not mutate". Is 'immutable' better here because we may pass that data to an immutable-requiring function? Perhaps we should learn from string and make it really 'immutable'? But it's not the same because here 'immutable' applies to the whole struct whereas 'string's immutability is only with its elements. There! Not simple! :)Ugh. I wish people would stop writing const/immutable without parentheses. ;-) Always write it with parentheses, and the problem goes away: you write const(T[]) when you wish the whole object to be const (resp. immutable), and you write const(T)[] when you want individual elements to be immutable but the outer structure to be mutable. But yeah, this only works at the bottom-most level of abstraction. Once your type becomes more complex, it quickly devolves into a cascade of exploding const/immutable complexity, and you start running into lovely conundrums like how to make const(SomeStruct!T) == SomeStruct!(const(T)). Also, const for local variables are really only necessary if you got the data from a function that returns const; otherwise, it's practically no different from immutable and you might as well use immutable for stronger guarantees. [...]Personally, I generally ignore 'immutable' (except, implicitly in 'string') both for parameters and local data.[...] Immutable is powerful because it has very strong guarantees. Unfortunately, these very strong guarantees also make its scope of applicability extremely narrow -- so narrow that you rarely need to use it. :-D T -- Frank disagreement binds closer than feigned agreement.
Sep 24 2021
On 9/24/21 10:20 AM, H. S. Teoh wrote:That's disconcerting and matches my experience as well.tl;dr Do you use 'const' or 'immutable' by-default for parameters and for local data?Nope. Probably should, but I don't because the transitivity of const/immutable makes it very difficult to use correctly in complex data structures.So IMHO, not worth the effort.I am going there as well. However, you seem to have some guidelines, which makes me think you take advantage of immutability as long as it is practical.Where I've had most success with const/immutable is with leaf-node modules implementing simple data structures, i.e., at the bottom of the dependency chain, where other code depends on it but it doesn't depend on other code. And perhaps one or two levels above that. But beyond that, it quickly becomes cumbersome and with benefits not proportional to the amount of effort required to make it work.That answers one of my questions: There are cases where some local variables cannot be const or immutable because they will inevitably passed to high-level functions.Use const when the data could be either immutable or mutable (usually this applies to function parameters),That's the simplest case to agree on: Yes, function parameters should be 'const'.immutable everywhere else.This matches what I currently have in the book, which bugs me a little bit. (Note the following example comes with a bonus minor and off-topic issue, which may be related to your complaints about complexity hindering immutability.) ``` import std.algorithm; import std.range; void foo(const(int)[] input) { immutable halfLength = input.length / 2; immutable processed = input.map!(i => immutable(int)(i * i)).array; // ... } void main() { foo([ 1, 2 ]); } ``` Main issue: Ok, local variables are defined as immutable there. However, I see that as being superfluous (presumptuous?) because I definitely did not mean "*others* may not mutate it please". All I meant is immutable. So, using immutable carries that extra meaning, which is not really used there. <off-topic> Bonus minor and off-topic issue: I think we have an issue in the type system. If we replace `immutable(int)(i * i)` with just `i * i`, we get the following compilation error: Error: cannot implicitly convert expression `array(map(input))` of type `const(int)[]` to `immutable(int[])` The problem is, `i * i` has no indirections and is starting life afresh. It can be `immutable`. But I understand how the type of the right-hand side is separate from the type of the left-hand side. Let's try casting the right-hand side: auto processed = cast(immutable)input.map!(i => i * i).array; Ok, it works! :) (And of course, `auto` does not mean mutable there.) More off-topic: Today, I learned that the type of a vector in Rust is determined at first use. (Or something like that.) So, if `auto` were legal there in D, it would be demonstarted like the following in D: auto[] arr; // No type assigned yet // ... arr ~= 42; // Ok, the type of arr is int[] Still statically-typed... Too flexible for D? Perhaps not because we have something similar already, which I think Rust does not have: The return type of an auto function is determined by the first return statement in D. </off-topic>I think it's fallacious to try to put const/immutable on a scale of "better" or "worse".Like you and others, I agree and I tried to convey that point.Parameters are where the const/immutable are most differentiated. Local data -- it depends. If you got it from another function call, it may already come as const or immutable, so you just have to follow what you receive (or weaken immutable to const if you wish, though I don't see the point unless you plan to rebind it to mutable later on).Unfortunately, intermediate functions do weaken `immutable` in the name of being "more useful". (This is related to my "functions should be templates" comment below. void main() { immutable a = [ 1, 2, 3 ]; // Passes the array through foo() and bar() immutable b = foo(a); // Error: cannot implicitly convert expression // `foo(cast(const(int)[])a)` of type // `const(int)[]` to `immutable(int[])` } auto foo(const(int)[] arr) { return bar(arr); } auto bar(T)(T arr) { return arr; } The code compiles if foo() is a template because in that case the type is preserved as `immutable(int)[]`. Off-topic: I am sure you are aware of the magic that happened there. The types are *usefully* different: immutable(int[]) --> The type of 'a' in main. immutable(int)[] --> The type of 'arr' parameter of foo().If it's pure POD locally-initialized, just use immutable.Ok, I used `immutable` for non-POD and some strange thing happened. :)In this case, I'd use `in` instead: this tells the caller "this parameter is an input; its value will not change afterwards". For PODs, it already doesn't change, but `in` reads nicer than `const`. :-DI agree. I have many examples in the book where parameters are 'in'. But I don't have a single 'in' keyword in my code at work. :( I will start taking advantage of --preview=in.Yes but confusingly `s.front` works.void printFirstChar(const char[] s) { write(s.front); } But wait! It works above only because 'front' happened to work there. The problem is, 's' is not an input range; and that may matter elsewhere: static assert(isInputRange!(typeof(s))); // Fails. :(It makes me cringe everytime someone writes const/immutable without parentheses, because it's AMBIGUOUS, and that's precisely the problem here. What you *really* meant to write is const(char)[], but by omitting the parentheses you accidentally defaulted to const(char[]), which no longer works as a range.Also, const(char)[] works as long as you don't need to rebind. But if you do, e.g., in a parsing function that advances the range based on what was consumed, you run into trouble: // N.B.: ref because on exit, we update input to after the // token. void consumeOneToken(ref const(char)[] input) { ... } string input = "..."; consumeOneToken(input); // Oops, compile error On the surface, this seems like a bug, because input can be rebound without violating immutable (e.g., input = input[1..$] is valid). But on closer inspection, this is not always true: void evilFunction(ref const(char)[] input) { char[] immaMutable; input = immaMutable; // muahaha } string input = "..."; evilFunction(input); // oops, we just bound mutable to immutableThis is an issue I am aware of from C, which manifests itself in two levels of pointer indirection. (I can't construct an example but I recognize it when I hit the issue. :) )The example above is related to this issue. But I admit: These are not cases we face every day.(Granted, why is it 'const' anyway? Shouldn't printFirstChar be a function template? Yes, it should.)Why should it be? Using a template here generates bloat for no good reason. Using const(char)[] makes it work for either case with just one function in the executable.Also, const for local variables are really only necessary if you got the data from a function that returns const; otherwise, it's practically no different from immutable and you might as well use immutable for stronger guarantees.That answers a question but as I said above, `immutable` carries extra meaning and it's longer. ;)Immutable is powerful because it has very strong guarantees. Unfortunately, these very strong guarantees also make its scope of applicability extremely narrow -- so narrow that you rarely need to use it. :-DAgreed. For example, I use `immutable` when I pass data between threads. (And implicitly, every time I use `string`. ;) ) Ali
Sep 24 2021
On Friday, 24 September 2021 at 17:20:04 UTC, H. S. Teoh wrote:On the surface, this seems like a bug, because input can be rebound without violating immutable (e.g., input = input[1..$] is valid). But on closer inspection, this is not always true: void evilFunction(ref const(char)[] input) { char[] immaMutable; input = immaMutable; // muahaha } string input = "..."; evilFunction(input); // oops, we just bound mutable to immutableI write parsers as splitters: the function takes a string, splits it in two parts and returns them, or the parser is a struct that keeps the string inside.
Sep 28 2021