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;
}
}
}
}