Runtime: Functional extensions for ValueTask

Created on 13 May 2020  路  28Comments  路  Source: dotnet/runtime

Hi all,
I find both Task and ValueTask a bit verbose, at times. Even though I'm not against using multiple async rows, I think that a functional approach, when available, is more elegant and concise.

My impression is that C# is moving to that direction, so would like to see if there's interest in adding more of that, similarly to how other languages do.

For instance here is an example of how a potential FlatMap method would look like:

public static class ValueTaskExtensions
    {
        public static async ValueTask<T2> FlatMap<T1, T2>(this ValueTask<T1> valueTask, Func<T1, ValueTask<T2>> function)
        {
            var result = await valueTask;
            return await function(result);
        }

        public static async ValueTask FlatMap<T>(this ValueTask<T> valueTask, Func<T, ValueTask> function)
        {
            var result = await valueTask;
            await function(result);
        }
    }

And of course there's more to add to simplify the language

api-suggestion area-System.Threading.Tasks

All 28 comments

... are you asking for new runtime methods, or for language changes?

Hi @yaakov-h I'm quite new here so not sure what to answer :D
I'm imagining this as coming from some new standard assembly/namespace as it was done with the Linq extensions for collections, but don't know if you would call that a language change or a runtime method

Using flatmap is the functional approach. However it's such a painful approach that many functional languages provide some syntax sugar to avoid having to do so. For example Haskell provides do notation and Scala provides for comprehensions. Async await is just C#s version of these language features. However it's heavily optimized, and works well with both a functional and imperative style. Scala is likely to add async await to version 3 for exactly that reason.

Note that FlatMap and Select already exist on Task. They're just called ContinueWith.

Hi @YairHalberstadt I've been working with Scala for a while and maybe that's is why I see it's OOP/Functional hybrid approach as what C# should aim to become!

I love the elegance of the for-comprehension, but I think it would be much more complex to achieve, given it would require to extend the language syntax. But of course that would be amazing to see in C#

As for the FlatMap I think is not soo bad in simple scenarios where for-comprehension might be overkilling, definitely better than multiple await rows. I've seen the Task.ContinueWith, but first of all is not available for ValueTask, and also it behaves like a Map, not like a FlatMap. For instance if I write:

var t1 = Task.FromResult("My Value 1");
var t2 = t1.ContinueWith(async v => await Task.FromResult("My Value 2"));

I get that t2 is a Task<Task<string>>, which is what a Map would return.

Another point, in my view, about Map and FlatMap is that they are general concepts that can be applied more broadly and in a consistent way, replacing things like Select and SelectMany that basically do the same, but are limited to collections (and have a SQL-like syntax that is not super intuitive)

Also speaking of functional extensions It can also be a good entry-point for introducing other concepts such as the Option pattern, which in my view is a more elegant and standard way of solving the nullable-reference problem than what was introduced with C#8.

I did some experiment here (@YairHalberstadt you will see that I'm taking the naming from Scala): https://github.com/Frank0101/FunctionalExtensions/blob/master/FunctionalExtensions.Nuget/DataTypes/Option.cs

I also use Scala professionally.

I cannot for the life of me imagine preferring for comprehensions to async await :). Async await is strictly more powerful, makes much more syntactic sense (why are you using a looping function to consume a future?), and is much more efficient. I guess different strokes for different folks :). Given the popularity of the scala-async library I'm not the only one either.

By the way C# already has for comprehensions in the form of Linq query syntax. I believe Scala for comprehensions are heavily influenced by Linq query syntax.

You can use Unwrap to turn ContinueWith into FlatMap. However as you showed yourself, you can add the extension methods yourself if you prefer :)

Also speaking of functional extensions It can also be a good entry-point for introducing other concepts such as the Option pattern

I've taken the opposing approach. How can we make async await work with Option: https://github.com/dotnet/csharplang/issues/3403 :)

One huge advantage is that flatmap on option allocates a delegate every single time. This pretty much makes the whole idea untenable IMO. Meanwhile async await is almost cost free.

I like using async await as well, is just less functional (as in one expression, instead of a code block) and in some case I prefer it's elegance :D

Also I think there's value in being able to use Map/FlatMap on different things like a Collection, an Option or a Future, which brings great consistency. In C# there's Select/SelectMany for Collections, async/await for Futures and nothing for Options :P

Will have a look to your issue, also as a good example of how to open an issue here :)

You can use Unwrap to turn ContinueWith into FlatMap. However as you showed yourself, you can add the extension methods yourself if you prefer :)

Yes, and there's already good Nugets out there, but I believe that without anything official coming from C# itself functional programming will never start a real adoption in C#, which for me is a bit of a shame

@YairHalberstadt happy to find a Scala dev here, though. What's your opinion about the nullable-reference types added with C# 8?

I don't think there's anything unfunctional about async await. In fact if anything t's more functional than Haskell do notation, since <- future is not an expression, but await future is.

What's your opinion about the nullable-reference types added with C# 8?

It's best to keep issues focused. I'd be happy to discuss more on the csharplang gitter.

I don't think there's anything unfunctional about async await. In fact if anything t's more functional than Haskell do notation, since <- future is not an expression, but await future is.

Not an expert of Haskell, unfortunately, but it seems to me that the do notation is similar to Scala's for-comprehension, as you also said before. Apart for the name containing "for", on which I agree with you might sound misleading, I'ts functional as is not requiring a code block.

I can write var x = {for-comprehension}

I can't use multiple async/await in the same way.. i have to create a code block like a function such as:

