Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functional approach to try-catch-finally #1108

Open
DumDumin opened this issue Aug 31, 2022 · 6 comments
Open

Functional approach to try-catch-finally #1108

DumDumin opened this issue Aug 31, 2022 · 6 comments

Comments

@DumDumin
Copy link

DumDumin commented Aug 31, 2022

Hello,

I find my way into functional programming and love to see, that I can get started without leaving C# behind. Thanks for the great work! But I cannot get my head around on how to implement a try-catch-finally behaviour in a FP style.

I have to following 3 functions to work with and every single one can throw an exception...

  int AllocatateResource(int id)
  void UseResource(int resId)
  void DeleteResource(int resId)

With try catch finally it would look like this (it is important, that the finally block can exit with an exception):

int resId;
try
{
    resId = AllocatateResource(1);
    UseResource(resId);
}
finally
{
    if(resId != 0)
    {
        DeleteResource(resId);
    }
}

This is a real mess in my opinion so I changed those methods to the following:

  Fin<int> AllocatateResource(int id)
  Fin<Unit> UseResource(int resId)
  Fin<Unit> DeleteResource(int resId)

Now, I can do this, which looks pretty good and is easier to understand.

return from resId in AllocatateResource(1)
           from unit in UseResource(resId)
           from _ in DeleteResource(resId)
           select _;

But if UseResource returns an Error, there will be an early out and DeleteResource is not called anymore. If I have to use Map and Match Functions to handle this, it will get more complicated again. I tried to use the Prelude.use function and wrap the allocate and delete function in an IDisposable, but in that case the possible Error from DeleteResource cannot be accessed anymore. So my best working state is this:

return from resId in AllocatateResource(1)
       from _ in UseResource(resId).Match(
           Succ: unit => DeleteResource(resId),
           Fail: err => DeleteResource(resId).Match(
                Succ: unit => err,
                Fail: error => error))     // Override the first error with the second one - same as with a throwing finally block in case of an "active" exception
       select _;

The Fail part in the first match clause is my main issue, because I need the Error from the UseResource call and not the success state of the DeleteResource call. In addition I dont like the two calls to DeleteResource.
Is there any smarter/better way of calling the DeleteResource method if AllocatateResource was sucessful and keep the error of the UseResource call?

Thanks for any help :)

@evermanwa
Copy link

Good afternoon, I believe you should post this in the Discussion section instead of the issues. Unless you believe something here doesn't behave as you think it should.

@louthy
Copy link
Owner

louthy commented Aug 31, 2022

You're using Fin<A> here, and Fin<A> is like an Either<Error, A>. What neither Fin, nor Either, do is catch exceptions. That means there no automatic way to clean up resources. And so, you have a couple of options:

  1. Use a different monad which does resource tracking and clean-up
  2. Build a simple function to do the work for you

The only monad that does 1 right now is Effect - which I wouldn't advise using for this. So, you can write a function called use:

public static class Resource
{
    public static Fin<B> use<A, B>(Fin<A> resource, Func<A, Fin<B>> op)
        where A : IDisposable
    {
        try
        {
            return resource.Bind(op);
        }
        catch (Exception e)
        {
            return Fin<B>.Fail(e);
        }
        finally
        {
            resource.Iter(r => r?.Dispose());   
        }
    }
}

Then you can do this:

Resource.use(AllocatateResource(1), 
    resId => from unit in UseResource(resId)
             from _ in DeleteResource(resId)
             select _);

With the Effect monad it's possible to just do this:

    from resId in use(AllocatateResource(1))
    from unit in UseResource(resId)
    select _);

It tracks the resources and cleans them up automatically. I am actually working on bringing resource tracking to every monadic type (for version 5), but for now it's a case of wrapping up the sub-expression with your own function.

For some monadic types there are some use functions already in the Prelude, just not for the value-type monads like Fin, Option, and Either.

