Skip to content

Instantly share code, notes, and snippets.

@ericrey85
Last active October 30, 2024 18:48
Show Gist options
  • Save ericrey85/da9671a22234ef981e5ee3653face4af to your computer and use it in GitHub Desktop.
Save ericrey85/da9671a22234ef981e5ee3653face4af to your computer and use it in GitHub Desktop.

AsyncLazyPipeline Monad (C#)

I had the need to create an async pipeline that consisted of actions (methods) composing over each other. I also wanted the construction of this pipeline to not generate any side effects, including Exceptions, Object state mutations, IO operations, etc. I had the need for function composition and Monadic Composition (Kleisli Composition), and did not want to use Task.Run, I do not like doing that in ASP.NET applications.

Having worked in JavaScript with the IO Monad, I decided to do something similar in C# that would fulfill my needs. In here, I leave the implementation I've come up so far with, and below, a simple code example that shows how to use it, plus some other code for the sake of completeness.

public class AsyncLazyPipeline<TSource>
{
private Func<Task<TSource>> Expression { get; }
public AsyncLazyPipeline(Func<Task<TSource>> expression)
{
Expression = expression;
}
public Task<TSource> Flatten() => Expression();
public AsyncLazyPipeline<TDestination> Select<TDestination>(Func<TSource, Task<TDestination>> fn)
{
async Task<TDestination> CombineExpressions()
{
var result = await Expression();
return await fn(result);
}
return CreatePipeLine.With(CombineExpressions);
}
public AsyncLazyPipeline<TDestination> Select<TDestination>(Func<TSource, TDestination> fn)
{
async Task<TDestination> CombineExpressions()
{
var result = await Expression();
return fn(result);
}
return CreatePipeLine.With(CombineExpressions);
}
public AsyncLazyPipeline<TDestination> SelectMany<TDestination>(Func<TSource, AsyncLazyPipeline<TDestination>> fn)
{
async Task<TDestination> CombineExpressions()
{
var result = await Expression();
return await fn(result).Flatten();
}
return CreatePipeLine.With(CombineExpressions);
}
public AsyncLazyPipeline<TDestination> SelectMany<TIntermediate, TDestination>(
Func<TSource, AsyncLazyPipeline<TIntermediate>> fn, Func<TSource, TIntermediate, TDestination> select)
=> SelectMany(a => fn(a).Select(b => select(a, b)));
}
public static class CreatePipeLine
{
public static AsyncLazyPipeline<TDestination> With<TDestination>(Func<Task<TDestination>> fn)
=> new AsyncLazyPipeline<TDestination>(fn);
public static AsyncLazyPipeline<TDestination> With<TDestination>(Func<TDestination> fn)
=> new AsyncLazyPipeline<TDestination>(() => fn().AsTask());
public static AsyncLazyPipeline<TDestination> Return<TDestination>(TDestination value)
=> new AsyncLazyPipeline<TDestination>(() => value.AsTask());
}
public interface IFinalNoteAppender
{
AsyncLazyPipeline<string> AppendFinalText(string value);
}
public class FinalNoteAppender : IFinalNoteAppender
{
public AsyncLazyPipeline<string> AppendFinalText(string value)
=> CreatePipeLine.With(() => $"{value} final text.".AsTask());
}
public interface IOperations
{
Task<string> GetFirstFileNote();
Task<string> CombineWithSecondFileNote(string firstNote);
}
public class Item
{
public string Notes { get; private set; }
public Item AddNotes(string notes)
{
Notes = notes;
return this;
}
}
public class ItemService
{
private IFinalNoteAppender NoteAppender { get; }
private IOperations Operations { get; }
public ItemService(IOperations operations, IFinalNoteAppender noteAppender)
{
NoteAppender = noteAppender;
Operations = operations;
}
public Task<Item> AddNotesToItem()
{
var combineNotes = CreatePipeLine
.With(Operations.GetFirstFileNote)
.Select(Operations.CombineWithSecondFileNote);
var appendLastNote = from notes in combineNotes
from finalNote in NoteAppender.AppendFinalText(notes)
select finalNote;
var combinedOperations = appendLastNote.Select(new Item().AddNotes);
return combinedOperations.Flatten();
}
}
public static class ObjectExtensions
{
public static Task<T> AsTask<T>(this T that) => Task.FromResult(that);
}
@benrobot
Copy link

I think I would have done this with Rx.Net, however having you're own implementation certainly makes it easier to debug.

@ericrey85
Copy link
Author

@benrobot I love Reactive Programming, I have used it for a few years now and I am an advocate for it. In fact, I like it so much that wrote two articles about it. That being said, it is often a style that does not mix well with other styles, I think that is the only thing that some people (certainly not me) have against it. I think the approach shown here can be better mingled with other code styles.

@benrobot
Copy link

I can definitely agree that the style you've shown here is easier to integrate into something like an ASP.NET project.

@JesseXia
Copy link

JesseXia commented May 4, 2020

Really appreciate sharing your code. It looks fairly cool.

@ericrey85
Copy link
Author

Thank you @JesseXia, I am glad you like this approach.

@damiensawyer
Copy link

Thanks for this. Very cool.

@ericrey85
Copy link
Author

Thank you @damiensawyer. I am glad you find it cool.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment