-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
C# Design Notes for Sep 8, 2015 #5383
Comments
"Withers for arbitrary types" sounds really cool. I'm not sure how it would work, though. Would this even feature be able to save Roslyn any code? My understanding is that under the hood the mutable green trees get mutated. I'm just not sure how you could do all this automatically, but maybe I'm misunderstanding the feature. |
For I'm a fan of "withers" being a new I think that copying Go doesn't make a lot of sense. They needed |
One potential syntax for a "wither" could be similar to the object initialization syntax:
See this webpage: https://msdn.microsoft.com/en-us/library/bb384062.aspx So, perhaps we could do:
instead of
|
+1, hope this is doable. |
You guys mean cat =cat { Age = 1, Name = "FizzBuzz" }; Right? The proposal is for immutable objects, so you'll need to capture that result somehow. |
I don't like the use of "private protected" as a name solely because you don't intend for it to be "private" but "internal" - is there some symbol that could be used as an operator? I'm only reading this out of interest and I don't know the ins and outs of language design so forgive me, but something like protected &internal - although I guess there's a memory addressing look to it, or protected +internal (the former makes more sense because you could do protected |internal to cover existing, and be explicit). |
With regards to Withers - could you see any value in having: I would assume it would throw if the properties didn't already exist, otherwise you'd need to: Animal ifStatement; Cat newIfStatement = ifStatement as Cat replaces { Condition newCondition, Statement = newStatement }; |
Correction: I came up with a better syntax: |
@Hosch250 The point of "withers" is specifically to "copy" the object to a new instance with specific changes. Using it to affect an existing instance doesn't make a lot of sense. var cat1 = new Cat { Name = "Fluffy", Gender = Gender.Female, Age = 6 };
var cat2 = cat1 with { Name = "Whumpus" };
Debug.Assert(cat1.Gender == cat2.Gender);
Debug.Assert(cat1.Age == cat2.Age); If a custom "wither" was implemented as an instance method then it would certainly be possible to affect the current instance. Although I am a proponent of going the operator route as being completely new syntax would give the compiler some additional control over how they are used, e.g. the compiler could fail to compile a class with a custom @paulomorgado You and everyone else, which was my point. Can't please anyone. |
@paulomorgado or something like |
Regarding A brand new keyword has the distinct benefit that on first sight all naïve developers would be equally uncertain of its meaning. Using a modifier like "and", "or", or "intersection" in the keyword ( |
@HaloFour @JoshVarty Yes, I meant to re-assign, not mutate. I have fixed this. |
+1 on the attribute stuff. Having compile-time attributes that worked on the source code would start to get down some very cool metaprogramming avenues. (And generic attributes, please! :)) |
+1 to |
Oh God, please stop. Not this argument again. C# team- you guys decide what to call it. I don't care if you call it I'm looking forward to seeing a more fleshed out design for the async streams. This will be a great area for people to play around with a prototype and compare it to using Rx, vanilla async. |
if async foreach is pattern based why not make the result of IAsyncEnumerator.MoveNextAsync() pattern based too? Then it could be smth more efficient than Task from allocation point of view. Just anything having instance or extension GetAwaiter method. |
private protected?? |
I dislike |
Seriously, that horse was beaten beyond recognition. Giant threads, surveys, flames galore. The consensus was that there is no consensus. There are more opinions than participants. It's not a useful dialog. For a feature that might be used a handful of times by a small minority of the development community the team should just pull the trigger (and go with their gut regarding the keyword(s)) or pull the plug. |
How about 'assembly protected' for the new visibility scope? |
@chaosrealm Then what would |
Did you consider TPL Dataflows's IPropagatorBlock for this? |
A crazy idea: if you have several asynchronous operations, it often makes sense to let them execute concurrently. To make this easier, async foreach could be an expression that returns a var task1 = async foreach (string s in asyncStream1)
{
// loop body
};
var task2 = async foreach (int i in asyncStream2)
{
// loop body
};
await Task.WhenAny(task1, task2); One reason why this is crazy is that it would probably be hard to make the statement form and the expression form work well at the same time. (Are they distinct? Very confusing. Are they the same? That would mean you would have to write something like |
What about using the existing async IObservable<int> F() {
foreach(var item in list)
yield return await item.MethodAsync();
} Then foreach(var item in observable) { } Making it an expression (#5402) might be useful (in @svick's example): var task1 = foreach (string s in asyncStream1) =>
{
// loop body
};
var task2 = foreach (int i in asyncStream2) =>
{
// loop body
};
await Task.WhenAny(task1, task2); I assumed that the |
private protected: just do it! It is rarely used anyway but I've missed it on more than one occasion... |
@alrz Check out the comments in #261 re async streams and @axel-habermaier Curious as to what scenarios you would've found |
@HaloFour |
@HaloFour My point is, the
so does
I think that this syntax for supporting IEnumerable<int> F() { /* contains yield return */ }
async IObservable<int> G() { /* contains yield return & await */ } In fact, the latter returns a |
@alrz I'd agree with you. A public async IObservable<int> G() {
for (int i = 0; i < 10; i++) {
await Task.Delay(1000);
yield return i;
}
} The big difference between |
@apskim How would that same functionality not be achieved through If I'm missing something more novel I'd be interested in seeing a gist containing an example. |
It doesn't matter, you can think of it as a superior abstraction, it represents one or more value, that is. In Rx you can await an
If it does so what's the point, you can return an |
@alrz It matters a lot if you're trying to generate or consume more than a scalar value. 😄 And yeah, Anywho these comments probably belong over at #261 since that is the proposal for async streams. |
Exactly, this is an extra layer of protection for yourself and your teammates, nothing more. My general policy is that any coding guidelines you have should be expressed in the code whenever possible, not in the docs or any other form of tribal knowledge. We don't treat our team developers any differently from other developers in that regard: a compiler is your best enforcer. Besides, |
@HaloFour: I don't recall the specific circumstances anymore. But at one point I had the need to add a So this is the scenario where So yes, |
Personally I don´t like the "with" syntax, I would prefer the following syntax to see, that it is a cloned/duplicated instance by a keyword like "dupl" or "clone". var cat = new Cat { Age = 10, Name = "Fluffy" }; |
@JohnnyBravo75 It's not a clone, it's actually a new object with following properties' values. The syntax you mentioned would imply cloning and re-initializing e.g.
based on #5676, which translates to
On the other hand, proposed syntax is more natural, implying a new |
@alrz |
@JohnnyBravo75 These comments belong to #5172, though. Check out the example to see what happens under the hood. |
Nested withers get ugly fast
A better notation for immutable update
It is similar to the mutable version in structure
and it would also be neat if you could curry it.
But what about updating the property of an immutable object in an immutable list
That's not pretty but if you can imagine composing "Withers" then you can imagine
or shorter
the first part builds the "wither"
then you can use the "wither" as a factory
|
Regarding
|
@eyalsk The problem with a new keyword is that none of these keywords are required to be in any specific order.
|
@HaloFour I get what you're saying and I you're right but I tend to read internal as adjective as opposed to noun so in my mind On second thought maybe |
You might want to take a look at this AsyncSeq library in F# - http://fsprojects.github.io/FSharp.Control.AsyncSeq/. This is done in library code through F# computation expressions (no need to change the language :) ) Cancellation is fully supported and you barely need to think about it in the implementation or user model. F# async propagates cancellation tokens behind the scenes (the C# version of the F# async programming feature made cancellation tokens more explicit when they did their version of the feature, for reasons I never fully understand except that it had something to do with the WiinRT API, see http://tomasp.net/blog/csharp-async-gotchas.aspx/ and http://research.microsoft.com/apps/pubs/default.aspx?id=147194) |
+1 for allowing attributes in more places. It'd be really cool if analyzers could leave "tips" for the compiler in places, such as mark something as definitely not-null, or being pure, or one of a hundred other invariants that the compiler doesn't take the time to try and prove. Then it can let the compiler (or maybe another optimizer) decide whether to optimize based on that. Ideally the attributes could even be allowed in the IL. Sure it'd increase IL bloat, but then it would be possible for the compiler or analyzers to leave tips for the IL (like |
@MadsTorgersen Any newer design meeting notes incoming? It looks like you guys are considering shipping stuff in a minor version? If so, which features are a part of that? |
Design notes have been archived at https://github.com/dotnet/roslyn/blob/future/docs/designNotes/2015-09-08%20C%23%20Design%20Meeting.md but discussion can continue here. |
Re pattern matching and control - especially over streams - the constructs "Join patterns" and "Joinads" are related https://en.wikipedia.org/wiki/Join-pattern and http://tomasp.net/academic/papers/joinads/ |
C# Design Notes for Sep 8, 2015
Agenda
Check-in on features
Bestest betterness
Overload resolution will sometimes pick a candidate method only to lead to an error later on. For instance, it may pick an instance method where only a static one is allowed, a generic method where the constraints aren't satisfied or an overload the relies on a method group conversion that will ultimately fail.
The reason for this behavior is usually to keep the rules simple, and to avoid accidentally picking another overload than what you meant. However, it also leads to quite a bit of frustration. It might be time to sharpen the rules.
Private protected
In C# 6 we considered adding a new accessibility level with the meaning protected and internal (
protected internal
means protected or internal), but gave up because it's hard to settle on a syntax. We wanted to useprivate protected
but got a lot of loud push back on it.No-one has come up with a better syntax in the meantime though. We are inclined to think that
private protected
just takes a little getting used to. We may want to try again.Attributes
There are a number of different features related to attributes. We should return to these in a dedicated meeting looking at the whole set of proposals together:
A lot of these would be particularly helpful to Roslyn based tools such as analyzers and metaprogramming.
Local extern functions
We should allow local functions to be extern. You'd almost always want to wrap an extern method in a safe to call wrapper method.
Withers for arbitrary types
If we want to start focusing more on immutable data structures, it would be nice if there was a language supported way of creating new objects from existing ones, changing some subset of properties along the way:
Currently the Roslyn code base, for example, uses the pattern of "withers", which are methods that return a new object with one property changed. There needs to be a wither per property, which is quite bloatful, and in Roslyn made feasible by having them automatically generated. Even so, changing multiple properties is suboptimal:
It would be nice if we could come up with an efficient API pattern to support a built-in, efficient
with
expression.Related to this, it would also be nice to support object initializers on immutable objects. Again, we would need to come up with an API pattern to support it; possibly the same that would support the with expression.
Params IEnumerable
This is a neat little feature that we ultimately rejected or didn't get to in C# 6. It lets you do away with the situation where you have to write two overloads of a method, one that takes an
IEnumerable<T>
(for generality) and another one that takesparams T[]
and calls the first one with its array.The main problem raised against params IEnumerable is that it encourages an inefficient pattern for how parameters are captured: An array is allocated even when there are very few arguments (even zero!), and the implementation then accesses the elements through interface dispatches and further allocation (of an IEnumerator).
Probably this won't matter for most people - they can start out this way, and then build a more optimal pattern if they need to. But it might be worthwhile considering a more general language pattern were folks can build a params implementation targeting something other than arrays.
Async streams
We shied back from a notion of asynchronous sequences when we originally introduced async to C#. Part of that was to see whether there was sufficient demand to introduce framework and language level concepts, and get more experience to base their design on. But also, we had some fear that using async on a per-element level would hide the true "chunky" degree of asynchrony under layers of fine-grained asynchronous abstractions, at great cost to performance.
IAsyncEnumerable
At this point in time, though, we think that there is definitely demand for common abstractions and language support: foreach'ing, iterators, etc. Furthermore we think that the performance risks - allocation overhead in particular - of fine grained asynchrony can large be met with a combination of the compiler's existing optimizations and a straightforward asynchronous "translation" of
IEnumerable<T>
intoIAsyncEnumerable<T>
:The only meaningful difference is that the
MoveNext
method of the enumerator interface has been made async: it returnsTask<bool>
rather thanbool
, so that you need to await it to find out whether there is a next element (which you can then acquire from theCurrent
property) or the sequence is finished.Allocations
Let's assume that you are foreach'ing over such an asynchronous sequence, which is buffered behind the scenes, so that 99.9% of the time an element is available locally and synchronously. Whenever a
Task
is awaited that is already completed, the compiler avoids the heavy machinery and just gets the value straight out of the task without pause. If all awaited Tasks in a given method call are already completed, then the method will never allocate a state machine, or a delegate to store as a continuation, since those are only constructed the first time they are needed.Even when the async method reaches its return statement synchronously, without the awaits having ever paused, it needs to construct a Task to return. So normally this would still require one allocation. However, the helper API that the compiler uses for this will actually cache completed Tasks for certain common values, including
true
andfalse
. In summary, aMoveNextAsync
call on a sequence that is buffered would typically not allocate anything, and the calling method often wouldn't either.The lesson is that fine-grained asynchrony is bad for performance if it is done in terms of
Task<T>
where completed Tasks are never or rarely cached, e.g.Task<string>
orTask<int>
. It should be done in terms ofTask<bool>
or even non-genericTask
.We think that there may or may not be scenarios where people want to get explicit about the "chunks" that data is transmitted in. If so, they can express this as
IAsyncEnumerable<Chunk<T>>
or some such thing. But there is no need to complicate asynchronous streaming by forcing people to deal with chunking by default.Linq bloat
Another concern is that there are many API's on
IEnumerable<T>
today; not least the Linq query operatorsSelect
,Where
and so on. Should all those be duplicated forIAsyncEnumerable<T>
?And when you think about it, we are not just talking about one extra set of overloads. Because once you have asynchronously foreach'able streams, you'll quickly want the delegates applied by the query operators to also be allowed to be async. So we have potentially four combinations:
So either we'd need to multiply the surface area of Linq by four, or we'd have to introduce some new implicit conversions to the language, e.g. from
IEnumerable<T>
toIAsyncEnumerable<T>
and fromFunc<S, T>
toFunc<S, Task<T>>
. Something to think about, but we think it is probably worth it to get Linq over asynchronous sequences one way or another.Along with this, we'd need to consider whether to extend the query syntax in the language to also produce async lambdas when necessary. It may not be worth it - using the query operators may be good enough when you want to pass async lambdas.
Language support
In the language we would add support for foreach'ing over async sequences to consume them, and for async iterators to produce them. Additionally (we don't discuss that further here) we may want to introduce a notion of
IAsyncDisposable
, for weach we could add an async version of theusing
statement.One concern about async versions of language features such as foreach (and using) is that they would generate
await
expressions that aren't there in source. Philosophically that may or may not be a problem: do you want to be able to see where all the awaiting happens in your async method? If that's important, we can maybe add theawait
orasync
keyword to these features somewhere:Equally problematic is when doing things such as
ConfigureAwait
, which is important for performance reasons in libraries. If you don't have your hands on the Task, how can youConfigureAwait
it? The best answer is to add aConfigureAwait
extension method toIAsyncEnumerable<T>
as well. It returns a wrapper sequence that will return a wrapper enumerator whoseMoveNextAsync
will return the result of callingConfigureAwait
on the task that the wrapped enumerator'sMoveNextAsync
method returns:For this to work, it is important that async foreach is pattern based, just like the synchronous foreach is today, where it will happily call any
GetEnumerator
,MoveNext
andCurrent
members, regardless of whether objects implement the official "interfaces". The reason for this is that the result ofTask.ConfigureAwait
is not aTask
.A related issue is cancellation, and whether there should be a way to flow a
CancellationToken
to theMoveNextAsync
method. It probably has a similar solution toConfigureAwait
.Channels in Go
The Go language has a notion of channels, which are communication pipes between threads. They can be buffered, and you can put things in and take them out. If you put things in while the channel is full, you wait. If you take things out while it is empty, you wait.
If you imagine a
Channel<T>
abstraction in .NET, it would not have blocking waits on the endpoints; those would instead be asynchronous methods returning Tasks.Go has an all-powerful language construct called
select
to consume the first available element from any set of channels, and choosing the logic to apply to that element based on which channel it came from. It is guaranteed to consume a value from only one of the channels.It is worthwhile for us to look at Go channels, learn from them and consider to what extent a) we need a similar abstraction and b) it is connected to the notion of async streams.
Some preliminary thoughts: Channels and select statements are very easy to understand, conceptually. On the other hand they are somewhat low-level, and extremely imperative: there is a strong coupling from the consumer to the producer, and in practice there would typically only be one consumer. It seems like a synchronization construct like semaphores or some of the types from the DataFlow library.
The "select" functionality is interesting to ponder more generally. If you think about it from an async streams perspective, maybe the similar thing you would do would be to merge streams. That would need to be coupled with the ability to tie different functionality to elements from different original streams - or with different types. Maybe pattern matching is our friend here?
Either way, it isn't as elegant by a long shot. If we find it's important, we'd need to consider language support.
Another important difference between IAsyncEnumerable and Channels is that an enumerable can have more than one consumer. Each enumerator is independent of the others, and provides access to all the members - at least from the point in time where it is requested.
Conclusion
We want to keep thinking about async streams, and probably do some prototyping.
The text was updated successfully, but these errors were encountered: