Language-ext: Feedback on usage in nested async scenarios

Created on 3 Jan 2021  路  9Comments  路  Source: louthy/language-ext

I use monads a lot for wrapping code of my WebApi call-chains, where I have to bundle different sources and scenarios.

So what I often come up with a big block that looks like this:

public async Task<Fin<Option<DataB>>> GetDataB(long infoId)
        {
            Fin<DataA> infoResult = 
                await _infoRepository.GetInfo(infoId);

            return await infoResult.Match<Task<Fin<Option<DataB>>>>(
                async info =>
                {
                    return await info.Nested.Match<Task<Fin<Option<DataB>>>>(
                        async nested =>
                            await nested.NextLevel.Match<Task<Fin<Option<DataB>>>>(
                                async derivedId =>
                                    await GetDataB(derivedId),
                                () => Task.FromResult<Fin<Option<DataB>>>(
                                    Error.New("Nested not available for Info")
                                )
                            ),
                            () => Task.FromResult<Fin<Option<DataB>>>(
                                Error.New("Could not obtain info")
                            )
                    );
                },
                f => Task.FromResult<Fin<Option<DataB>>>(
                    f
                )
            );
        }

I'm not coming from a functional programming language and found my way to this style through this library. Therefore I use the library in a way, how it makes sense to me or how I feel it should be used. There may be better ways to do stuff. That's why I want to check with you guys.

At first: Is it recommended to use the library in the way as shown above? Although I feel it brings much more control into my code I'm not sure how it will perform in the end with so many nested tasks and anonymous methods (often I chain multiple methods that all look like the example above).

I find myself typing a lot of template parameters, as it often cannot get detected from usage. E.g. Task.FromResult<Fin<Option<DataB>>>(...). I could write some local methods that wrap those tasks into the correct Fin but this is often refactoring work. Are there other ways to handle this?

All 9 comments

If you share a code example that complies (because you have included stubs of all types and methods involved), them I will snow you how I would write the method in question.

I see, my example was not very clear.

Let's take the following:

        public class ResultObject
        {
            public Option<NestedObject> Nested { get; set; }
        }

        public class NestedObject
        {
            public Option<int> Id { get; set; }
        }

        public Task<Fin<ResultObject>> GetResultObject()
        {
            var x = new ResultObject
            {
                Nested = new NestedObject
                {
                    Id = 42
                }
            };

            return Task.FromResult(FinSucc(x));
        }


        public Task<Fin<Option<string>>> GetStringById(int id)
        {
            var result = Option<string>.None;

            if (id == 42)
            {
                result = "Ok";
            }

            return Task.FromResult(FinSucc(result));
        }

        public Task<Fin<Option<string>>> UserApiRequestResultStringValue()
        {
            var result = GetResultObject();

            //TODO: await result
            //TODO: if result doesn't have Nested return error "nonested"
            //TODO: if Nested doesn't have Id return "noid"
            //TODO: return result of GetStringById()
        }

Doesn't compile. The generic type Fin<> is missing. Also the function FinSucc.

