www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Open question: what code pattern you use usually for null safety

reply ddcovery <antoniocabreraperez gmail.com> writes:
I know there is other threads about null safety and the 
"possible" ways to support this in D and so on.

This is only an open question to know what code patterns you 
usually use to solve this situation in D:

   if(person.father.father.name == "Peter") doSomething();
   if(person.father.age > 80 ) doSomething();

knowing that *person*, or its *father* property can be null

i.e.: the incremental null check solution

if(
   person !is null &&
   person.father !is null &&
   person.father.father !is null &&
   person.father.father.name == "Peter"
)
{
   doSomething();
}

or the "monad" way

[person].
   filter!"a !is null".map!"a.father".
   filter!"a !is null".map!"a.father".
   filter!"a !is null".map!"a.name".
   each!( (name) {
     if(name == "Peter") doSomething();
   });

or, may be, you have some helper function/structs/templates

if( person.d!"father".d!"father".d!"name".get == "Peter" ){
   doSomething()
}
if( person.d!"father".d!"age".get(0) > 80 ){
   doSomething()
}

or an "xml path" like template

if( person.get!"father.father.name" == "Peter" )
if( person.get!"father.father.name.length"(0) == 5 )
if( person.get!"father.father.age"(0) > 80 )


If it's not a bother, I'd like to know how you usually approach it

Thanks!!!
Jan 14
next sibling parent reply Adam D. Ruppe <destructionator gmail.com> writes:
On Thursday, 14 January 2021 at 18:24:44 UTC, ddcovery wrote:
 This is only an open question to know what code patterns you 
 usually use to solve this situation in D:
I'm almost never in this situation except for reading things like xml or json data that may be missing. So I just special cased those. My json lib doesn't return null per se, it returns var(null) which is allowed to just return more harmless nulls. Thus you write `person.father.father.name.get!string` and it will be empty if anything was null in the chain. With dom, you can optionSelector("person > father > father > name").innerText and again if the selector returned null, all its methods also just return empty strings or whatever. So the library handles these special cases and then I don't worry about nested nulls anywhere else since I consider it bad style to even be in that situation in the first place.
Jan 14
parent ddcovery <antoniocabreraperez gmail.com> writes:
On Thursday, 14 January 2021 at 19:24:54 UTC, Adam D. Ruppe wrote:
 On Thursday, 14 January 2021 at 18:24:44 UTC, ddcovery wrote:
 This is only an open question to know what code patterns you 
 usually use to solve this situation in D:
I'm almost never in this situation except for reading things like xml or json data that may be missing.
Yes, this is the usual situation (Personally, I use "DTO" structured objects... that are serialized/unserialized to JSON)
 So I just special cased those. My json lib doesn't return null 
 per se, it returns var(null) which is allowed to just return 
 more harmless nulls. Thus you write 
 `person.father.father.name.get!string` and it will be empty if 
 anything was null in the chain.

 With dom, you can optionSelector("person > father > father > 
 name").innerText and again if the selector returned null, all 
 its methods also just return empty strings or whatever.
Selectors are a good option to navigate on dom/json, but on structured objects too. The good think with "templates" in D is that this "path/selector" can be compiled internally to a map/filter combination completly "null" free... I was experimenting last days with this and I think that a functional orientation (using a MayBe monad implemented as Range ) is the best way to begin.
 So the library handles these special cases and then I don't 
 worry about nested nulls anywhere else since I consider it bad 
 style to even be in that situation in the first place.
I agree: it is a bad style. Personally I allways use MayBe monads in my "DTO"s (that is the effect of having worked with scala :-). The only "cons" with Nullable!T (the "standard" D MayBe equivalent) is that it is not "compatible" with Range libraries (it is not a Range: you use "apply" instead "map", you have not "filter", you can't "chain" a range and Nullable, you can't "join" a range of Nullables like a Range of ranges). This is the reason I'm "experimenting" with my own "MayBe" InputRange that can be created in the form of "Some" or "None" (it's inmutable contrary to what happens with Nullable) and is compatible with all std.algorithm (and array) library.
Jan 14
prev sibling next sibling parent mw <mingwu gmail.com> writes:
On Thursday, 14 January 2021 at 18:24:44 UTC, ddcovery wrote:
 I know there is other threads about null safety and the 
 "possible" ways to support this in D and so on.

 This is only an open question to know what code patterns you 
 usually use to solve this situation in D:

   if(person.father.father.name == "Peter") doSomething();
   if(person.father.age > 80 ) doSomething();

 knowing that *person*, or its *father* property can be null

 i.e.: the incremental null check solution
I just use this most simple one:
 if(
   person !is null &&
   person.father !is null &&
   person.father.father !is null &&
   person.father.father.name == "Peter"
 )
 {
   doSomething();
 }
Reason: easy to read and reason about, esp for non-authors of this piece of the code.
Jan 14
prev sibling next sibling parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 1/14/21 1:24 PM, ddcovery wrote:
 I know there is other threads about null safety and the "possible" ways 
 to support this in D and so on.
 
 This is only an open question to know what code patterns you usually use 
 to solve this situation in D:
 
    if(person.father.father.name == "Peter") doSomething();
    if(person.father.age > 80 ) doSomething();
 
 knowing that *person*, or its *father* property can be null
 
 i.e.: the incremental null check solution
 
 if(
    person !is null &&
    person.father !is null &&
    person.father.father !is null &&
    person.father.father.name == "Peter"
 )
 {
    doSomething();
 }
 
 or the "monad" way
 
 [person].
    filter!"a !is null".map!"a.father".
    filter!"a !is null".map!"a.father".
    filter!"a !is null".map!"a.name".
    each!( (name) {
      if(name == "Peter") doSomething();
    });
 
 or, may be, you have some helper function/structs/templates
 
 if( person.d!"father".d!"father".d!"name".get == "Peter" ){
    doSomething()
 }
 if( person.d!"father".d!"age".get(0) > 80 ){
    doSomething()
 }
 
 or an "xml path" like template
 
 if( person.get!"father.father.name" == "Peter" )
 if( person.get!"father.father.name.length"(0) == 5 )
 if( person.get!"father.father.age"(0) > 80 )
 
 
 If it's not a bother, I'd like to know how you usually approach it
 
 Thanks!!!
 
 
You could kinda automate it like: struct NullCheck(T) { private T* _val; auto opDispatch(string mem)() if (__traits(hasMember, T, mem)) { alias Ret = typeof(() { return __traits(getMember, *_val, mem); }()); if(_val is null) return NullCheck!(Ret)(null); else return NullCheck!(Ret)(__trats(getMember, *_val, mem)); } bool opCast(V: bool)() { return _val !is null; } } auto nullCheck(T)(T *val) { return AutoNullCheck!T(val);} // usage if(nullCheck(person).father.father && person.father.father.name == "Peter") Probably doesn't work for many circumstances, and I'm sure I messed something up. -Steve
Jan 14
parent reply ddcovery <antoniocabreraperez gmail.com> writes:
On Thursday, 14 January 2021 at 20:23:08 UTC, Steven 
Schveighoffer wrote:
 You could kinda automate it like:

 struct NullCheck(T)
 {
    private T* _val;
    auto opDispatch(string mem)() if (__traits(hasMember, T, 
 mem)) {
        alias Ret = typeof(() { return __traits(getMember, 
 *_val, mem); }());
        if(_val is null) return NullCheck!(Ret)(null);
        else return NullCheck!(Ret)(__trats(getMember, *_val, 
 mem));
    }

    bool opCast(V: bool)() { return _val !is null; }
 }

 auto nullCheck(T)(T *val) { return AutoNullCheck!T(val);}

 // usage
 if(nullCheck(person).father.father && person.father.father.name 
 == "Peter")

 Probably doesn't work for many circumstances, and I'm sure I 
 messed something up.

 -Steve
