6
\$\begingroup\$

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;
        }
    }
}
\$\endgroup\$
8
  • 1
    \$\begingroup\$ Also, I think you should consider that C# already has idiomatic way to do this: null for reference types and Nullable<T> for value types. What's worse a variable of type Option<T> can still be null, so your type doesn't really work like true Option. \$\endgroup\$
    – svick
    Commented Feb 8, 2013 at 13:35
  • 4
    \$\begingroup\$ Usually when I see people doing this, they make it a struct instead of a class. Also, for some reason the Haskell-ish names Maybe, Just, Nothing seem more popular in C# implementations than the ML-ish Option, Some, None as seen in Scala and F#. \$\endgroup\$ Commented Feb 8, 2013 at 13:55
  • 1
    \$\begingroup\$ Yeah, I guess struct would indeed be a better fit, and it would automatically alleviate the problem of the 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\$ Commented Feb 8, 2013 at 14:13
  • \$\begingroup\$ @svick By the way, I was curious and I checked: scala > val opt : Option[Int] = null works, and then opt.getOrElse(2) throws NullPointerException, 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\$ Commented Feb 8, 2013 at 15:47
  • \$\begingroup\$ The Option names are certainly more traditional, and probably make more sense in C# than the Maybe 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\$ Commented Feb 8, 2013 at 17:17

2 Answers 2

4
\$\begingroup\$
  1. You have several extension methods with Option<T> as first parameter, why don't you use normal instance methods?

  2. I prefer having an implicit conversion from T to Option<T>

    Option<T> seems like a nice choice for optional function parameters. In that case calling ToOption() everywhere is a bit annoying.

  3. Why make the initialization of None lazy? Creating a None<T> is probably cheaper than creating a Lazy<T>. I recommend lazy initialization only if it's expensive.

  4. I'd rename IsEmpty to HasValue to match Nullable<T>. (Inverted meaning obv.)

  5. I'd add some kind of protection against other classes deriving from Option<T>. For example an internal abstract method.

  6. public abstract override string ToString(); Seems weird. Don't touch it on Option<T> and simply override it in the child classes.

  7. Why disallow a value of null?

  8. I prefer using a struct, so the option itself cannot be null.

  9. One could consider implementing IEnumerable<T>, instead of a ToEnumerable() method. But I'm not sure if that's a good idea.

\$\endgroup\$
0
2
\$\begingroup\$

What you have done is usually called a Maybe Monad. There are several implementations of those for .NET (one, two), and usually they just use extension methods to apply operations to nullable object.

Concerning your code: you don't need a hierarchy here, since most of the code is already in base Option class.

\$\endgroup\$
1
  • \$\begingroup\$ Hey, thanks. I'm aware that it's a Maybe monad, I didn't know though that there are implementations ready. It was a fun exercise though :) I will definitely use the ready version if the need arises. I tried to make it as similar to Scala's Option as I could, and I guess the current version resembles it the most. Of course there are flaws, as others pointed out in comments, but I can't e.g. make it a struct without modifying it heavily. Thank you very much for your input though, I'll save the github links. :) \$\endgroup\$ Commented Feb 8, 2013 at 15:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.