Skip to content

Instantly share code, notes, and snippets.

@pblasucci
Last active April 28, 2021 19:00
Show Gist options
  • Save pblasucci/de1220bc2075372c91423430143f0cfc to your computer and use it in GitHub Desktop.
Save pblasucci/de1220bc2075372c91423430143f0cfc to your computer and use it in GitHub Desktop.
(Yet another) Riff on Option/Maybe/etc. for C# 9
/*
* 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