One other thing, seeing as it looks like you're doing IO side-effects, I'd recommend using Eff and Aff over Fin. They also have the use functions built into the Prelude - and also have the ability to catch errors, which means you won't have to write them yourself.

@DumDumin
Copy link
Author

DumDumin commented Sep 1, 2022

@louthy thanks for you fast reply!

I thought it would be beneficial to hide the exceptions inside those methods, thats why I changed the return type to Fin and work with the Error struct from there. Is this assumtion flawed?

I tried the solution with the Resource.use function, but it also hides errors, that occur in the Dispose call, if I dont use exceptions. (The Dispose function cannot "return" anything, except an exception :D )

The example with the Effect monad confused me, because I cannot find any use Function for Eff or Aff, that does not take a function as second parameter. But from the use code I found, that solution would have the same Error propagation problem based on the Dispose call, as the Resource.use function.

If I want to get rid of exceptions, I have to go with my presented solution for now I guess.
My main problem is most likely the possibility of DeleteResource to fail, which can happen at any given time (remote call) and I currently have no possibility to change that behaviour in the context of my application.

@louthy
Copy link
Owner

louthy commented Sep 1, 2022

I thought it would be beneficial to hide the exceptions inside those methods, thats why I changed the return type to Fin and work with the Error struct from there. Is this assumtion flawed?

No, but the way to think about the monadic types, like Fin, Either, Eff, etc. is that they all have a set of built-in capabilities, which make them what they are. Fin has the 'alternative value' capability, where the alternative value is an Error, but that's all it does. Try has the 'alternative value' capability where the alternative value is an Exception, but it also has the 'catch exceptions' capability,

The trick for any part of your application is to use the monad with the smallest set of capabilities for the subdomain you're working on. When you need to expand those you pick a 'bigger' monad with more built-in capabilities.

Eff for example has:

  • Alternative values (Error)
  • Try/Catch
  • Laziness
  • Pairs with Aff to support asynchrony
  • Has a declarative runtime version (Eff<RT, A> for dependency injection)

It's our IO monad. It should be at the outer edges of your application.

You can think of the smallest capability being entirely pure functions. That's the 'inner' layer. Then maybe Option, then Try, then Eff etc. Like an onion of expanding capability.

The example with the Effect monad confused me, because I cannot find any use Function for Eff or Aff, that does not take a function as second parameter

Effect and Eff are not the same. It's an unfortunate naming conflict that I'm not super happy about, but it's there now. Effect is part of the Pipes functionality (a composed Producer, Pipe, and Consumer fuse together into an Effect); it's probably not appropriate for what you're doing.

Eff (and it's asynchronous variant Aff) are the monads for doing IO side-effects, and has lots of built-in stuff for dealing with IO (like @catch, scheduled retries, repeats, etc.)

Eff and Aff will eventually gain the capability to track resources automatically, but it's not there today, so you need to use the use function with the second function argument to wrap up usage of the resource.

I tried the solution with the Resource.use function, but it also hides errors, that occur in the Dispose call

You can remove the catch part so exceptions are exposed normally?

@EivindAntonsen
Copy link

The trick for any part of your application is to use the monad with the smallest set of capabilities for the subdomain you're working on. When you need to expand those you pick a 'bigger' monad with more built-in capabilities.

This is a very interesting way to think about the monads. Are the monads and their capabilities listed somewhere for comparison, or is this the type of thing one should just learn over time?

@louthy
Copy link
Owner

louthy commented Sep 22, 2022

The trick for any part of your application is to use the monad with the smallest set of capabilities for the subdomain you're working on. When you need to expand those you pick a 'bigger' monad with more built-in capabilities.

This is a very interesting way to think about the monads. Are the monads and their capabilities listed somewhere for comparison, or is this the type of thing one should just learn over time?

You could check the Features list on the ReadME. Also the reference documentation also includes a number of preambles that I’ve written for each section.

Most of the names of the monadic types are fairly standard for many different FP languages, so googling “Either monad” would likely get you plenty of hits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants