'async call inside a Map() or Bind() for Option<T> (Functional programming in C#)

Note, if you think this post is long, over half of it is just reference code, for completion

I have started looking at functional programming in C#, mostly through this book and this tutorial

So I have implemented the Option, with extension method Map and Bind. So far so good. Lets say that I want to build a pipeline with some filtering in the beginning and then call a async method, then continue the processing with the result. For example:

Ok, this pseudo code isnt super useful, but hopefully its shows what I mean. I want to first make chainable "if name == Kalle"-substitute. Then if so, get products from a async method and some more processing. The first Map, however will return a Option<Task> and I have to await it inside second Map... not pretty.

Option<Customer> cust = Some(new Customer(){Name ="Kalle"});
     cust.Bind(NamedKalle)  //yes I could use where here, not the point though :)
    .Map(async c => await myHttpClient.GetAdressAsync(c))
    .Map(addressTask=> (await addressTask).DoSomethingMore(address))
//...

public Customer NamedKalle(Customer c) =>
   c.Name == "Kalle" ? Some(c) : None;

I have managed to make an async Match and async Map, which seem to work, but I haven't seen this, which leads me to believe that I'm missing something. Can I use the original map and bind in some way?

 // Match that awaits (member on Option<T>)
   public async Task<R> MatchAsync<R>(Func<R> noneFunc, Func<T, Task<R>> someFunc) =>
            IsSome 
                ? await someFunc(_value)
                : await Task.FromResult(noneFunc());

 // Map that awaits, extension method 
  public static async Task<Option<R>> MapAsync<T, R>(this Option<Task<T>> optT, Func<T, R> func) =>
            await optT.MatchAsync(
                ()=> F.None, 
                async v => F.Some<R>(func(await v)));

BELOW IS JUST FOR REFERENCE, ITs an Implementation of Option, helpers and Extension method. This is a subset of the open source project at https://github.com/la-yumba/functional-csharp-code/

namespace ProductCatalog.Api.Functional
{
    public static class FunkyExtensions
    {
        public static Option<R> Map<T, R>(this Option<T> opt, Func<T, R> func) =>
            opt.Match(() => F.None, v => F.Some<R>(func(v)));

        public static async Task<Option<R>> Map2<T, R>(this Option<Task<T>> optT, Func<T, R> func) =>
            await optT.Match2(
                ()=> F.None, 
                async v => F.Some<R>(func(await v)));

        public static Option<T> Where<T>(this Option<T> opt, Func<T, bool> pred) =>
            opt.Match(() => F.None, 
                      t => pred(t) ? opt: F.None);


        public static Option<R> Bind<T, R>(this Option<T> opt, Func<T, Option<R>> func) =>
            opt.Match(() => F.None, (v) => func(v));

    }

    public static partial class F
    {
        public static Option<T> Some<T>(T value) => new Option.Some<T>(value); // wrap the given value into a Some
        public static Option.None None => Option.None.Default;  // the None value
    }

    public struct Option<T>
    {
        T _value;
        public bool IsSome { get; set; }
        public bool IsNone => !IsSome;
        public override string ToString() => IsNone ? $"👍({_value})" : $"👎({_value})";
        private Option(T value)
        {
            if (value == null)
                throw new Exception("Cant be null");

            _value = value;
            IsSome = true;
        }

        public static implicit operator Option<T>(Option.None _) => new Option<T>();
        public static implicit operator Option<T>(Option.Some<T> some) => new Option<T>(some.Value);
        public R Match<R>(Func<R> noneFunc, Func<T, R> someFunc) =>
            IsSome ? someFunc(_value)
                : noneFunc();

        public async Task<R> Match2<R>(Func<R> noneFunc, Func<T, Task<R>> someFunc) =>
            IsSome 
                ? await someFunc(_value)
                : await Task.FromResult(noneFunc());

    }

    namespace Option
    {
        public struct None
        {
            internal static readonly None Default = new None();
        }
        public struct Some<T>
        {
            public T Value { get; set; }
            public Some(T value)
            {
                if (value == null)
                    throw new Exception("Value should not be null when creating Some, use None instead");
                Value = value;
            }
        }
    }
}


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source