dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.42k stars 4.76k 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.

karelz commented 7 years ago

FYI: Here's our API process Could you please provide brief description/motivation of the API? What is the problem you're trying to solve? Why do you need API in BCL?

ElemarJR commented 7 years ago

C# (and VB) are evolving and providing support for functional programming. Today we have pattern matching, better support for tuples and local functions. The BCL should provide some common functional programming containers. I suggested Maybe/Option, but we could have Either<TLeft, TRight> as well.

Today, I've been using my implementations (https://github.com/ElemarJR/ElemarJR.FunctionalCSharp), but I really would love to have some BCL support for functional programming.

karelz commented 7 years ago

@ElemarJR I'm sorry, but I still don't get it. Can you please be more specific? Where exactly would it help? Which patterns does it enable? Imagine you have to convince 100 C# developers who don't have the functional programming background you do. Pitch your idea to them - motivate them.

ElemarJR commented 7 years ago

Consider the following method:

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

What result should be expected from this? An Employee instance, right? Yes, but not only this! The result of this method could be null if there are no records that satisfy the specified criteria or an exception (if it is no possible to connect with the database, for example)

Actually, the programmer who will consume this method will have no idea of what results he would get invoking it.

Much better would be:

public Either<Exception, Option<Employee>> GetById(string id)
{
  try
  {
     return (new DbContext().Find(id));
  }
  catch (Exception e)  {  return e;  }
}

Now, it's clear what results this method could provide. No more NullReferenceExceptions or unhandled exceptions.

ElemarJR commented 7 years ago

Talking about patterns, we would be able to adopt a much more expressive (and declarative) coding style:

public Try<Exception, Unit> RaiseSalary(string id, decimal amount) => 
    _employeeRepository
        .GeyById(id)
        .IfNone(() => 
            new InvalidOperationException("There is no employee with the specified id.")
        )
        .Then(employee => employee.RaiseSalary(amount))
        .Then(employee => _employeeRepository.Save(employee));

Expressiveness and "NO-MORE-EXCEPTIONS"

Clockwork-Muse commented 7 years ago

.... Would they even be "Exceptions" anymore? You're turning them into error codes (not that I disagree that many exception cases should be switched to error codes or pre-emptive checks, especially in I/O cases).

karelz commented 7 years ago

@ElemarJR your latest examples of Try and Either are going above the original scope. Is that intentional? Personally, I don't like such editions to C# -- it might be best to experiment with such things outside of CoreFX first and see if they gain momentum and interest from larger community. Would that make sense?

Your original Option<T> proposal looks like a spin on non-nullable reference types - see UserVoice issue. Or did I miss your point?

Overall it would help to stay focused on one thing and properly provide motivation.

mellinoe commented 7 years ago

This has been a common request in the past, see https://github.com/dotnet/corefx/issues/538, https://github.com/dotnet/corefx/issues/748, https://github.com/dotnet/corefx/issues/1291

Previously, we've said that this should be tied to new C# language features, in order for it to be usable / efficient. @jaredpar Are there any C# proposals covering this topic? Perhaps a pattern-matching-related feature?

svick commented 7 years ago

I don't think it makes sense to add Option to C#. If you want to make nullability explicit, what you need is to remove null, which is what non-nullable reference types (https://github.com/dotnet/csharplang/issues/36) are about. I don't see how renaming null to None helps.

To expand upon this some more:

For a reference type T, the possible values are:

For Option<T>, the possible values are:*

The two are effectively identical, so adding Option<T> does not bring much benefit. It makes the possibility of not having an instance of T more explicit, but since it does not remove null, it also adds confusion: if you see T, does it mean it's a "new-style T", so it shouldn't be null (but nobody is checking that) or is it an "old-style T", in which case null should be expected as a possible value?

And adding Either also does not make much sense to me. For better or worse, the error reporting mechanism in .Net is exceptions. Adding a second mechanism to do the same thing would require for it to bring significant benefits, which I don't see, especially not without other features, like not being able to ignore the return value of a method accidentally.


* If I'm reading the code right, your implementation of Option<T> also allows a third possibility: Some holding null. I'm going to assume that's not intended.

jaredpar commented 7 years ago

@mellinoe yes. C# is actively working on a non-null proptotype feature. It's still in the early phases so don't expect concecrete feedback anytime soon. But it is an idea we are persuing.

ElemarJR commented 7 years ago

