dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.98k stars 4.66k forks source link

Option<T>/Maybe<T> and Either<TLeft, TRight> #21014

Open ElemarJR opened 7 years ago

ElemarJR commented 7 years ago
public Option<Employee> GetById(string id)
    => new DbContext().Find(id);

instead of

public Employee GetById(string id)
    => new DbContext().Find(id);

The proposed method signature reveals much better the real intent of this method/function. Right?

Option<T> could provide a Match function....

Here is a very basic implementation:

namespace ElemarJR.FunctionalCSharp
{
    using static Helpers;

    public struct Option<T>
    {
        internal T Value { get; }
        public bool IsSome { get; }
        public bool IsNone => !IsSome;

        internal Option(T value, bool isSome)
        {
            Value = value;
            IsSome = isSome;
        }

        public TR Match<TR>(Func<T, TR> some, Func<TR> none)
            => IsSome ? some(Value) : none();

        public Unit Match(Action<T> some, Action none)
            => Match(ToFunc(some), ToFunc(none));

        public static readonly Option<T> None = new Option<T>();

        public static implicit operator Option<T>(T value)
            => Some(value);

        public static implicit operator Option<T>(NoneType _)
            => None;
    }
}

with some additional operators:

namespace ElemarJR.FunctionalCSharp
{
    using static Helpers;

    public static class Option
    {
        #region Of
        public static Option<T> Of<T>(T value)
            => new Option<T>(value, value != null);
        #endregion

        #region Apply
        public static Option<TResult> Apply<T, TResult>
            (this Option<Func<T, TResult>> @this, Option<T> arg)
            => @this.Bind(f => arg.Map(f));

        public static Option<Func<TB, TResult>> Apply<TA, TB, TResult>
             (this Option<Func<TA, TB, TResult>> @this, Option<TA> arg)
             => Apply(@this.Map(Helpers.Curry), arg);
        #endregion

        #region Map
        public static Option<TR> Map<T, TR>(
            this Option<T> @this,
            Func<T, TR> mapfunc
            ) =>
                @this.IsSome
                    ? Some(mapfunc(@this.Value))
                    : None;

        public static Option<Func<TB, TResult>> Map<TA, TB, TResult>(
            this Option<TA> @this,
            Func<TA, TB, TResult> func
        ) => @this.Map(func.Curry());
        #endregion

        #region Bind
        public static Option<TR> Bind<T, TR>(
                this Option<T> @this,
                Func<T, Option<TR>> bindfunc
            ) =>
            @this.IsSome
                ? bindfunc(@this.Value)
                : None;
        #endregion

        #region GetOrElse
        public static T GetOrElse<T>(
            this Option<T> @this,
            Func<T> fallback
            ) =>
                @this.Match(
                    some: value => value,
                    none: fallback
                    );

        public static T GetOrElse<T>(
            this Option<T> @this,
            T @else
            ) =>
                GetOrElse(@this, () => @else);
        #endregion

        #region OrElse
        public static Option<T> OrElse<T>(
            this Option<T> @this,
            Option<T> @else
        ) => @this.Match(
            some: _ => @this,
            none: () => @else
        );

        public static Option<T> OrElse<T>(
            this Option<T> @this,
            Func<Option<T>> fallback
        ) => @this.Match(
            some: _ => @this,
            none: fallback
        );
        #endregion
    }
}

and some LINQ support:

namespace System.Linq
{
    using static Helpers;
    public static partial class LinqExtensions
    {
        public static Option<TResult> Select<T, TResult>(
                this Option<T> @this,
                Func<T, TResult> func)
            => @this.Map(func);

        public static Option<TResult> SelectMany<T, TB, TResult>(
            this Option<T> @this,
            Func<T, Option<TB>> binder,
            Func<T, TB, TResult> projector
        ) => @this.Match(
            none: () => None,
            some: (t) => @this.Bind(binder).Match(
                none: () => None,
                some: (tb) => Some(projector(t, tb))
            )
        );

        public static Option<T> Where<T>(
            this Option<T> option,
            Func<T, bool> predicate
        ) => option.Match(
            none: () => None,
            some: o => predicate(o) ? option : None
        );
    }
}

