views:

107

answers:

2

So I have this in my C# lib:

public static TOut IfNotNull<TIn, TOut>
    (this TIn instance, Func<TIn, TOut> func)
{
    return instance == null ? default(TOut) : func(instance);
}

Used like:

DateTime? expiration = promo.IfNotNull(p => p.TermsAndConditions.Expiration)
                            .IfNotNull(e => e.Date);

I keep wracking my brain trying to figure out how to use the C# 4 dynamic keyword to enable this syntax instead:

DateTime? expiration = promoOffer.TermsAndConditions.Maybe()
                                 .Expiration.Maybe()
                                 .Date;

I had a couple examples that I thought worked but they broke down when you start chaining the Maybe()s.

Any ideas?

(Am I wasting my time? Is Maybe() a win over IfNotNull())?

+1  A: 

I don't think that using dynamic type is a good idea here, because the syntax isn't going to look much better and you sacrifice the type safety (and IntelliSense) by using dynamic typing.

However, here is an example of what you can do. The idea is that you wrap objects into DynamicWrapper (your monadic value :-)) that can either contain a null value or an actual value. It would inherit from DynamicObject and delegate all calls to the actual object (if there is any) or immediately return null (that would be monadic bind):

class DynamicWrapper : DynamicObject {
  public object Object { get; private set; }
  public DynamicWrapper(object o) { Object = o; }
  public override bool TryGetMember(GetMemberBinder binder, out object result) {
    // Special case to be used at the end to get the actual value
    if (binder.Name == "Value") result = Object;
    // Binding on 'null' value - return 'null'
    else if (Object == null) result = new DynamicWrapper(null);
    else {
      // Binding on some value - delegate to the underlying object
      var getMeth = Object.GetType().GetProperty(binder.Name).GetGetMethod();
      result = new DynamicWrapper(getMeth.Invoke(Object, new object[0]));
    return true;
  }
  public static dynamic Wrap(object o) {
    return new DynamicWrapper(o);
  }
}

The example supports only properties and it uses reflection in a pretty inefficient way (I think it could be optimized using DLR). Here is an example how it works:

class Product {
  public Product Another { get; set; }
  public string Name { get; set; }
}

var p1 = new Product { Another = null };
var p2 = new Product { Another = new Product { Name = "Foo" } };
var p3 = (Product)null;

// prints '' for p1 and p3 (null value) and 'Foo' for p2 (actual value)
string name = DynamicWrapper.Wrap(p1).Another.Name.Value;
Console.WriteLine(name);

Note that you can chain the calls freely - there is only something special at the beginning (Wrap) and at the end (Value), but in the middle, you can write .Another.Another.Another... as many times you want.

Tomas Petricek
+1  A: 

This solution is similar to Tomas' except that it uses CallSite to invoke properties on the target instance and also supports casting and extra calls to Maybe (as per your example).

public static dynamic Maybe(this object target)
{
    return new MaybeObject(target);
}

private class MaybeObject : DynamicObject
{
    private readonly object _target;

    public MaybeObject(object target)
    {
        _target = target;
    }

    public override bool TryGetMember(GetMemberBinder binder,
                                      out object result)
    {
        result = _target != null ? Execute<object>(binder).Maybe() : this;
        return true;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder,
                                         object[] args, out object result)
    {
        if (binder.Name == "Maybe" &&
            binder.ReturnType == typeof (object) &&
            binder.CallInfo.ArgumentCount == 0)
        {
            // skip extra calls to Maybe
            result = this;
            return true;
        }

        return base.TryInvokeMember(binder, args, out result);
    }

    public override bool TryConvert(ConvertBinder binder, out object result)
    {
        if (_target != null)
        {
            // call Execute with an appropriate return type
            result = GetType()
                .GetMethod("Execute", BindingFlags.NonPublic | BindingFlags.Instance)
                .MakeGenericMethod(binder.ReturnType)
                .Invoke(this, new object[] {binder});
        }
        else
        {
            result = null;
        }
        return true;
    }

    private object Execute<T>(CallSiteBinder binder)
    {
        var site = CallSite<Func<CallSite, object, T>>.Create(binder);
        return site.Target(site, _target);
    }
}

The following code should demonstrate it in use:

var promoOffer = new PromoOffer();
var expDate = promoOffer.TermsAndConditions.Maybe().Expiration.Maybe().Date;
Debug.Assert((DateTime?) expDate == null);

promoOffer.TermsAndConditions = new TermsAndConditions();
expDate = promoOffer.TermsAndConditions.Maybe().Expiration.Maybe().Date;
Debug.Assert((DateTime?) expDate == null);

promoOffer.TermsAndConditions.Expiration = new Expiration();
expDate = promoOffer.TermsAndConditions.Maybe().Expiration.Maybe().Date;
Debug.Assert((DateTime?) expDate == null);

promoOffer.TermsAndConditions.Expiration.Date = DateTime.Now;
expDate = promoOffer.TermsAndConditions.Maybe().Expiration.Maybe().Date;
Debug.Assert((DateTime?) expDate != null);
Nathan Baulch