I'm seeing "opDispatch" everywhere last days :-). It's really powerful!!! If we define an special T _(){ return _val; } method, then you can write if( nullCheck(person).father.father.name._ == "Peter") And renaming if( ns(person).father.father.name._ == "Peter" ) And adding some extra check like ** isAssignable!(Ret, typeof(null) )** we can add special treatment for not nullable types if( ns(person).father.father.age._(0) == 92 ){ ... } assert( ns(person).father.father.father.age._ == int.init ); If for some strange reason I ever need null safety, I think this is the most beautiful solution or at least a great lesson on templates. Thanks a lot for the lesson, Steve
Jan 14
parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 1/14/21 7:27 PM, ddcovery wrote:
 On Thursday, 14 January 2021 at 20:23:08 UTC, Steven Schveighoffer wrote:
 You could kinda automate it like:

 struct NullCheck(T)
 {
    private T* _val;
    auto opDispatch(string mem)() if (__traits(hasMember, T, mem)) {
        alias Ret = typeof(() { return __traits(getMember, *_val, mem); 
 }());
        if(_val is null) return NullCheck!(Ret)(null);
        else return NullCheck!(Ret)(__trats(getMember, *_val, mem));
    }

    bool opCast(V: bool)() { return _val !is null; }
 }

 auto nullCheck(T)(T *val) { return AutoNullCheck!T(val);}

 // usage
 if(nullCheck(person).father.father && person.father.father.name == 
 "Peter")

 Probably doesn't work for many circumstances, and I'm sure I messed 
 something up.

 -Steve
I'm seeing "opDispatch" everywhere last days :-). It's really powerful!!! If we define an special T _(){ return _val; } method, then you can write   if( nullCheck(person).father.father.name._ == "Peter") And renaming   if( ns(person).father.father.name._ == "Peter" )
This doesn't work, if person, person.father, or person.father.father is null, because now you are dereferencing null again. But something like this might work: NullCheck(T) { ... // opdispatch and stuff bool opEquals(auto ref T other) { return _val is null ? false : *_val == other; } } Something similar to BlackHole or WhiteHole. Essentially there's a default action for null for all types/fields/methods, and everything else is passed through. Swift has stuff like this built-in. But D might look better because you wouldn't need a chain of question marks. -Steve
Jan 15
next sibling parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 1/15/21 9:19 AM, Steven Schveighoffer wrote:

 Something similar to BlackHole or WhiteHole. Essentially there's a 
 default action for null for all types/fields/methods, and everything 
 else is passed through.
And now reading the other thread about this above, it looks like this type is already written: https://code.dlang.org/packages/optional I'd say use that. -Steve
Jan 15
next sibling parent Imperatorn <johan_forsberg_86 hotmail.com> writes:
On Friday, 15 January 2021 at 14:25:09 UTC, Steven Schveighoffer 
wrote:
 On 1/15/21 9:19 AM, Steven Schveighoffer wrote:

 Something similar to BlackHole or WhiteHole. Essentially 
 there's a default action for null for all 
 types/fields/methods, and everything else is passed through.
And now reading the other thread about this above, it looks like this type is already written: https://code.dlang.org/packages/optional I'd say use that. -Steve
That could be useful actually
Jan 15
prev sibling parent ddcovery <antoniocabreraperez gmail.com> writes:
On Friday, 15 January 2021 at 14:25:09 UTC, Steven Schveighoffer 
wrote:
 On 1/15/21 9:19 AM, Steven Schveighoffer wrote:

 Something similar to BlackHole or WhiteHole. Essentially 
 there's a default action for null for all 
 types/fields/methods, and everything else is passed through.
