Jump to page: 1 2
Thread overview
Open question: what code pattern you use usually for null safety problem
Jan 14, 2021
ddcovery
Jan 14, 2021
Adam D. Ruppe
Jan 14, 2021
ddcovery
Jan 14, 2021
mw
Jan 15, 2021
ddcovery
Jan 15, 2021
Imperatorn
Jan 15, 2021
ddcovery
Jan 15, 2021
ddcovery
Jan 14, 2021
Dennis
Jan 15, 2021
ddcovery
Jan 15, 2021
ddcovery
Jan 15, 2021
Dukc
Jan 15, 2021
Basile B.
January 14, 2021
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!!!


January 14, 2021
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.
January 14, 2021
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.

January 14, 2021
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
January 14, 2021
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.
January 14, 2021
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.




January 15, 2021
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

January 15, 2021
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!!!
January 15, 2021
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 :-).



January 15, 2021
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
« First   ‹ Prev
1 2