dotnet / runtime

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

RFC: Add a Unit type to the System namespace #33083

Open richbryant opened 4 years ago

richbryant commented 4 years ago

Abstract

Units are proliferating in the NetCore ecosystem. Nobody can deny their utility (since you can't return a void) but this does introduce the issue of incompatible structs all called Unit provided by different packages.

Proposal

Implement a System.Unit struct, taking on board the needs of the major libraries providing Units at the moment.

Benefits

Example

This file is an example of integrating System.Reactive.Unit (paging @ghuntley @onovotny ) with Mediatr.Unit (credit @jbogard) and language-ext.Unit (credit @louthy ).

This covers 90% of the Use Cases for Unit. If this were implemented in the System namespace with other Types, it would solve a lot of issues and prevent a lot of future issues .

masonwheeler commented 4 years ago

Nobody can deny their utility (since you can't return a void)

I deny their utility. Having a data type that contains no data does nothing useful; all it does is cause confusion. We have void methods for a reason.

glennawatson commented 4 years ago

Often not used with methods though. Often used as part of lambda based constructs. Essentially tends to be used with more c# functional libraries.

mrpmorris commented 4 years ago

I like this, but would prefer to be able to pass void as a type, because Unit is a silly name :)

masonwheeler commented 4 years ago

This is a bad idea that comes up over and over again every few months, generally by people who appear to be unaware of the concept of minus 100 points, because they never provide any explanation as to why Unit is useful; always simply taking it as an article of faith, with no proof offered or required.

The distinction between methods that have a return type and methods that don't is a useful one. Blurring that line would be harmful. For example, LINQ methods are written, by design, in a pure-functional style, taking Funcs as lambda parameters that look at data but don't touch it. A Unit type would make it easy for people who don't know what they're doing to pass a lambda with side effects to Select and end up breaking a bunch of implicit assumptions that everyone who uses LINQ relies on.

richbryant commented 4 years ago

Unit types are already out there and are proliferating. That's a headache for everyone. If you don't like System.Unit you can always not use it. I'd recommend it was abstract if it weren't a struct but it is - for obvious reasons - so here we are.

masonwheeler commented 4 years ago

That's a headache for everyone.

Yes, it really is! Especially those of us who are aware of its harmful nature!

If you don't like System.Unit you can always not use it.

No, not really. Because if it exists in a standardized form in the BCL -- rather than its current status as some niche thing that only a few FP crazies on the fringe of the ecosystem care about and they can't even settle on a single implementation -- then it's going to get used, and sooner or later I'll end up in a situation where I have to deal with it because some code I need that was written by someone else has Units everywhere.

I recently had exactly this happen to me with dynamic, and I'd really prefer not to have to go through it again with yet another bad idea.

glennawatson commented 4 years ago

It's a struct to avoid allocations.

Frameworks often use it when they don't care about the value anymore in a chain of methods.

For example in reactive it means you may have processed the data in the chain and now you just care about something has happened. Without having a common struct for providing this you wouldn't be able to combine together chains of reactive events together.

masonwheeler commented 4 years ago

@glennawatson That seems really short-sighted. Even if you don't care about the data you're processing anymore, how do you know that whoever's next in the chain won't?

glennawatson commented 4 years ago

Well. It's not necessarily data in the reactive world you care about.

It's about something has happened.

Eg a mouse click. You don't want your view model knowing about a mouse event Arg. You only care that something is invoking my logic and my command should run. You can then change it so you respond to another thing happen eg a key down and you don't have to change your code base because all the events are telling you something has happened (eg unit)

masonwheeler commented 4 years ago

Then why does all this data on MouseEventArgs exist in the first place? This sounds like an argument made by someone unfamiliar with Chesterton's Fence. Just because you don't think it's useful right this moment doesn't mean it should be thrown away.

chrisgate commented 4 years ago

I think this will be cool if it can come on board in System namespace

glennawatson commented 4 years ago

MouseEventArgs is useful in the context of the view in my example. Maybe you want to have it only respond when there is a mouse event between certain mouse coordinates. You can do a Where() statement restricting that.

In the view model how that happens is not a concern for it.

Now you can have your view model running on xamarin forms and wpf. A lot of users do.

glennawatson commented 4 years ago

@masonwheeler by your reasoning we should he passing the MouseEventArgs everywhere and never stop using it.

grokys commented 4 years ago

its current status as some niche thing that only a few FP crazies on the fringe of the ecosystem care about

I don't think this is true and calling us "fringe crazies" is unnecessarily insulting.

As for it being niche:

In Haskell and Rust, the unit type is called () and its only value is also (), reflecting the 0-tuple interpretation. In ML descendants (including OCaml, Standard ML, and F#), the type is called unit but the value is written as (). In Scala, the unit type is called Unit and its only value is written as (). In Common Lisp the type named NULL is a unit type which has one value, namely the symbol NIL. This should not be confused with the NIL type, which is the bottom type. In Python, there is a type called NoneType which allows the single value of None. In Swift, the unit type is called Void or () and its only value is also (), reflecting the 0-tuple interpretation. In Go, the unit type is written struct{} and its value is struct{}{}. In PHP, the unit type is called null, which only value is NULL itself. In JavaScript, both null and undefined are built-in unit types. in Kotlin, Unit is a singleton with only one value: the Unit object. In Ruby, nil is the only instance of the NilClass class. In C++, the std::monostate unit type was added in C++17.

Basically every language in common use except C# and Java (F# also has one).

A Unit type would make it easy for people who don't know what they're doing to pass a lambda with side effects to Select and end up breaking a bunch of implicit assumptions that everyone who uses LINQ relies on.

How does not having a Unit type prevent passing a lambda with side-effects?

@masonwheeler I'd suggest using System.Reactive for a while and seeing whether you have a need for a Unit class. Spoiler: you will.

richbryant commented 4 years ago

How does not having a Unit type prevent passing a lambda with side-effects?

It doesn't.

var newList = list.Select(x => { item = x; return x.ToUpper(); });

david-driscoll commented 4 years ago

:trollface: Try to keep it civil, he is just trying to stir the pot by trolling the thread. :trollface:

I like and support the idea as a user of both System.Reactive and MediatR it is frustrating having to interop between the two. I kinda wish it was added with netstandard2.1 because this just means more change.

This feels very similar to the ML.NET types (Microsoft.ML.*) and a possible solution that might work would be a single "blessed" nupkg that contains System.Unit that anyone can reference. Then perhaps it can make it into netstandard at a later date like System.ValueTuple did a few years ago.

RLittlesII commented 4 years ago

That seems really short-sighted. Even if you don't care about the data you're processing anymore, how do you know that whoever's next in the chain won't?

You can compose your observable pipeline and split it before the call to transform to Unit. That way anyone who wants to process the value can, and whoever just needs the notification gets the notification. Problem solved.

masonwheeler commented 4 years ago

How does not having a Unit type prevent passing a lambda with side-effects?

It doesn't, but it does discourage it. Taking that discouragement away would not be an improvement.

jspuij commented 4 years ago

A Unit type would make it easy for people who don't know what they're doing to pass a lambda with side effects to Select and end up breaking a bunch of implicit assumptions that everyone who uses LINQ relies on.

It is easy already, just start an anonymous method that has side effects. The return type does not change anything. People write ForEach extensions methods themselves and for some reason the List<T> type has exactly such a method.

masonwheeler commented 4 years ago

@jspuij Yes, and the one on List<T> takes an Action. There's a nice, clear semantic distinction there as part of the type system so you can tell at a glance exactly how it's expected to work. No Unit needed.

richbryant commented 4 years ago

Yes, and the one on List takes an Action.

but since an Action can have just as many side-effects as a Func<T, Unit>, I do not see the relevance to this issue of the existing proliferation of Units.

david-driscoll commented 4 years ago

The key advantage that Unit brings is that you don't have to code around the edge case of void returning methods. It aids in composition of methods and even compositions for the internals of your application.

MediatR added unit to get around having to have implement two different pipelines IRequest (Task) and IRequest<T> (Task`). Before having unit if you wanted to add a behavior (a kind of middleware) you had to implement the behavior twice. once for void returning and once for value returning cases.

System.Reactive uses the unit type for composition. You want to get a notification for mouse drag, and only mouse drags? Rx makes this trivial, you get a unit notification for Mousedown and Mouseup and then compute the x/y for each Mousemove in between. To compose Mousedown/Mouseup you have to be able to "transform" them into something, Unit simplifies this because all you care about is the notification, not the value. Unit also allows decoupling here. You want your business logic to be as environment agnostic as possible. Therefore your BL lib cannot take a dependency on MouseEventArgs. This is what @glennawatson is referring to, your ViewModel is agnostic to the display technology so it shouldn't ever see MouseEventArgs and there is no reason to capture the data if it is not required.

jspuij commented 4 years ago

Then perhaps it can make it into netstandard at a later date like System.ValueTuple did a few years ago.

Come to think of it, in most functional languages Unit is equivalent to the 0-element tuple. MAybe we are just after the System.ValueTuple struct after all.

Edit: I was not the only one who realized this: https://github.com/dotnet/csharplang/issues/1279

mrpmorris commented 4 years ago

@masonwheeler here is an example of when Unit is useful - and it's not a functional programming fringe https://github.com/jbogard/MediatR/blob/c1ad66ef52434a22c10a0de5e060d13b185ef80b/src/MediatR/IRequestHandler.cs#L28

Could you provide an example of how having Unit is harmful, and can allow people to accidentally "pass a lambda with side effects"?

Thanks

glennawatson commented 4 years ago

@jspuij interesting point. Wonder if that could use like a () like syntax in that case also like a value tuple without arguments or if that has the potential to confuse users.

jspuij commented 4 years ago

@jspuij interesting point. Wonder if that could use like a () like syntax in that case also like a value tuple without arguments or if that has the potential to confuse users.

It is already used to signal an anonymous method without arguments... Would not be very difficult to get used to.

jbogard commented 4 years ago

Just an FYI, this has come up a lot in various C# language repos. Here's a recent one, to support zero-index value tuples:

https://github.com/dotnet/csharplang/issues/883

And unit-as-valuetuple:

https://github.com/dotnet/csharplang/issues/1279

etc.

I looked for something "official" but my options were pretty limited, and still are. I don't really want to use System.Reactive.Unit or the F# version, but here we are.

jbogard commented 4 years ago

Oh and this one, which is a much longer discussion of first-class support for an actual unit type:

https://github.com/dotnet/csharplang/issues/1604

jspuij commented 4 years ago

That seems as a pretty well thought out proposal. Upvoting it too.

StefanBertels commented 4 years ago

Please make this happen. A built-in Unit will really make life easier when using and writing generic code.

bartonjs commented 4 years ago

For what it's worth, absent following links to Wikipedia, my assumption was that a type named Unit is either a strongly-typed string ("Kilometer", "Mile", "Gram", etc) or a combination of a value and a string/strongly-typed-string (return new Unit(5280, "Foot")).

It may be the term of art in functional programming, but it's probably something that would need a different name to fit in with our mostly-procedural libraries.

jbogard commented 4 years ago

@bartonjs it's not functional-language-specific, but type-theory-specific. It just so happens that C# picked a different name to match C-based languages, with different semantics and now we have this "fun" Func/Action dichotomy.

bartonjs commented 4 years ago

Okay, so maybe I have my domain names wrong. But I just wanted to point out that I thought of unit in the sense of a unit of measure, and was surprised to find out that it meant something else. So I'm suggesting that the name is likely going to be a hindrance.

Aside from the semi-obvious void (which has language restrictions), I'd expect None or NoValue to be a better name for this concept in the System namespace than Unit.

tannergooding commented 4 years ago

Unit Type is a fairly common name, across many languages (and even outside computer programming): https://en.wikipedia.org/wiki/Unit_type

I'd so it is no more hindering than Rune

DavidArno commented 4 years ago

Aside from the semi-obvious void (which has language restrictions), I'd expect None or NoValue to be a better name for this concept in the System namespace than Unit.

None also has a common pre-existing use with option/maybe types. They either have Some(value) or None (no value). With discriminated unions (DUs) potentially being added to C# 9, Unit won't be the only type people will be asking to be added to the System namespace. If Option/Maybe, Either and other common DUs aren't added, we'll end up with a proliferation of incompatible versions of those types too across a number of libraries.

mrpmorris commented 4 years ago

If Option/Maybe, Either and other common DUs aren't added, we'll end up with a proliferation of incompatible versions of those types too across a number of libraries.

That's no reason to not consider Unit on its own. Option/Either/etc are specifically used in functional programming, Unit is a concept used across various libraries.

StefanBertels commented 4 years ago

@DavidArno While I really like types like Option, Either and other discriminated unions. I think we shouldn't extend this RFC to those. It will be a much longer discussion how to get them right. IMO LanguageExt is a good start for them, too -- Paul Louth did great work to get a very good solution there.

Unit alone is already very useful because of the problems with void not being a Type (Generics, Action/Func).

Regarding naming I favor Unit. It's already used in libraries, and it's a proper name for the concept as @tannergooding mentioned. I don't see a big conflict with "units of measurement".

mrpmorris commented 4 years ago

Okay, so maybe I have my domain names wrong. But I just wanted to point out that I thought of unit in the sense of a unit of measure, and was surprised to find out that it meant something else. So I'm suggesting that the name is likely going to be a hindrance.

Aside from the semi-obvious void (which has language restrictions), I'd expect None or NoValue to be a better name for this concept in the System namespace than Unit.

Unit isn't a value, it's a type, so NoValue wouldn't make sense. NoType would make more sense, but that looks a bit Visual Basic :(

DavidArno commented 4 years ago

@StefanBertels, that is a fair point. I'll say no more on DUs here. 👍

danmoseley commented 4 years ago

The right place for such a proposal is dotnet/runtime. The dotnet/standard repo does not create API - it defines a group of API and infrastructure around it.

davidfowl commented 2 years ago

FWIW we have versions of this in the runtime itself

TeddyAlbina commented 2 years ago

@DavidArno While I really like types like Option, Either and other discriminated unions. I think we shouldn't extend this RFC to those. It will be a much longer discussion how to get them right. IMO LanguageExt is a good start for them, too -- Paul Louth did great work to get a very good solution there.

Unit alone is already very useful because of the problems with void not being a Type (Generics, Action/Func).

Regarding naming I favor Unit. It's already used in libraries, and it's a proper name for the concept as @tannergooding mentioned. I don't see a big conflict with "units of measurement".

void is System.Void struct in dotnet

RenderMichael commented 1 year ago

Another point of contention I've ran into and see others run into is void-returning switch expressions. There are in fact proposals seeking to address this very thing: https://github.com/dotnet/csharplang/issues/3038.

It seems to me void already exists, is an empty struct, and is highly proliferated throughout the ecosystem. If the C# language allows void as a generic type argument, expressions like default(void) sizeof(void) etc, probably some interface implementations, and undoubtedly a few other things, this feature could be widely adopted rather than yet another thing devs have to think about when the latest .NET version comes out.

The posts on this thread from years ago suggest adding this to .NET standard. That ship has probably already sailed. That said, using void as the Unit type would prevent the NuGet package approach, and could only be consumed by the latest .NET version (whichever this would be released in). I think the tradeoff is worth it, but it's worth taking into consideration.