Good morning everyone,
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.
[EDIT] Per comment:
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;
}
}
}
[EDIT2] After all the comments and answers, I have introduced a few changes. Here's modified reference type, which has the same semantics as the previous one, save for few changes.
- Renamed 'IsEmpty' to 'HasValue'.
- Made the
Optionimplicitly sealed by addinginternal abstractmethod. - Removed the
abstract ToString(). - Added implicit conversion.
- Got rid of
Lazyin favor of simplestatic readonlyfield.
Decision whether to leave the methods as extensions or not is mostly philosophical, they can be included into the class or not, I don't think it makes any difference in this case.
I feel against implementing IEnumerable, Option may be treated as a sequence with 0 or 1 element, but (IMO) it isn't technically a sequence. So an explicit 'conversion' to IEnumerable may be clearer, but I might be very well wrong here.
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 HasValue { 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 !HasValue ? 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 !HasValue ? 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));
}
public static implicit operator Option<T>(T value)
{
return From(value);
}
public static Option<T> From(T value)
{
if (value == null)
return None;
return new Some<T>(value);
}
public static readonly None<T> None = new None<T>();
internal abstract void ImplicitlySealed();
}
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 HasValue
{
get
{
return true;
}
}
internal override void ImplicitlySealed()
{
}
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 HasValue
{
get
{
return false;
}
}
internal override void ImplicitlySealed()
{
}
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)
{
return Option<T>.From(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.HasValue)
return Option<U>.None;
return Option<U>.From(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.HasValue)
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.HasValue && 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.HasValue && 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.HasValue)
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.HasValue ? 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.HasValue ? 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.HasValue) yield break;
yield return option.Value;
}
}
}
And here is the value type, which has a bit different semantics. That's just my purely subjective opinion, but having Some and None as different 'types' of Option looks better, but the version below has this nice property of not being null, ever.
Also, there's no direct access to the value (Value property is missing), but there's always GetOrDefault().
Which one is 'better'? I don't know, but the value type certainly looks more robust null-wise.
public struct Option<T>
{
private readonly T value;
private readonly bool hasValue;
/// <summary>
/// Indicates whether the option holds a value or not
/// </summary>
public bool HasValue
{
get { return hasValue; }
}
private Option(T value)
{
this.value = value;
hasValue = true;
}
/// <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 HasValue ? value : defaultValue;
}
/// <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 HasValue ? value : defaultValue();
}
/// <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));
}
public override string ToString()
{
return !HasValue ? "None" : value.ToString();
}
public static readonly Option<T> None = new Option<T>();
public static Option<T> Some(T value)
{
if(value == null)
throw new ArgumentNullException("value");
return new Option<T>(value);
}
public static Option<T> From(T value)
{
if (value == null)
return None;
return Some(value);
}
/// <summary>
/// Applies the specified function to option's value (if the option is non-empty) and yields a new option.
/// </summary>
/// <typeparam name="U"></typeparam>
/// <param name="func"></param>
/// <returns>A new option after applying the function to current option's value if the current option is non-empty, <see cref="Option{T}.None"/> otherwise.</returns>
public Option<U> Select<U>(Func<T, U> func)
{
if (!HasValue)
return Option<U>.None;
return Option<U>.From(func(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{U}"/>, expects a function that returns an <see cref="Option{T}"/></remarks>
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="func"></param>
/// <returns>A new option if the current option is non-empty, <see cref="Option{T}.None"/> otherwise.</returns>
public Option<U> SelectMany<U>(Func<T, Option<U>> func)
{
if (!HasValue)
return Option<U>.None;
return func(value);
}
/// <summary>
/// Filters the option by the passed predicate function
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="func"></param>
/// <returns>Returns the option if the option is non-empty and the value underneath satisfied the predicate, <see cref="Option{T}.None"/> otherwise.</returns>
public Option<T> Where(Func<T, bool> func)
{
if (HasValue && func(value))
return this;
return None;
}
/// <summary>
/// Checks if the value 'exists' inside the option.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="func"></param>
/// <returns><code>true</code> if the option is not empty and if it satisfied the predicate, <see cref="Option{T}.None"/> otherwise.</returns>
public bool Any(Func<T, bool> func)
{
return HasValue && func(value);
}
/// <summary>
/// Executes a specified action on the option, if the option is non-empty.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="action"></param>
public void ForEach(Action<T> action)
{
if (HasValue)
action(value);
}
/// <summary>
/// Returns the <paramref name="alternative"/> if the current option is empty. Returns the option itself otherwise.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="alternative"></param>
/// <remarks>It's recommended to use <see cref="OrElse{T}(Func{Option{T}}"/> overload.</remarks>
/// <returns></returns>
public Option<T> OrElse(Option<T> alternative)
{
return !HasValue ? alternative : this;
}
/// <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 calling instance is returned.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="alternative"></param>
/// <returns>The current option if it's non-empty and the evaluation result of the alternative function otherwise.</returns>
public Option<T> OrElse(Func<Option<T>> alternative)
{
return !HasValue ? alternative() : this;
}
/// <summary>
/// Converts the option to a sequence (<see cref="IEnumerable{T}"/>)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>One element sequence containing the option's value if the option was non-empty, empty sequence otherwise</returns>
public IEnumerable<T> ToEnumerable()
{
if (!HasValue) yield break;
yield return value;
}
public static implicit operator Option<T>(T value)
{
return From(value);
}
}
public static class Extensions
{
/// <summary>
/// Wraps the specified object in an option. If the object is null, returns <see cref="Option{T}.None"/>, otherwise creates a new option
/// </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)
{
return Option<T>.From(value);
}
}
Is there anything else missing here?
By the way, thanks for all the input everyone, if there's nothing more to add I'll probably accept an answer today or tomorrow!
nullfor 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. – svick Feb 8 at 13:35Maybe,Just,Nothingseem more popular in C# implementations than the ML-ishOption,Some,Noneas seen in Scala and F#. – C. A. McCann Feb 8 at 13:55Option<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. :) – Trust me - I'm a Doctor Feb 8 at 14:13