question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Chaining methods - Breaking out when first method provides a Some(x)

See original GitHub issue

First, I love this library. I’m learning so much about FP and this library has helped me a ton!

Question: How do I take the imperative logic below and make it FP using the library?

Problem - Get the first Some value from a chain of delegates that call external sources or provide a default value

I was reading your wiki page on Function Signatures. You stated it is best to stay in context and use Match as a last resort when you need to get to the bound values. I’m trying to figure out the best way to build a pipeline and stay in context (I assume this means that evaluation stays lazy and at the end of the pipeline, I end up with something monadic over T).

Requirements

  • Chain of external sources will return an Option<string>.
  • All external calls should catch exceptions, write them to our telemetry client through an Action<Exception> delegate. and return an Option<string>.None.
  • At the end of the chain of external calls, if the current result is still None then return a default value provided by a Func<string> getDefaultValue = () => {...} delegate.
  • Keep everything lazy. No delegates should be called unless they are needed.
    • Even better if we could memoize some calls but not others.

Example - Imperative

static readonly Action<Exception> WriteExceptionsToTelemetryClient = (Exception ex) => {...};
static Option<string> HandleDelegatesToExternalSources(Func<string> GetStringFromExternalSource)
{
    Option<string> result;
    try
    {
        result = GetStringFromExternalSource();
    }
    catch (Exception ex)
    {
        WriteExceptionsToTelemetryClient(ex);
        result = Option<string>.None;
    }
    return result;
}

static readonly Func<string> GetStringFromFirstExternalSource = () =>  throw new Exception();
static readonly Func<string> GetStringFromSecondExternalSource = () => null;
static readonly Func<Some<string>> GetStringFromSourceThatDoesntThrowExceptions = () => LanguageExt.Some.Create<string>("foo");

// Function signature indicates that caller gets an Option in Some state. Returning None is not an option.
static Some<string> GetString()
{
    Option<string> result;

    // First external source
    result = HandleDelegatesToExternalSources(GetStringFromFirstExternalSource);
    if (result.IsSome) return new Some<string>(result);

    // Second external source
    result = HandleDelegatesToExternalSources(GetStringFromSecondExternalSource);
    if (result.IsSome) return new Some<string>(result);

    // Default value
    return GetStringFromSourceThatDoesntThrowExceptions();
}

The result of calling GetString() is Some<string>("foo"). Also the Telemetry Client wrote an exception.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:5 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
louthycommented, Oct 10, 2018

Can you explain what you mean by “global” fields

Essentially lots of public static fields or properties. It will create a messy set of dependencies that will be hard to reason about and maintain. For example if I have a function prototype like this:

   int Divide(int x, int y);

It should do nothing other than divide two numbers. If behind the scenes it fires off calls to various global services then it hides what’s really going on, that approach over time makes code brittle and hard to deal with.

Now imagine this:

   int Divide(int x, int y, Func<Exception, Unit> telemetry);

It’s obvious to all users of your code that it may do something more than add two numbers together.

The problem with that approach is that there are still possible side-effects happening during the function. Which may have performance implications, race condition implications, or just extra argument to every function implications.

So, we could change Divide to not do any telemetry at all, and just return the result or the error:

   Either<Exception, int> Divide(int x, int y);

Now, that talks to me. The function should have no external dependencies other than the arguments being passed, but the result could be either an Exception or an int. Obviously the exception could come from divide-by-zero, and so now you can imagine what the function might look like:

    Either<Exception, int> Divide(int x, int y) => 
        Try(() => x / y).ToEither();

We now have a declarative and pure function.

So, what happens with the Exception, when does it get logged?

Let’s imagine we had a small API that was declarative and pure like the Divide function:

public class Error : NewType<Error, string>
{
    public Error(string value) : base(value) { }
    public static Error FromEx(Exception e) => new Error(e.Message);
}

Either<Error, int> Div(int x, int y) =>
    Try(() => x / y).ToEither(Error.FromEx);

Either<Error, int> FromString(string value) =>
    parseInt(value).ToEither(Error.New("String can't be parsed into an integer"));

Either<Error, int> Div(string left, string right) =>
    from l in FromString(left)
    from r in FromString(right)
    from n in Div(l, r)
    select n;

Either<Error, int> Add(string left, string right) =>
    from l in FromString(left)
    from r in FromString(right)
    select l + r;

Either<Error, int> Sub(string left, string right) =>
    from l in FromString(left)
    from r in FromString(right)
    select l - r;

Either<Error, int> Mul(string left, string right) =>
    from l in FromString(left)
    from r in FromString(right)
    select l * r;

Either<Error, int> Foo() =>
    from x in Div("48", "2")
    from y in Add("10", "12")
    from z in Sub("5", "3")
    select x + y + z;

Notice how the Error type propagates, and function Foo which is using other possible error throwing functions is relatively more complex. You can maintain this right up to the ‘edges’ of your application. i.e. away from your pure code. That means if something goes wrong, the expression backs out and then you Match on failure and call your telemetry providers there. No need for injection.

That works well if you never recover from failure. In your example you try to return a default value if the others fail (I’d argue this isn’t always great practice, it can make much more sense to propagate the error, but it really does depend on the use-case). But if you do need call IO functionality mid-way through an expression then there are a number of routes to achieve this.

Reader monad

This is a built in type in lang-ext that allows you to pass an environment to your computations. It isn’t always the easiest thing to use, but it is effective in that you can gain access to telemetry without having to explicitly pass an extra argument:

First, you create a type to pass in as the telemetry:

public class World
{
    public readonly Func<string, Unit> SendTelemetry;

    public World(Func<string, Unit> sendTelemetry)
    {
        SendTelemetry = sendTelemetry;
    }
}

Then we’ll add a couple of extension methods that work with Reader<World, A>. Writing bespoke extensions for your environment type tends to make it easier to work with. These will convert between Try<A>, Option<A> to Reader<World, A>:

        public static Reader<World, A> ToReader<A>(this Try<A> ma) => env =>
            ma.Match(
                Succ: x => (x, false),
                Fail: ex =>
                {
                    env.SendTelemetry(ex.ToString());
                    return (default(A), true);
                });

        public static Reader<World, A> ToReader<A>(this Option<A> ma, string message) => env =>
            ma.Match(
                Some: x  => (x, false),
                None: () =>
                {
                    env.SendTelemetry(message);
                    return (default(A), true);
                });

Notice the Try<A> variant will send the telemetry if there’s an exception caught.

If the returning of (x, false) and (default(A), true) looks wierd. Take a look at the declaration of Reader:

namespace LanguageExt
{
    public delegate (A Value, bool IsFaulted) Reader<Env, A>(Env env);
}

It’s just a delegate that takes an Env and returns (A, bool) the bool indicates whether it’s faulted or not.

Before we use the Reader in anger, we can add a helper function to send random telemetry whenever we want:

public static Reader<World, Unit> SendTelemetry(string message) =>
    from world in ask<World>()
    let _ = world.SendTelemetry(message)
    select _;

Notice how the function only takes a string, but somehow returns a Reader<World, Unit>. That’s because of the ask<World>() call. Which goes and gets the environment for you and puts it in world. We can then invoke the injected Func to deliver the message.

Now we can use for real:

static Reader<World, int> Div(int x, int y) =>
    Try(() => x / y).ToReader();

static Reader<World, int> FromString(string value) =>
    parseInt(value).ToReader("String can't be parsed into an integer");

static Reader<World, int> Div(string left, string right) =>
    from l in FromString(left)
    from r in FromString(right)
    from n in Div(l, r)
    from _ in SendTelemetry($"Result of {left} / {right} = {n}")
    select n;

static Reader<World, int> Add(string left, string right) =>
    from l in FromString(left)
    from r in FromString(right)
    from _ in SendTelemetry($"Result of {left} + {right} = {l + r}")
    select l + r;

static Reader<World, int> Sub(string left, string right) =>
    from l in FromString(left)
    from r in FromString(right)
    from _ in SendTelemetry($"Result of {left} - {right} = {l - r}")
    select l - r;

static Reader<World, int> Mul(string left, string right) =>
    from l in FromString(left)
    from r in FromString(right)
    from _ in SendTelemetry($"Result of {left} - {right} = {l * r}")
    select l * r;

Notice the calls to SendTelemetry.

Writer monad

Obviously that still calls the telemetry during the computation. So, another approach is to use a Writer<Exception, A> monad to collect telemetry and then dump it all in one go when you’re done. Instead of using ask to get the environment, you call tell(exception) to log your output.

Roll your own

Another approach is to build your own monadic type. That might be a bit out of your reach at the moment if you’re just getting started, but once you know how to write a Bind function the rest is trivial.

For example if we create a lightweight Maybe monad (which works like the Option monad):

    public class Maybe<A>
    {
        public static Maybe<A> Just(A value) => new Just<A>(value);
        public readonly static Maybe<A> Nothing = new Nothing<A>();
    }

    public class Just<A> : Maybe<A>
    {
        public readonly A Value;
        public Just(A value)
        {
            Value = value;
        }
    }

    public class Nothing<A> : Maybe<A>
    {
        public Nothing()
        {
        }
    }

The Bind function would look like this:

    public static class MaybeExtensions
    {
        public static Maybe<B> Bind<A, B>(this Maybe<A> ma, Func<A, Maybe<B>> f) =>
            ma is Just<A> just
                ? f(just.Value)
                : Maybe<B>.Nothing;
    }

It encapsulates the rules of the monad (i.e. don’t run anything else if we get a Nothing).

Once the Bind is written, the extensions to make it work with LINQ is almost cut n paste:

        public static Maybe<B> Select<A, B>(this Maybe<A> ma, Func<A, B> f) =>
            ma.Bind(x => Maybe<B>.Just(f(x)));

        public static Maybe<B> SelectMany<A, B>(this Maybe<A> ma, Func<A, Maybe<B>> f) =>
            ma.Bind(f);

        public static Maybe<C> SelectMany<A, B, C>(this Maybe<A> ma, Func<A, Maybe<B>> bind, Func<A, B, C> project) =>
            ma.Bind(a => bind(a).Select(b => project(a, b)));

Just change Maybe to the name of your monad and it will work for anything.

The point of this is that you can relatively easily roll your own monadic types, which carry injected functionality and are less cumbersome than Reader or Writer; and easily allow you to change what you do with IO or any external dependencies.

Free monad

And the final, most pure method, is the Free monad approach. This creates true separation between the pure and IO code. I’ve written about this in depth before and there are two samples in the Samples folders that use this approach.

It is very powerful, but requires a reasonable amount of typing, however it’s a reeeeally powerful technique.

1reaction
louthycommented, Oct 8, 2018

I’d do something like this:

using LanguageExt;
using static LanguageExt.Prelude;
using System;

namespace ConsoleApp11
{
    public static class Telemetry
    {
        public static readonly Action<Exception> Client = (Exception ex) =>
        {
            Console.WriteLine(ex.Message);
        };
    }

    public static class TryExtensions
    {
        public static Try<A> LogFailures<A>(this Try<A> ma) =>
            ma.Match(
                Succ: x => Try(x),
                Fail: ex =>
                {
                    Telemetry.Client(ex);
                    return Try<A>(ex);
                });
    }

    class Program
    {
        static readonly Func<string> GetStringFromFirstExternalSource = () => throw new Exception();
        static readonly Func<string> GetStringFromSecondExternalSource = () => null;
        static readonly Func<string> GetStringFromSourceThatDoesntThrowExceptions = () => LanguageExt.Some.Create<string>("foo");

        static string GetStringOrDefault() =>
           GetString().IfNone(GetStringFromSourceThatDoesntThrowExceptions());

        static Option<string> GetString() =>
            Get(Seq(GetStringFromFirstExternalSource, GetStringFromSecondExternalSource));

        static Option<A> Get<A>(Seq<Func<A>> delegates) =>
            delegates.FoldWhile(
                Option<A>.None,
                (state, fun) => Try(fun).LogFailures().ToOption(),
                (state) => state.IsNone);
    }
}

Personally I wouldn’t bother so much with the Some<A> type, mostly because now that I always use Option for optional values, when I see a reference type that isn’t an Option then I know it should never be null. This does require a certain amount of team discipline obviously.

You’ll notice I created an extension method for Try<A> for logging, this is only required if you don’t use the built in error-logger in TryConfig, and instead you want to make a decision on a case-by-case basis whether you want to log or not.

I have split the GetString method up into a more general Get and then less general GetString, and GetStringOrDefault method. This makes each function much simpler and easier to reason about.

One thing I’d warn against is the path you appear to be going down of creating lots of ‘global’ fields. That’s going to get you in to trouble quite quickly. Consider either passing external IO functions through to the functions, or trying to move IO to the edges, or build your own monadic types to capture IO operations.

Pure functions are your best friend, and that’s where you’ll get the most wins from taking a functional approach.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Method chaining - why is it a good practice, or not?
Method chaining is acceptable if it involves performing the same action on the same object - but only if it actually enhances readability,...
Read more >
JavaScript Method Chaining… It's All So STUPID!
First loop/call is a filter. So it loops four times, calling the arrow function four times, returning a new array that has to...
Read more >
object oriented - Method chaining vs encapsulation
One of the consequences of using Method Chaining in internal DSLs is that it usually breaks this principle - each method alters state...
Read more >
Professional Pandas: The Pandas Assign Method and Chaining
To explore uses of the Pandas assign method, I'm going to use a dataset of meteorological data from a ski resort called Alta....
Read more >
Method Chaining - Modern Pandas (Part 2) - Tom's Blog
Method chaining, where you call methods on an object one after another, is in vogue at the moment. It's always been a style...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found