www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Reopening the debate about non-nullable-by-default: initialization of

reply "Idan Arye" <GenericNPC gmail.com> writes:
We are all sick and tired of this debate, but today I've seen a 
question in Stack Exchange's Programmers board that raises a 
point I don't recall being discussed here:

http://programmers.stackexchange.com/questions/237749/how-do-languages-with-maybe-types-instead-of-nulls-handle-edge-conditions

Consider the following code:

     class Foo{
         void doSomething(){
         }
     }

     class Bar{
         Foo foo;

         this(Foo foo){
             doSomething();
             this.foo=foo;
         }

         void doSomething(){
             foo.doSomething();
         }
     }

Constructing an instance of `Bar`, of course, segfaults when it 
calls `doSomething` that tries to call `foo`'s `doSomething`. The 
non-nullable-by-default should avoid such problems, but in this 
case it doesn't work since we call `doSomething` in the 
constructor, before we initialized `foo`.

Non-nullable-by-default is usually used in functional languages, 
where the emphasis on immutability requires a syntax that always 
allow initialization at declaration, so they avoid this problem 
elegantly. This is not the case in D - member fields are declared 
at the body of the class or struct and initialized at the 
constructor - separate statements that nothing stops you from 
putting other statements between them.

Of course, D does support initialization at declaration for 
member fields, but this is far from sufficient a solution since 
very often the information required for setting the member field 
resides in the constructor's arguments. In the example, we can't 
really initialize `foo` at the declaration since we are supposed 
to get it's initial value in the constructor's argument `Foo foo`.



I can think of 3 solutions - each with it's own major drawback 
and each with a flaw that prevents it from actually solving the 
problem, but I'll write them here anyways:

1) Using a static analysis that probes into function calls. The 
major drawback is that it'll probably be very hard to implement. 
The reason it won't work is that it won't be able to probe into 
overriding methods, which might use an uninitialized member field 
that the overrided method doesn't use.

2) Disallow calling functions in the constructor before *all* 
non-nullable member fields are initialized(and of course, the 
simple static analysis that prevent usage before initialization 
directly in the constructor code). The major drawback is that 
sometimes you need to call a function in order to initialize the 
member field. The reason is best demonstrated with code:
     class Foo{
         void doSomething(){
         }
     }

     class Bar{
         this{
             doSomething();
         }

         void doSomething(){
         }
     }

     class Baz : Bar{
         Foo foo;

         this(Foo foo){
             this.foo=foo;
         }

         override void doSomething(){
             foo.doSomething();
         }
     }
`Bar`'s constructor is implicitly called before `Baz`'s 
constructor.

3) Use a Scala-like syntax where the class' body is a constructor 
that all other constructors must call, allowing initialization on 
declaration for member fields in all cases. The major drawback is 
that this is a new syntax that'll have to be used in order to 
have non-nullable member fields - which means it'll break almost 
every existing code that uses classes. Not fun. The reason it 
won't work is that declarations in the struct\class' body are not 
ordered. In Scala, for example, this compiles and breaks with 
null pointer exception when trying to construct `Bar`:
     class Foo{
         def doSomething(){
         }
     }

     class Bar(foo : Foo){
         doSomething();
         val m_foo=foo;

         def doSomething(){
             m_foo.doSomething();
         }
     }
Also, like the previous two methods, overriding methods breaks 
it's promises.



This issue should be addressed before implementing 
non-nullable-by-default.
May 02 2014
next sibling parent "bearophile" <bearophileHUGS lycos.com> writes:
Idan Arye:

 today I've seen a question in Stack Exchange's Programmers 
 board that raises a point I don't recall being discussed here:

This program: class A { immutable int x; this() { foo(); x = 1; x = 2; } void foo() { auto y = x; } } void main() {} Gives: temp.d(6,9): Error: immutable field 'x' initialized multiple times So D can tell x is initialized more than 1 time, but it can't tell x is initialized 0 times inside foo(). Bye, bearophile
May 02 2014
prev sibling next sibling parent Jonathan M Davis via Digitalmars-d <digitalmars-d puremagic.com> writes:
On Sat, 03 May 2014 00:50:14 +0000
Idan Arye via Digitalmars-d <digitalmars-d puremagic.com> wrote:

 We are all sick and tired of this debate, but today I've seen a
 question in Stack Exchange's Programmers board that raises a
 point I don't recall being discussed here:

 http://programmers.stackexchange.com/questions/237749/how-do-languages-with-maybe-types-instead-of-nulls-handle-edge-conditions

 Consider the following code:

 class Foo{
 void doSomething(){
 }
 }

 class Bar{
 Foo foo;

 this(Foo foo){
 doSomething();
 this.foo=foo;
 }

 void doSomething(){
 foo.doSomething();
 }
 }

 Constructing an instance of `Bar`, of course, segfaults when it
 calls `doSomething` that tries to call `foo`'s `doSomething`. The
 non-nullable-by-default should avoid such problems, but in this
 case it doesn't work since we call `doSomething` in the
 constructor, before we initialized `foo`.

Yeah, I brought this up before, and it's one of the reasons why I'm against non-nullable by default. It means that class references will need to be treated the same as structs whose init property is disabled, which can be _very_ limiting. And I don't know if we currently handle structs with disabled init properties correctly in all cases, since it's not all that hard for something subtle to have been missed and allow for such a struct to be used before it was actually initialized (and the fact that not much code uses them would make it that much more likely that such a bug would go unnoticed). Hopefully, all those issues have been sorted out by now though. If so, then I would think that we already have all of the rules in place for how non-nullable references would be dealt with with regards to initialization, but they'd still be very limiting, because most of D expects that all types have an init property. - Jonathan M Davis
May 02 2014
prev sibling next sibling parent "bearophile" <bearophileHUGS lycos.com> writes:
Jonathan M Davis:

 Idan Arye:
 We are all sick and tired of this debate, but today I've seen a
 question in Stack Exchange's Programmers board that raises a
 point I don't recall being discussed here:


 Yeah, I brought this up before, and it's one of the reasons why 
 I'm against non-nullable by default.

I think that problem was raised plenty of times during the discussions about non-nullable. And I think this paper (and successive ones) solve enough the problems: http://research.microsoft.com/en-us/um/people/leino/papers/krml109.pdf And aren't the ideas about cooked/uncooked (referred as raw in the paper) already partially present in D? Bye, bearophile
May 03 2014
prev sibling parent "w0rp" <devw0rp gmail.com> writes:
I would allow this bug. This also happens with 'final' member in 
Java, and there you expect the value to not be null. A rule of 
thumb should be to never call virtual methods from inside of a 
constructor. Bad things happen.
May 03 2014