var a = await ...
var b = await func(a)
var c = await func(b)
return c

Which is less idiomatic

@Frank0101

Yes you can:
return await func(await func(await ...)))

Yes you can:
return await func(await func(await ...)))

Absolutely, everything can be done regardless, I'm not saying that is not possible. But my point is to have a slightly better syntax than the one above (which, sorry, it's horrible :D).

Having a better/lighter/less-verbose syntax is why the functional switch was added to C#8 for instance, not that before it wasn't possible to have the same, isn't it? My proposal goes in the same direction.

With FlatMap I could have something like:

var result = await asyncFunction1()
   .FlatMap(v => asyncFunction2(v))
   .FlatMap(v => asyncFunction3(v))
   .FlatMap(v => asyncFunction4(v))

But also as said I would also love to see a proper for comprehension statement in C#, just wasn't dreaming so big :)

With FlatMap I could have something like:

var result = await asyncFunction1()
   .FlatMap(v => asyncFunction2(v))
   .FlatMap(v => asyncFunction3(v))
   .FlatMap(v => asyncFunction4(v))

I don't understand what async await has to do with this.
If it was non async, you would either have to do:

var a = ...
var b = func(a)
var c = func(b)
return c

Or

return func(func(...));

I don't see why the fact that await doesn't magically let you avoid having to do this makes await less functionall?

Perhaps a better solution would be a pipeline operator https://github.com/dotnet/csharplang/issues/74 so you could do :

    return await ... |> await func |> await func;

@YairHalberstadt

Ok, I think I've been mixing up the discussion about for-comprehension with the discussion about the FlatMap. Sorry for that, Going to focus on the FlatMap and to better explain: I'm talking about case where we have to chain multiple calls.

In the sync case I can do:

var result = func1().func2().func3()

In the async case I have to do:

var v1 = await func1();
var v2 = await v1.func2();
var result = await v2.func3();

async/await in the case above is less functional than having something like:

var result = await func1()
   .FlatMap(v => v.func2())
   .FlatMap(v => v.func3());

Your pipeline operator wouldn't be bad, but I think we would still lose the opportunity to have a consistent Map/FlatMap operator that can be used across different things (Collections, Options etc..) instead of defining different syntaxes to achieve the same logical action

Note that rust solves this by making await a postfix operator:

func1().await.func2().await.func3();

Unfortunately that ship has sailed with C#, although if a new operator is introduced as per https://github.com/dotnet/csharplang/issues/3403 that would give us a chance to revisit it.

I still think that adding a pipelining operator is a more natural solution than adding methods which lead to more nested delegates, and are much less performant.

Note that rust solves this by making await a postfix operator:

This looks similar to what you can have with the .Result or with the await in Scala, but it would block the thread isn't it?

In regards to the performance of adding a delegate don't know how much we are talking about and maybe we can have both approaches so that you can use the more performing one if you are on a real-time application. But in general I always put readability before performances, unless the situation strictly requires it

It's exactly the same as (await (await (await func1()).func2()).func3())

It's exactly the same as (await (await (await func1()).func2()).func3())

ok, this is definitely not readable :)

In general I think that making functions a first class citizen in terms of accepting it as parameter it's key to even start any functional programming in C#. This is more and more used across the language and don't see much performance issues. Nobody prevent people to opt out the functional extensions (as they can alreay do for Linq and everywhere else delegates are strongly used) if performances are a great concern.

But that being said nothing prevent having multiple ways, I'm not against your pipe operator :)

Tagging subscribers to this area: @tarekgh
Notify danmosemsft if you want to be subscribed.

CC @stephentoub

This looks like a monadic bind for ValueTask. I appreciate that everyone has their own perspective for what makes for easily-read and maintained code, but from my perspective, this would be a net negative. It adds another way to achieve the same thing that's already possible with just as little code (less even) and it does so in a more expensive way, one that not only requires additional delegate invocations but that will also lead to more heap allocation.

If you believe it's important, I'd encourage you to create a library with such extensions and put it up on nuget for others to use when they desire this form of consumption.

@Frank0101 per the discussion here, I am closing this issue as it looks like it is not preferred to add these methods for the reasons mentioned by @stephentoub and @YairHalberstadt. It would be nice to start with the suggestion of creating a NuGet package with such methods. Thanks for your suggestions and thoughts and feel free to respond back with any questions or comments.

Thank you all for your time. My view is that the same that was said here could also be said to Linq or others widely used and much appreciated language features, that are maybe not as performing as other ways to achieve the same, but are much more elegant and readable. And for enterprise applications there's more value in that than in counting the microseconds, at least in 99% of cases. If performances are the only thing that matters we should all go back programming in C, or in Assembly. Besides, something being less functional that something else is a fact, not an opinion, and being forced to write multiple lines (or being forced to write some unreadable horror to avoid it) fits the case of a non-functional (or non functional-friendly) approach.

I know that C# is a language designed almost 20 years ago, but I see the effort in covering gaps with more modern languages and patters, so I'm a bit surprised that functional programming is still seen as something that "you should implement yourself". I've seen good improvements on this side, like the functional switch recently introduced, so I'm a bit surprised.

Thanks anyway

Was this page helpful?
0 / 5 - 0 ratings

Related issues

aggieben picture aggieben  路  3Comments

jzabroski picture jzabroski  路  3Comments

Timovzl picture Timovzl  路  3Comments

v0l picture v0l  路  3Comments

btecu picture btecu  路  3Comments