June 19, 2014
On Thu, Jun 19, 2014 at 04:29:12PM -0400, Etienne via Digitalmars-d wrote:
> On 2014-06-19 3:11 PM, H. S. Teoh via Digitalmars-d wrote:
> >
> >	safeDeref(obj).subobj.prop.field.failsafe!val(defaultVal)
> >
> >where the last element is passed as an alias to the failsafe template, which then does the .init-replacement magic. It looks a lot uglier, though. :-(
> >
> >
> >T
> >
> 
> meh, this works:
> 
> 
> writeln(currAssignment.safeDeref.typeInfo.ident.or("meh"));
> 
> ..
> 	static struct SafeDeref {
> 		T t;
> 
> 		// Make the wrapper as transparent as possible.
> 		alias t this;
> 
> 		// this overrides if null
> 		T or(T defVal){
> 			if (t is t.init)

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)!


> 			{
> 				return defVal;
> 			}
> 			else
> 			{
> 				return t;
> 			}
> 
> 		}
> ..


T

-- 
Gone Chopin. Bach in a minuet.
June 19, 2014
On Thu, Jun 19, 2014 at 01:51:17PM -0700, H. S. Teoh via Digitalmars-d wrote:
> On Thu, Jun 19, 2014 at 04:29:12PM -0400, Etienne via Digitalmars-d wrote:
[...]
> > meh, this works:
> > 
> > 
> > writeln(currAssignment.safeDeref.typeInfo.ident.or("meh"));
> > 
> > ..
> > 	static struct SafeDeref {
> > 		T t;
> > 
> > 		// Make the wrapper as transparent as possible.
> > 		alias t this;
> > 
> > 		// this overrides if null
> > 		T or(T defVal){
> > 			if (t is t.init)
> 
> 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)!
[...]

Actually, on second thoughts, maybe it *is* possible after all. The key is to differentiate between null-able types, and non-nullable types. For the latter, we expand the SafeDeref struct to store a boolean flag to indicate whether or not the value exists, then .or() can check that flag to see if it should return the default value. Basically, something like this:

	struct SafeDeref {
		T t;

		static if (!is(t = null))
			bool exists = true;

		...
		T or(T defVal) {
			return (exists) ? t : defVal;
		}
		...
		auto opDispatch(...) {
			...
			if (t is null)
				return safeDeref(Memb.init, false)
			else
				return safeDeref(__traits(getMember...), true);
		}
	}

I'll try this out to see if it works!

This also has the advantage of becoming even closer to a real monad: by checking for .exists, we can also overload operators to return a wrapper with exists=false whenever one of the operands also has exists=false.


T

-- 
Why can't you just be a nonconformist like everyone else? -- YHL
June 19, 2014
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;
		// Make the wrapper as transparent as possible.
		alias t this;

		auto or(T defVal){
			if (fail)
			{
				return defVal;
			}
			else
			{
				return 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));


			static if (is(typeof(t is null))) {
				return safeDeref((t is null) ? Memb.init
				                 : __traits(getMember, t, field), (t is null) ? true : false);
			} else {
				return safeDeref(__traits(getMember, t, field), fail);
			}
		}
	}
	return SafeDeref(t, failed);
}


June 19, 2014
On 2014-06-19 5:05 PM, Etienne wrote:
> : __traits(getMember, t, field), (t is null) ? true : false);


(t is null || fail) ? true : false
June 19, 2014
On 6/18/14, 10:39 AM, deadalnix wrote:
> Use a maybe monad :
> Maybe(obj).memeber.nested.val

Yah, I keep on thinking we should explore the maybe monad more thoroughly as a library in D. -- Andrei

June 19, 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
June 19, 2014
On Thursday, 19 June 2014 at 21:34:07 UTC, Andrei Alexandrescu wrote:
> On 6/18/14, 10:39 AM, deadalnix wrote:
>> Use a maybe monad :
>> Maybe(obj).memeber.nested.val
>
> Yah, I keep on thinking we should explore the maybe monad more thoroughly as a library in D. -- Andrei

The other night for fun I tried implementing an Option type as a RandomAccessRange. When a value exists, the range is not empty and has length 1, otherwise it is empty and has length 0, with the other primitives doing as expected. It's neat how you then automatically get all current and future range algorithms for free. One problem I found trying to use map like bind in Haskell is that it does the exact opposite of what I want. The result is not Option!int, but MapResult!(__lambda3, Option!int).
June 19, 2014
On Thu, Jun 19, 2014 at 02:37:33PM -0700, H. S. Teoh via Digitalmars-d wrote: [...]
> 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.)
[...]

Unfortunately, it appears that opDispatch has become too complex to be inlined, so now gdc is unable to simplify it to a series of nested if's. :-(


T

-- 
It's bad luck to be superstitious. -- YHL
June 19, 2014
On Thu, Jun 19, 2014 at 03:23:33PM -0700, H. S. Teoh via Digitalmars-d wrote: [...]
> Unfortunately, it appears that opDispatch has become too complex to be inlined, so now gdc is unable to simplify it to a series of nested if's.  :-(
[...]

Surprisingly, if we just stick .exists in there unconditionally, like you did, then gdc actually optimizes it away completely, so that we're back to the equivalent of nested if's! So your solution is superior after all.  :)


T

-- 
"No, John.  I want formats that are actually useful, rather than over-featured megaliths that address all questions by piling on ridiculous internal links in forms which are hideously over-complex." -- Simon St. Laurent on xml-dev
June 20, 2014
On 2014-06-19 6:23 PM, H. S. Teoh via Digitalmars-d wrote:
>
> Unfortunately, it appears that opDispatch has become too complex to be
> inlined, so now gdc is unable to simplify it to a series of nested if's.
> :-(
>
>
> T
>

Meh, I don't mind specifying that condition manually after all... having a default value isn't really on top of my list =)