Skip to content

Repository files navigation

[This is not being maintained anymore - no time - and not using D anymore].

Optional type for D with safe dispatching

Latest versionBuild StatuscodecovlicenseOpen on run.dlang.io

Full API docs available here

Features

  • @nogc and @safe
  • Shows the intent of your code that may or may not return a value
    Optional!intfun() {} // Might return an int, or might not
  • Includes a generic or range algorithm:
    auto a = some(3);
    auto b = a.or(7);
    auto c = a.or(some(4));
    c.or!(() => writeln("c is empty"));
  • Use pattern matching
    fun.match!(
    (int value) => writeln("it returns an int"),
    () => writeln("did not return anything"),
    );
  • Safely call functions on classes that are null, structs that don't exist, or std.typecons.Nullable
    classC { int fun() { return3; } }
    Optional!C a = null;
    oc(a).fun; // no crash, returns no!int
  • Forwards any operator calls to the wrapped typed only if it exists, else just returns a none
    Optional!int a = 3;
    Optional!int b = none;
    a +3; // evaluates to some(6);
    b +3; // evaluates to no!int;intf0(int) { return4; }
    auto a0 = some(&f0); // return some(4)
  • Compatible with std.algorithm and std.range
    fun.each!(value => writeln("I got the value"));
    fun.filter!"a % 2 == 0".each!(value => writeln("got even value"));

Summary

The pupose of this library is to provide an Optional type.

It contains the following constructs:

  • Optional!T: Represents an optional data type that may or may not contain a value that acts like a range.
  • oc: A null-safe optional chaining (oc) utility that allows you to chain methos through possible empty objects.
  • or: A range algorithm that also acts as a coalescing operator
  • match: Pattern match on optionals

An Optional!T signifies the intent of your code, works as a range and is therefore usable with Phobos algorithms, and allows you to call methods and operators on your types even if they are null references - i.e. safe dispatching.

Some use cases:

  • When you need a type that may have a value or may not (Optional!Type)
  • When you want to safely dispatch on types (oc(obj).someFunction // always safe)
  • When you want to not crash with array access (some([1, 2])[7] == none // no out of bounds exception)
  • When you want to perform an operation if you get a value (obj.map!doSomething.or!doSomethingElse)

Motivation for Optional

Let's take a very contrived example, and say you have a function that may return a value (that should be some integer) or not (config file, server, find operation, whatever), and then you have functions add1 and add2, that have the requirements that they may or may not produce a valid value. (maybe they do some crazy division, or they contact a server themselves to fetch a value, whatevs).

How can you go about this?

Use pointers?

int*add1(int*v) {
// Gotta remember to protect against nullif (!v) {
return v;
}
*v +=1;
return v;
}
int*add2(int*v); // might forget to check for nullvoidf() {
int* v = maybeGet();
if (v)
v = v.add1;
if (v)
v = v.add2;
if (v)
writeln(*v);
}

You can also replace int* with Nullable!int and then instead of if (v) you'd have to do if (!v.isNull) and instead of *v you'd do v.get.

How about ranges?

There's std.range.only:

autoadd2(Range)(Range r)
if (isInputRange!Range&&is(ElementType!Range==int))
// constrain to range type only and int element type?// I need to ensure it has a length of one.// And there's no way to ensure that in compile time without severly constraigning the type
{
// do we have one element or more now?// what do we do if there's more than one?// do we restrain it at run time to being there?
enforce(r.walkLength <=1); // ??// Should we map all of it?return v.map!(a => a +1);
// Or just the first?return v.take(1).map!(a => a +1);
// But what do I do with the rest then?
}
autoadd2(Range)(Range r) if (isInputRange!Range) {
// same headache as above
}
voidf() {
auto v = maybeGet();
// can we assign it to itself?
v = v.add1.add2;
// No, no idea what it returns, not really the same type// so this...
refRange(&v).add1.add2; // ??// no that won't work (can it?), lets create a new varauto v2 = v.add1.add2 // and let type inference do its thing
writeln(v2); // now ok.
}

Let's try an Optional!int

autoadd1(Optional!int v) {
v +=1;
return v;
}
autoadd2(Optional!int v); // same as abovevoidf() {
auto v = maybeGet().add1.add2;
writeln(v);
}

FAQ

Can't I just use a pointer as an optional

