I have decided to create a simple Option
type for C#. I have essentially based it on the
Scala's Option
.
It was mostly a fun exercise, but it might come in handy at work or any other project, even though it's less powerful than the Scala version.
Now I'd appreciate all remarks regarding this little piece of code, but essentially now I'm thinking whether or not to introduce an implicit conversion between the type and the Option
, which would allow for:
MyType obj;
// ... something interesting happens (or not)
Option<MyType> optionalObj = obj;
I have already included ToOption
extension method for convenient creation of the option from existing object:
var optionalObj = obj.ToOption();
which returns either Some<T>
or None<T>
and the implicit conversion would do just that, but I'm just wondering if it's not too 'hidden', non-obvious and/or counter-intuitive(?).
Anyway, I'd like to see what you think.
public abstract class Option<T>
{
/// <summary>
/// Option's value
/// </summary>
public abstract T Value { get; }
/// <summary>
/// Indicates whether the option holds a value or not
/// </summary>
public abstract bool IsEmpty { get; }
/// <summary>
/// Unwraps the option.
/// </summary>
/// <param name="defaultValue"></param>
/// <returns>Specified, default value if the option is empty, or the option's value if present</returns>
/// <remarks>It's recommended to use the <see cref="GetOrElse(Func{T})"/> when passing a parameter expression to be evaluated.
/// In this case, the condition will get evaluated AFTER evaluation of the parameter, which may be costly.</remarks>
public T GetOrElse(T defaultValue)
{
return IsEmpty ? defaultValue : Value;
}
/// <summary>
/// Unwraps the option
/// </summary>
/// <param name="defaultValue">Function returning the default value</param>
/// <returns>The evaluation of the specified function if option is empty, or the option's value if present</returns>
/// <remarks>Recommended overload. The function returning defaultValue will only get evaluated if
/// the option is empty.</remarks>
public T GetOrElse(Func<T> defaultValue)
{
return IsEmpty ? defaultValue() : Value;
}
/// <summary>
/// Unwraps the option
/// </summary>
/// <returns>The default value for the type if option is empty, or the option's value if present</returns>
public T GetOrDefault()
{
return GetOrElse(default(T));
}
private static readonly Lazy<None<T>> NoneInstance = new Lazy<None<T>>(() => new None<T>());
/// <summary>
/// Shared <code>None{T}</code> instance.
/// </summary>
public static None<T> None
{
get
{
return NoneInstance.Value;
}
}
public abstract override string ToString();
}
public sealed class Some<T> : Option<T>
{
/// <summary>
/// Creates a new option holding the value.
/// </summary>
/// <exception cref="ArgumentNullException">When the value passed is null. <code>None{T}</code> (or the extension method which creates appropriate type) should be used instead.</exception>
/// <param name="value"></param>
public Some(T value)
{
if(value == null)
throw new ArgumentNullException("value", "Argument passed to Some was null - use None<T> instead.");
this.value = value;
}
private readonly T value;
public override T Value
{
get
{
return value;
}
}
public override bool IsEmpty
{
get
{
return false;
}
}
public override string ToString()
{
return Value.ToString();
}
}
public sealed class None<T> : Option<T>
{
/// <summary>
/// <exception cref="InvalidOperationException">Thrown when accessing the value. <code>None{T}</code> has no value.</exception>
/// </summary>
public override T Value
{
get
{
throw new InvalidOperationException();
}
}
public override bool IsEmpty
{
get
{
return true;
}
}
public override string ToString()
{
return "None";
}
}
namespace OptionType.Extensions
{
/// <summary>
/// Extension methods for <see cref="Option{T}"/>
/// </summary>
public static class OptionExtensions
{
/// <summary>
/// Wraps the specified object in an option. If the object is null, returns <see cref="None{T}"/>, otherwise creates <see cref="Some{T}"/>
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <returns>A new <see cref="Option{T}"/></returns>
public static Option<T> ToOption<T>(this T value)
{
if (value == null) return Option<T>.None;
return new Some<T>(value);
}
/// <summary>
/// Applies a specified function to the option's value and yields a new option if the option is non-empty.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="option"></param>
/// <param name="func"></param>
/// <returns><see cref="Some{T}"/> if the option is non-empty, <see cref="None{T}"/> otherwise.</returns>
public static Option<U> Select<T, U>(this Option<T> option, Func<T, U> func)
{
if (option.IsEmpty) return Option<U>.None;
return new Some<U>(func(option.Value));
}
/// <summary>
/// Applies a specified function to the option's value and yields a new option if the option is non-empty.
/// <remarks>Different from <see cref="Select{T,U}"/>, expects a function that returns an <see cref="Option{T}"/></remarks>
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="option"></param>
/// <param name="func"></param>
/// <returns><see cref="Some{T}"/> if the option is non-empty, <see cref="None{T}"/> otherwise.</returns>
public static Option<U> SelectMany<T, U>(this Option<T> option, Func<T, Option<U>> func)
{
if (option.IsEmpty) return Option<U>.None;
return func(option.Value);
}
/// <summary>
/// Filters the option by the passed predicate function
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="option"></param>
/// <param name="func"></param>
/// <returns>Returns the option if the option is non-empty and the value underneath satisfied the predicate, <see cref="None{T}"/> otherwise.</returns>
public static Option<T> Where<T>(this Option<T> option, Func<T, bool> func)
{
if (option.IsEmpty || func(option.Value)) return option;
return Option<T>.None;
}
/// <summary>
/// Checks if the value 'exists' inside the option.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="option"></param>
/// <param name="func"></param>
/// <returns><code>true</code> if the option is not empty and if it satisfied the predicate, <see cref="None{T}"/> otherwise.</returns>
public static bool Any<T>(this Option<T> option, Func<T, bool> func)
{
return !option.IsEmpty && func(option.Value);
}
/// <summary>
/// Executes a specified action on the option, if the option is non-empty.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="option"></param>
/// <param name="action"></param>
public static void ForEach<T>(this Option<T> option, Action<T> action)
{
if (!option.IsEmpty) action(option.Value);
}
/// <summary>
/// Returns the <paramref name="alternative"/> if the specified <paramref name="option"/> is empty. Returns the <paramref name="option"/> itself otherwise.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="option"></param>
/// <param name="alternative"></param>
/// <remarks>It's recommended to use <see cref="OrElse{T}(Option{T}, Func{Option{T}}"/> overload.</remarks>
/// <returns></returns>
public static Option<T> OrElse<T>(this Option<T> option, Option<T> alternative)
{
return option.IsEmpty ? alternative : option;
}
/// <summary>
/// Checks whether the current option is empty; if it is, the <paramref name="alternative"/> function is evaluated and the result is returned. Otherwise,
/// the <paramref name="option"/> is returned.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="option"></param>
/// <param name="alternative"></param>
/// <returns>The current option if it's non-empty and the evaluation result of the alternative function otherwise.</returns>
public static Option<T> OrElse<T>(this Option<T> option, Func<Option<T>> alternative)
{
return option.IsEmpty ? alternative() : option;
}
/// <summary>
/// Converts the option to a sequence (<see cref="IEnumerable{T}"/>)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="option"></param>
/// <returns>One element sequence containing the option's value if the option was non-empty, empty sequence otherwise</returns>
public static IEnumerable<T> ToEnumerable<T>(this Option<T> option)
{
if (option.IsEmpty) yield break;
yield return option.Value;
}
}
}
null
for reference types andNullable<T>
for value types. What's worse a variable of typeOption<T>
can still benull
, so your type doesn't really work like trueOption
. \$\endgroup\$Maybe
,Just
,Nothing
seem more popular in C# implementations than the ML-ishOption
,Some
,None
as seen in Scala and F#. \$\endgroup\$Option<T>
being null itself. As for nullable, that's true, Option its superfluous for value types, but reference types can sometimes be a pain with null checks... I can't really say anything about the naming, I'm just more used to ML conventions, as I have dipped my toes in both F# and Scala, but not Haskell. :) \$\endgroup\$scala > val opt : Option[Int] = null
works, and thenopt.getOrElse(2)
throwsNullPointerException
, so I guess it works in the same way :) You have a point though, maybe that's why implementations from almaz' answer use extension methods on reference types and work with nulls instead... \$\endgroup\$Option
names are certainly more traditional, and probably make more sense in C# than theMaybe
naming scheme (and I say that as someone very familiar with Haskell and its influence on C#). Also, some people really don't like calling extension methods on a null reference, in which case a struct is the only reasonable choice. \$\endgroup\$