So far in the book you’ve seen a number of different containers that add some effect to the underlying value(s)—Option
for optionality, IEnumerable
for aggregation, Task
for asynchrony, and so on. As our list of containers keeps growing, we’ll inevitably hit the problem of combining different containers:
If you have a list of Task
s that you want to execute, how can you combine them into a single Task
that will complete when all the operations have completed?
If you have a value of type Task<Validation<T>>
, how do you compose it with a function of type T
→
Task<R>
with the least amount of noise?
This chapter will give you the tools to combine the effects of different containers and show how to avoid an excess of nested containers.
Traverse
is one of the slightly more esoteric core functions in FP, and it allows you to work with lists of elevated values. It’s probably easiest to approach through an example.
Imagine a simple command-line application that reads a comma-separated list of numbers entered by the user and returns the sum of all the given numbers. We could start along these lines:
using Double = LaYumba.Functional.Double; ❶ using String = LaYumba.Functional.String; ❷ var input = Console.ReadLine(); var nums = input.Split(',') // Array<string> .Map(String.Trim) // IEnumerable<string> .Map(Double.Parse); // IEnumerable<Option<double>>
❶ Exposes an Option
-returning function for parsing a double
❷ Exposes a static Trim
function
We split the input string to get an array of strings and remove any whitespace with Trim
. We can then map onto this list the parsing function Double.Parse
, which has the signature string
→
Option<double>
. As a result, we get an IEnumerable<Option<double>>
.
Instead, what we really want is an Option<IEnumerable<double>>
, which should be None
if any of the numbers failed to parse. In this case, we can warn the user to correct their input.1 We saw that Map
yields a type where the effects are stacked up in the opposite order than the one we need.
This is a common enough scenario that there’s a specific function called Traverse
to address it, and a type for which Traverse
is defined is called a traversable. Figure 17.1 shows the relationship between Map
and Traverse
.
Let’s generalize the idea of a traversable:
We have a traversable structure of T
’s, so let’s indicate this with Tr<T>
. In this example, it’s IEnumerable<string>
.
We have a world-crossing function, f
:
T
→
A<R>
, where A
must be at least an applicative. In this example, it’s Double.Parse
, which has type string
→
Option<double>
.
The general signature for Traverse
is in this form
Particularized for this example, it’s
Let’s see how we can go about implementing Traverse
with the preceding signature. If you look at the top-level types in the signature, you’ll see that we start with a list and end up with a single value. Remember, we reduce lists to a single value using Aggregate
, which was covered in section 9.6.
Aggregate
takes an accumulator and a reducer function, which combines each element in the list with the accumulator. The accumulator is returned as the result if the list is empty. This is easy; we just create an empty IEnumerable
and lift it into an Option
using Some
, as the following listing shows.
public static Option<IEnumerable<R>> Traverse<T, R> ( this IEnumerable<T> ts, Func<T, Option<R>> f ) => ts.Aggregate ( seed: Some(Enumerable.Empty<R>()), ❶ func: (optRs, t) => from rs in optRs ❷ from r in f(t) ❸ select rs.Append(r) ❹ );
❶ If the traversable is empty, lifts an empty instance
❷ Extracts the accumulated list of R
's from the Option
❸ Applies the function to the current element, and extracts the value from the resulting Option
❹ Appends the value to the list, and lifts the resulting list into an Option
Now, let’s look at the reducer function—that’s the interesting bit. Its type is
When we apply the function f
to the value t
, we get an Option<R>
. After that, we must satisfy the signature:
Let’s simplify this for a moment by removing the Option
from each element:
Now it becomes clear that the problem is that of appending a single R
to an IEnumerable<R>
, yielding an IEnumerable<R>
with all the elements traversed so far. The appending should happen within the elevated world of Option
because all the values are wrapped in an Option
. As you learned in chapter 10, we can apply functions in the elevated world in the applicative or the monadic way. Here we use the monadic flow.
Now that you’ve seen the definition of Traverse
, let’s go back to the scenario of parsing a comma-separated list of numbers typed by the user. The following listing shows how we can achieve this with Traverse
.
using Double = LaYumba.Functional.Double; using String = LaYumba.Functional.String; var input = Console.ReadLine(); var result = Process(input); Console.WriteLine(result); static string Process(string input) => input.Split(',') // Array<string> .Map(String.Trim) // IEnumerable<string> .Traverse(Double.Parse) // Option<IEnumerable<double>> .Map(Enumerable.Sum) // Option<double> .Match ( () => "Some of your inputs could not be parsed", (sum) => $"The sum is {sum}" );
In the preceding listing, the top-level statements perform I/O, while all the logic is in the Process
function. Let’s test it to see the behavior:
Process("1, 2, 3") // => "The sum is 6" Process("one, two, 3") // => "Some of your inputs could not be parsed"
Let’s improve error handling so we can tell the user which values are wrong. For that, we need Validation
, which can contain a list of errors. This means we’ll need an implementation of Traverse
that takes a list of values and a Validation
-returning function. This is shown in the following listing.
public static Validation<IEnumerable<R>> TraverseM<T, R> ( this IEnumerable<T> ts, Func<T, Validation<R>> f ) => ts.Aggregate ( seed: Valid(Enumerable.Empty<R>()), func: (valRs, t) => from rs in valRs from r in f(t) select rs.Append(r) );
This implementation is similar to the implementation that takes an Option
-returning function (listing 17.1) except for the signature and the fact that the Return
function being used is Valid
instead of Some
. This duplication is due to the lack of an abstraction common to both Option
and Validation
.2
Notice that I’ve called the function TraverseM
(for monadic) because the implementation is monadic. If one item fails validation, the validation function won’t be called for any of the subsequent items.
If, instead, we want errors to accumulate, we should use the applicative flow (if you need a refresher on why this is the case, refer back to section 10.5). We can, therefore, define TraverseA
(for applicative) with the same signature but using the applicative flow, as the following listing shows.
static Func<IEnumerable<T>, T, IEnumerable<T>> Append<T>() => (ts, t) => ts.Append(t); public static Validation<IEnumerable<R>> TraverseA<T, R> ( this IEnumerable<T> ts, Func<T, Validation<R>> f ) => ts.Aggregate ( seed: Valid(Enumerable.Empty<R>()), ❶ func: (valRs, t) => Valid(Append<R>()) ❷ .Apply(valRs) ❸ .Apply(f(t)) ❹ ); public static Validation<IEnumerable<R>> Traverse<T, R> (this IEnumerable<T> list, Func<T, Validation<R>> f) => TraverseA(list, f); ❺
❶ If the traversable is empty, lifts an empty instance
❷ Lifts the Append
function, particularized for R
❸ Applies it to the accumulator
❹ Applies f
to the current element; the R
wrapped in the resulting Validation
is the second argument to Append
.
❺ For Validation
, Traverse
should default to the applicative implementation.
The implementation of TraverseA
is similar to TraverseM
, except that in the reducer function, the appending is done with the applicative rather than the monadic flow. As a result, the validation function f
is called for each T
in ts
, and all validation errors will accumulate in the resulting Validation
.
Because this is the behavior we usually want with Validation
, I’ve defined Traverse
to point to the applicative implementation TraverseA
, but there’s still scope for having TraverseM
if you want a short-circuiting behavior.
The following listing shows the program, refactored to use Validation
.
static Validation<double> Validate(string s) => Double.Parse(s).Match ( () => Error($"'{s}' is not a valid number"), (d) => Valid(d) ); static string Process(string input) => input.Split(',') // Array<string> .Map(String.Trim) // IEnumerable<string> .Traverse(Validate) // Validation<IEnumerable<double>> .Map(Enumerable.Sum) // Validation<double> .Match ( errs => string.Join(", ", errs), sum => $"The sum is {sum}" );
The listing only shows the updated implementation of Process
(the top-level statements are the same as previously). If we test this enhanced implementation, we now get this:
Process("1, 2, 3") // => "The sum is 6" Process("one, two, 3") // => "'one' is not a valid number, 'two' is not a valid number"
As you can see, in the second example, the validation errors have accumulated as we have traversed the list of inputs. If we used the monadic implementation, TraverseM
, we’d only get the first error.
The preceding example demonstrates how to apply a single validation function to a list of values you want to validate. What about the case in which you have a single value to validate and many validation functions?
We tackled such a scenario in section 9.6.3, where we had a request object to validate and a list of validators, each checking that certain conditions for validity are met. As a reminder, we defined a Validator
delegate to capture a function performing validation:
The challenge was to write a single Validator
function combining the validation of a list of Validator
’s, harvesting all errors. We had to jump through a few hoops to define a HarvestErrors
function with this behavior (listing 9.22).
Now that you know about using Traverse
with a Validation
-returning function, we can rewrite HarvestErrors
much more concisely, as the following listing shows.
public static Validator<T> HarvestErrors<T> (params Validator<T>[] validators) => t => validators .Traverse(validate => validate(t)) .Map(_ => t);
Here, Traverse
returns a Validation<IEnumerable<T>>
, collecting all the errors. If there are no errors, the inner value of type IEnumerable<T>
will contain as many instances of the input value t
as there are validators. The subsequent call to Map
disregards this IEnumerable
and replaces it with the original object being validated. Here’s an example of using HarvestErrors
in practice:
Validator<string> ShouldBeLowerCase = s => (s == s.ToLower()) ? Valid(s) : Error($"{s} should be lower case"); Validator<string> ShouldBeOfLength(int n) => s => (s.Length == n) ? Valid(s) : Error($"{s} should be of length {n}"); Validator<string> ValidateCountryCode = HarvestErrors(ShouldBeLowerCase, ShouldBeOfLength(2)); ValidateCountryCode("us") // => Valid(us) ValidateCountryCode("US") // => Invalid([US should be lower case]) ValidateCountryCode("USA") // => Invalid([USA should be lower case, USA should be of length 2])
Traverse
works with Task
much as it does with Validation
. We can define TraverseA
, which uses the applicative flow and runs all tasks in parallel, TraverseM
, which uses the monadic flow and runs the tasks sequentially, and Traverse
, which defaults to TraverseA
because running independent asynchronous operations in parallel is usually preferable. Given a list of long-running operations, we can use Traverse
to obtain a single Task
that we can use to await all the results.
In section 16.1.7, we looked at comparing flight fares from two airlines. With Traverse
, we’re equipped to deal with a list of airlines. Imagine that each airline’s flights can be queried with a method that returns a Task<IEnumerable<Flight>>
, and we want to get all flights available on a given date and route, sorted by price:
interface Airline { Task<IEnumerable<Flight>> Flights (string origin, string destination, DateTime departure); }
How do we get all flights from all the airlines? Notice what happens if we use Map
:
IEnumerable<Airline> airlines; IEnumerable<Task<IEnumerable<Flight>>> flights = airlines.Map(a => a.Flights(from, to, on));
We end up with an IEnumerable<Task<IEnumerable<Flight>>>
. This isn’t at all what we want!
With Traverse
, instead, we’ll end up with a Task<IEnumerable<IEnumerable <Flight>>>
, a single task that completes when all airlines have been queried (and fails if any query fails). The task’s inner value is a list of lists (one list for each airline), which can then be flattened and sorted to get our list of results sorted by price:
async Task<IEnumerable<Flight>> Search(IEnumerable<Airline> airlines , string origin, string dest, DateTime departure) { var flights = await airlines .Traverse(a => a.Flights(origin, dest, departure)); return flights.Flatten().OrderBy(f => f.Price); }
Flatten
is simply a convenience function that calls Bind
with the identity function, hence flattening the nested IEnumerable
into a single list including flights from all airlines. This list is then sorted by price.
Most of the time, you’ll want the parallel behavior, so I’ve defined Traverse
to be the same as TraverseA
. On the other hand, if you have 100 tasks and the second task fails, then monadic traverse will save you from running 98 tasks that would still be kicked off when using applicative traverse. So the implementation you choose depends on the use case, and this is the reason for including both.
Let’s look at one final variation on this example. In real life, you probably don’t want your search to fail if one of perhaps dozens of queries to a third-party API fails. Imagine you want to display the best results available, like on many price comparison websites. If a provider’s API is down, results from that provider won’t be available, but we still want to see results from all the others.
The change is easy! We can use the Recover
function shown in section 16.1.4 so that each query returns an empty list of flights if the remote query fails:
async Task<IEnumerable<Flight>> Search(IEnumerable<Airline> airlines , string origin, string dest, DateTime departure) { var flights = await airlines .Traverse(a => a.Flights(origin, dest, departure) .Recover(ex => Enumerable.Empty<Flight>())); return flights.Flatten().OrderBy(f => f.Price); }
Here we have a function that queries several APIs in parallel, disregards any failures, and aggregates all the successful results into a single list sorted by price. I find this a great example of how composing core functions like Traverse
, Bind
, and others allows you to specify rich behavior with little code and effort.
So far you’ve seen how to use Traverse
with an IEnumerable
and a function that returns an Option
, Validation
, Task
, or any other applicative. It turns out that Traverse
is even more general. IEnumerable
isn’t the only traversable structure out there; you can define Traverse
for many of the constructs you’ve seen in the book. If we take a nuts and bolts approach, we can see Traverse
as a utility that stacks effects up the opposite way as when performing Map
:
If we have a function that returns an applicative A
, Map
returns a type with the A
on the inside, whereas Traverse
returns a type with the A
on the outside.
For example, in chapter 8, we had a scenario in which we used Map
to combine a Validation
-returning function with an Exceptional
-returning function. The code went along these lines:
Func<MakeTransfer, Validation<MakeTransfer>> validate; Func<MakeTransfer, Exceptional<Unit>> save; public Validation<Exceptional<Unit>> Handle(MakeTransfer request) => validate(request).Map(save);
What if, for some reason, we wanted to return an Exceptional<Validation<Unit>>
instead? Well, now you know the trick: just replace Map
with Traverse
!
public Exceptional<Validation<Unit>> Handle(MakeTransfer request) => validate(request).Traverse(save);
But can we make Validation
traversable? The answer is yes. Remember that we can view Option
as a list with at most one element. The same goes for Either
, Validation
, and Exceptional
: the success case can be treated as a traversable with a single element; the failure case as empty.
In this scenario, we need a definition of Traverse
taking a Validation
and an Exceptional
-returning function. The following listing shows the implementation.
public static Exceptional<Validation<R>> Traverse<T, R> ( this Validation<T> valT, Func<T, Exceptional<R>> f ) => valT.Match ( Invalid: errs => Exceptional(Invalid<R>(errs)), Valid: t => f(t).Map(Valid) );
The base case is if the Validation
is Invalid
; that’s analogous to the empty list case. Here we create a value of the required output type, preserving the validation errors. If the Validation
is Valid
, that means we should “traverse” the single element it contains, named t
. We apply the Exception
-returning function f
to it to get an Exceptional<R>
and then we Map
the Valid
function on it, which lifts the inner value r
into a Validation<R>
, giving us the required output type, Exceptional<Validation<R>>
.
You can follow this pattern to define Traverse
for the other one-or-no-value structures. Notice that once you’ve defined Traverse
, then when you have, say, a Validation<Exceptional<T>>
and want to reverse the order of the effects, you could just use Traverse
with the identity function.
In summary, Traverse
is useful not just to handle lists of elevated values but, more generally, whenever you have stacked effects. As you encode your application’s requirements through Option
, Validation
, and others, Traverse
is one of the tools you can use to ensure that the types don’t get the better of you.
Most enterprise applications are distributed and depend on a number of external systems, so much if not most of your code runs asynchronously. If you want to use constructs like Option
or Validation
, soon enough you’ll deal with Task<Option<T>>
, Task<Validation<T>>
, Validation<Task<T>>
, and so on.
These nested types can be difficult to work with. When you work within one monad, such as Option
, everything is fine because you can use Bind
to compose several Option
-returning computations. But what if you have a function that returns an Option<Task<T>>
and another function of type T
→
Option<R>
? How can you combine them? And how would you use an Option<Task<T>>
with a function of type T
→
Task<Option<R>>
?
We can refer to this more generally as the problem of stacked monads. In order to illustrate this problem and how it can be addressed, let’s revisit one of the examples from chapter 13. The following listing shows a skeletal version of the endpoint that handles API requests to make a transfer.
public static void ConfigureMakeTransferEndpoint ( WebApplication app, Func<Guid, AccountState> getAccount, Action<Event> saveAndPublish ) => app.MapPost("/Transfer/Make", (MakeTransfer cmd) => ❶ { var account = getAccount(cmd.DebitedAccountId); ❷ var (evt, newState) = account.Debit(cmd); ❸ saveAndPublish(evt); ❹ return Ok(new { newState.Balance }); ❺ });
❸ Performs the state transition; returns a tuple with the event and the new state
❹ Persists the event and publishes it to interested parties
❺ Returns information to the client about the new state
The preceding code serves as an outline. Next, you’ll see how to add asynchrony, error handling, and validation.
First, we’ll inject a new dependency to perform validation on the MakeTransfer
command. Its type will be Validator<MakeTransfer>
, which is a delegate with this signature:
Next, we need to revise the signatures of the existing dependencies. When we call getAccount
to retrieve the current state of an account, that operation will hit the database. We want to make it asynchronous, so the result type should be wrapped in a Task
. Furthermore, errors can occur when connecting to the DB. Fortunately, Task
already captures this. Finally, there’s the possibility that the account doesn’t exist (no events were ever recorded for the given ID), so the result should also be wrapped in an Option
. The full signature will be
Saving and publishing an event should also be asynchronous so that the signature should be
Finally, remember that Account.Debit
also returns its result wrapped in a Validation
:
Now let’s write a skeleton of the command handler with all these effects in place:
public static void ConfigureMakeTransferEndpoint ( WebApplication app, Validator<MakeTransfer> validate, Func<Guid, Task<Option<AccountState>>> getAccount, Func<Event, Task> saveAndPublish ) => app.MapPost("/Transfer/Make", (MakeTransfer transfer) => { Task<Validation<AccountState>> outcome = // ... return outcome.Map ( Faulted: ex => StatusCode(StatusCodes.Status500InternalServerError), Completed: val => val.Match ( Invalid: errs => BadRequest(new { Errors = errs }), Valid: newState => Ok(new { Balance = newState.Balance }) ) ); });
So far, we’ve listed the dependencies with the new signatures, established that the main workflow will return a Task<Validation<AccountState>>
(because there will be some asynchronous operations, and there will be some validation), and mapped its possible states to appropriately populated HTTP responses. Now comes the real work: How do we put together the functions we need to consume?
First, we’ll need a couple of adapters. Notice that getAccount
returns an Option
(wrapped in a Task
), meaning we should cater for the case in which no account is found. What does it mean if there’s no account? It means the command is incorrectly populated, so we can map None
to a Validation
with an appropriate error.
LaYumba.Functional
defines ToValidation
, a natural transformation that “promotes” an Option
to a Validation
. It maps Some
to Valid
, using the Option
's inner value, and None
to Invalid
, using the provided Error
:
public static Validation<T> ToValidation<T> (this Option<T> opt, Error error) => opt.Match ( () => Invalid(error), (t) => Valid(t) );
In the case of getAccount
, the returned Option
is wrapped in a Task
so that we don’t apply ToValidation
directly, but use Map
instead:
Func<Guid, Task<Option<AccountState>>> getAccount; Func<Guid, Task<Validation<AccountState>>> getAccountVal = id => getAccount(id) .Map(opt => opt.ToValidation(Errors.UnknownAccountId(id)));
At least now we’re only dealing with two monads: Task
and Validation
.
Second, saveAndPublish
returns a Task
, which doesn’t have an inner value, so it won’t compose well. Let’s write an adapter that returns a Task<Unit>
instead:
Func<Event, Task> saveAndPublish; Func<Event, Task<Unit>> saveAndPublishF = async e => { await saveAndPublish(e); return Unit(); };
Let’s look again at the functions we must compose in order to compute the workflow’s outcome:
validate : MakeTransfer→
Validation<MakeTransfer> getAccountVal : Guid→
Task<Validation<AccountState>> Account.Debit : AccountState→
MakeTransfer→
Validation<(Event, AccountState)> saveAndPublishF : Event→
Task<Unit>
If we used Map
the whole way through, we’d get a result type of Validation<Task <Validation<Validation<Task<Unit>>>>>
. We could try using a sophisticated combination of calls to Traverse
to change the order of monads and Bind
to flatten them. Honestly, I tried. It took me about half an hour to figure it out, and the result was cryptic and not something you’d be keen to ever refactor!
We have to look for a better way. Ideally, we’d like to write something like this:
from tr in validate(transfer) from acc in GetAccount(tr.DebitedAccountId) from result in Account.Debit(acc, tr) from _ in SaveAndPublish(result.Event) select result.NewState
We’d then have some underlying implementations of Select
and SelectMany
that figure out how to combine the types together. Unfortunately, this can’t be achieved in a general enough way: add too many overloads of SelectMany
and this will cause overload resolution to fail. The good news is that we can have a close approximation. You’ll see this next.
We can implement Bind
and the LINQ query pattern for a specific monad stack; in this case, Task<Validation<T>>
.3 This allows us to compose several functions that return a Task<Validation<>>
within a LINQ expression. With this in mind, we can adapt our existing functions to this type by following these rules:
If we have a Task<Validation<T>>
(or a function that returns such a type), then there’s nothing to do. That’s the monad we’re working in.
If we have a Validation<T>
, we can use the Async
function to lift it into a Task
, obtaining a Task<Validation<T>>
.
If we have a Task<T>
, we can map the Valid
function onto it to again obtain Task<Validation<T>>
.
If we have a Validation<Task<T>>
, we can call Traverse
with the identity function to swap the containers around.
So our previous query needs to be modified as follows:
from tr in Async(validate(transfer)) ❶ from acc in GetAccount(tr.DebitedAccountId) ❷ from result in Async(Account.Debit(acc, tr)) ❶ from _ in SaveAndPublish(result.Event).Map(Valid) ❸ select result.NewState;
❶ Uses Async
to lift the Validation
into a Task<Validation<>>
❷ GetAccount
returns a Task<Validation<>>
, which is the monad stack we’re working with.
❸ Uses Map(Valid)
to turn a Task
into a Task<Validation<>>
This will work as long as the appropriate implementations of Select
and SelectMany
for Task<Validation<T>>
are defined. As you can see, the resulting code is still reasonably clean and easy to understand and refactor. We just had to add a few calls to Async
and Map(Valid)
to make the types line up. The following listing shows the complete implementation of the command handler, refactored to include asynchrony and validation.
public static void ConfigureMakeTransferEndpoint ( WebApplication app, Validator<MakeTransfer> validate, Func<Guid, Task<Option<AccountState>>> getAccount, Func<Event, Task> saveAndPublish ) { var getAccountVal = (Guid id) => getAccount(id) .Map(opt => opt.ToValidation(Errors.UnknownAccountId(id))); var saveAndPublishF = async (Event e) => { await saveAndPublish(e); return Unit(); }; app.MapPost("/Transfer/Make", (MakeTransfer transfer) => { Task<Validation<AccountState>> outcome = from tr in Async(validate(transfer)) from acc in getAccountVal(tr.DebitedAccountId) from result in Async(Account.Debit(acc, tr)) from _ in saveAndPublishF(result.Event).Map(Valid) select result.NewState; return outcome.Map ( Faulted: ex => StatusCode(StatusCodes.Status500InternalServerError), Completed: val => val.Match ( Invalid: errs => BadRequest(new { Errors = errs }), Valid: newState => Ok(new { Balance = newState.Balance }) ) ); }); }
Let’s look at the code. First, the operations that need to be completed as part of the workflow are injected as dependencies. Next, we have a couple of adapter functions to go from Option
to Validation
and from Task
to Task<Unit>
. We then configure the endpoint that handles transfer requests. Here, we use a LINQ comprehension to combine the different operations in the workflow. Finally, we translate the resulting outcome into an object representing the HTTP response we’d like to return.
As you saw in this chapter, although monads are nice and pleasant to work with in the context of a single monadic type, things get more complicated when you need to combine several monadic effects. Note that this isn’t just the case in C# but even in functional languages. Even in Haskell, where monads are used everywhere, stacked monads are usually dealt with via the rather clunky monad transformers. A more promising approach is called composable effects, and it has first-class support in a rather niche-functional language called Idris. It’s possible that the programming languages of the future will not only have syntax optimized for monads, such as LINQ, but also syntax optimized for monad stacks.
As a practical guideline, remember that combining several monads adds complexity, and limit the nesting of different monads to what you really need. For instance, once we simplified things in the previous example by transforming Option
to Validation
, we only had to deal with two stacked monads rather than three. Similarly, if you have a Task<Try<T>>
, you can probably reduce it to a Task<T>
because Task
can capture any exceptions raised when running the Try
. Finally, if you find yourself always using a stack of two monads, you can write a new type that encapsulates both effects into that single type. For example, Task
encapsulates both asynchrony and error handling.
If you have two monads, A
and B
, you might like to stack them up in values like A<B<T>>
to combine the effects of both monads.
You can use Traverse
to invert the order of monads in the stack.
Implementing the LINQ query pattern for such a stack allows you to combine A
’s, B
’s, and A<B<>>
’s with relative ease.
Still, stacked monads tend to be cumbersome, so use them sparingly.
1 You may remember from chapter 6 that we could use Bind
instead of Map
to filter out all the None
values and only add up the numbers that were successfully parsed. That’s not desirable for this scenario: we’d be silently removing values that the user probably mistyped in error, effectively giving an incorrect result.
2 The reasons for this were discussed in chapter 6, in the “Why is functor not an interface?” sidebar.
3 I won’t show the implementation, which is included in the code samples. This really is library code, not code that a library user should worry about. You may also ask whether an implementation is required for every stack of monads, and indeed, this is the case, given the pattern-based approach we’ve been following in the book.
18.221.231.97