I have developed the following implementation of an immutable struct
Maybe<T>
monad for use in my game project; as an practical exposition on monads to present to my local User Group; and an a demonstration of the use of Static Code Analysis for verification in conjunction with TDD. I am interested in any and all comments on coding style, and completeness and correctness of the implementation.
The base Maybe<T>
class:
/// <summary>An immutable value-type Maybe<T> monad.</summary>
/// <typeparam name="T">The base type, which can be either a class or struct type,
/// and will have the Equality definition track the default for the base-type:
/// Value-equality for structs and string, reference equality for other classes.
/// </typeparam>
/// <remarks
/// >Being a value-type reduces memory pressure on <see cref="System.GC"/>.
///
/// Equality tracks the base type (struct or class), with the further proviseo
/// that two instances can only be equal when <see cref="HasValue"/> is true
/// for both instances.
/// </remarks>
public struct Maybe<T> : IEquatable<Maybe<T>> {
#region static support for IEquatable<Maybe<T>>
static Maybe() {
_valueIsStruct = typeof(ValueType).IsAssignableFrom(typeof(T));
_equals = ValueIsStruct ? (Func<T,T,bool>)ValEquals
: (Func<T,T,bool>)RefEquals;
_nothing = new Maybe<T>();
}
static bool ValueIsStruct { [Pure]get {return _valueIsStruct;} } static bool _valueIsStruct;
static readonly Func<T,T,bool> _equals;
[Pure]private static bool ValEquals(T lhs, T rhs) { return lhs.Equals(rhs); }
[Pure]private static bool RefEquals(T lhs, T rhs) {
return typeof(string).IsAssignableFrom(typeof(T)) ? lhs.Equals(rhs)
: object.ReferenceEquals(lhs, rhs);
}
#endregion
public static Maybe<T> Nothing {
[Pure]get { Contract.Ensures( ! Contract.Result<Maybe<T>>().HasValue);
Contract.Assume( ! _nothing.HasValue );
return _nothing;
}
} static Maybe<T> _nothing;
///<summary>Create a new Maybe<T>.</summary>
public Maybe(T value) : this() {
value.ContractedNotNull("value");
_value = value;
_hasValue = true;
}
///<summary>The monadic Bind operation from type T to type Maybe<TResult>.</summary>
[Pure]public Maybe<TResult> Bind<TResult>(Func<T, Maybe<TResult>> selector) {
selector.ContractedNotNull("selector");
return ! HasValue ? Maybe<TResult>.Nothing : selector(_value);
}
///<summary>Extract value of the Maybe<T>, substituting <paramref name="defaultValue"/> as needed.</summary>
[Pure]public T Extract(T defaultValue) {
defaultValue.ContractedNotNull("defaultValue");
Contract.Ensures(Contract.Result<T>() != null);
return ! HasValue ? defaultValue : _value;
}
///<summary>Wraps a T as a Maybe<T>.</summary>
[Pure]public static implicit operator Maybe<T>(T value) {
Contract.Assume( ValueIsStruct.Implies(value != null) );
return ValueIsStruct ? new Maybe<T>(value)
: value == null ? Maybe<T>.Nothing
: new Maybe<T>(value);
}
///<summary>Returns whether this Maybe<T> has a value.</summary>
public bool HasValue { [Pure]get { return _hasValue;} } readonly bool _hasValue;
///<summary>If this Maybe<T> has a value, returns it.</summary>
internal T Value { [Pure]get {Contract.Requires(HasValue); return _value;} } readonly T _value;
/// <summary>The invariants enforced by this struct type.</summary>
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
[ContractInvariantMethod]
[Pure]private void ObjectInvariant() {
Contract.Invariant( HasValue == _hasValue );
Contract.Invariant( HasValue.Implies(_value != null) );
Contract.Invariant( ValueIsStruct.Implies(_value != null) );
}
#region Value Equality with IEquatable<T> and "excluded middle" present w/ either side has no value.
/// <inheritdoc/>
[Pure]public override bool Equals(object obj) {
var other = obj as Maybe<T>?;
return other.HasValue && other.Equals(obj);
}
/// <summary>Tests value-equality, returning <b>false</b> if either value doesn't exist.</summary>
[Pure]public bool Equals(Maybe<T> other) {
return this.HasValue && other.HasValue && _equals(this._value,other._value);
}
/// <summary>Tests value-inequality, returning <b>false</b> if either value doesn't exist.</summary>
[Pure]public bool NotEquals(Maybe<T> other) {
return !this.HasValue || !other.HasValue || _equals(this._value,other._value);
}
/// <inheritdoc/>
[Pure]public override int GetHashCode() { return Bind(v=>v.GetHashCode().ToMaybe()).Extract(0); }
/// <inheritdoc/>
[Pure]public override string ToString() {
Contract.Ensures(Contract.Result<string>() != null);
return Bind<string>(v => v.ToString()).Extract("");
}
/// <summary>Tests value-equality, returning <b>false</b> if either value doesn't exist.</summary>
[Pure]public static bool operator == (Maybe<T> lhs, Maybe<T> rhs) { return lhs.Equals(rhs); }
/// <summary>Tests value-inequality, returning <b>false</b> if either value doesn't exist..</summary>
[Pure]public static bool operator != (Maybe<T> lhs, Maybe<T> rhs) { return ! lhs.NotEquals(rhs); }
#endregion
}
The LINQ-supporting extensions:
/// <summary>Extension methods for Maybe<T> to support LINQ "Comprehension" and "Fluent" syntax.</summary>
/// <remarks>
/// Unoptimized implementations of both Select() and SelectMany() have been retained
/// as comments for documentation purposes, to emphasize the evolution to the
/// optimized forms currently in use.
///
/// The intent is also to use this class as a tutorial for the exposition on
/// generating the ptimized forms from the standard Monad implementations.
/// </remarks>
public static partial class MaybeExtensions {
/// <summary>Amplifies a T to a Maybe<T>.</summary>
/// <remarks>The monad <i>unit</i> function.</remarks>
[Pure]public static Maybe<T> ToMaybe<T>(this T @this) { return @this; }
/// <summary>LINQ-compatible implementation of Bind/Map as Select.</summary>
[Pure]public static Maybe<TResult> Select<T,TResult>(
this Maybe<T> @this,
Func<T,TResult> projector
) {
projector.ContractedNotNull("projector");
return ! @this.HasValue ? Maybe<TResult>.Nothing // Optimized implementation
: projector(@this.Value);
//return @this.Bind(v => projector(v).ToMaybe()); // Always available from Bind()
}
/// <summary>LINQ-compatible implementation of Bind as SelectMany.</summary>
[Pure]public static Maybe<TResult> SelectMany<T, TResult>(
this Maybe<T> @this,
Func<T, Maybe<TResult>> selector
) {
selector.ContractedNotNull("selector");
return @this.Bind(selector);
}
/// <summary>LINQ-compatible implementation of Bind/FlatMap as SelectMany.</summary>
[Pure]public static Maybe<TResult> SelectMany<T, TSelection, TResult>(
this Maybe<T> @this,
Func<T, Maybe<TSelection>> selector,
Func<T,TSelection,TResult> projector
) {
selector.ContractedNotNull("selector");
projector.ContractedNotNull("projector");
return ! @this.HasValue // Optimized implementation
? Maybe<TResult>.Nothing
: selector(@this.Value).Select(e => projector(@this.Value,e));
//return @this.Bind( aval => // Always available from Bind()
// selector(aval).Bind( bval =>
// projector(aval,bval).ToMaybe() ) );
//return @this.Bind( aval => // Available from Bind() & Select()
// selector(aval).Select(bval =>
// projector(aval,bval) ) );
}
}
Finally, some CodeContract
extension to support the static analysis, normally run with RUNTIME_NULL_CHECKS
not defined:
/// <summary>Extension methods to enhance Code Contracts and integration with Code Analysis.</summary>
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
[Pure]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")]
[Pure]internal static void AssumeInvariant<T>(this T t) { }
/// <summary>Asserts the 'truth' of the logical implication <paramref name="condition"/> => <paramref name="contract"/>.</summary>
[Pure]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>
[Pure]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 {}
This code and it's unit-tests and demo program runs clean through static Code Analysis with these settings:
Update - Corrected an Error in !=
operator
Contrary to the spec., I noticed that ternary logic was not being properly implemented. To correct this the method NotEquals() was added, with the operator !=
defined with it. When so desired, an excluded middle can be recovered by negating either the Equal() method or the ==
operator.