Replies: 3 comments 4 replies
-
The short answer is "yes", anything that does mutation or touches an external changeable thing (IO), should be wrapped up in IO. And wrapping existing .NET APIs in more declarative interfaces, although a bit annoying, is really the best approach. Otherwise you end up fighting against the libraries throughout the code-base, rather than encapsulating that in one place. The So, as with your previous efforts you're nested two layers of things that already have error handling support. When The one place where you will want to If you still want something that does explicit catching of errors on So, I would factor your code like this... First, create an public static class AppError
{
public enum Code
{
BlobClientFailedToOpen = 10000,
// ... add more here as you need
}
public static readonly Error BlobClientFailedToOpen =
Error.New((int)Code.BlobClientFailedToOpen, "Could not open blob container client.");
// ... add more here as you need
} Then your code (without the excessively long names ;-) ) public static class BlobStorage
{
public static IO<Unit> Upload(
string connectionString,
string containerName,
string blobName,
byte[] data,
bool overwrite = false) =>
from client in GetContainerBlobClient(connectionString, containerName, blobName)
from stream in use(() => new MemoryStream(data))
from _ in UploadFromStream(client, stream, overwrite)
from __ in release(stream)
select unit;
public static IO<byte[]> Download(
string connectionString,
string containerName,
string blobName) =>
from client in GetContainerBlobClient(connectionString, containerName, blobName)
from stream in use(() => new MemoryStream())
from _ in DownloadToStream(client, stream)
let data = stream.ToArray()
from __ in release(stream)
select data;
static IO<Response> DownloadToStream(BlobClient client, Stream stream) =>
lift(() => client.DownloadTo(stream));
static IO<Response<BlobContentInfo>> UploadFromStream(BlobClient client, Stream stream, bool overwrite) =>
lift(() => client.Upload(stream, overwrite: overwrite));
static IO<BlobClient> GetContainerBlobClient(
string connectionString,
string containerName,
string blobName) =>
IO.lift(() => new BlobServiceClient(connectionString)
.GetBlobContainerClient(containerName)
.GetBlobClient(blobName))
| AppError.BlobClientFailedToOpen;
} But, what I would do is I would realise that accessing Azure blob-storage is a key piece of functionality. I'd want to make that into a more featured and declarative thing. I'd also notice that these appear everywhere: string connectionString,
string containerName,
string blobName They clutter up the interface and the resulting code will have to manage them and propagate them through from application configuration or something like that. So, firstly, I'd make sure those strings can't accidentally be provided in the wrong order, by making them into stronger types: public readonly record struct ConnectionString(string Value);
public readonly record struct ContainerName(string Value);
public readonly record struct BlobName(string Value); Then I'd create an environment type that carries the configuration and current-state: /// <summary>
/// Blob context environment
/// </summary>
public record BlobEnv(
ConnectionString ConnectionString,
Option<ContainerName> ContainerName,
Option<BlobName> BlobName,
Option<BlobClient> BlobClient); Next, I'd create a new /// <summary>
/// Blob monad
/// </summary>
public record Blob<A>(ReaderT<BlobEnv, IO, A> runBlob) : K<Blob, A>
{
public static Blob<A> Pure(A value) => new(ReaderT<BlobEnv, IO, A>.Pure(value));
public static Blob<A> Fail(Error value) => new(ReaderT<BlobEnv, IO, A>.LiftIO(IO.fail<A>(value)));
public static Blob<A> LiftIO(Func<A> f) => new(ReaderT<BlobEnv, IO, A>.LiftIO(IO.lift(f)));
public static Blob<A> LiftIO(Func<Task<A>> f) => new(ReaderT<BlobEnv, IO, A>.LiftIO(IO.liftAsync(f)));
public Blob<B> Map<B>(Func<A, B> f) => this.Kind().Map(f).As();
public Blob<B> Select<B>(Func<A, B> f) => this.Kind().Map(f).As();
public Blob<A> MapFail(Func<Error, Error> f) => this.Kind().Catch(f).As();
public Blob<B> Bind<B>(Func<A, K<Blob, B>> f) => this.Kind().Bind(f).As();
public Blob<B> Bind<B>(Func<A, K<IO, B>> f) => this.Kind().Bind(f).As();
public Blob<C> SelectMany<B, C>(Func<A, K<Blob, B>> b, Func<A, B, C> p) => this.Kind().SelectMany(b, p).As();
public Blob<C> SelectMany<B, C>(Func<A, K<IO, B>> b, Func<A, B, C> p) => this.Kind().SelectMany(b, p).As();
public Blob<C> SelectMany<C>(Func<A, Guard<Error, Unit>> b, Func<A, Unit, C> p) => SelectMany(a => Blob.lift(b(a)), p);
public static implicit operator Blob<A>(Lift<A> lift) => LiftIO(lift.Function);
public static implicit operator Blob<A>(Pure<A> pure) => Pure(pure.Value);
public static implicit operator Blob<A>(Error fail) => Fail(fail);
public static implicit operator Blob<A>(Fail<Error> fail) => Fail(fail.Value);
public static Blob<A> operator |(Blob<A> ma, K<Blob, A> mb) => ma.Choose(mb).As();
public static Blob<A> operator |(Blob<A> ma, Error mb) => ma.Choose(Fail(mb)).As();
public static Blob<A> operator |(Blob<A> ma, CatchM<Error, Blob, A> mb) => ma.Catch(mb).As();
public static Blob<A> operator >> (Blob<A> ma, K<Blob, A> mb) => ma.Bind(_ => mb);
public static Blob<A> operator >> (Blob<A> ma, K<Blob, A> mb) => ma.Bind(_ => mb);
public static Blob<A> operator >> (Blob<A> ma, K<IO, A> mb) => ma.Bind(_ => mb);
public static Blob<Unit> operator >> (Blob<A> ma, K<Blob, Unit> mb) => ma.Bind(_ => mb);
public static Blob<Unit> operator >> (Blob<A> ma, K<IO, Unit> mb) => ma.Bind(_ => mb);
} The methods are mostly optional, but it just makes everything easier. Next, I'd add some extensions to /// <summary>
/// Blob extensions
/// </summary>
public static class BlobExtensions
{
public static Blob<A> As<A>(this K<Blob, A> ma) =>
(Blob<A>)ma;
public static IO<A> Run<A>(
this K<Blob, A> ma,
ConnectionString connectionString,
Option<ContainerName> containerName = default,
Option<BlobName> blobName = default) =>
ma.Run(new BlobEnv(connectionString, containerName, blobName, None)).As();
public static IO<A> Run<A>(this K<Blob, A> ma, BlobEnv env) =>
ma.As().runBlob.Run(env).As();
public static Blob<C> SelectMany<A, B, C>(this IO<A> ma, Func<A, K<Blob, B>> b, Func<A, B, C> p) => Blob.liftIO(ma).SelectMany(b, p).As();
public static Blob<C> SelectMany<B, C>(this Guard<Error, Unit> ma, Func<Unit, K<Blob, B>> b, Func<Unit, B, C> p) => Blob.lift(ma).SelectMany(b, p);
} Next, I would implement the traits:
These expose the capabilities of the underlying /// <summary>
/// Blob trait implementations
/// </summary>
public partial class Blob :
Monad<Blob>,
Alternative<Blob>,
Fallible<Error, Blob>,
Readable<Blob, BlobEnv>
{
static K<Blob, B> Monad<Blob>.Bind<A, B>(K<Blob, A> ma, Func<A, K<Blob, B>> f) =>
new Blob<B>(ma.As().runBlob.Bind(x => f(x).As().runBlob));
static K<Blob, B> Functor<Blob>.Map<A, B>(Func<A, B> f, K<Blob, A> ma) =>
new Blob<B>(ma.As().runBlob.Map(f));
static K<Blob, A> Applicative<Blob>.Pure<A>(A value) =>
new Blob<A>(ReaderT.pure<BlobEnv, IO, A>(value));
static K<Blob, B> Applicative<Blob>.Apply<A, B>(K<Blob, Func<A, B>> mf, K<Blob, A> ma) =>
new Blob<B>(mf.As().runBlob.Apply(ma.As().runBlob));
static K<Blob, A> Choice<Blob>.Choose<A>(K<Blob, A> fa, K<Blob, A> fb) =>
new Blob<A>(fa.As().runBlob.Choose(fb.As().runBlob).As());
static K<Blob, A> MonoidK<Blob>.Empty<A>() =>
new Blob<A>(ReaderT.lift<BlobEnv, IO, A>(IO.empty<A>()));
static K<Blob, A> Fallible<Error, Blob>.Fail<A>(Error error) =>
new Blob<A>(ReaderT.lift<BlobEnv, IO, A>(IO.fail<A>(error)));
static K<Blob, A> Fallible<Error, Blob>.Catch<A>(
K<Blob, A> fa, Func<Error, bool> Predicate,
Func<Error, K<Blob, A>> Fail) =>
new Blob<A>(
new ReaderT<BlobEnv, IO, A>(env =>
fa.Run(env).Catch(Predicate, e => Fail(e).Run(env))));
static K<Blob, A> Readable<Blob, BlobEnv>.Asks<A>(Func<BlobEnv, A> f) =>
new Blob<A>(ReaderT.asks<IO, A, BlobEnv>(f));
static K<Blob, A> Readable<Blob, BlobEnv>.Local<A>(Func<BlobEnv, BlobEnv> f, K<Blob, A> ma) =>
new Blob<A>(ReaderT.local(f, ma.As().runBlob));
static K<Blob, A> MonadIO<Blob>.LiftIO<A>(IO<A> ma) =>
new Blob<A>(ReaderT.liftIO<BlobEnv, IO, A>(ma));
} Now onto the core functionality of public partial class Blob
{
public static Blob<A> pure<A>(A value) =>
Blob<A>.Pure(value);
public static Blob<A> fail<A>(Error value) =>
Blob<A>.Fail(value);
public static Blob<A> liftIO<A>(IO<A> ma) =>
MonadIO.liftIO<Blob, A>(ma).As();
public static Blob<A> liftIO<A>(Func<A> ma) =>
MonadIO.liftIO<Blob, A>(IO.lift(ma)).As();
public static Blob<A> liftIO<A>(Func<Task<A>> ma) =>
MonadIO.liftIO<Blob, A>(IO.liftAsync(ma)).As();
public static Blob<A> lift<A>(Option<A> ma) =>
ma.Match(Some: pure, None: fail<A>(Errors.None));
public static Blob<Unit> lift(Guard<Error, Unit> guard) =>
liftIO(guard.ToIO());
public static Blob<A> bracket<A>(K<Blob, A> ma) =>
bracketIO(ma).As();
public static Blob<BlobEnv> env =>
Readable.ask<Blob, BlobEnv>().As();
public static Blob<A> local<A>(Func<BlobEnv, BlobEnv> f, Blob<A> ma) =>
Readable.local(f, ma).As();
public static Blob<ConnectionString> connectionString =>
env.Map(e => e.ConnectionString);
public static Blob<ContainerName> container =>
env.Bind(e => lift(e.ContainerName))
| AppError.BlobContainerNotSet;
public static Blob<BlobName> name =>
env.Bind(e => lift(e.BlobName))
| AppError.BlobNotSet;
public static Blob<BlobClient> client =>
env.Bind(e => lift(e.BlobClient))
| AppError.BlobClientNotSet;
static Blob<BlobClient> openClient =>
(from conn in connectionString
from cont in container
from name in name
select new BlobServiceClient(conn.Value)
.GetBlobContainerClient(cont.Value)
.GetBlobClient(name.Value))
| AppError.BlobClientFailedToOpen;
public static Blob<A> withClient<A>(Blob<A> ma) =>
from c in openClient
from r in local(e => e with { BlobClient = c}, ma)
select r;
public static Blob<A> with<A>(BlobName name, Blob<A> ma) =>
local(e => e with { BlobName = name }, ma);
public static Blob<A> with<A>(ContainerName container, BlobName blob, Blob<A> ma) =>
local(e => e with { ContainerName = container, BlobName = blob }, ma);
} So, we supporting construction, lifting, accessing the Now, we can rewrite your original functions (but with a consistent interface between public partial class Blob
{
public static Blob<Unit> upload(byte[] data, bool overwrite = false) =>
bracket(from stream in use(() => new MemoryStream(data))
from _ in upload(stream, overwrite)
select unit);
public static Blob<Unit> upload(Stream stream, bool overwrite = false) =>
withClient(client.Map(c => c.Upload(stream, overwrite: overwrite)).Map(_ => unit));
public static Blob<byte[]> download() =>
bracket(from stream in use(() => new MemoryStream())
from _ in download(stream)
select stream.ToArray());
public static Blob<Unit> download(Stream stream) =>
withClient(client.Map(c => c.DownloadTo(stream)).Map(_ => unit));
} So, now we don't have to deal with and manage the configuration, it all gets dealt with automatically. Every new function you write doesn't need to deal with it, and every composition of For example, before: var operation = from data in BlobStorageHelper.Download(connectionString, containerName, blobName)
from data2 in ModifyData(data)
from _ in BlobStorageHelper.Upload(connectionString, containerName, blobName, data2, true)
select unit; After: var operation = from data in Blob.download()
from data2 in modifyData(data)
from _ in Blob.upload(data2, true)
select unit; If you wanted to change blobs on-the-fly: var blob1 = new BlobName("blob-1");
var blob2 = new BlobName("blob-2");
var operation = from data in Blob.with(blob1, Blob.download())
from data2 in modifyData(data)
from _ in Blob.with(blob2, Blob.upload(data2))
select unit; When you This for me is the power of creating bespoke monadic types for various domains in the application. The Anyway, that's just how I would approach it, sorry if it's thrown a spanner in the works! |
Beta Was this translation helpful? Give feedback.
-
Not sure I fully understand the question, it seems a little broad. But both That obviously doesn't mean that exceptions are never expressed by any of the wrapped behaviours, it just meas that those types have primitives that deal with exceptional events.
So with this: liftIO(async () =>
{
await context.AddAsync(entity);
await context.SaveChangesAsync();
return Right<AddDocumentError, Document>(document);
} You can extract the individual methods (I'm guessing at the types): IO<Unit> Add(Context context, Entity entity) =>
liftIO(e => context.AddAsync(entity, e.Token));
IO<Unit> SaveChanges(Context context) =>
liftIO(e => context.SaveChangesAsync(e.Token)); Then the original expression is await free: from _ in Add(context, entity) >>
SaveChanges(context, entity)
select Right<AddDocumentError, Document>(document) Obviously, at some point you will need to invoke the underlying app.MapGet("/sync",
() =>
{
var effect = yieldFor(1000).Map("Hello, World");
return effect.Run();
});
app.MapGet("/async",
async () =>
{
var effect = yieldFor(1000).Map("Hello, World");
return await effect.RunAsync();
}); The first handler, The second handler is the same, but it uses the asynchronous In NBomber benchmarks both perform exactly the same. There's no thread starvation and the performance is equal. So, really, you don't have to use So, really it's about getting the ugly .NET APIs you're using and wrapping them up into fine-grained IO monads. Then lifting those into either an I'd love to provide my own wrappers at some point for some of the major libraries like ASP.NET Core, EntityFramework, AWS/Azure, etc. but there are only so many hours in a day. I decided to try writing an Avalonia project over the weekend to try it out and I wanted to rip the whole thing to pieces and start building a proper declarative UI library for it! But again, I have too much on my plate to do all of that right now. So, I think if you pay a small investment upfront in packaging up the handful of core libraries that you use, it will pay dividends down the track. |
Beta Was this translation helpful? Give feedback.
-
“Whereas Eff won't throw at all and will always return a Fin<A>. So, Effis
what you want to stop all exceptions.”
Can you elaborate on this? Are you saying that I don’t need to catch
exceptions inside of an Eff monad? Where would they go? Is there any
implicit functionality occurring here or do I need to explicitly define how
Exceptions get translated to Errors in all cases?
I want it to be as unlikely as possible that an exception occurs at runtime
due to forgetting to wrap something in a try catch. I’m introducing this to
a lot of developers who have never done functional programming before and
who frankly aren’t the greatest at exception handling to begin with (lol),
hence why I’ve been putting this new stack together.
Appreciate your help 🤘🏼
…On Mon, Nov 18, 2024 at 4:47 PM Paul Louth ***@***.***> wrote:
I've got another question that is related to this. Is there any way to get
rid of try-catch blocks entirely?
Not sure I fully understand the question, it seems a little broad. But
both Eff and IO have combinators that will allow you to catch exceptions
and turn the exceptions into Error values. IO will throw (when you invoke
Run) if exceptions aren't caught using either |, @catch, or .Catch.
Whereas Eff won't throw at all and will always return a Fin<A>. So, Eff
is what you want to stop all exceptions.
That obviously doesn't mean that exceptions are never expressed by any of
the wrapped behaviours, it just meas that those types have primitives that
deal with exceptional events.
I'm also wondering if there is a way to remove use of 'await' or if it is
necessary when dealing with external libraries that use Task?
Task doesn't have to be awaited, you can wrap it up in an IO monad and
then just work with the IO monad type with LINQ. You only need to await a
Task if you want to do sequential operations (statements).
So with this:
liftIO(async () => { await context.AddAsync(entity); await context.SaveChangesAsync(); return Right<AddDocumentError, Document>(document);}
You can extract the individual methods (I'm guessing at the types):
IO<Unit> Add(Context context, Entity entity) =>
liftIO(e => context.AddAsync(entity, e.Token));
IO<Unit> SaveChanges(Context context) =>
liftIO(e => context.SaveChangesAsync(e.Token));
Then the original expression is await free:
from _ in Add(context, entity) >>
SaveChanges(context, entity)select Right<AddDocumentError, Document>(document)
Obviously, at some point you will need to invoke the underlying IO
computation to realise the value (usually in Main or in a
web-request/response handler). You are most likely to benefit from awaiting
the resulting Task. However, it still isn't entirely necessary. Take a
look at this ASP.NET Core test-app
<https://github.com/louthy/language-ext/blob/main/Samples/TestBed.Web/Program.cs>
:
app.MapGet("/sync",
() => { var effect = yieldFor(1000).Map("Hello, World"); return effect.Run(); });
app.MapGet("/async",
async () => { var effect = yieldFor(1000).Map("Hello, World"); return await effect.RunAsync(); });
The first handler, sync, runs an IO operation (yieldFor) which yields for
1000 milliseconds before returning. It uses the *synchronous* Run method.
The second handler is the same, but it uses the *asynchronous* RunAsync
method (which is awaited).
In NBomber benchmarks
<https://github.com/louthy/language-ext/blob/main/Samples/TestBed.Web.Runner/Program.cs>
both perform exactly the same. There's no thread starvation and the
performance is equal. So, really, you don't have to use await at all
(which was one of the goals of the v5 refactor). Some people are more
comfortable using it though and there are times when you want the Task
and to not await it, or you may want to await multiple results, so
RunAsync works better then.
So, really it's about getting the ugly .NET APIs you're using and wrapping
them up into fine-grained IO monads. Then lifting those into either an Eff
monad (which you seem to be using), or take some of my earlier advice and
build bespoke monads around the various frameworks (as I showed with the
Blob monad).
I'd love to provide my own wrappers at some point for some of the major
libraries like ASP.NET Core, EntityFramework, AWS/Azure, etc. but there
are only so many hours in a day. I decided to try writing an Avalonia
project over the weekend to try it out and I wanted to rip the whole thing
to pieces and start building a proper declarative UI library for it! But
again, I have too much on my plate to do all of that right now.
So, I think if you pay a small investment upfront in packaging up the
handful of core libraries that you use, it will pay dividends down the
track.
—
Reply to this email directly, view it on GitHub
<#1400 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACCSYKUMRLJNB5QHJ7O7BY32BJN57AVCNFSM6AAAAABQYN7EFCVHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTCMRZG4ZDANI>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
I've created a helper class for document management prototype that I'll be presenting to my team shortly.
One thing I was curious about was how to manage existing .NET library usage. A lot of their functions do indeed mutate state, which is technically against the rules in FP.
I'm wondering if there are sufficient ways to manage Pure and Impure functions either with the LanguageExt library or others. I've seen the [Pure] attribute that I wanted to play around with and was curious what your thoughts were on this topic.
I have the following code:
The piece I'm concerned with us this:
Not because it doesn't work, but mostly from a matter of principal.
I will be writing a developer manifesto for this team and I want to ensure that I'm not contradicting myself when I say "No mutation", as this is technically mutating state of the memory stream.
Is there a way I can do this in a more "functional" approach? Or maybe just some patterns I can introduce for any time where we have to deal with things like "using" blocks or "try-catch", that are otherwise necessary when dealing with built-in functions that were not written with immutability in mind?
Beta Was this translation helpful? Give feedback.
All reactions