www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - const assignments problem again

reply bearophile <bearophileHUGS lycos.com> writes:
I have discussed about this topic once in past, but in the meantime I have seen
this is a quite common problem, so I think it doesn't harm to touch this topic
again.

This is a direct D translation of the original C or C++ code:


// version #1
double foo;
if (abs(e.x - v.x) > double.min)
    foo = (v.y - e.y) / (v.x - e.x);
else
    foo = double.max;


This version is clear, easy to understand, efficient, and it's not bug-prone.
Some coding standards require those to use full braces there. But it has some
problems too:
- It's not DRY, "foo" is repeated three times;
- You can't use "auto" there, so if the type of e.x changes, you have to change
the foo type manually (double.min is replaceable with typeof(e.x).min).
- It's not short code.
- And foo can't be const or immutable, I don't like this.


In this specific example one of the two branches of the if contains just a
constant, so you are allowed to write:

// version #2
double foo = double.max;
if (abs(e.x - v.x) > double.min)
    foo = (v.y - e.y) / (v.x - e.x);


But generally you can't do that because the then-else branches of the if are
meant to be computed lazily, only one of them.


To turn foo constant, you are free to use a temporary mutable variable, but
this makes the local namespace even more dirty:

// version #3
double _blue;
if (abs(e.x - v.x) > double.min)
    foo = (v.y - e.y) / (v.x - e.x);
else
    foo = double.max;
immutable foo = _blue;


In D you are allowed to create a function and call it in place:

// version #4
const foo = {
    if (abs(e.x - v.x) > double.min)
        return (v.y - e.y) / (v.x - e.x);
    else
        return double.max;
}();


Version #4 has some downsides:
- The return type of this delegate is inferenced. This means that both branches
of the if must return the same type. Currently in D this disallows some
possibilities (I hope this will be fixed), in some cases you have to cast the
empty result to the same type of the other if branch;
- The code is even longer and I don't like its look a lot (in JavaScript it's
fine);
- It's not certain that every D compiler will always inline that delegate. This
risks lower performance;
- That delegate uses values defined outside it, so it can't be pure, so the
function that contains such code can't be pure. You solve this problem making
the code more complex, passing the needed local variables as arguments to the
delegate, but this is not handy.


Version #5 is an acceptable solution, it is compact and it defines just one
variable, that is constant, but the C conditional expression is bug-prone and
it's a bit tricky (it's a common source of various bugs), I don't like it. This
code is less maintainable (if you want to add something you sometimes need to
convert it again into a normal if). 

// version #5
const foo = (abs(e.x - v.x) > double.min) ?
             ((v.y - e.y) / (v.x - e.x)) :
             double.max;


In my precedent post about this topic I have discussed "nested constness" and
another partially silly idea. Recently I have seen a language that suggests me
this:

// version #6
const foo;
if (abs(e.x - v.x) > double.min)
    foo = (v.y - e.y) / (v.x - e.x);
else
    foo = double.max;


The compiler makes sure all paths assign values with the same type to foo (as
in the case of the two returns inside the delegate, that must be of the same
type). But if you introduce goto instructions version #6 looks fragile. If the
assign is far away from the definition the code looks not so nice any more, so
this feature is meant for short-range initializations only.

Bye,
bearophile
Aug 06 2011
next sibling parent reply Adam D. Ruppe <destructionator gmail.com> writes:
Reading this post, I kept thinking "why aren't you just using
the ternary operator?"

 I don't like it.

That's not good enough.
Aug 06 2011
next sibling parent reply bearophile <bearophileHUGS lycos.com> writes:
Adam D. Ruppe:

 I don't like it.

That's not good enough.

I agree, that's why I have added two more points: - conditional expression is bug-prone and it's a bit tricky (it's a common source of various bugs), because of it operator precedence too; - It is less maintainable (if you want to add something you sometimes need to convert it again into a normal if). Bye, bearophile
Aug 06 2011
parent Adam D. Ruppe <destructionator gmail.com> writes:
bearophile:
 I agree, that's why I have added two more points

The precedence of the ternary is pretty easy to handle - if there's more than one thing in there, just use parenthesis. That's the only thing I can think of that would make it any more bug prone than other branches. Perhaps it'd be worth thinking if parens should be required in any non-trivial cases. auto a = something ? "one" : "two"; // ok auto b = a ~ something ? "one" : "two"; // error, please add parens for clarity
 - It is less maintainable

I don't agree that changing to an if is any burden at all. This is like saying "if(x) y;" is unacceptable because you might have to add braces later. Real maintenance headaches are things like duplicated code or unnecessary complexity. This is just a very simple case of syntax translation.
Aug 06 2011
prev sibling parent "Marco Leise" <Marco.Leise gmx.de> writes:
Am 07.08.2011, 06:31 Uhr, schrieb Adam D. Ruppe  
<destructionator gmail.com>:

 bearophile:
 I agree, that's why I have added two more points

The precedence of the ternary is pretty easy to handle - if there's more than one thing in there, just use parenthesis. That's the only thing I can think of that would make it any more bug prone than other branches. Perhaps it'd be worth thinking if parens should be required in any non-trivial cases. auto a = something ? "one" : "two"; // ok auto b = a ~ something ? "one" : "two"; // error, please add parens for clarity
 - It is less maintainable

I don't agree that changing to an if is any burden at all. This is like saying "if(x) y;" is unacceptable because you might have to add braces later. Real maintenance headaches are things like duplicated code or unnecessary complexity. This is just a very simple case of syntax translation.

The anonymous function solution looks good to me. You have full flexibility while keeping the variable const and the initialization code is at the declaration site.
Aug 07 2011
prev sibling next sibling parent reply Kagamin <spam here.lot> writes:
bearophile Wrote:

 - And foo can't be const or immutable, I don't like this.

I suppose accidental overwrite bugs are overrated. I have never seen them even from evil programmers. If you write random code, overwrite is not your only problem, you can as well read wrong variable or call wrong function. No language will help you if your code is junk. You should fix this in a different way.
Aug 07 2011
next sibling parent reply Kagamin <spam here.lot> writes:
P.S. you would make a better point if it were a const instance of a class.
Aug 07 2011
parent bearophile <bearophileHUGS lycos.com> writes:
Kagamin:

 P.S. you would make a better point if it were a const instance of a class.

You can't even define V as "const struct" and use it like this: const struct V { double x, y; } // wrong void main() { double x = 1, y = 2; int c = 1; V v; switch (c) { case 1: v = V(x, y); break; case 2: v = V(-x, -y); break; // other cases here default: assert(0); } } This works (the asm shows DMD doesn't inline the function): const struct V { double x, y; } void main() { double x = 1, y = 2; int c = 1; V v = { switch (c) { case 1: return V(x, y); case 2: return V(-x, -y); // other cases here default: assert(0); } }(); } Bye, bearophile
Aug 20 2011
prev sibling parent reply Jonathan M Davis <jmdavisProg gmx.com> writes:
On Sunday 07 August 2011 04:40:52 Kagamin wrote:
 bearophile Wrote:
 - And foo can't be const or immutable, I don't like this.

I suppose accidental overwrite bugs are overrated. I have never seen them even from evil programmers. If you write random code, overwrite is not your only problem, you can as well read wrong variable or call wrong function. No language will help you if your code is junk. You should fix this in a different way.

Being able to use const can be very valuable. For instance, what if you were using std.algorithm.copy and got the arguments backwards? If the source is const, then the compiler will complain, and you'll quickly find the bug. If the source isn't const, then you could accidentally end up copying the target to the source, and it may or may not be an easy bug to catch. I made that exact mistake in C++ with its copy function just the other day. const saved me a lot of headaches. Now, I think that D gives us enough ways to deal with the problem that Bearophile illustrates here (and Bearophile actually showed us a number of ways that D allows us to do what he's trying to do), so I don't think that we really need to do anything to the language to better deal with this situation. But accidental overwrites _can_ be a problem, and that's one of the things that const catches. So, not being able to use const when you should logically be able to due to syntax problems in the language would definitely be a problem - not the biggest problem ever perhaps, but it _would_ be a problem. Fortunately however, D gives us plenty of ways to get around the problem. - Jonathan M Davis
Aug 07 2011
parent Kagamin <spam here.lot> writes:
Jonathan M Davis Wrote:

 Being able to use const can be very valuable. For instance, what if you were 
 using std.algorithm.copy and got the arguments backwards? If the source is 
 const, then the compiler will complain, and you'll quickly find the bug. If
the 
 source isn't const, then you could accidentally end up copying the target to 
 the source, and it may or may not be an easy bug to catch.

I've seen a bug. I fixed two methods: begin and end and sent the patch to a man. They were calling other methods (sort of begin1 and end1). The man slightly fixed my patch and copied the fixed body of the first method to both of them (they look very similar). The compiler was happy, the application even worked. The bug was catched only because I checked whether the man did it right.
Aug 07 2011
prev sibling next sibling parent Kagamin <spam here.lot> writes:
bearophile Wrote:

 - You can't use "auto" there, so if the type of e.x changes, you have to
change the foo type manually (double.min is replaceable with typeof(e.x).min).

This is a bad idea too. The code assumes the values are double. If this assumption is not true, the code is broken.
Aug 07 2011
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Sunday 07 August 2011 06:02:36 Kagamin wrote:
 Jonathan M Davis Wrote:
 Being able to use const can be very valuable. For instance, what if you
 were using std.algorithm.copy and got the arguments backwards? If the
 source is const, then the compiler will complain, and you'll quickly
 find the bug. If the source isn't const, then you could accidentally
 end up copying the target to the source, and it may or may not be an
 easy bug to catch.

I've seen a bug. I fixed two methods: begin and end and sent the patch to a man. They were calling other methods (sort of begin1 and end1). The man slightly fixed my patch and copied the fixed body of the first method to both of them (they look very similar). The compiler was happy, the application even worked. The bug was catched only because I checked whether the man did it right.

??? I don't understand what you're trying to say. I don't know if your English is just poor or if you're trying to make fun of me. const has value. It obviously doesn't solve everything or catch every bug, but it can help a lot in ensuring that variables that are not supposed to be mutated aren't mutated. You can choose not to use it, and that's fine, but it's part of the language, and many other people value it highly. - Jonathan M Davis
Aug 07 2011
prev sibling next sibling parent reply Lutger Blijdestijn <lutger.blijdestijn gmail.com> writes:
bearophile wrote:
... 
 In my precedent post about this topic I have discussed "nested constness"
 and another partially silly idea. Recently I have seen a language that
 suggests me this:
 
 // version #6
 const foo;
 if (abs(e.x - v.x) > double.min)
     foo = (v.y - e.y) / (v.x - e.x);
 else
     foo = double.max;
 
 
 The compiler makes sure all paths assign values with the same type to foo
 (as in the case of the two returns inside the delegate, that must be of
 the same type). But if you introduce goto instructions version #6 looks
 fragile. If the assign is far away from the definition the code looks not
 so nice any more, so this feature is meant for short-range initializations
 only.
 
 Bye,
 bearophile

I like the ternary operator the best for this, as it is the simplest. I always write them like this though, liberally include parenthesis and never nest: (condition) ? (truth value) : (false value) When it gets more complicated, you can always rewrite either the whole expression (to if/else or a function) or refactor parts of the expression to small functions. I never find this to be much of a burden. Python has if/else expressions, but due to it's (messy) scoping rules I almost never find them an improvement to if/else statements. I like the single assignment feature, but not for this reason. I think it would be more useful for creating immutable data in general.
Aug 07 2011
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
Lutger Blijdestin wrote:
 I like the ternary operator the best for this, as it is the simplest. I
 always write them like this though, liberally include parenthesis and never
 nest:
  (condition)
     ? (truth value)
     : (false value)

What is the benefit of this, compared to leaving parentheses away entirely?
Aug 07 2011
parent Lutger Blijdestijn <lutger.blijdestijn gmail.com> writes:
Timon Gehr wrote:

 Lutger Blijdestin wrote:
 I like the ternary operator the best for this, as it is the simplest. I
 always write them like this though, liberally include parenthesis and
 never nest:
  (condition)
     ? (truth value)
     : (false value)

What is the benefit of this, compared to leaving parentheses away entirely?

Oh, I didn't mean to *always* include parenthesis. Just that when a more complicated expression is involved, I find it often faster to understand if there are some redundant parenthesis.
Aug 07 2011
prev sibling parent reply Jacob Carlborg <doob me.com> writes:
On 2011-08-07 03:19, bearophile wrote:
 I have discussed about this topic once in past, but in the meantime I have
seen this is a quite common problem, so I think it doesn't harm to touch this
topic again.

 This is a direct D translation of the original C or C++ code:


 // version #1
 double foo;
 if (abs(e.x - v.x)>  double.min)
      foo = (v.y - e.y) / (v.x - e.x);
 else
      foo = double.max;

If D's statements were expressions instead, this could work: const foo = if (abs(e.x - v.x)> double.min) (v.y - e.y) / (v.x - e.x); else double.max; -- /Jacob Carlborg
Aug 07 2011
parent reply Don <nospam nospam.com> writes:
Jacob Carlborg wrote:
 On 2011-08-07 03:19, bearophile wrote:
 I have discussed about this topic once in past, but in the meantime I 
 have seen this is a quite common problem, so I think it doesn't harm 
 to touch this topic again.

 This is a direct D translation of the original C or C++ code:


 // version #1
 double foo;
 if (abs(e.x - v.x)>  double.min)
      foo = (v.y - e.y) / (v.x - e.x);
 else
      foo = double.max;

If D's statements were expressions instead, this could work: const foo = if (abs(e.x - v.x)> double.min) (v.y - e.y) / (v.x - e.x); else double.max;

I find that much more difficult to read. Especially consider const foo = (a > b) ? bar() : baz(); compared to const foo = if (a > b) bar(); else baz(); You have to read quite a lot of code before you get any visual cue that the return value of baz() is used. IMHO: to understand code, I think you really need to know if you're looking at an expression or a statement, so making 'if' do both jobs reduces code clarity. (Would be OK in a language where it was _always_ an expression).
Aug 08 2011
parent Jacob Carlborg <doob me.com> writes:
On 2011-08-09 07:04, Don wrote:
 Jacob Carlborg wrote:
 On 2011-08-07 03:19, bearophile wrote:
 I have discussed about this topic once in past, but in the meantime I
 have seen this is a quite common problem, so I think it doesn't harm
 to touch this topic again.

 This is a direct D translation of the original C or C++ code:


 // version #1
 double foo;
 if (abs(e.x - v.x)> double.min)
 foo = (v.y - e.y) / (v.x - e.x);
 else
 foo = double.max;

If D's statements were expressions instead, this could work: const foo = if (abs(e.x - v.x)> double.min) (v.y - e.y) / (v.x - e.x); else double.max;

I find that much more difficult to read. Especially consider const foo = (a > b) ? bar() : baz(); compared to const foo = if (a > b) bar(); else baz(); You have to read quite a lot of code before you get any visual cue that the return value of baz() is used. IMHO: to understand code, I think you really need to know if you're looking at an expression or a statement, so making 'if' do both jobs reduces code clarity. (Would be OK in a language where it was _always_ an expression).

I think this syntax fits when using a ternary operator would be too long and creating one (or two) new function(s) would just be annoying. But if the if-statement wasn't an expression in all cases it would be very confusing. I see no reason why many of the statements in D couldn't be expressions instead, but I can understand that it's way too late to change that now. -- /Jacob Carlborg
Aug 09 2011