Adding the ?. null verification

H. S. Teoh via Digitalmars-d digitalmars-d at puremagic.com
Thu Jun 19 14:37:33 PDT 2014


On Thu, Jun 19, 2014 at 05:05:51PM -0400, Etienne via Digitalmars-d wrote:
> On 2014-06-19 4:51 PM, H. S. Teoh via Digitalmars-d wrote:
> >This assumes that t.init is not a possible valid field value. But in
> >that case, there's no need to remap it, you just check for t.init
> >instead. For pointers, where .init is null, this isn't a problem, but
> >for things like int, where 0 is possible valid value, you may be
> >accidentally mapping 0 to the default value when the given field
> >actually exists (and has value 0)!
> 
> True, you need to mark failure and drag it to the end. Here's another
> try at it:
> 
> auto safeDeref(T)(T t, bool failed = false) {
> 	static struct SafeDeref {
> 		T t;
> 		bool fail;

The trouble with this is that you pay for the storage cost of .fail even
if T already has a perfectly fine null value which serves the same
purpose. Here's my take on it:

	/**
	 * A safe-dereferencing wrapper resembling a Maybe monad.
	 *
	 * If the wrapped object is null, any further member dereferences will simply
	 * return a wrapper around the .init value of the member's type. Since non-null
	 * member dereferences will also return a wrapped value, any null value in the
	 * middle of a chain of nested dereferences will simply cause the final result
	 * to default to the .init value of the final member's type.
	 */
	template SafeDeref(T)
	{
	    static if (is(T U == SafeDeref!V, V))
	    {
	        // Merge SafeDeref!(SafeDeref!X) into just SafeDeref!X.
	        alias SafeDeref = U;
	    }
	    else
	    {
	        enum hasNullValue(U) = is(typeof(U is null));
	
	        struct SafeDeref
	        {
	            T t;
	
	            // Make the wrapper as transparent as possible.
	            alias t this;
	
	            // Wrapped types that don't have a null value get an additional
	            // flag to indicate existence.
	            static if (!hasNullValue!T)
	            {
	                bool exists = true;
	
	                T or(T defaultVal) { return exists ? t : defaultVal; }
	            }
	            else
	            {
	                T or(T defaultVal) { return t is null ? T.init : t; }
	            }
	
	            // 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)))
	                {
	                    if (t is null)
	                    {
	                        static if (hasNullValue!Memb)
	                            return SafeDeref!Memb(Memb.init);
	                        else
	                            return SafeDeref!Memb(Memb.init, false);
	                    }
	                    return SafeDeref!Memb(__traits(getMember, t, field));
	                } else {
	                    return SafeDeref!Memb(__traits(getMember, t, field));
	                }
	            }
	        }
	    }
	}
	
	/**
	 * Wraps an object in a safe dereferencing wrapper resembling a Maybe monad.
	 *
	 * If the object is null, then any further member dereferences will just return
	 * a wrapper around the .init value of the wrapped type, instead of
	 * dereferencing null. This applies recursively to any element in a chain of
	 * dereferences.
	 *
	 * Params: t = data to wrap.
	 * Returns: A wrapper around the given type, with "safe" member dereference
	 * semantics.
	 */
	auto safeDeref(T)(T t)
	{
	    return SafeDeref!T(t);
	}
	
	///
	unittest
	{
	    class Node
	    {
	        int val;
	        Node left, right;
	
	        this(int _val, Node _left=null, Node _right=null)
	        {
	            val = _val;
	            left = _left;
	            right = _right;
	        }
	    }
	
	    auto tree = new Node(1,
	        new Node(2),
	        new Node(3,
	            null,
	            new Node(4)
	        )
	    );
	
	    import std.stdio;
	    assert(safeDeref(tree).right.right.val == 4);
	    assert(safeDeref(tree).left.right.left.right is null);
	    assert(safeDeref(tree).left.right.left.right.val == 0);
	
	    // The wrapper also exposes an .or method that returns the specified
	    // default value if the dereferenced value doesn't exist.
	    assert(safeDeref(tree).right.right.val.or(-1) == 4);
	    assert(safeDeref(tree).left.right.left.right.val.or(-1) == -1);
	}

	// ... snip ...

The last 2 lines of the unittest demonstrate the new functionality.

Here, I've made it so that the .exists field is only present if the
wrapped type T doesn't have a null value that can serve as an existence
marker.

I'm not sure if this is the right thing to do 100% of the time, because
conceivably, somebody might want to distinguish between a pointer field
that has null as a value, as opposed to a pointer field that doesn't
exist. But I think such cases should be very rare; and cutting out the
.exists field where unnecessary also keeps the size of the wrapper
struct minimal, so that it's easier for the compiler's optimizer to
completely optimize it out for pointer values. (Unfortunately I ran into
a snag with gdc due to latest dmd features that haven't made it into
gdc, so I haven't been able to actually check the assembly output --
I'll try to iron that out and post the results.)


T

-- 
I am not young enough to know everything. -- Oscar Wilde


More information about the Digitalmars-d mailing list