And now reading the other thread about this above, it looks like this type is already written: https://code.dlang.org/packages/optional I'd say use that. -Steve
Yes, the Optional/Some/None pattern is the "functional" orientation for avoiding the use of "null". Swift uses a similar pattern (and scala too) and supports the "null safety operators" ?. and ?? (it doesn't work on "null" but on optional/nil). The more I think about it, the more fervent defender of the use of ?. and ?? I am. The misinterpretation about "null safety" is we talk about "null" reference safety, but this pattern can be used with "optional" to... D has not optional/none/some native implementation and this is the reason we think about "?." as a "bad pattern" because we imagine it is for "null" values exclusively. But like other operators, they could be overloaded and adapted to each library. Well, I'm digressing: good night!!!
Jan 15
prev sibling parent ddcovery <antoniocabreraperez gmail.com> writes:
On Friday, 15 January 2021 at 14:19:35 UTC, Steven Schveighoffer 
wrote:
 On 1/14/21 7:27 PM, ddcovery wrote:
 On Thursday, 14 January 2021 at 20:23:08 UTC, Steven 
 Schveighoffer wrote:
 You could kinda automate it like:

 struct NullCheck(T)
 {
    private T* _val;
    auto opDispatch(string mem)() if (__traits(hasMember, T, 
 mem)) {
        alias Ret = typeof(() { return __traits(getMember, 
 *_val, mem); }());
        if(_val is null) return NullCheck!(Ret)(null);
        else return NullCheck!(Ret)(__trats(getMember, *_val, 
 mem));
    }

    bool opCast(V: bool)() { return _val !is null; }
 }

 auto nullCheck(T)(T *val) { return AutoNullCheck!T(val);}

 // usage
 if(nullCheck(person).father.father && 
 person.father.father.name == "Peter")

 Probably doesn't work for many circumstances, and I'm sure I 
 messed something up.

 -Steve
I'm seeing "opDispatch" everywhere last days :-). It's really powerful!!! If we define an special T _(){ return _val; } method, then you can write   if( nullCheck(person).father.father.name._ == "Peter") And renaming   if( ns(person).father.father.name._ == "Peter" )
This doesn't work, if person, person.father, or person.father.father is null, because now you are dereferencing null again. But something like this might work: NullCheck(T) { ... // opdispatch and stuff bool opEquals(auto ref T other) { return _val is null ? false : *_val == other; } } Something similar to BlackHole or WhiteHole. Essentially there's a default action for null for all types/fields/methods, and everything else is passed through. Swift has stuff like this built-in. But D might look better because you wouldn't need a chain of question marks. -Steve
I don't know if I can add this to Dlang IDE and then share a link... links that I generate doesn't work... * I have adapted the "onDispatch" and the factory method to manage nullable and not nullable values * The unwrapper "T _()" method returns Nullable!T for nullable * I removed the T* when testing changes (I discovered after 1000 changes that template errors are not well informed by the compiler... I losted a lot to discover a missing import)... I will try to restore. import std.typecons; import std.traits; void main() { Person person = new Person("Andres", 10, new Person("Peter", 40, null)); // null reference assert(ns(person).father.father._ is null); // null reference assert(ns(person).father.father.name._ is null); // reference value assert(ns(person).father.name._ == "Peter"); // Nullable!int assert(ns(person).father.father.age._.isNull); assert(ns(person).father.father.age._.get(0) == 0); assert(ns(11)._.get == 11); } struct NullSafety(T) { private T _val; private bool _isEmpty; auto opDispatch(string name)() if (__traits(hasMember, T, name)) { alias Ret = typeof((() => __traits(getMember, _val, name))()); if (_val is null) { static if (isAssignable!(Ret, typeof(null))) return NullSafety!(Ret)(null, true); else return NullSafety!(Ret)(Ret.init, true); } else { return NullSafety!(Ret)(__traits(getMember, _val, name), false); } } static if (isAssignable!(T, typeof(null))) // Reference types unwrapper T _() { return _val; } else // value types unwrapper Nullable!T _() { return _isEmpty ? Nullable!T() : Nullable!T(_val); } } auto ns(T)(T val) { static if (isAssignable!(T, typeof(null))) return NullSafety!T(val, val is null); else return NullSafety!T(val, false); } class Person { public string name; public Person father; public int age; this(string name, int age, Person father) { this.name = name; this.father = father; this.age = age; } }
Jan 15
prev sibling next sibling parent reply Dennis <dkorpel gmail.com> writes:
On Thursday, 14 January 2021 at 18:24:44 UTC, ddcovery wrote:
 If it's not a bother, I'd like to know how you usually approach 
 it
Usually I don't deal with null because my functions get primitive types, slices, or structs. `ref` parameters can be used to replace pointers that may not be null. When something is nullable by design, I usually do this: ``` if (!person) { return; // early return if possible } if (auto f0 = person.father) { if (auto f1 = f0.father) { if (f1.name == "Peter") { doSomething(); } } } ``` It doesn't matter whether you're working with a class, pointer, or struct with opCast, this works. When access patterns get complex the nesting may get very deep. Only if you can't avoid this I would consider using fancy helper functions, otherwise just use an if-statement or the && operator.
Jan 14
parent ddcovery <antoniocabreraperez gmail.com> writes:
On Thursday, 14 January 2021 at 20:35:49 UTC, Dennis wrote:
 On Thursday, 14 January 2021 at 18:24:44 UTC, ddcovery wrote:
 If it's not a bother, I'd like to know how you usually 
 approach it
Usually I don't deal with null because my functions get primitive types, slices, or structs. `ref` parameters can be used to replace pointers that may not be null. When something is nullable by design, I usually do this: ``` if (!person) { return; // early return if possible } if (auto f0 = person.father) { if (auto f1 = f0.father) { if (f1.name == "Peter") { doSomething(); } } } ``` It doesn't matter whether you're working with a class, pointer, or struct with opCast, this works. When access patterns get complex the nesting may get very deep. Only if you can't avoid this I would consider using fancy helper functions, otherwise just use an if-statement or the && operator.
I agree: using null safety is a sign of something wrong in the design (the need of dealing with nulls)... but if eventually you need it, simple **if** or **&&** should be enough. Curiously, languages like Dart (and its flutter framework) performs extensive use of null safety (null is everywhere!!!) and it seems that every "modern" language must deal with it. Any case, I'm learning a lot: thank you Dennis for sharing!!!
Jan 14
prev sibling next sibling parent ddcovery <antoniocabreraperez gmail.com> writes:
On Thursday, 14 January 2021 at 18:24:44 UTC, ddcovery wrote:
 I know there is other threads about null safety and the 
 "possible" ways to support this in D and so on.

 This is only an open question to know what code patterns you 
 usually use to solve this situation in D
I'm writing a "personal" article/study about "null safety" anti-pattern in form of github project (to include some examples) I really thank you for your answers here that I will use (and mention with your permission) in this small article. The actual version can be found here https://github.com/ddcovery/d_null_safety/blob/main/README.md It is under construction :-).
Jan 15
prev sibling next sibling parent Dukc <ajieskola gmail.com> writes:
On Thursday, 14 January 2021 at 18:24:44 UTC, ddcovery wrote:
 This is only an open question to know what code patterns you 
 usually use to solve this situation in D:

   if(person.father.father.name == "Peter") doSomething();
   if(person.father.age > 80 ) doSomething();

 knowing that *person*, or its *father* property can be null
Probably the incremental check solution. A helper function if I find myself doing that more than two or three times. On the other hand, I don't have to do this that often. I usually design the functions to either except non-null values, or to return early in case of null.
Jan 15
prev sibling parent Basile B. <b2.temp gmx.com> writes:
On Thursday, 14 January 2021 at 18:24:44 UTC, ddcovery wrote:
 I know there is other threads about null safety and the 
 "possible" ways to support this in D and so on.
 [...]
 If it's not a bother, I'd like to know how you usually approach 
 it

 [...]

 Thanks!!!
I have a opDispatch solution here [1], probably very similar to the other opDispatch solution mentioned. It is used in d-scanner since several years, e.g here [2]. I'd like to have this as a first class operator because as usual in D, you can do great things with templates but then completion is totally unable to deal with them. Also There's a great difference between using the template to do refacts and using it to write new code. Very frustrating to write `safeAcess(stuff). ` and no completion popup appears. [1]: https://gitlab.com/basile.b/iz/-/blob/master/import/iz/sugar.d#L1655 [2]: https://github.com/dlang-community/D-Scanner/blob/2963358eb4a24064b0893493684d4075361297eb/src/dscanner/analysis/assert_without_msg.d#L42
Jan 15