@Clockwork-Muse Not simple error codes. If you use Either/Try you would force the "dev client" to handle it. I like to think Exceptions as result value from failed executions. The idea is to avoid dangerous side-effects.

@karelz It's not simple non-nullable objects. There are contexts where you want to return None indicating you have no value related. The Maybe meta-type is important because it indicates that the result could be None (It's different of null that is implicit possible all the time). About the momentum, Functional Programming and Reactive Programming are two very hot topics. F#, as a (almost) functional language already provides it.

@mellinoe I first created an issue in the csharlang repo. They recommended me to create an issue here.

@svick None is not a "renamed null". There are very different use cases and semantic implications. Today, there is no way to explicitly indicate that a function/method could return no value - so, we are forced to follow a defensive way every time.

About Either it is not only for errors. It could be applied to any function that could return two different result types . Exceptions are a very expensive way to indicate that something went wrong. Don't you think?

Finally, I have a separated assembly for my high-level/functional types. It's not possible to create a Some(null).

In the end of the day, I just want to write more resilient code. I've been adopting these types for years with very good results.

karelz commented 7 years ago

@ElemarJR thanks for the info. It gives me some high-level idea what you're trying to achieve. I think it is an interesting (and bold) idea.

Regarding momentum - I was not talking about functional languages (e.g. F#) momentum, but FunctionalCSharp momentum. Given that you work on these for years now, maybe it would be best to create and release your library as separate NuGet package and see how popular it becomes. Feel free to use this issue to flush out basic idea / direction.

For context, here is our overall bar for new CoreFX repo APIs:

  1. useful for implementing CoreFX itself (e.g. PriorityQueue - dotnet/runtime#14032), or
  2. natural extensions of APIs already present in CoreFX (adding methods, similar types), or
  3. types ported from .NET Framework (although not all would make the cut and some might be chased away into separate repos), or
  4. deeply thought-trough, and ideally proven, API which is truly 'core' (e.g. Span<T> & friends experiments in CoreFXLab repo)

Example of types we believe do not belong into CoreFX repo are e.g. PowerCollections - while useful, we think they can be shipped in separate package from separate repo (something like CoreFXExtensions - plans are not finalized yet). Examples of some interesting controversial/border-line cases: SemanticVersion, Disposables, nearly-sorted sorting algo

RenanCarlosPereira commented 7 years ago

If C# get this feature what would be the propose of an exception?

Richiban commented 7 years ago

For anyone curious about how types such as Option<T>, Either<L, R> and others work I recommend checking out @louthy's excellent LanguageExt package (https://github.com/louthy/language-ext, https://www.nuget.org/packages/LanguageExt.Core/). It's mature and feature-rich; we've been using it in production for some time now. I love the explicitness that Optional<T> gives you in an API:

public Optional<Person> GetById(PersonId personId);

It's super clear from the signature alone what happens if the person with that ID is not found. It also prevents exceptions because Option<Person> is not the same type as Person (as is the case with null):

var person = GetById(personId);

Console.WriteLine(person.Name); // Error: Option<Person> doesn't have the member "Name"
match(GetById(personId),
    Some: person => Console.WriteLine(person.Name), // This is safe because the wrapped Person instance is guaranteed non-null
    None: () => HandleMissingPerson() // I *must* explicitly handle the case where the Person was not found
)

While @louthy's library is great and all, I really do feel that something like this should be baked into the language. Maybe it's something that will, one day, be superseded by nullable reference types being baked into the language but I think we're a long way away from that, to be honest.

louthy commented 7 years ago

For anyone curious about how types such as Option, Either<L, R> and others work I recommend checking out @louthy's excellent LanguageExt package (https://github.com/louthy/language-ext, https://www.nuget.org/packages/LanguageExt.Core/). It's mature and feature-rich; we've been using it in production for some time now.

@Richiban Hi Richard, thanks for the kind words :)

One comment I'd make on the usefulness of Option in the BCL is that when I created my first functional project (csharp-monad) which was just simple monadic types like Option Either and nothing else; I found it to be much less useful. Not totally useless, but I find the real power of Option (and similar ADTs) to really come to life when there's a framework that is bought into the idea. That's why my language-ext project is more than just the monadic types, it's trying to be a functional BCL.

For example gettting a value from a Map (dictionary):

    var result = map.Find(thing).Match(
          Some: x  => UseValue(x),
          None: () => DefaultValue
    );

So I think unless the BCL is going to retrofit Option everywhere that currently returns null then it's of limited use. Because some parts of the BCL will be 'lying' (return null), some will be telling the truth (have Option in their signatures). If no parts of the BCL uses the Option type, then it's probably best left to third party libraries like mine. My gut feeling is that the BCL is too big of a ship to turn here.

svick commented 7 years ago

@ElemarJR

None is not a "renamed null". There are very different use cases and semantic implications.

Your very first example suggests to use None instead of null. How does that make the use cases and semantic implications different? Could you explain what does that actually mean?

Today, there is no way to explicitly indicate that a function/method could return no value - so, we are forced to follow a defensive way every time.

Yes, but my point is that adding Option<T> doesn't really help. You add a way to indicate that None is a valid value, but you still have no way to indicate that null is not a valid value. So if you have a method that returns Employee, you still don't know if null is valid. And even if you change all your code, you still have to work with other code (including old code), which won't use Option.

And even if all code you use is changed, then a method that returns Employee can still return null, because of a bug, because there is nothing checking that it doesn't.

Exceptions are a very expensive way to indicate that something went wrong. Don't you think?

That depends. Sometimes exceptions are indeed inappropriate, which is why .Net also has the bool Try*(out T) pattern. I don't see how adding a third pattern to the mix helps much.

LuizAdolphs commented 7 years ago

I think this proposal is a real "nice-to-have", In the past years C# brought to us some really nice FP features (eg. whole LINQ,) which is a differential against other languages. C# is walking to a multi-paradigm approach (as stated in Developer on Fire ep. 224 with Mads Torgensen) and I guess those suggestions are welcome.

ElemarJR commented 7 years ago

@svick

Answering your questions here would be too long. I just wrote two blog posts about my point about exceptions and nulls.

fish-ter commented 7 years ago

I looked for Option / Maybe on the second day of trying out C#. It's a natural complement to the various LINQ features I'm now exploring.

What I'm sensing is that C# is incorporating useful functional programming features, but there might be a gap in the holistic understanding of FP.

With an Option, you can for example use it functor fashion, e.g. in pseudocode:

Option<Street> maybeStreet = maybePerson.Select(Address).Select(Street)

You don't need to check whether the maybePerson actually had Some person or None in there - you just chain away, and in the case of Option, the None is propagated. No person, address or street? You got no street.

Something is going on that's similar in some ways to internal iteration. With external iteration you have a loop, an index, and a collection to iterate over (by indexing) say. Internal iteration offers you a way to provide a function to apply to each element, and the mechanics of iteration are brought inside the type system. Here optionality is brought inside, and you don't need to be keeping track of whether you have something or not. These are examples of Functor and friends.

In Haskell, my ghci is reporting 10 typeclass instances for Maybe. There are lots of ways in which would you wish a computation to proceed when given a type that potentially contains no value. What about a basic equality check?: null.Equals("bad"). Meh, null simply causes your program to fall over; it's a programming dead-end.

With respect to the ongoing non-nullable proposal, I really like the idea of preventing nulls (by default!). But I don't see that the proposal will be giving null the capacity to act like a real type / value.

Either<L,R> can be used for error handing, but actually it is a canonical sum type (as in abstract data types). It's useful when something can be one thing or another, and you could use it to build all the other sum types.

svick commented 7 years ago

@fish-ter

With an Option, you can for example use it functor fashion, e.g. in pseudocode:

Option<Street> maybeStreet = maybePerson.Select(Address).Select(Street)

Real code would have to use lambdas, which would make it even more verbose and less readable. Also, to be consistent with other monads, it should be SelectMany, not Select. But with null, you can do:

Street? nullableStreet = nullablePerson?.Address?.Street;

Which I think is much better than even your pseudocode.

(Okay, you can't write Street? right now, since it's most likely not a value type, but hopefully you will in C# 8. Otherwise, that code already works.)

fish-ter commented 7 years ago

Hi @svick, SelectManylooks right, thanks.

And what you said about lambdas was interesting. I see now that an Address getter function would have to be defined outside of Person for a function reference to work. Know of any plans to add support for class methods and the like?

Java8 is quite flexible in that regard (though composing the plethora of function types in Java can be "interesting").

Your null safe code looks a sound improvement in practical terms, but it does leak the null concept, as per my "internal" vs "external" point. My pseudocode example needed to join Options (implicitly fixing it as the monadic type), but generally I'd prefer to operate via common abstractions like IEnumerable<T>.

The null safe code looked like adding Swifty terseness to the checks, and I'll include some info here:

 The Swift language defines the postfix ? as syntactic sugar for the named type Optional<Wrapped>, which is defined in the Swift standard library. In other words, the following two declarations are equivalent:

    var optionalInteger: Int?
    var optionalInteger: Optional<Int>

In both cases, the variable optionalInteger is declared to have the type of an optional integer. Note that no whitespace may appear between the type and the ?.

The type Optional<Wrapped> is an enumeration with two cases, none and some(Wrapped), which are used to represent values that may or may not be present. Any type can be explicitly declared to be (or implicitly converted to) an optional type. If you don’t provide an initial value when you declare an optional variable or property, its value automatically defaults to nil.

If an instance of an optional type contains a value, you can access that value using the postfix operator !, as shown below:

    optionalInteger = 42
    optionalInteger! // 42

Using the ! operator to unwrap an optional that has a value of nil results in a runtime error.

You can also use optional chaining and optional binding to conditionally perform an operation on an optional expression. If the value is nil, no operation is performed and therefore no runtime error is produced. 

What are your thoughts on that approach? I'm not aware of the discussions that led them to it, but I can imagine similar rumblings went on.

ElemarJR commented 7 years ago

@svick

Real code would have to use lambdas, which would make it even more verbose and less readable.

It depends on what coding style you follow. Right?

It is important to say that the improvements I am asking for are already available in F# - which is a great language. Anyway, I love C# and I would like to continue writing code using it.

svick commented 7 years ago

@fish-ter

I see now that an Address getter function would have to be defined outside of Person for a function reference to work. Know of any plans to add support for class methods and the like?

Yes. It would probably require using static and would be very unusual coding style for C#.

I'm not sure what you mean by "class methods", how would they be different from static methods?

svick commented 7 years ago

@ElemarJR

C# started as an imperative language. It's never going to be a functional-first language, like F#, and I don't think it makes sense to try to move it in that direction. Adding functional features to C# can be very useful, but it should be done carefully, to stay consistent with the rest of C#. If you want to program in a functional style, I think you should use F# instead of trying to twist C# to make it as functional as possible.

You apparently prefer the "imperative style" - with null, exceptions and side-effects.

null, exceptions and side-effects are how programming in C# is done, you can't get away from them. I don't think adding alternative systems (Option, Either) makes much sense, because you end up with a language that sometimes uses one way, sometimes the other, resulting in an inconsistent mess.

Instead, I think the existing systems should be improved, which is why I think non-nullable reference types are the way to go. I also wouldn't be opposed to well-designed checked exceptions (assuming such a thing is possible).

You are ok to write code writing sequences of statements. I prefer to have nested functions calls.

Sequence of statements is one of the most basic syntactic constructs in C#. Yes, I do think using them is okay. Even in functional languages, let is used very often, which makes you code look like a sequence of assignments.

You are ok using if to check if some argument is valid (not null, for example) - I prefer to use the type system instead.

You got this one wrong. I prefer to use the type system, when it makes sense. But I want the type system to actually solve the issue. Which is why I want non-nullable reference types and why I think Option is not a good solution. With Option, you're pretending the issue is solved, that null does not exist anymore, only None does.

But null still exists, a method that returns Person can still return null and you have to way to tell whether it does or not. Which is why I don't consider Option to be a good solution for C#.

ElemarJR commented 7 years ago

@svick

I think that the best attribute of a good programming language would be to allow to express ideas in a clean and elegant way.

Even with non-nullable types, I would need to communicate "None" sometimes - I just want to do that being safe and expressive. That is what Option/Maybe allows me to do.

Having side-effects is an API design choice - not a language imposition. I want to be able to create side-effects free APIs - I've been doing that for years with very good results - and I think it would be amazing to make it easier for everyone.

svick commented 7 years ago

@ElemarJR

Even with non-nullable types, I would need to communicate "None" sometimes

Why? If you had the choice of using non-nullable T and nullable T?, why would you choose Option<T> instead? What makes None different from null?

Having side-effects is an API design choice - not a language imposition. I want to be able to create side-effects free APIs - I've been doing that for years with very good results - and I think it would be amazing to make it easier for everyone.

While throwing exceptions is considered a side-effect, I think you can get most of the advantages of side-effect-free programming even with exceptions. For example, consider the Roslyn API, which avoids mutating state, but still throws exceptions.

fish-ter commented 7 years ago

@svick

Yes. It would probably require using static and would be very unusual coding style for C#.

I'm not sure what you mean by "class methods", how would they be different from static methods?

The following looks good to me:

new List<string> {"this", "works"}.ForEach(Console.WriteLine);

And it seems completely reasonable to wish for something like Select(Person.GimmeTheName), with GimmeTheName being a method defined in the class Person. But I'd like to drop that and focus on the Option issue.

@svick & @ElemarJR

What are your thoughts on the Swift approach? Why shouldn't nullable types become Option<T>s under potential nullability enhancements?

fish-ter commented 7 years ago

Why? If you had the choice of using non-nullable T and nullable T?, why would you choose Option instead? What makes None different from null?

Right, so nullable T would be Option<T>, with None being null. We could both get the kind of interface we desire, and interoperate.

DavidArno commented 7 years ago

@svick,

Why? If you had the choice of using non-nullable T and nullable T?, why would you choose Option instead? What makes None different from null?

none is an absence of a value; null is an uninitialised reference, thus has a value. none is non-zero and non-null because both of those are values. One can "hack" systems to behave like option monads, such that null (or 0) behave like none, but as the XXXOrDefault methods in linq show, this means overloading 0/null with two meanings: an absence of a value and the default value:

var x = (new List<int> {0}).FirstOrDefault();
var y = (new List()).FirstOrDefault();
// x == y; we have information loss

Whereas, if one uses Option<T>, then this becomes:

var x = (new List<int> {0}).TryDefault();
var y = (new List()).TryDefault();
// x is 0, y is none; information loss avoided.
DavidArno commented 7 years ago

@svick,

null, exceptions and side-effects are how programming in C# is done, you can't get away from them

Um, yes you can. I write C# code, yet those features live on the periphery for me.

If you want to program in a functional style, I think you should use F# instead of trying to twist C# to make it as functional as possible.

Why? Is this because you don't want to have to learn functional techniques, or is there a real reason for this? Why should I have to learn a new language just to use modern programming techniques? Why should Luddites stop my preferred language keeping up with my advancements in programming? C# already has plenty of functional features. I see no reason, to not add lots more.

But ultimately, this discussion is weird. It's on the corefx repo; not the C# one. Given that F# is a .NET language that has had to add its own Option<T> type (many years ago), beyond a lack of proper CLR support for discriminated unions, I see no reason whatsoever why Option<T> isn't baked into the core types. Talk of what C# devs want should be of no real consequence here.

louthy commented 7 years ago

@svick

Why? If you had the choice of using non-nullable T and nullable T?, why would you choose Option instead? What makes None different from null?

None has no contract. It doesn't honour anything. null of T claims to honour the contract of T, yet it's a lie. It's a massive hole in the type-system, and that's why null is considered the 'billion dollar mistake'. You can't dereference None, and therefore it's safer. You have to pattern-match to either extract a concrete value of T that will do what its contract says, or None which you can do nothing with. That's its value.

I still agree that it probably doesn't belong in the BCL. If Microsoft decided to develop a functional BCL (which I'd be all for, even if it would wipe out a few years of my work on language-ext), then it would definitely have a home there. I think the BCL in general is 'OO land', and it would be difficult to retrofit functional APIs onto it.

DavidArno commented 7 years ago

@louthy,

I still agree that it probably doesn't belong in the BCL. If Microsoft decided to develop a functional BCL (which I'd be all for, even if it would wipe out a few years of my work on language-ext), then it would definitely have a home there. I think the BCL in general is 'OO land', and it would be difficult to retrofit functional APIs onto it.

I don't agree here. I have put a lot of effort into Succinc<T>, only to recently discover that I'd duplicated a whole load of stuff that you'd already put into language-ext. And in both cases, we are duplicating work already done for F#. All of this duplication exists because core functional features, like Option<T> and option-returning TryXXX features aren't baked into the BCL.

It's really depressing to read comments from the likes of @karelz, that we must imagine we "have to convince 100 C# developers who don't have the functional programming background...". How about those 100 C# devs make the effort to learn about functional programming and then offer coherent arguments against a feature, to prevent its incorporation into the BCL? That is the way to aim the BCL at the future...

louthy commented 7 years ago

@DavidArno I do see your point, and mostly agree, but I don't really agree on the approach. I think the BCL being both a functional and OO repository is a recipe for disaster. One strength of the BCL (for all its OO-ness) is that it's consistent, there are pretty much never any surprises. The biggest surprises come from the functional types that are already in there (IEnumerable, IObservable, etc.)

If Microsoft created SystemF (pun intended) as a namespace for the functional world that'd be great, because then we could have functional IO, net, web, etc. Or Microsoft could sanction one of the functional frameworks. I was asked recently if I'd like language-ext to join the .NET Foundation, which I need to chase up at some point. Maybe that will create more traction.

I definitely don't have all the answers here, but I would be concerned about a bastardised BCL becoming less consistent and therefore a less effective core.

