The key to making this work is to make the property Value
private, accessible only through a monadic Bind operation or an Extract method that supplies a default value of interest. This avoids the possibility of a null value ever being exposed.
public struct MaybeX<TValue> : IEquatable<MaybeX<TValue>> where TValue:class {
/// <summary>TODO</summary>
public static readonly MaybeX<TValue> Nothing = new MaybeX<TValue>(null);
///<summary>Create a new MaybeX<T>.</summary>
public MaybeX(TValue value) : this() { _value = value; }
///<summary>The monadic Bind operation of type T to type MaybeX<TResult>.</summary>
[Pure]
public MaybeX<TResult> Bind<TResult>(Func<TValue, MaybeX<TResult>> selector)
where TResult:class {
return! HasValue ? MaybeX<TResult>.Nothing : selector(Value);
}
///<summary>Extract value of the MaybeX<T>, substituting
///<paramref name="defaultValue"/> as needed.</summary>
[Pure]
public TValue Extract(TValue defaultValue) {
return ! HasValue ? defaultValue : Value;
}
///<summary>Wraps a T as a MaybeX<T>.</summary>
[Pure]
public static implicit operator MaybeX<TValue>(TValue value) {
return value == null ? MaybeX<TValue>.Nothing : new MaybeX<TValue>(value);
}
///<summary>Returns whether this MaybeX<T> has a value.</summary>
public bool HasValue { [Pure]get {return Value != null;} }
///<summary>If this MaybeX<T> has a value, returns it.</summary>
internal TValue Value { [Pure]get {return _value;} } readonly TValue _value;
}
By then extending the base class with LINQ-compatible extension methods like this
[Pure]
public static class MaybeX {
[Pure]
public static MaybeX<TResult> Select<T, TResult>(
this MaybeX<T> @this,
Func<T, TResult> projector
) where T : class where TResult : class {
return ! @this.HasValue ? MaybeX<TResult>.Nothing
: projector(@this.Value).ToMaybeX();
}
[Pure]
public static MaybeX<TResult> SelectMany<TValue, TResult>(
this MaybeX<TValue> @this,
Func<TValue, MaybeX<TResult>> selector
) where TValue : class where TResult : class {
return @this.Bind(selector);
}
[Pure]
public static MaybeX<TResult> SelectMany<TValue, T, TResult>(
this MaybeX<TValue> @this,
Func<TValue, MaybeX<T>> selector,
Func<TValue, T, TResult> projector
) where T : class where TValue : class where TResult : class {
return ! @this.HasValue ? MaybeX<TResult>.Nothing
: selector(@this.Value).Map(e => projector(@this.Value,e));
}
}
the LINQ Comprehension syntax becomes available also. The above actually makes the Value property internal instead of private in order to allow the LINQ aliases to be implemented in place more efficiently; this assumes that the monads and their LINQ extension methods are alone in their assembly.
In my own library I annotate this class completely with Code Contracts like this
[Pure]
public MaybeX<TResult> Bind<TResult>(
Func<TValue, MaybeX<TResult>> selector
) where TResult:class {
selector.ContractedNotNull("selector");
Contract.Ensures(Contract.Result<MaybeX<TResult>>() != null);
MaybeX<TResult>.Nothing.AssumeInvariant();
var result = ! HasValue ? MaybeX<TResult>.Nothing
: selector(Value);
Contract.Assume(result != null); // struct's never null
//result.AssumeInvariant(); // TODO - Why is this inadequate?
return result;
}
and use Code Contracts to statically prove the absence of any escaping null values.
This small class of extension methods makes writing many of the code contracts much simpler and more effective:
/// <summary>Extension methods to enhance Code Contracts and integration with Code Analysis.</summary>
[Pure]
public static class ContractExtensions {
#if RUNTIME_NULL_CHECKS
/// <summary>Throws <c>ArgumentNullException{name}</c> if <c>value</c> is null.</summary>
/// <param name="value">Value to be tested.</param>
/// <param name="name">Name of the parameter being tested, for use in the exception thrown.</param>
[ContractArgumentValidator] // Requires Assemble Mode = Custom Parameter Validation
public static void ContractedNotNull<T>([ValidatedNotNull]this T value, string name) where T : class {
if (value == null) throw new ArgumentNullException(name);
Contract.EndContractBlock();
}
#else
/// <summary>Throws <c>ContractException{name}</c> if <c>value</c> is null.</summary>
/// <param name="value">Value to be tested.</param>
/// <param name="name">Name of the parameter being tested, for use in the exception thrown.</param>
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value")]
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "name")]
[ContractAbbreviator] // Requires Assemble Mode = Standard Contract Requires
public static void ContractedNotNull<T>([ValidatedNotNull]this T value, string name) {
Contract.Requires(value != null, name);
}
#endif
/// <summary>Decorator for an object which is to have it's object invariants assumed.</summary>
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "t")]
public static void AssumeInvariant<T>(this T t) { }
/// <summary>Asserts the 'truth' of the logical implication
<paramref name="condition"/> => <paramref name="contract"/>.</summary>
public static bool Implies(this bool condition, bool contract) {
Contract.Ensures((! condition || contract) == Contract.Result<bool>() );
return ! condition || contract;
}
/// <summary>Returns true exactly if lower <= value < lower+height</summary>
/// <param name="value">Vlaue being tested.</param>
/// <param name="lower">Inclusive lower bound for the range.</param>
/// <param name="height">Height of the range.</param>
public static bool InRange(this int value, int lower, int height) {
Contract.Ensures( (lower <= value && value < lower+height) == Contract.Result<bool>() );
return lower <= value && value < lower+height;
}
}
/// <summary>Decorator for an incoming parameter that is contractually enforced as NotNull.</summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class ValidatedNotNullAttribute : global::System.Attribute {}
Nullable<T>
has astruct
constrain onT
, so you only use it on value types.Nullable<T>
, the question can take a form of “What if this was possible?”, which is perfectly valid and answerable.