Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
richbryant opened this issue Jan 13, 2020 · 43 comments
Open

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

richbryant opened this issue Jan 13, 2020 · 43 comments
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-System.Runtime
Milestone

Comments

@richbryant
Copy link

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

  • Provide a Type that can be used interchangeably between the major libraries
  • Provide a Unit for future libraries, preventing yet more proliferation
  • Make the lives of everyone who has to work these so much better

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link

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

@masonwheeler
Copy link
Contributor

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
Copy link
Author

richbryant commented Jan 13, 2020

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
Copy link
Contributor

masonwheeler commented Jan 13, 2020

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
Copy link
Contributor

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
Copy link
Contributor

@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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link

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

@glennawatson
Copy link
Contributor

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
Copy link
Contributor

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

@grokys
Copy link

grokys commented Jan 13, 2020

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:

  • This already exists and is used heavily in System.Reactive and other libraries
  • The BCL has had to implement two different delegate types: Action<> and Func<> in order to work around this. If there was a unit type Action<int> would be Func<int, Unit>
  • Other languages that have a unit type (from https://en.wikipedia.org/wiki/Unit_type):

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
Copy link
Author

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
Copy link

: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
Copy link

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
Copy link
Contributor

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
Copy link

jspuij commented Jan 13, 2020

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
Copy link
Contributor

@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
Copy link
Author

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
Copy link

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
Copy link

jspuij commented Jan 13, 2020

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:
dotnet/csharplang#1279

@mrpmorris
Copy link

@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
Copy link
Contributor

@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
Copy link

jspuij commented Jan 13, 2020

@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
Copy link
Contributor

jbogard commented Jan 13, 2020

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:

dotnet/csharplang#883

And unit-as-valuetuple:

dotnet/csharplang#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
Copy link
Contributor

jbogard commented Jan 13, 2020

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

dotnet/csharplang#1604

@jspuij
Copy link

jspuij commented Jan 13, 2020

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

@StefanBertels
Copy link

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

@bartonjs
Copy link
Member

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
Copy link
Contributor

jbogard commented Jan 15, 2020

@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
Copy link
Member

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
Copy link
Member

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
Copy link

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
Copy link

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
Copy link

@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
Copy link

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
Copy link

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

@danmoseley
Copy link
Member

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.

@danmoseley danmoseley transferred this issue from dotnet/standard Mar 2, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Mar 2, 2020
@joperezr joperezr added api-needs-work API needs work before it is approved, it is NOT ready for implementation api-suggestion Early API idea and discussion, it is NOT ready for implementation and removed untriaged New issue has not been triaged by the area owner labels Jul 7, 2020
@joperezr joperezr added this to the Future milestone Jul 7, 2020
@terrajobst terrajobst removed the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Jun 25, 2021
@davidfowl
Copy link
Member

davidfowl commented Jul 12, 2022

FWIW we have versions of this in the runtime itself

@TeddyAlbina
Copy link

@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
Copy link
Contributor

RenderMichael commented Oct 27, 2023

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: dotnet/csharplang#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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-System.Runtime
Projects
None yet
Development

No branches or pull requests