DavidArno commented 7 years ago

@louthy,

Maybe we are arguing at cross-purposes then, as SystemF... would serve nicely. Those old out p and XXXOrDefault methods could be left in place and a separate set of methods/functions could be created in new namespaces that behave in a more "modern" way. This would keep the two worlds separate, keeping both parties happy, but avoiding the need for "function C# folk" to re-invent the F# wheel.

ElemarJR commented 7 years ago

IEnumerable and the C# support for enumerations is one of the best features from dotnet.

I can't see any reason why BCL could not support more than a programming style.

Em ter, 18 de abr de 2017 às 18:54, Paul Louth notifications@github.com escreveu:

@DavidArno https://github.com/DavidArno I do see your point, and mostly agree, but I don't really agree on the approach. I think the BCL being both a functional and OO repository is a recipe for disaster. One strength of the BCL (for all its OO-ness) is that it's consistent, there are pretty much never any surprises. The biggest surprises come from the functional types that are already in there (IEnumerable, IObservable, etc.)

If Microsoft created SystemF (pun intended) as a namespace for the functional world that'd be great, because then we could have functional IO, net, web, etc. Or Microsoft could sanction one of the functional frameworks. I was asked recently if I'd like language-ext to join the .NET Foundation, which I need to chase up at some point. Maybe that will create more traction.

I definitely don't have all the answers here, but I would be concerned about a bastardised BCL becoming less consistent and therefore a less effective core.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/dotnet/corefx/issues/18159#issuecomment-294995307, or mute the thread https://github.com/notifications/unsubscribe-auth/AAGd8o_2Sd6L04vOcdn92v4V4HGbg6zrks5rxTEcgaJpZM4M45Es .

louthy commented 7 years ago

@ElemarJR I'm not suggesting that IEnumerable (or more precisely the LINQ extensions) aren't amazing. They are. But the interface is 'surprising' if all you know is the rest of the BCL style.

@DavidArno I guess I'm trying to be as pragmatic as possible, because it would be easy to say "But it's only an Option type", however if it's done badly it could backfire on any drive for a functional framework for C#. If the rest of the BCL methods that might return null aren't retrofitted to return Option then there's not much of a win for the BCL as a whole. If there's going to be a SystemF namespace, then I'd expect a total rethink of the way the BCL is written to be more functional in style (more static 'module' like types, more purity, more immutable structures, etc. for IO, Web, Net, Text, Environment, etc.); it could just be a layer over System (which I've actually considered doing myself, but would be an enormous task for one person).

Things I could imagine that go wrong (with Option):

I don't doubt that with the political will that all of these issues could be resolved. It does seem fraught with potential pitfalls, and that's just for one type.

