Adding the ?. null verification

H. S. Teoh via Digitalmars-d digitalmars-d at puremagic.com
Wed Jun 18 12:36:01 PDT 2014


On Wed, Jun 18, 2014 at 07:04:33PM +0000, via Digitalmars-d wrote:
> On Wednesday, 18 June 2014 at 17:56:46 UTC, Mattcoder wrote:
> >On Wednesday, 18 June 2014 at 15:42:04 UTC, Etienne wrote:
> >>it would be a little more practical to be able to write
> >>
> >>writeln(obj.member?.nested?.val);
> >
> >If one of these: member, nested or val == null, what will happen with
> >writeln()? It will print null or it will be avoided?
> 
> The expression needs to have exactly one type, and because all of the
> components can be non-null, it needs to be the type of the last
> component, in this case `val`. This means that if the one of the
> components is null, the entire expression needs to return a value of
> this type, presumably the `.init` value.
> 
> The alternative is to raise an exception (or error), but that would
> defeat the purpose (almost, as it would be slightly better than
> segfaulting).

Here's a first stab at a library solution:

	/**
	 * Simple-minded implementation of a Maybe monad.
	 *
	 * Params: t = data to wrap.
	 * Returns: A wrapper around the given type, with "safe" member dereference
	 * semantics, that is, if t is null, then any further member dereferences will
	 * just return a wrapper around the .init value of the wrapped type, instead of
	 * deferencing the null and crashing.
	 */
	auto maybe(T)(T t) {
		static struct Maybe {
			T t;
	
			// Make the wrapper as transparent as possible.
			alias t this;
	
			// This is the magic that makes it all work.
			auto opDispatch(string field)()
				if (is(typeof(__traits(getMember, t, field))))
			{
				alias Memb = typeof(__traits(getMember, t, field));
	
				// If T is comparable with null, then we do a null
				// check. Otherwise, we just dereference the member
				// since it's guaranteed to be safe of null
				// dereferences.
				//
				// N.B.: we always return a wrapped type in case the
				// return type contains further nullable fields.
				static if (is(typeof(t is null))) {
					return maybe((t is null) ? Memb.init
								: __traits(getMember, t, field));
				} else {
					return maybe(__traits(getMember, t, field));
				}
			}
		}
		return Maybe(t);
	}
	
	/**
	 * A sample tree structure to test the above code.
	 */
	class Node {
		int val;
		Node left, right;
	
		this(int _val, Node _left=null, Node _right=null) {
			val = _val;
			left = _left;
			right = _right;
		}
	}
	
	void main() {
		import std.stdio;
	
		auto tree = new Node(1,
			new Node(2),
			new Node(3,
				null,
				new Node(4)
			)
		);
	
		writeln(maybe(tree).right.right.val);
		writeln(maybe(tree).left.right.left.right);
		writeln(maybe(tree).left.right.left.right.val);
	}

Program output:

	4
	Maybe(null)
	0

It's not perfect, but as you can see, attempting to dereference null
just returns the result type's .init value. You can also modify the code
where it returns Memb.init, to also log a message to a debug log that
indicates a possible logic problem with the code (failure to check for
null, etc.).

This also allows you to do a complete null check in a single statement:

	if (maybe(tree).left.right.right.left.right !is null) {
		// do something with that value
	}

If nothing else, this at least saves you the trouble of having to check
every intermediate reference in the chain. :)

The only wart I can see currently is the "Maybe(null)" appearing in the
writeln output, instead of just "null", but that can be worked around by
implementing a toString method in the Maybe struct that forwards to the
wrapped type's toString method (or something along those lines).

Anyway, this is just a first shot at it. You can probably build
something more sophisticated out of it. :)


T

-- 
Just because you survived after you did it, doesn't mean it wasn't stupid!


More information about the Digitalmars-d mailing list