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.