svick commented 7 years ago

@DavidArno

this means overloading 0/null with two meanings: an absence of a value and the default value:

Yes, using 0 like this is problematic, because it's also a valid value of type int. But null does not have that problem, it's not a normal value now. And it won't be a possible value of a non-nullable reference type T in the future. I don't understand how your FirstOrDefault example is in any way relevant here, 0 is not null.

null, exceptions and side-effects are how programming in C# is done, you can't get away from them

Um, yes you can. I write C# code, yet those features live on the periphery for me.

You can try, but without a whole new standard library (and non-standard libraries), you still have to deal with them.

If you want to program in a functional style, I think you should use F# instead of trying to twist C# to make it as functional as possible.

Why? Is this because you don't want to have to learn functional techniques, or is there a real reason for this?

Have you tried programming in an object-oriented and imperative style in F#? (Yes, I know you don't like OOP, this is just for the sake of comparison.) It is possible, but the language does not make it easy. It's because OOP is a secondary concern in F#, you're supposed to use functional approach, and mostly use OOP only for interop.

It's similar with C#, if you try to add all functional features to it, they're never going to be as nice as in a dedicated functional language. That's why I think if you want to program in a functional style, you should use a functional language. And if you hate object-oriented programming, maybe avoid languages that have OOP as their primary paradigm.