Well yes, you can, but you can also stick a pencil up your nostril. It's a bad idea for the following reasons:

  1. In order to achieve stability, you have to enforce checking for null. Which you cannot do
  2. Null is part of the value domain of pointers. This means you can't use an optional of null
  3. The caller doesn't know who owns the pointer returned. Is it garbage collected? If not should you deallocate it?
  4. It says nothing about intent.

What about std.typecons.Nullable?

It is not like the Nullable type in Phobos. Nullable is basically a pointer and applies pointer semantics to value types. It does not give you any safety guarantees and says nothing about the intent of "I might not return a value". It does not have range semantics so you cannot use it with algorithms in phobos. And it treats null class objects as valid.

It does, however, tell you if something has been assigned a value or not. Albeit a bit counterintuitively, and in some cases nonsensically:

classC {}
Nullable!C a = null;
writeln(a.isNull); // prints false

With refernece types (e.g., pointers, classes, functions) you end up having to write code like this:

voidf(T)(Nullable!T a) {
if (!a.isNull) {
staticif (is(T ==class) || (T ==interface) ||/* what else have I missed? */) {
if (a.get!isnull) {
a.callSomeFunction;
}
} else {
a.callSomeFunction;
}
}
}

Scala we have a Swift comparison

In this section we'll see how this Optional is similar to Scala's Option[T] and Swift's Optional<T> type (similar to Kotlin's nullable type handling)

Idiomatic usage of optionals in Swift do not involve treating it like a range. They use optional unwrapping to ensure safety and dispatch chaining. Scala on the other hand, treats optionals like a range and provides primitives to get at the values safely.

Like in swift, you can chain functions safely so in case they are null, nothing will happen:

D: Unfortunately the lack of operator overloading makes dispatching a bit verbose.

classResidence {
auto numberOfRooms = 1;
}
classPerson {
Optional!Residence residence = new Residence();
}
auto john = some(new Person());
auto n = oc(john).residence.numberOfRooms;
writeln(n); // prints [1]

Swift

classPerson{varresidence:Residence?}classResidence{varnumberOfRooms=1}letjohn:Person?=Person()letn= john?.residence?.numberOfRooms;
print(n) // prints "nil"

Like in Scala, a number of range primitives are provided to help (not to mention we have Phobos as well)

D

auto x = toInt("1").or(0);
importstd.algorithm: each;
importstd.stdio: writeln;
toInt("1").each!writeln;
toInt("1").match!(
(i) => writeln(i),
() => writeln("😱"),
);
// For completeness, the implementation of toInt:
Optional!inttoInt(string str) {
importstd.conv: to;
scope(failure) return no!int;
returnsome(str.to!int);
}

Scala

valx= toInt("1").getOrElse(0)
toInt("1").foreach{ i =>
println(s"Got an int: $i")
}
toInt("1") match {
caseSome(i) => println(i)
caseNone=> println("😱")
}
// Implementation of toIntdeftoInt(s: String):Option[Int] = {
try {
Some(Integer.parseInt(s.trim))
} catch {
casee: Exception=>None
}
}

Examples

The following section has example usage of the various types

Example Optional!T usage

import optional;
// Create empty optionalauto a = no!int;
assert(a == none);
++a; // safe;
a -1; // safe;// Assign and try doing the same stuff
a = 9;
assert(a == some(9));
++a; // some(10);
a -1; // some(9);// Acts like a range as wellimportstd.algorithm : map;
importstd.conv : to;
cast(void)some(10).map!(to!double); // [10.0]cast(void)no!int.map!(to!double); // emptyauto r = some(1).match!((int a) =>"yes", () =>"no",);
assert(r =="yes");

Open on run.dlang.io

Example optional chaining usage

// Safely dispatch to whatever inner type isstructA {
structInner {
intg() { return7; }
}
Inner inner() { return Inner(); }
intf() { return4; }
}
auto d = some(A());
// Dispatch to one of its methods
oc(d).f(); // calls a.f, returns some(4)
oc(d).inner.g(); // calls a.inner.g, returns some(7)// Use on a pointer or reference type as well
A* e = null;
// If there's no value in the reference type, dispatching works, and produces an optionalassert(e.oc.f() == none);
assert(e.oc.inner.g() == none);

Open on run.dlang.io

About

An optional/maybe type for D

Resources

Stars

48 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages