(Work in progress!!!)
About accessing properties of an object using a reference that can be null
in a safe way, that is: to be sure that reference is not null after accessing the property.
i.e.:
Person p;
if(p !is null)
{
// It is safe to access [p] reference
}
When a member of an object can be null too, accessing its properties require a safe chacke too
class Person { string name; Person father; this(string name, Person father){ this.name = name; this.father = father; } }
auto peter = new Person( "Peter", new Person("Dad", null) );
if( peter !is null && peter.father !is null )
{
// it is safe to acces [perter.father] reference
}
This solution takes advantage of the boolean operators "lazy" evaluation: operators terms are evaluated left to right and, in the case of &&
, if left terms evaluates to false
it is not necessary evaluate the right side term (because false && true == false
)
We can use the ternary condiciontal ?:
operator to write null safe assignments:
auto age = (peter !is null && peter.father !is null) ? peter.father.age : 0;
or, if you prefer to use Nullable!T
to avoid dealing with "default" values:
auto age = (peter !is null && peter.father !is null) ? nullable(peter.father.age) : nullable!int();
Writting guard expressions for each "dot" of the chain is tedious (a repetitive) task that modern languages tries to solve
Mothern languages incorporates 2 new operators: ?.
(null conditional) and ??
(null coalesce)
a?.b
: ifa
has not value then complete expresion evaluates to a "not value" acording tob
returning type. This can be applied "recursively" to large references chains likea?.b?.c?.d
that can be read as((a?.b)?.c)?.d
a ?? c
: beinga
andb
of the same type, this expresion returns this type. The returned value isa
if it has a value orc
ifa
has not a value
What "has value" and "has not a value" means depends on each programming language:
C# has reference types (object, string, ...) and value types (int, bool, char, ...).
- Reference types accept the
null
value. - Value types don't accept
null
value, but can be wrapped wit theNullable<T>
struct allowing to represent the "not value" state. When wrapped, they are called nullable value types
null-conditional and null-coalesce operators accept for the "left side" expresion both reference types and nullable value types
// C#
// Right side evaluates to a reference type (and value can be null)
var dad = peter?.father
// Right side evaluates to a value type (int) because the coalesce operator
int age1 = peter?.father?.age ?? 0;
// Right side has not coalesce operator. it evaluates to Nullable<int>
int? age2 = peter?.father?.age;
// Although age2 is [Nullable<int>], complete right expression evaluates to [int] because the coalesce operator
int age3 = age2 ?? 0;
Typescript use unions of types in the form of type1 | type2 | ... | typeN
. A variable can accept the null
value if it is specified in the union types list.
// typescript
// age1 doesn't accept the [null] value... the coalesce operator ensures a default value if left side expresion evaluates to null
const age1: int = peter?.father?.age ?? 0;
// age2 is defined to accept [int] or [null]: we don't need to specified a default value.
const age2: int | null = peter?.father?.age;
In typescript there is another possible type: undefined
. It is applied to unexisting properties (when you access a Map by key that doesn't exist, undefined
is returned). undefined
is specially useful for JSON serialization, because properties with undefined
values are not serialized (properties with null
values are serializaed as null
)
Dart variables are allways nullable regardless of its type. It is named null type safe because null
value doesn't propatage as an independent type like typescript.
// dart
// age1 is of type [int] and its value is not [null]
final age1 = p?.father?.father?.age ?? 0;
// age2 is of type [int] and it's value could be [null]
final age2 = p?.father?.father?.age;
Swift takes a different aproach: the functional paradigm one. It uses the Optional
type that is an enumeration with two cases: Optional.none
( equivalent to the nil
literal) and Optional.some(value)
that wraps a value.
- The ?. operator is named "optional chaining operator" because it operates on Optional types (not on value types).
- The ?? operator is named "nil-coalescing operator" (a default value that applies when left side evaluates to
Optional.none
).
// swift
// age1 is of type Int
var age1 = peter?.father?.age ?? 0;
// age2 is of type Int? (Optional integer. It can be Optional.none or Optional.some(x) ).
var age2 = peter?.father?.age;
- A third operator is required (because the functional paradigm aproach): the unwrapping operator
!
. It is required to extract the value contained into the optional (into the Optional.some).
let optAge = peter?.father?.age;
if(age!=nil){
let age = optAge!;
...
}
Because D has not ?.
neither ??
operators, we will propose solutions to imitate them mainly based on templates.
What "nullable" means in D?
- Like C#, D has types that accept the
null
value (i.e.isAssignable(T, type(null))
is true) and types that doesn't accept thenull
value. - D incorporates the
Nullable!T
struct similar tuNullable<T>
in C# that can be used to associate a "null state" to types that doesn't acceptnull
In summary, D approach solution must deal with the null
and Nullable!T
in a similar whay as C# does because it's similitudes.
What about the "optional/some/none" pattern used in swift?
- D null state is represented with
null
, (because C compatibility) instead onOptional.none
: This article is about dealing with native Dnull
, not about adding functional paradigms to avoid the use of null when developing inD
(It could be a great new article).
Although, all is said, we will use wrappers (similar to Optional) to implement the proposed solutions one of which will be based on the Wrap/flatMap/Unwrap functional aproach.
The final objective is to write thinks like:
// peter?.name?.length ?? 0 == 5
assert( peter.d!"name".d!"length".get(0) == 5 )
// peter?.parent?.name == "John"
assert( peter.d!"parent".d!"name".get == "John" )
It is, basically, a monad like struct named Dot!T
with a map method named dot
and an unwrapper named get
// peter?.name == "Peter"
assert( Dot!Person(peter).dot(a=>a.name).get == "Peter" );
assert( Dot!Person(peter).dot(a=>a.father).dot(a=>a.father).dot(a=>a.name).get is null );
The get
unwrapper must include a default value for non nullable types
// peter?.name?.length ?? 0 == 5
Dot!string(peter).dot(a=>a.name).dot(a=>a.length).get(0) == 5;
Result can be obtained as Nullable!T
instead unwrapping its value
Dot!string(peter).dot(a=>a.parent).dot(a=>a.name).asNullable.isNull;
For a normalized notation, you can use the dot
factory method instead Dot!T
constructor
// peter?.name?.length ?? 0 == 5
dot(peter).dot(a=>a.name).dot(a=>a.length).get(0)==5;
To avoid the "lambda" notation, the struct offers the d! method member that allows you to write the name of the property directly
// peter?.name?.length ?? 0 == 5
dot(peter).d!"name".d!"length".get(0) == 5;
Finally, you can create the Dot!T
struct and access one property directly using the d!
factory method (unifying the syntax withy the d!
struct member).
// peter?.name?.length ?? 0 == 5
peter.d!"name".d!"length".get(0) == 5
// peter?.parent?.name == "John"
peter.d!"parent".d!"name".get == "John"
(This was an example proposed by Steven Schveighoffer in https://forum.dlang.org/post/rtq97c$1k5f$1@digitalmars.com)