Oh, I forgot to mention Fin<> is available in the beta-releases.
In theory it does a similar job as Result<> (which isn't recommended for direct use, why Fin<> was introduced)

So this should compile for you:

public class ResultObject
        {
            public Option<NestedObject> Nested { get; set; }
        }

        public class NestedObject
        {
            public Option<int> Id { get; set; }
        }

        public Task<Result<ResultObject>> GetResultObject()
        {
            var x = new ResultObject
            {
                Nested = new NestedObject
                {
                    Id = 42
                }
            };

            return Task.FromResult(new Result<ResultObject>(x));
        }


        public Task<Result<Option<string>>> GetStringById(int id)
        {
            var result = Option<string>.None;

            if (id == 42)
            {
                result = "Ok";
            }

            return Task.FromResult(new Result<Option<string>>(result));
        }

        public Task<Result<Option<string>>> UserApiRequestResultStringValue()
        {
            var result = GetResultObject();

            //TODO: await result
            //TODO: if result doesn't have Nested return error "nonested"
            //TODO: if Nested doesn't have Id return "noid"
            //TODO: return result of GetStringById()
        }

Doesn't compile. The generic type Fin<> is missing. Also the function FinSucc.

Oh, I forgot to mention Fin<> is available in the beta-releases.

Got it. Let's keep using Fin<> then.

//TODO: await result
//TODO: if result doesn't have Nested return error "nonested"
//TODO: if Nested doesn't have Id return "noid"
//TODO: return result of GetStringById()

Instead of expressing this in comments, can you write it like you did in your initial comment but with no compiler errors by using your new types ResultObject and NestedObject?

Sure, I would implement it in this way:

        public class ResultObject
        {
            public Option<NestedObject> Nested { get; set; }
        }

        public class NestedObject
        {
            public Option<int> Id { get; set; }
        }

        public static Task<Fin<ResultObject>> GetResultObject()
        {
            var x = new ResultObject
            {
                Nested = new NestedObject
                {
                    Id = 42
                }
            };

            return Task.FromResult(FinSucc(x));
        }


        public static Task<Fin<Option<string>>> GetStringById(int id)
        {
            var result = Option<string>.None;

            if (id == 42)
            {
                result = "Ok";
            }

            return Task.FromResult(FinSucc(result));
        }

        public static async Task<Fin<Option<string>>> UserApiRequestResultStringValue()
        {
            var objectResult = await GetResultObject();
            return await objectResult.Match<Task<Fin<Option<string>>>>(
                obj => obj.Nested.Match<Task<Fin<Option<string>>>>(
                    nested => nested.Id.Match<Task<Fin<Option<string>>>>(
                        id => GetStringById(id),
                        () => Task.FromResult<Fin<Option<string>>>(
                            Error.New("no id")
                        )
                    ),
                    () => Task.FromResult<Fin<Option<string>>>(
                        Error.New("no nested")
                    )
                ),
                f => Task.FromResult<Fin<Option<string>>>(f)
            );
        }

It is hard to work with triply-nested monads. To improve the code further, you probably need to a better job separating pure and impure code. I am just guessing though, I don't really know without seeing more of your code.

Given that we have to work with three monads in a nested fashion, this might be the best way to express things. I think it is more readable than what you had, but I am not satisfied with its readability.

public class ResultObject {
  public Option<NestedObject> Nested { get; set; }
}

public class NestedObject {
  public Option<int> Id { get; set; }
}

public class MyClass {
  public Task<Fin<ResultObject>> GetResultObject() => throw new NotImplementedException();
  public Task<Fin<Option<string>>> GetStringById(int id) => throw new NotImplementedException();

  public Task<Fin<Option<string>>> UserApiRequestResultStringValue() =>
    GetResultObject()
      .Bind(objectResult => objectResult
        .Bind(obj => obj.Nested.ToFin(Error.New("no nested"))
          .Bind(nested => nested.Id.ToFin(Error.New("no id")).Map(GetStringById)))
        .Sequence().Map(x => x.Flatten()));
}

public static class Extensions {
  public static Fin<A> ToFin<A>(this Option<A> ma, Error e) => ma.ToEff(e).RunIO();
}

I am not familiar with these new types Fin<>, Aff<>, and Eff<>. I wanted a way to convert from Option<A> to Fin<A> given an Error. That extension method seems to do the trick.

A couple of things:

```c#
Task>>

Is isomorphic to:
```c#
Aff<Option<DataB>>

So, that will save some unnecessary nesting. You could also leverage the fact that Aff is optional and just use:
```c#
Aff

Which will definitely save a lot of hassle.  

Second, whenever you have nested matches, you have a `from ... in ...`

So,
```c#
    public static Aff<ResultObject> GetResultObject()
    {
        var x = new ResultObject
                {
                    Nested = new NestedObject
                             {
                                 Id = 42
                             }
                };

        return SuccessAff(x);
    }


    public static Aff<string> GetStringById(int id)
    {
        var result = Option<string>.None;

        if (id == 42)
        {
            result = "Ok";
        }

        return result.ToAff(Error.New("Invalid ID"));
    }

    public static Aff<string> UserApiRequestResultStringValue() =>
        from obj in GetResultObject()
        from nst in obj.Nested.ToAff(Error.New("Invalid nested"))
        from nid in nst.Id.ToAff(Error.New("Invalid nested id"))
        from res in GetStringById(nid)
        select res;

To get the task out of it, you can call await aff.RunIO(), but the longer you stay in the Aff context, the more power you have to do simple constructions like the UserApiRequestResultStringValue example above.

There are two Aff types, Aff<A> and Aff<Env, A>. Along with synchronous versions called Eff<A> and Eff<Env, A> - which work side-by-side with the async versions. These are officially going to be published and documented when I release version 4, but are stable today in the beta.

Thank you guys, appreciate it! I like those concepts and learned new things through your feedback. The query is an interesting feature. I will need to play around a bit more with the library to grasp more of the concepts.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jltrem picture jltrem  路  7Comments

TysonMN picture TysonMN  路  4Comments

martasp picture martasp  路  3Comments

bent95 picture bent95  路  6Comments

Zordid picture Zordid  路  5Comments