This proposal does not incur any CLR changes.

DavidArno commented 6 years ago

@karelz,

I'd say "quite heavily". If the feature works well, then T? can be used as a Maybe<T> in most cases. The only limitation been that eg:

string? s = null;
s.ToString();

would result in a NRE, rather than null. But 90% of a maybe type functionality is usable in 90% of cases...

yahorsi commented 5 years ago

How is the proposal affected by Nullable reference types in C# feature?

The proposal above adresses a bit different problem a bit different way. It helps you to deal with existing nulls and avoids breaking API changes.

What we as developers need is to explicitly say the function may or may not return result. My todays case: There is func that takes some input and runs several validation functions The it wants to aggregate all errors is there are and return them upper on the stack Every validation func may or may not return error So, some pseudo code

Task<List> async ValidateOrder(Order order) { var errors = new List();

 var r = await ValidateCreditCard(order);
 if (r != null) {errors.Add(r);}

 var r = await ValidateProductAvailable(order);
 if (r != null) {errors.Add(r);}

}

This is how usually null is used to indicate there is no result and that is scary and ugly. And no, I can't use yield. Ideally I want to be able to do just the following Task<List> async ValidateOrder(Order order) { var errors = new List(); var r = errors.Add(await ValidateCreditCard(order)); var r = errors.Add(await ValidateProductAvailable(order)); }

Where ValidateCreditCard and ValidateProductAvailable return Maybe and all collections have built in support for the Maybe so if there is result it gets added and no if no.

svick commented 5 years ago

@yahorsi

What we as developers need is to explicitly say the function may or may not return result.

How is that different from nullable reference types? Those also let you explicitly say whether a function will always return result, or only sometimes.

Regarding your code:

  1. That's not how APIs that use Maybe<T> are usually built. For example, 'T list in F# does not have a function for appending 'T option, even though there are some functions in the List module that use option.
  2. What's preventing you from adding an extension method like AddIfNotNull that adds the value to the list if it's not null? And with C# 8.0, using the regular Add would produce a warning, which should steer people towards AddIfNotNull, if you had it in your library.
yahorsi commented 5 years ago
  1. That's not how APIs that use Maybe<T> are usually built

That is exactly how usually such API is built. E.g. in scala: List(1,2,3) ++ None ++ Some(4) ++ Some(5) ++ None And it must work this way, otherwise the whole idea is almost useless. By having Maybe/Option we want to avoid If's at all where possible, and that's the goal.

  1. or example, 'T list in F# does not have a function for appending 'T option, even though there are some functions in the List module that use option.

F# is far from being the best functional lang to take examples from as it shares BCL with .NET and BCL is not designed for this kind of functional API's

2. What's preventing you from adding an extension method like AddIfNotNull that adds the value to the list if it's not null?

Nothing stops me from doing anything myself ;) What we really want is standard API that can be used and consumed by everybody. My

mrpantsuit commented 5 years ago

There is an excellent course on PluralSight that explains the benefits of applying functional principles in C# named, aptly, "Applying functional principles in C#". The gist is that a method signature should be honest. If you return T and sometimes return null, you're lying to those that would call your method. Return Option on the other hand and your signature is honest and explicit about what it may return.

You could perhaps remedy this with a new language feature (non-nullables) -- expanding the surface area of what devs need to learn/know, BTW -- but why wouldn't you just solve it using an existing language feature: the type system? Especially since you could solve some other problems of dishonest method signatures (e.g., exceptions) the exact same way.

Option was added to Java 5+ years ago; what's taking C# so long to catch on? I get the sense that the C# community has a lack of reverence for and understanding of ideas from other communities (Haskell, Scala, Java), evidenced by the fact that svick and karelz aren't/weren't even familiar with the arguments for Option (whether you agree with them or not) that have been well documented and discussed for years.

I'm sympathetic to @louthy's point that retrofitting functional types to the core libs could be problematic, but the NRE problem is big enough that just adding Option type to the core libs seems appropriate, even if it's only used to reduce NREs in app code, not retrofitted to the core libs, and the other functional advantages @louthy mentioned of using Option with other functional types are not immediately available.

It's not just a matter of taste or style here (functional/imperative). This dishonesty of method signatures problem is an actual and practical deficiency in the way C# code is currently written, and has real-world fallout, e.g. ubiquitous NREs in C# programs. I don't buy the argument that C# shouldn't adopt functional patterns because it's hopelessly imperative. "We don't want that because it's functional" is not an argument. I believe C# can be both OO and functional, applying concepts from each where appropriate, and be better off for it.

Clockwork-Muse commented 5 years ago

@mrpantsuit - We're getting nullable reference types (although I personally might prefer stronger enforcement). That takes care of the primary use case for Option<T>. We'd probably have to use static, non-extension methods for all the mapping you can do, if we wanted a more functional style, but at the very least we are going to have the necessary information in the type system.

svick commented 5 years ago

@mrpantsuit

If you return T and sometimes return null, you're lying to those that would call your method.

You're not lying. For a reference type T, null is always a valid value in C# 7. The problem is that there is no way to express that a method will never return null. And C# 8.0 nullable reference types solve that.

why wouldn't you just solve it using an existing language feature: the type system?

In my opinion, solving this just by adding the Option<T> type is not good enough. Adding nullable reference types is more work, but the end result is much better.

"We don't want that because it's functional" is not an argument.

No, it's not. But "we want it because it's functional" is also not an argument. That's why C# 8.0 is going to have nullable reference types: it recognizes that null-safety is a problem, it realizes that retrofitting Option<T> into the existing ecosystem would not work well, and so it creates its own solution: one that is designed specifically for an OOP-first language with more than a decade of history.

mrpantsuit commented 5 years ago

So then how will you solve the further dishonesty of exceptions, i.e., method signatures lie by pretending that they'll never throw? (The dishonesty here is lying by omission.) Another language feature, like, God forbid, checked exceptions? :-O Alternatively, you could just solve it by returning a Result type (holds a result OR an error/exception), similar to how you solved the null dishonesty with Option. Seems a simpler, more explicit, holistic solution.

No one was making the argument to use Option simply because it's functional. And it should be noted that C# is getting more functional with each release, which is great: pattern matching, throws in expressions, lambdas, LINQ, switch expressions. C# strayed from it's OO-first history long ago, so no need to draw a line in the sand now.

JoergWMittag commented 5 years ago

The main advantage of an Option type is that Option is essentially a collection with a maximum size of 1, i.e. it is either empty or contains exactly one element. What this means is that Option implements IEnumerable and thus automagically works everywhere IEnumerable works. What this means is that I never need to care whether there is a value there or not, I can, for example, simply foreach over it, and the loop body will either be executed once or not at all, without me having to check.

Additionally, Option is a Monad and thus automagically works everywhere a monad works, e.g. in a LINQ Query Expression. Again, like above this means I never actually need to check whether there is a value or not. I simply do from value in valueThatMayOrMayNotExist select doSomethingWith(value) and this will always work no matter whether valueThatMayOrMayNotExist has a value or not.

With Nullable Reference Types, I generally need to do an explicit null check before dereferencing it, or the type checker needs to perform sophisticated dynamic flow analysis to prove that the reference is not null.

Clockwork-Muse commented 5 years ago

This is the first time I've heard of such a use of Option<T>.

To be fair to nullable reference types, it's pretty trivially solvable via something like:


public static IEnumerable<T> SingleOrEmpty<T>(T? element)
{
    if (element == null)
    {
        return Enumerable.Empty();
    }
    else
    {
        return Enumerable.Single(element);
    }
}
jzabroski commented 4 years ago

The main advantage of an Option type is

The main advantage of an Option type is that you can build a "tower of Options.". Option<Option<T>> is a perfectly valid type, and makes it easy to compose type properties and write API libraries that use the type system to guarantee success ("fallback plans guaranteed to succeed"). The same is true for Either.

With a nullable, you cannot do that, because you can't do Nullable<Nullable<T>>. The engineer has to write imperative logic to achieve the same, and because there is less structure (less type guarantees), there is more chance the engineer can commit a modus ponens or similar "case fall through" common logic error.