I am all for learning functional techniques and applying them where appropriate. But I prefer not to use them all the time, which is why my primary language is C# and not F#.

Why should I have to learn a new language just to use modern programming techniques?

Because what you consider "modern programming techniques" does not align with what I think most C# programmers consider to be "modern programming techniques".

Why do you think I should learn functional techniques, but you shouldn't have to learn F#, if you want to program in a style that is different from what C# programmers like?

Why should Luddites stop my preferred language keeping up with my advancements in programming?

I never said C# should stop advancing. But it seems what you consider to be "advancements in programming" means "ignore everything related to OOP, copy everything functional languages do". That is not a direction I think makes sense.

C# already has plenty of functional features. I see no reason, to not add lots more.

When it makes sense, sure. But it should not be done blindly, ignoring what C# already has. And C# already has a way of representing absence of value, there is no reason to add a new one with a different name, just because functional languages do it that way.

Talk of what C# devs want should be of no real consequence here.

C# is used by the vast majority of .Net developers, so of course what they want matters, even though it's not the only thing that matters.

F# differs a lot in what it wants from a standard library from other .Net languages, which is why it has its own (to a certain degree). So, moving Option<T> to corefx would need a better reason than just "it's in the F# standard library".

svick commented 7 years ago

@louthy

None has no contract. It doesn't honour anything. null of T claims to honour the contract of T, yet it's a lie. It's a massive hole in the type-system, and that's why null is considered the 'billion dollar mistake'. You can't dereference None, and therefore it's safer. You have to pattern-match to either extract a concrete value of T that will do what its contract says, or None which you can do nothing with. That's it's value.

Except for the specifics of pattern matching, don't non-nullable reference types solve all those problems?

ElemarJR commented 7 years ago

@svick

Are you happy with NullReferenceExceptions? Did you understand that Option<T> could represent the end of this nightmare?

svick commented 7 years ago

@ElemarJR

How is Option<T> going to change all the existing code and libraries? On the other hand, all of those can be updated with non-nullable reference types, ending the nightmare of NullReferenceExceptions in a much more friendly way.

Also, like I said many times before, Option<T> on its own does not prevent NullReferenceExceptions, since null is still valid for T, it just adds a convention that null shouldn't be used. I don't think that's good enough.

BTW, Java now has Option<T>, did that solve NullPointerExceptions?

So, what I understand: Option<T> would not end the nightmare, non-nullable reference types probably will.

ElemarJR commented 7 years ago

@svick

We could have Some(null) but it would be strange.

I've been using Option for years and creating adapters when implementing my APIs using non-functional libraries. I have no more NullReferenceException occurrences.

That's my experience. I am not talking about features that I don't have today. :)

MovGP0 commented 7 years ago

@Patrikkk has a very good library called FunctionalSharp that implements Maybe-Monads (Option Types), Discriminated Unions and Pattern Matching. I use it in most of my projects because it is very useful and you might want to check it out before coming up with a new implementation.

DavidArno commented 7 years ago

@svick,

So, what I understand: Option<T> would not end the nightmare, non-nullable reference types probably will.

Seriously? Have you seen how often folk use T? to enable their structs to be null? Whilst better coders will enjoy the benefits non-nullable ref types, the vast majority of devs will just spread even more T? throughout their code...

DavidArno commented 7 years ago

@svick,

Yes, using 0 like this is problematic, because it's also a valid value of type int. But null does not have that problem, it's not a normal value now. And it won't be a possible value of a non-nullable reference type T in the future. I don't understand how your FirstOrDefault example is in any way relevant here, 0 is not null.

Because, with non-nullable ref types, FirstOrDefault won't work ... because it returns null. So the return type will be T?. Use an Option<T> and T can remain null-free.

Um, yes you can. I write C# code, yet those features live on the periphery for me.

You can try, but without a whole new standard library (and non-standard libraries), you still have to deal with them.

