Last active
April 28, 2021 19:00
-
-
Save pblasucci/de1220bc2075372c91423430143f0cfc to your computer and use it in GitHub Desktop.
(Yet another) Riff on Option/Maybe/etc. for C# 9
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* This is free and unencumbered software released into the public domain. | |
* | |
* Anyone is free to copy, modify, publish, use, compile, sell, or | |
* distribute this software, either in source code form or as a compiled | |
* binary, for any purpose, commercial or non-commercial, and by any | |
* means. | |
* | |
* In jurisdictions that recognize copyright laws, the author or authors | |
* of this software dedicate any and all copyright interest in the | |
* software to the public domain. We make this dedication for the benefit | |
* of the public at large and to the detriment of our heirs and | |
* successors. We intend this dedication to be an overt act of | |
* relinquishment in perpetuity of all present and future rights to this | |
* software under copyright law. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR | |
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
* OTHER DEALINGS IN THE SOFTWARE. | |
* | |
* For more information, please refer to <https://unlicense.org> | |
* | |
* Credit is greatly appreciated -- but not required! Happy Coding! | |
*/ | |
using System; | |
using System.Collections.Generic; | |
using System.Collections.Immutable; | |
using System.Linq; | |
namespace Hubris | |
{ | |
/// Represents a value in one of two, mutually exclusive, cases: | |
/// 'Some value' or nothing at all (colloquially termed 'None', | |
/// which is also the default value for instances of this structure). | |
public readonly struct Optional<T> | |
: IEquatable<Optional<T>>, IComparable<Optional<T>>, IComparable | |
{ | |
private readonly T value; | |
private readonly bool hasValue; | |
/// Creates a new instance wrapping the given value. | |
public Optional(T value) | |
{ | |
this.value = value; | |
hasValue = true; | |
} | |
/// <summary> | |
/// Invokes either one of the given callbacks, based on the state of the | |
/// optional instance, passing in an underlying value (if appropriate). | |
/// </summary> | |
/// <param name="some">Invoked when there is an underlying value.</param> | |
/// <param name="none">Invoked when there is no underlying value.</param> | |
/// <returns>The result of the invoked callback.</returns> | |
/// <exception cref="ArgumentNullException"> | |
/// Raised if either callback is null. | |
/// </exception> | |
public TReturn Either<TReturn>(Func<T, TReturn> some, Func<TReturn> none) | |
{ | |
if (some is null) throw new ArgumentNullException(nameof(some)); | |
if (none is null) throw new ArgumentNullException(nameof(none)); | |
return hasValue ? some(value) : none(); | |
} | |
/// <inheritdoc /> | |
public override string ToString() | |
=> this switch | |
{ | |
(true, null ) => "Some(NULL)", | |
(true, var v) => $"Some({v})", | |
_ => $"{nameof(None)}" | |
}; | |
/// <inheritdoc /> | |
public override int GetHashCode() => HashCode.Combine(hasValue, value); | |
/// <inheritdoc /> | |
public override bool Equals(object? obj) | |
=> obj is Optional<T> other && Equals(other); | |
/// <inheritdoc /> | |
public bool Equals(Optional<T> other) | |
=> hasValue == other.hasValue | |
&& EqualityComparer<T>.Default.Equals(value, other.value); | |
/// <inheritdoc /> | |
public int CompareTo(Optional<T> other) | |
{ | |
return hasValue.CompareTo(other.hasValue) switch | |
{ | |
0 => Comparer<T>.Default.Compare(value, other.value), | |
var notEqual => notEqual | |
}; | |
} | |
/// <inheritdoc /> | |
public int CompareTo(object? obj) | |
=> obj switch | |
{ | |
null => 1, | |
Optional<T> other => CompareTo(other), | |
_ => throw new ArgumentException( | |
$"must be of type {nameof(Optional<T>)}!", | |
nameof(obj) | |
) | |
}; | |
/// Structural equality comparison for two Optional instances. | |
public static bool operator == | |
(Optional<T> left, Optional<T> right) => left.Equals(right); | |
/// Structural equality comparison for two Optional instances. | |
public static bool operator != | |
(Optional<T> left, Optional<T> right) => !left.Equals(right); | |
/// Structural inequality comparison for two Optional instances. | |
public static bool operator < | |
(Optional<T> left, Optional<T> right) => left.CompareTo(right) < 0; | |
/// Structural inequality comparison for two Optional instances. | |
public static bool operator > | |
(Optional<T> left, Optional<T> right) => 0 < left.CompareTo(right); | |
/// Structural inequality comparison for two Optional instances. | |
public static bool operator <= | |
(Optional<T> left, Optional<T> right) => left.CompareTo(right) <= 0; | |
/// Structural inequality comparison for two Optional instances. | |
public static bool operator >= | |
(Optional<T> left, Optional<T> right) => 0 <= left.CompareTo(right); | |
/// The representation of "no value" (the default value for Optional). | |
public static readonly Optional<T> None = new (); | |
/// Implicitly construct an Optional instance | |
/// (this makes some call sites less clustered). | |
public static implicit operator Optional<T>(T value) => new (value); | |
} | |
/// Additional operations on Optional{T} instances. | |
public static class Optional | |
{ | |
/// The representation of "no value". | |
public static Optional<T> None<T>() => Optional<T>.None; | |
/// <summary> | |
/// Creates a new Optional instance from the given value; However, | |
/// <c>null</c> inputs result in <c>None</c> (as compared to the | |
/// regular constructor, which would result in <c>Some(null)</c>). | |
/// </summary> | |
/// <param name="value">Value to be wrapped.</param> | |
/// <returns>A new Optional instance.</returns> | |
public static Optional<T> ToOptional<T>(this T? value) | |
=> value is null ? default : new Optional<T>(value); | |
/// <summary> | |
/// Attempts to "unwrap" an Optional instance, | |
/// succeeding or failing based on the state of the instance in question. | |
/// </summary> | |
/// <param name="option">Target to be unwrapped.</param> | |
/// <param name="value"> | |
/// if the given Optional contains an underlying value, it will be | |
/// copied to this output parameter (n.b. the value of this parameter | |
/// can not be relied upon when the method returns <c>false</c>). | |
/// </param> | |
/// <returns> | |
/// <c>true</c> if the Optional has an underlying value; | |
/// <c>false</c> otherwise. | |
/// </returns> | |
public static bool TryGetValue<T>(this Optional<T> option, out T? value) | |
{ | |
var (hasValue, some) = option.Either( | |
some: uv => (true, uv), | |
none: () => (false, default(T?)) | |
); | |
value = some; | |
return hasValue; | |
} | |
/// <summary> | |
/// Decomposes an Optional instance into a two-tuple, wherein the first | |
/// element is a boolean indicating whether or not the second element | |
/// has a valid value (i.e. <c>true</c> means 'value present' and | |
/// <c>false</c> means 'no value'). | |
/// </summary> | |
public static void Deconstruct<T> | |
(this Optional<T> option, out bool hasValue, out T? value) | |
{ | |
hasValue = TryGetValue(option, out value); | |
} | |
/// <summary> | |
/// Returns the underlying value of the Optional instance, | |
/// or invokes the given callback (i.e. when the Optional has 'no value'). | |
/// </summary> | |
/// <exception cref="ArgumentNullException"> | |
/// Raised when the default value callback is <c>null</c>. | |
/// </exception> | |
public static T GetValueOrDefault<T> | |
(this Optional<T> option, Func<T> defaultValue) | |
{ | |
if (defaultValue is null) throw new ArgumentNullException(nameof(defaultValue)); | |
return option.Either(some: value => value, none: defaultValue); | |
} | |
/// <summary> | |
/// If, and only if, the given Optional has 'no value', | |
/// invokes the compensation callback to generate a new Optional instance. | |
/// </summary> | |
/// <exception cref="ArgumentNullException"> | |
/// Raised when the compensation callback in <c>null</c>. | |
/// </exception> | |
public static Optional<T> IfNone<T> | |
(this Optional<T> option, Func<Optional<T>> otherwise) | |
{ | |
if (otherwise is null) throw new ArgumentNullException(nameof(otherwise)); | |
return option is (false, _) ? otherwise() : option; | |
} | |
/// <summary> | |
/// Calls each of the given projection callbacks in sequence (i.e. | |
/// the result of calling <c>itemSelector</c> is passed into | |
/// <c>returnSelector</c>). However, if the given Optional instance is | |
/// 'no value', then neither callback is invoked and the method simply | |
/// returns <c>None</c>. Similarly, if the result of <c>itemSelector</c> | |
/// is 'no value', then <c>returnSelector</c> is never invoked (again, | |
/// the method just returns <c>None</c>). | |
/// </summary> | |
/// <param name="option">the initial value on which to operate</param> | |
/// <param name="itemSelector">initial projection callback</param> | |
/// <param name="returnSelector">final projection callback</param> | |
/// <returns>The result of calling the final projection, or <c>None</c></returns> | |
/// <exception cref="ArgumentNullException"> | |
/// Raised if either <c>itemSelector</c> or | |
/// <c>returnSelector</c> are <c>null</c>. | |
/// </exception> | |
public static Optional<TReturn> SelectMany<T, TItem, TReturn>( | |
this Optional<T> option, | |
Func<T?, Optional<TItem>> itemSelector, | |
Func<T?, TItem?, TReturn> returnSelector | |
){ | |
if (itemSelector is null) throw new ArgumentNullException(nameof(itemSelector)); | |
if (returnSelector is null) throw new ArgumentNullException(nameof(returnSelector)); | |
if (option is (true, var v1) && itemSelector(v1) is (true, var v2)) | |
return returnSelector(v1, v2).ToOptional(); | |
return None<TReturn>(); | |
} | |
/// <summary> | |
/// If, and only if, the given Optional instance has an underlying value, | |
/// then the given projection callback is invoked with said value. In | |
/// other words, if the given <c>option</c> is 'no value', then the | |
/// projection is not called (the method just returns <c>None</c>). | |
/// </summary> | |
/// <param name="option">the value on which to operate</param> | |
/// <param name="selector">the projection callback</param> | |
/// <returns>The result of calling the projection, or <c>None</c></returns> | |
/// <exception cref="ArgumentNullException"> | |
/// Raised if the projection callback is <c>null</c>. | |
/// </exception> | |
public static Optional<TReturn> SelectMany<T, TReturn> | |
(this Optional<T> option, Func<T, Optional<TReturn>> selector) | |
{ | |
if (selector is null) throw new ArgumentNullException(nameof(selector)); | |
return option.Either(some: selector, () => default); | |
} | |
/// If, and only if, the given Optional instance has an underlying value, | |
/// then the given projection callback is invoked with said value. In | |
/// other words, if the given <c>option</c> is 'no value', then the | |
/// projection is not called (the method just returns <c>None</c>). | |
/// <param name="option">the value on which to operate</param> | |
/// <param name="selector">the projection callback</param> | |
/// <returns> | |
/// The result of calling the projection, lifted into a new Optional | |
/// instance (n.b. <c>null</c> values are coerces into <c>None</c>). | |
/// </returns> | |
/// <exception cref="ArgumentNullException"> | |
/// Raised if the projection callback is <c>null</c>. | |
/// </exception> | |
public static Optional<TReturn> Select<T, TReturn> | |
(this Optional<T> option, Func<T, TReturn> selector) | |
{ | |
if (selector is null) throw new ArgumentNullException(nameof(selector)); | |
return option.SelectMany(value => selector(value).ToOptional()); | |
} | |
/// <summary> | |
/// Applies the given predicate to the underlying value | |
/// of the given Optional (if it exists). The result of the predicate is | |
/// mapped into a new Optional instance (such that <c>false => None</c> | |
/// and <c>true => Some(value)</c>). | |
/// </summary> | |
/// <param name="option">The Optional to be interrogated.</param> | |
/// <param name="predicate">Invoked when the underlying value is present.</param> | |
/// <returns> | |
/// <c>Some(value)</c> when the predicate returns <c>true</c>. | |
/// In all other cases, returns <c>None</c>. | |
/// </returns> | |
/// <exception cref="ArgumentNullException"> | |
/// Raised if the predicate callback is null. | |
/// </exception> | |
public static Optional<T> Where<T> | |
(this Optional<T> option, Func<T?, bool> predicate) | |
{ | |
if (predicate is null) throw new ArgumentNullException(nameof(predicate)); | |
return option.SelectMany(v => predicate(v) ? option : default); | |
} | |
/// <summary> | |
/// Applies the given predicate to the underlying value | |
/// of the given Optional (if it exists). | |
/// </summary> | |
/// <param name="option">The Optional to be interrogated.</param> | |
/// <param name="predicate">Invoked when the underlying value is present.</param> | |
/// <returns> | |
/// The result of applying the predicate to the underlying value | |
/// (n.b. returns <c>true</c> when the Optional in question has 'no value'). | |
/// </returns> | |
/// <exception cref="ArgumentNullException"> | |
/// Raised if the predicate callback is null. | |
/// </exception> | |
public static bool All<T> | |
(this Optional<T> option, Func<T?, bool> predicate) | |
{ | |
if (predicate is null) throw new ArgumentNullException(nameof(predicate)); | |
return option.Either(some: predicate, none: () => true); | |
} | |
/// <summary> | |
/// Applies the given predicate to the underlying value | |
/// of the given Optional (if it exists). Note, if no predicate is | |
/// supplied, this call functions as a simple existence check. | |
/// </summary> | |
/// <param name="option">The Optional to be interrogated.</param> | |
/// <param name="predicate">Invoked when the underlying value is present.</param> | |
/// The result of applying the predicate to the underlying value | |
/// (n.b. returns <c>false</c> when the Optional in question has 'no value'). | |
public static bool Any<T> | |
(this Optional<T> option, Func<T?, bool>? predicate = default) | |
=> option.Either(some: predicate ?? (_ => true), none: () => false); | |
/// Converts an Optional instance into a sequence | |
/// containing either zero or one value (i.e. 'none' or 'some'). | |
public static IEnumerable<T> ToEnumerable<T>(this Optional<T> option) | |
=> option | |
.Either(some: value => new [] { value }, none: Array.Empty<T>) | |
.AsEnumerable(); | |
/// <summary> | |
/// Applied the given traversal callback to each item in the given | |
/// collection, accumulating the results into a new Optional instance. | |
/// If any invocation of the traversal callback returns <c>None</c>, | |
/// then the overall result of the call will be <c>None</c>. | |
/// </summary> | |
/// <param name="items">collection to be traversed</param> | |
/// <param name="traversal"> | |
/// Evaluated for each item in the collection. Returning <c>None</c> | |
/// from this callback prevents accumulation of other results. | |
/// </param> | |
/// <returns>A new Optional instance.</returns> | |
/// <exception cref="ArgumentNullException"> | |
/// Raised when the traversal callback is <c>null</c>. | |
/// </exception> | |
public static Optional<IEnumerable<TReturn>> Traverse<T, TReturn> | |
(this IEnumerable<T>? items, Func<T, Optional<TReturn>> traversal) | |
{ | |
if (traversal is null) throw new ArgumentNullException(nameof(traversal)); | |
var empty = ImmutableList.Create<TReturn>(); | |
var given = items?.ToArray() ?? Array.Empty<T>(); | |
if (!given.Any()) return empty.AsEnumerable().ToOptional(); | |
return given.Aggregate( | |
seed: empty.ToOptional(), | |
(buffer, item) => | |
from soFar in buffer | |
from value in traversal(item) | |
select soFar.Add(value), | |
buffer => | |
from soFar in buffer | |
select soFar.AsEnumerable() | |
); | |
} | |
/// Converts a sequence of Optional instances into a single Optional | |
/// instance containing a sequence of values. If any element of the | |
/// input sequence is <c>None</c>, the resultant value is <c>None</c>. | |
public static Optional<IEnumerable<T>> Sequence<T> | |
(this IEnumerable<Optional<T>> items) => items.Traverse(item => item); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment