Chaining methods - Breaking out when first method provides a Some(x)
See original GitHub issueFirst, 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 anOption<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:
- Created 5 years ago
- Comments:5 (4 by maintainers)
Top 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 >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
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:
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:
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: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 anint
. Obviously the exception could come from divide-by-zero, and so now you can imagine what the function might look like: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:Notice how the
Error
type propagates, and functionFoo
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 youMatch
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:
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 betweenTry<A>
,Option<A>
toReader<World, A>
: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 ofReader
:It’s just a
delegate
that takes anEnv
and returns(A, bool)
thebool
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:Notice how the function only takes a
string
, but somehow returns aReader<World, Unit>
. That’s because of theask<World>()
call. Which goes and gets the environment for you and puts it inworld
. We can then invoke the injectedFunc
to deliver the message.Now we can use for real:
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 usingask
to get the environment, you calltell(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 theOption
monad):The
Bind
function would look like this: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: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
orWriter
; 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.
I’d do something like this:
Personally I wouldn’t bother so much with the
Some<A>
type, mostly because now that I always useOption
for optional values, when I see a reference type that isn’t anOption
then I know it should never benull
. 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 inTryConfig
, 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 generalGet
and then less generalGetString
, andGetStringOrDefault
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.