Very rarely in my experience and wrapping robust, null-free code around those weak points isn't that hard.

Have you tried programming in an object-oriented and imperative style in F#? (Yes, I know you don't like OOP, this is just for the sake of comparison.) It is possible, but the language does not make it easy.

I disagree. The syntax is as weird as anything else in F# (for folk like me who have spent too many decades using C-style languages) and the explicit interface-only rule catches me out. Aside from that, it's as usable for OOP as C#.

It's similar with C#, if you try to add all functional features to it, they're never going to be as nice as in a dedicated functional language.

Only time will tell. Of course, only by adding such features will time be able to tell. Since F# isn't a dedicated functional language (it's a multiple paradigm language with strong FP features), we could use it as a model for judging C#'s functional success.

Why should I have to learn a new language just to use modern programming techniques?

Because what you consider "modern programming techniques" does not align with what I think most C# programmers consider to be "modern programming techniques".

Since when did mob rule govern programming language evolution?

I never said C# should stop advancing. But it seems what you consider to be "advancements in programming" means "ignore everything related to OOP, copy everything functional languages do". That is not a direction I think makes sense.

C# is pretty much done as far as OO is concerned (save for copying Java's "default methods in interfaces"). And obviously, "advancement" means moving forward. Implementing failed ideas isn't advancement.

When it makes sense, sure. But it should not be done blindly, ignoring what C# already has. And C# already has a way of representing absence of value, there is no reason to add a new one with a different name, just because functional languages do it that way.

You are right "just because functional languages do it that way" would not be a reason. "Do it for the same reasons that functional languages do it that way" is a very sensible reason for doing something though.

C# is used by the vast majority of .Net developers, so of course what they want matters, even though it's not the only thing that matters.

Nope. What they will need, when they learn new techniques and advance their skills, is what matters. Language advancements lead developers into better practices. What they are asking for is often not what they need though. Just giving developers what they asked for is selling them short. Language development is no different to any other software project in that regard.

F# differs a lot in what it wants from a standard library from other .Net languages, which is why it has its own (to a certain degree). So, moving Option to corefx would need a better reason than just "it's in the F# standard library".

"It's not in the corefx library" is all the reason needed as it's of little to use to me, as someone who mainly uses C#, being tucked away in an F# library.

Joe4evr commented 7 years ago

the vast majority of devs will just spread even more T? throughout their code...

Well, joke's on them, because if they do the compiler will warn those developers every single time they want to dereference them without a check and they'll only have themselves to blame.

Also, I seriously doubt that devs that would use T? so irresponsibly are in the top percent of NuGet packages used, so the likelyhood of pulling in an API designed like that is almost non-existent.

pgolebiowski commented 7 years ago
MgSam commented 7 years ago

Also agree with @svick.

I've used Deedle, a data frame library, extensively in C#. It depends on FSharp.Core and thus relies heavily on an Option<T> type.

The Option<T> type is unbelievable annoying to work with. While in C#, we have always had nullable value types (ex. int?), Deedle tries to express a lack of value using the F# Option type. For doubles, this leads to essentially three different, incompatible ways that lack of a value is expressed- null, NaN, and !Option.HasValue.

Adding Option into an existing ecosystem which already supports null means tons of extra boilerplate to check for both. It means more bugs, not less. It means more developer confusion, not less. It is a bad idea.

DavidArno commented 7 years ago

For doubles, this leads to essentially three different, incompatible ways that lack of a value is expressed- null, NaN, and !Option.HasValue.

Only three? You are forgetting the bool Foo(out double value) pattern, and having (bool valid, double value) tuples as ways of expressing whether the double has a value or not. So that's at least five...

Deedle tries to express a lack of value using the F# Option type

Since Deedle is an F# library it clearly succeeds in using options. The trouble comes when trying to use them with C#. As the latter doesn't yet have discriminated unions and has very poor pattern matching support, union types, like Option<T>, can be awkward to use in that language.

ankitbko commented 6 years ago

I am also looking forward to having Maybe monad in C#. I just want to do away with the nulls. I feel like half of the time I write code I spend worrying about null. The damn thing jut pops out of nowhere.

Anyway I found this blog post also explaining Maybe monad uses quite nicely. The ability to compose functions are really nice.

PS: BTW I just spent an hour trying to figure out where did null cropped in when I was doing some huge xml parsing. So apologies for rambling here.

karelz commented 6 years ago

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