© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
K. EasonStylish F# 6https://doi.org/10.1007/978-1-4842-7205-3_11

11. Railway Oriented Programming

Kit Eason1  
(1)
Farnham, Surrey, UK
 

On two occasions I have been asked, “Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out?” I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question.

—Charles Babbage, Computer Pioneer

Going Off the Rails

Railway Oriented Programming (ROP) is an analogy invented by F#’s premier educator, Scott Wlaschin. It describes a programming philosophy in which we embrace errors as a core part of program flow, rather than exiling them to the separate domain of exception handling. Scott didn’t invent the technique, but he did invent the analogy, which has helped many F# developers understand this initially daunting but powerful technique. Although I’ve titled this chapter according to Scott’s analogy, I’m going to use a slightly different way to describe what is going on. Rest assured, I am still talking about ROP. I just thought it might be interesting to look at it using an alternative mental image. You may want to treat this chapter as a companion piece to Scott’s own description of ROP, which you can find (among a cornucopia of other invaluable material) at https://fsharpforfunandprofit.com.

On the Factory Floor

You’ve decided to get into the widget business! You are going to build your very own highly automated widget factory. You’ll make them so cheaply the entire widget industry will be disrupted. Investors and admirers will flock to your door!

One small problem – how do you lay this factory out? Widget making is a complex process: machining the raw material into shape, polishing certain parts, coating other parts, bolting on subassemblies, and so on. You want to keep your factory compact and easy to manage; otherwise, you’ll just be another widget wannabe. Your initial layout design is like this (Figure 11-1).
../images/462726_2_En_11_Chapter/462726_2_En_11_Fig1_HTML.png
Figure 11-1

Naive layout for a widget factory

Each box represents a machine tool performing one of the manufacturing processes. Leading into each box is a conveyor for taking items into the machine, and leading out of each box is another conveyor that takes items on to the next process. The conveyor coming out of a machine will not be the same as the one going in because work items that come out will be a different shape from what goes in. Luckily, numerous styles of conveyors are available, so you just pick (or even build) the conveyor style to suit each stage. Laying out the machines is pretty straightforward: you place them in a line, in the order the processes must be performed. This way the conveyors with matching styles naturally link up. You can hardly go wrong.

You show your production line design to an experienced manufacturing engineer. But she isn’t impressed.

“What about quality control?” she asks. “Is the whole production line going to stop every time a single step goes wrong for one widget?”

Shamefaced you return, literally, to the drawing board. Diversion is your answer! Within each process, there will be a quality control step. If the widget being processed fails that step, it is shot out on a different conveyor and into a rejects hopper (Figure 11-2).
../images/462726_2_En_11_Chapter/462726_2_En_11_Fig2_HTML.png
Figure 11-2

Simple handling for rejects

You show the engineer your new layout.

“That’s better,” she says, “But still not great. Someone will have to keep an eye on all those rejects hoppers, and they’re scattered all the way along the line.” She grabs the pencil. “Maybe you want something more like this?” (Figure 11-3).
../images/462726_2_En_11_Chapter/462726_2_En_11_Fig3_HTML.png
Figure 11-3

Combining rejects

This solves the multiple-rejects-hoppers problem, but it’s messy in other ways. It’s going to be fiddly linking up both sets of conveyors, especially with the rejects conveyors sticking out of the side like that. It feels repetitive somehow.

“No worries,” says the engineer. “I know some folks who build special adapter housings for machine tools. The housing has two conveyors going in. The main one takes good parts into the machine tool inside. The other one takes rejects in and just passes them straight on out again. If there is a new reject from the current machine, it gets put onto the rejects conveyer along with any existing rejects. Once you’ve put each of your machine tools in one of those housings, you can join the housings together as simply as your original concept.” She draws another diagram (Figure 11-4).
../images/462726_2_En_11_Chapter/462726_2_En_11_Fig4_HTML.png
Figure 11-4

Adapter housings for easier process linkage

This is getting exciting, but you need to check your understanding.

“So internally the housing looks something like this, right? The machine tool needs to put rejects from its own process on the rejects conveyer, and good parts on the main out-conveyor. And it can pass through incoming rejects untouched?” (Figure 11-5).
../images/462726_2_En_11_Chapter/462726_2_En_11_Fig5_HTML.png
Figure 11-5

Adapter housing detail

“Spot on,” replies the engineer. “And you’re going to need a couple of other housings. I’m guessing some of your processes never fail, so better include a housing which just passes rejects through” (Figure 11-6).
../images/462726_2_En_11_Chapter/462726_2_En_11_Fig6_HTML.png
Figure 11-6

Adapter housing detail for processes that never fail

“And you’ll also need to pay a bit of attention to what happens at the end of the line. Do you really want to just toss all the rejects into the trash? I think you might want to count them by kind of failure, report them, or something like that. If so, you’ll need the reject adapter housing” (Figure 11-7).
../images/462726_2_En_11_Chapter/462726_2_En_11_Fig7_HTML.png
Figure 11-7

Reject adapter housing detail

“You can put in any machine you like to handle incoming rejects. It might pass them on in some form, it might report them, or it might just destroy them. Good inputs just pass straight through the adapter untouched.”

“What about the first machine on the line?” you ask. “Won’t that need a special type of adapter?”

“Nope,” replies the engineer. “If it’s a typical machine that takes a good input and produces a good output or a failure, it can just sit at the front of the line, because its good/bad outputs will fit into the second machine’s adapted inputs.”

This makes so much sense, and you’re keen to get on with the details.

“Can you hook me up with the folks who make these magic housings?” you ask.

“Sure!” replies the engineer. “There’s just the small matter of my fee.”

“Will you accept stock options?” you ask….

Adapting Functions for Failure

In the widget manufacturing example, your initial stab at rejects handling (Figure 11-2) is like the concept of raising and handling exceptions in .NET languages like C# and F#. When something goes wrong, you “jump out” of the natural sequence of processing (you raise an exception), and what happens to that exception is of no concern to the local section of code. The exception will be handled elsewhere or (all too often) simply ignored (Listing 11-1).
open System
let checkString (s : string) =
    if isNull(s) then
        raise <| ArgumentNullException("Must not be null")
    elif String.IsNullOrEmpty(s) then
        raise <| ArgumentException("Must not be empty")
    elif String.IsNullOrWhiteSpace(s) then
        raise <| ArgumentException("Must not be white space")
    else
        s
// I love F#
let r1 = checkString "I love F#"
r1
// Error: System.ArgumentException: Must not be white space
// let r2 = checkString " "
Listing 11-1

Raising an exception. Where she stops, nobody knows!

This makes the type signature of the function a lie: the function can really either return its official result type or an exception. This is, arguably, a violation of the principle of semantic focus. You can’t tell from the outside (by its signature) what kinds of things a function will do under all circumstances; and you can’t tell from the inside (looking at the body of the function) whether the function’s callers have any strategy at all for handling errors. The aim of ROP is to get away from this by making failures part of the signature of a function and by providing a bypass mechanism so that, as in Figure 11-4, functions can be joined together in such a way that failures whizz past any later functions in the production line.

Writing a Bypass Adapter

Although the “adapters” you’ll need do exist in F#, it’s worth trying to write a couple of them from scratch, as this makes it much easier to understand how the whole concept works. Let’s start with the adapter from Figure 11-5. It needs to take a function (the equivalent to the machine tool hidden within the adapter housing) and an input (the equivalent of an incoming, partially made widget). If the input is currently valid, it needs to be processed using the supplied function. If the input is already a failure, it needs to be passed through untouched.

Since a function can only have one type, this means we need to bundle together good values and failures in the same type. And by now you probably realize that bundling different things together usually means a Discriminated Union. Let’s call it Outcome (Listing 11-2).
type Outcome<'TSuccess, 'TFailure> =
    | Success of 'TSuccess
    | Failure of 'TFailure
Listing 11-2

An Outcome Discriminated Union

In Listing 11-2, there’s a Success case and a Failure case. We keep the payload types of the DU generic using 'TSuccess and 'TFailure because we don’t want to commit to a specific payload type for either the success or the failure path.

Now we need to write the adapter itself. Let’s start with a spot of pseudocode.
  • Take a function and an input (which might already be a success or a failure).

  • If the input is valid so far, pass it to the supplied function.

  • If the input is already an error, pass it through untouched.

It only takes a few lines of F# to achieve this (Listing 11-3).
type Outcome<'TSuccess, 'TFailure> =
    | Success of 'TSuccess
    | Failure of 'TFailure
let adapt func input =
    match input with
    | Success x -> func x
    | Failure f -> Failure f
Listing 11-3

The basic adapter in code

Writing a Pass-Through Adapter

Now we need the second kind of adapter the manufacturing engineer suggested (Figure 11-6): a “pass-through” adapter, which is used to wrap processes that can’t themselves fail and which allows failure inputs to whizz by (Listing 11-4).
let passThrough func input =
    match input with
    | Success x -> func x |> Success
    | Failure f -> Failure f
Listing 11-4

The pass-through adapter in code

Listing 11-4 is almost laughably similar to Listing 11-3; I have highlighted the only difference. Whereas the func of Listing 11-3 is itself capable of returning Success or Failure, the func of Listing 11-4 is (by definition) one which can’t fail. Therefore, to let it participate in the pipeline, its result has to be wrapped in a Success case. So we simply say func x |> Success.

Building the Production Line

Now we’ll need an example process to try out this new concept. Let’s take a requirement to accept a password, validate it in various ways, and then save it if it is valid. The validations will be the following:
  • The password string can’t be null, empty, or just whitespace.

  • It must contain mixed case alphabetic characters.

  • It must contain at least one of these characters: - _ ! ?

  • Any leading/trailing whitespace must be trimmed.

The password should be saved to a database if valid; if not, there needs to be an error message.

Listing 11-5 shows the code to perform each of these steps individually. (We haven’t joined them into a pipeline yet.)
 open System
let notEmpty (s : string) =
    if isNull(s) then
        Failure "Must not be null"
    elif String.IsNullOrEmpty(s) then
        Failure "Must not be empty"
    elif String.IsNullOrWhiteSpace(s) then
        Failure "Must not be white space"
    else
        Success s
let mixedCase (s : string) =
    let hasUpper =
        s |> Seq.exists (Char.IsUpper)
    let hasLower =
        s |> Seq.exists (Char.IsLower)
    if hasUpper && hasLower then
        Success s
    else
        Failure "Must contain mixed case"
let containsAny (cs : string) (s : string) =
    if s.IndexOfAny(cs.ToCharArray()) > -1 then
        Success s
    else
        Failure (sprintf "Must contain at least one of %A" cs)
let tidy (s : string) =
    s.Trim()
let save (s : string) =
    let dbSave s : unit =
        printfn "Saving password '%s'" s
        // Uncomment this to simulate an exception:
        // raise <| Exception "Dummy exception"
    let log m =
        printfn "Logging error: %s" m
    try
        dbSave s
        |> Success
    with
    | e ->
        log e.Message
        Failure "Sorry, there was an internal error saving your password"
Listing 11-5

Some password validation code

The exact details of the code in Listing 11-5 are less important than the general pattern of these functions: if validation succeeds, they return a value wrapped in a Success case. If validation fails, they return an error message wrapped in a Failure case. The save() function is slightly more complicated: it handles any exceptions that come back from writing to the (imaginary) database and returns a message wrapped in a Failure case if an exception occurred. It just happens that the result of a successful database save operation is just unit, but unit can still be returned wrapped in a Success like any other type. The tidy() function is an example of a “can’t fail” process (assuming the string isn’t null, which is tackled in an earlier step).

Now we need to make sure these functions are all called in the right order – the equivalent of rolling the machines onto the factory floor, putting them inside their adapters, and bolting them all together into a production line. Listing 11-6 shows a first cut of this stage. (It assumes that the Outcome DU, the adapt and passThrough functions , and the password validation functions from previous listings are available.)
    // password:string -> Outcome<unit, string>
    let validateAndSave password =
    let mixedCase' = adapt mixedCase
    let containsAny' = adapt (containsAny "-_!?")
    let tidy' = passThrough tidy
    let save' = adapt save
    password
    |> notEmpty
    |> mixedCase'
    |> containsAny'
    |> tidy'
    |> save'
// Success ()
validateAndSave "Correct-Horse-Battery-Staple-9"
// Failure 'Must contain at least one of "-_!?"'
validateAndSave "LetMeIn"
Listing 11-6

Lining the machines up on the factory floor

In Listing 11-6, we take each of the validation functions (apart from the first) and partially apply adapt or passThrough by providing the validation function as an argument. This is the precise equivalent, in our analogy, to putting the machine tool inside its adapter. In each case, I’ve just added a single quote (') to the name of the adapted version, just so you can tell which functions have been adapted. Items such as mixedCase' are now functions that require their input value to be wrapped in Outcome and which will just pass on Failure cases untouched.

Why didn’t we have to adapt the first function (notEmpty)? Well, exactly as the manufacturing engineer said, the very first machine tool doesn’t need an adapter because it already takes nonwrapped input and returns an Outcome case, and so it can be plugged into the second (adapted) machine without change.

At this point, we can do a sanity check by looking at the signature of the validateAndSave function . We see that the signature is password:string -> Outcome<unit, string>. This makes sense because we want to accept a string password and get back either an Outcome.Success with a payload of unit (because the database save operation returns unit) or an Outcome.Failure with a payload of string, which will be the validation or saving error message.

Now we need to try this all out. Listing 11-7 exercises our code for various invalid passwords and one valid one.
// Failure "Must not be null"
null |> validateAndSave |> printfn "%A"
// Failure "Must not be empty"
"" |> validateAndSave |> printfn "%A"
// Failure "Must not be white space"
" " |> validateAndSave |> printfn "%A"
// Failure "Must contain mixed case"
"the quick brown fox" |> validateAndSave |> printfn "%A"
// Failure "Must contain at least one of "-_!?""
"The quick brown fox" |> validateAndSave |> printfn "%A"
// Success ()
"The quick brown fox!" |> validateAndSave |> printfn "%A"
Listing 11-7

Exercising the validateAndSave function

Listing 11-7 shows that our function works – invalid passwords are rejected with a user-friendly message, and valid ones are “saved.” If you want to see what happens when there is an exception during the save process (maybe we lost the database connection?), simply uncomment the line in the save function (in Listing 11-5) that raises an exception. In that case, the specific error details will be logged, and a more general error message will be returned that would be safe to show to the user (Listing 11-8).
    Saving password 'The quick brown fox!'
    Logging error: Dummy exception
    Failure "Sorry, there was an internal error saving your password"
Listing 11-8

Results of an exception during saving

Listing 11-6 is a little wordy! If you were paying attention in Chapter 9, you might recognize this as a prime candidate for function composition using the >> operator. Listing 11-9 shows the magic that happens when you do this!
// string -> Outcome<unit, string>
let validateAndSave =
    notEmpty
    >> adapt mixedCase
    >> adapt (containsAny "-_!?")
    >> passThrough tidy
    >> adapt save
Listing 11-9

Composing adapted functions

We’ve moved the “adapting” of the various functions into the body of the pipeline and joined the adapted functions with the >> operator. We get rid of the password parameter because a string input is expected anyway by notEmpty, and this requirement of a parameter “bubbles out” to the validateAndSave function . The type signature of validateAndSave is unchanged (although the password string is now unlabeled), and if we run it again using the code from Listing 11-7, it works exactly the same. Amazing!

Making It Official

I said at the outset that F# has its own ROP types. So how do we use these rather than our handcrafted Outcome type? The DU we named Outcome is officially called Result, and the DU cases are Ok and Error. So each of the password validation and processing functions needs some tiny naming changes (e.g., Listing 11-10).
let notEmpty (s : string) =
    if isNull(s) then
        Error "Must not be null"
    elif String.IsNullOrEmpty(s) then
        Error "Must not be empty"
    elif String.IsNullOrWhiteSpace(s) then
        Error "Must not be white space"
    else
        Ok s
Listing 11-10

Using the official Result DU

Likewise, the official name for what I called adapt is bind, and the official name for passThrough is map. So the validateAndSave function needs to open the Result namespace and call map and bind (Listing 11-11).
open Result
// string -> Result<unit, string>
let validateAndSave =
    notEmpty
    >> bind mixedCase
    >> bind (containsAny "-_!?")
    >> map tidy
    >> bind save
Listing 11-11

Using bind and map

Incidentally, you may notice a close resemblance between Result.bind/Result.map and Option.bind/Option.map, which we discussed way back in Chapter 3. These two names, map and bind, are pretty standard in functional programming and theory. You eventually get used to them.

Love Your Errors

Remember when the engineer said you were going to need an adapter for the rejects? Well it’s time to tackle that. At the moment, we have cheated a little, by making all the functions return Error cases that have strings as payloads. It’s as if we assume that on the production line, rejects at every stage would fit on the same rejects conveyor – which might well not be the case if the rejects from different stages were different shapes. Luckily in F# world, we can force all the kinds of rejects into the same wrapper by (say it aloud with me) creating another Discriminated Union!

This DU will have to list all the kinds of things that can go wrong, together with payloads for any further information that might need to be passed along (Listing 11-12).
open System
type ValidationError =
    | MustNotBeNull
    | MustNotBeEmpty
    | MustNotBeWhiteSpace
    | MustContainMixedCase
    | MustContainOne of chars:string
    | ErrorSaving of exn:Exception
let notEmpty (s : string) =
    if isNull(s) then
        Error MustNotBeNull
    elif String.IsNullOrEmpty(s) then
        Error MustNotBeEmpty
    elif String.IsNullOrWhiteSpace(s) then
        Error MustNotBeWhiteSpace
    else
        Ok s
let mixedCase (s : string) =
    let hasUpper =
        s |> Seq.exists (Char.IsUpper)
    let hasLower =
        s |> Seq.exists (Char.IsLower)
    if hasUpper && hasLower then
        Ok s
    else
        Error MustContainMixedCase
let containsAny (cs : string) (s : string) =
    if s.IndexOfAny(cs.ToCharArray()) > -1 then
        Ok s
    else
        Error (MustContainOne cs)
let tidy (s : string) =
    s.Trim()
let save (s : string) =
    let dbSave s : unit =
        printfn "Saving password '%s'" s
        // Uncomment this to simulate an exception:
        raise <| Exception "Dummy exception"
    try
        dbSave s
        |> Ok
    with
    | e ->
        Error (ErrorSaving e)
Listing 11-12

An error-types Discriminated Union

Listing 11-12 starts with the new DU. Most of the cases have no payload because they just need to convey the fact that a certain kind of thing went wrong. The MustContainOne has a payload that lets you say what characters were expected. The ErrorSaving case has a slot to carry the exception that was raised, which a later step may choose to inspect if it needs to. See how we also had to change most of the validation functions so that their Error results wrap a ValidationError case – for example, Error MustNotBeNull. Here, to be clear, we have a DU wrapped up in another DU. Another small change in Listing 11-12 is that I’ve removed the log function from the save() function , for reasons that will become clear in a moment.

Now we need the “rejects adapter” that the engineer suggested. The adapter function lives with map and bind in the Result namespace, and it is called mapError. The best way to think about mapError is by comparing the physical diagrams from Figures 11-6 and 11-7. Here, they are again side by side (Figure 11-8).
../images/462726_2_En_11_Chapter/462726_2_En_11_Fig8_HTML.png
Figure 11-8

Comparing map and mapError

The map function takes an input, and if it is good, it processes it using a supplied function (which cannot fail) and returns a good output. It passes through preexisting bad inputs untouched. mapError is like a vertical flip of the same thing. It takes an input, and if it is good, it passes it through untouched. If the input is bad, it processes it using a supplied function, which itself returns a bad result.

We can use mapError to branch our logic depending on what kind of error occurred, maybe just translating it into a readable message, maybe logging exceptions (but hiding them from the end user), and so forth (Listing 11-13).
open Result
// string -> Result<unit, ValidationError>
let validateAndSave =
    notEmpty
    >> bind mixedCase
    >> bind (containsAny "-_!?")
    >> map tidy
    >> bind save
let savePassword =
    let log m =
        printfn "Logging error: %s" m
    validateAndSave
    >> mapError (fun err ->
        match err with
        | MustNotBeNull
        | MustNotBeEmpty
        | MustNotBeWhiteSpace ->
            sprintf "Password must be entered"
        | MustContainMixedCase ->
            sprintf "Password must contain upper and lower case characters"
        | MustContainOne cs ->
            sprintf "Password must contain one of %A" cs
        | ErrorSaving e ->
            log e.Message
            sprintf "Sorry there was an internal error saving the password")
Listing 11-13

Using mapError

See in Listing 11-13 how the signature of validateAndSave has changed to string -> Result<unit, ValidationError>, because we made all the validation functions return ValidationError cases when there was a problem. Then in the savePassword function, we composed validateAndSave with Result.mapError. We gave mapError a lambda function that matches on the ValidationError cases to generate suitable messages and in one case to log an exception.

This approach has the interesting consequence that it forces you to enumerate every kind of thing that could go wrong with your process, all in a single DU. This certainly takes some getting used to, but it is potentially a very useful discipline. It helps you avoid wishful thinking or an inconsistent approach to errors.

Recommendations

If you’ve been enthused about Railway Oriented Programming, here’s how I recommend you get started:
  • Identify processes that involve several steps, each of which might fail in predictable ways.

  • Write a DU that enumerates the kinds of errors that can occur. (You can obviously add cases to this as you go along.) Some cases might just identify the kind of error; others might have a payload with more information, such as an exception instance, or more details about the input that triggered the failure.

  • Write a function for each step in your process. Each should take a nonwrapped input (or inputs) and return either a good output in the form of a Result.Ok that wraps the step’s successful output or a Result.Error that wraps a case from your error-types DU.

  • Compose the steps into a single pipeline. To do this, wrap each function but the first using Result.bind (or Result.map for operations that need to fit into the pipeline but which can’t fail). Compose the wrapped functions with the function composition operator >>.

  • Use Result.mapError at the end of the pipeline to process failure cases, for example, by attaching error messages or writing to a log.

Summary

I hope you now understand enough about ROP to make an informed decision about whether to use it. You’re also equipped to dive in and maintain existing code bases that use ROP or some variation of it.

I’d worry, though, if I succeeded too well and left you an uncritical enthusiast for the technique. The truth is that ROP is rather controversial in the F# community, with both passionate advocates and passionate critics. The official F# coding conventions have quite a lot to say on the subject. They conclude:

Types such as Result<‘Success, ‘Error> are appropriate for basic operations where they aren’t nested, and F# optional types are perfect for representing when something could either return something or nothing. They are not a replacement for exceptions, though, and should not be used in an attempt to replace exceptions. Rather, they should be applied judiciously to address specific aspects of exception and error management policy in targeted ways.

—F# Style Guide, Microsoft and contributors

In my opinion, ROP works rather nicely in the same sorts of places where function composition works nicely: constrained pipelines of operations, where the pipeline has limited scope, such as our password validation example. Using it at an architectural level works less well in my experience, tending to blur motivational transparency, at least for ordinary mortals.

In the next chapter, we’ll look at performance – how to measure the speed of F# functions and how to make them faster.

Exercises

Exercise 11-1 – Reproducing mapError
You might remember that we started by writing our own versions of map and bind in the form of adapt and passThrough functions :
type Outcome<'TSuccess, 'TFailure> =
    | Success of 'TSuccess
    | Failure of 'TFailure
let adapt func input =
    match input with
    | Success x -> func x
    | Failure f -> Failure f
let passThrough func input =
    match input with
    | Success x -> func x |> Success
    | Failure f -> Failure f

Can you implement a passThroughRejects function, with the same behavior as the built-in mapError function?

Hint: Look carefully at Figure 11-8 and the surrounding text.

Exercise 11-2 – Writing an ROP Pipeline

You are working on a project to handle some incoming messages, each containing a file name and some data. The file name is a string representation of a DateTimeOffset when the data was captured. The data is an array of floating-point values. The process should attempt to parse the file name as a DateTimeOffset (some might fail due to spurious messages) and should also reject any messages where the data array contains any NaN (“not-a-number”) values. Any rejects need to be logged.

The following listing contains a partial implementation of the requirement. Your task is to fill in the code marked with TODO, removing the exceptions that have been placed there. Each TODO should only take a line or two of code to complete.
open System
type Message =
    { FileName : string
      Content : float[] }
type Reading =
    { TimeStamp : DateTimeOffset
      Data : float[] }
let example =
    [|
        { FileName = "2019-02-23T02:00:00-05:00"
          Content = [|1.0; 2.0; 3.0; 4.0|] }
        { FileName = "2019-02-23T02:00:10-05:00"
          Content = [|5.0; 6.0; 7.0; 8.0|] }
        { FileName = "error"
          Content = [||] }
        { FileName = "2019-02-23T02:00:20-05:00"
          Content = [|1.0; 2.0; 3.0; Double.NaN|] }
    |]
let log s = printfn "Logging: %s" s
type MessageError =
    | InvalidFileName of fileName:string
    | DataContainsNaN of fileName:string * index:int
let getReading message =
    match DateTimeOffset.TryParse(message.FileName) with
    | true, dt ->
        let reading = { TimeStamp = dt; Data = message.Content }
        // TODO Return an OK result containing a tuple of the
        // message file name and the reading:
        raise <| NotImplementedException()
    | false, _ ->
        // TODO Return an Error result containing an
        // InvalidFileName error, which itself contains
        // the message file name:
        raise <| NotImplementedException()
let validateData(fileName, reading) =
    let nanIndex =
        reading.Data
        |> Array.tryFindIndex (Double.IsNaN)
    match nanIndex with
    | Some i ->
        // TODO Return an Error result containing an
        // DataContainsNaN error, which itself contains
        // the file name and error index:
        raise <| NotImplementedException()
    | None ->
        // TODO Return an Ok result containing the reading:
        raise <| NotImplementedException()
let logError (e : MessageError) =
    // TODO match on the MessageError cases
    // and call log with suitable information
    // for each case.
    raise <| NotImplementedException()
// When all the TODOs are done, uncomment this code
// and see if it works!
//
//open Result
//
//let processMessage =
//    getReading
//    >> bind validateData
//    >> mapError logError
//
//let processData data =
//    data
//    |> Array.map processMessage
//    |> Array.choose (fun result ->
//        match result with
//        | Ok reading -> reading |> Some
//        | Error _ -> None)
//
//example
//|> processData
//|> Array.iter (printfn "%A")

Exercise Solutions

Exercise 11-1 – Reproducing mapError
The function you want is a kind of mirror image of passThrough. I’ve repeated passThrough here for comparison:
let passThrough func input =
    match input with
    | Success x -> func x |> Success
    | Failure f -> Failure f
let passThroughRejects func input =
    match input with
    | Success x -> Success x
    | Failure f -> func f |> Failure
Exercise 11-2 – Writing an ROP Pipeline
Here is a possible solution. Added lines are marked with DONE.
open System
type Message =
    { FileName : string
      Content : float[] }
type Reading =
    { TimeStamp : DateTimeOffset
      Data : float[] }
let example =
    [|
        { FileName = "2019-02-23T02:00:00-05:00"
          Content = [|1.0; 2.0; 3.0; 4.0|] }
        { FileName = "2019-02-23T02:00:10-05:00"
          Content = [|5.0; 6.0; 7.0; 8.0|] }
        { FileName = "error"
          Content = [||] }
        { FileName = "2019-02-23T02:00:20-05:00"
          Content = [|1.0; 2.0; 3.0; Double.NaN|] }
    |]
let log s = printfn "Logging: %s" s
type MessageError =
    | InvalidFileName of fileName:string
    | DataContainsNaN of fileName:string * index:int
let getReading message =
    match DateTimeOffset.TryParse(message.FileName) with
    | true, dt ->
        let reading = { TimeStamp = dt; Data = message.Content }
        // DONE
        Ok(message.FileName, reading)
    | false, _ ->
        // DONE
        Error (InvalidFileName message.FileName)
let validateData(fileName, reading) =
    let nanIndex =
        reading.Data
        |> Array.tryFindIndex (Double.IsNaN)
    match nanIndex with
    | Some i ->
        // DONE
        Error (DataContainsNaN(fileName, i))
    | None ->
        // DONE
        Ok reading
let logError (e : MessageError) =
    // DONE
    match e with
    | InvalidFileName fn ->
        log (sprintf "Invalid file name: %s" fn)
    | DataContainsNaN (fn, i) ->
        log (sprintf "Data contains NaN at position: %i in file: %s" i fn)
open Result
let processMessage =
    getReading
    >> bind validateData
    >> mapError logError
let processData data =
    data
    |> Array.map processMessage
    |> Array.choose (fun result ->
        match result with
        | Ok reading -> reading |> Some
        | Error _ -> None)
example
|> processData
|> Array.iter (printfn "%A")
Logging: Invalid file name: error
Logging: Data contains NaN at position: 3 in file: 2019-02-23T02:00:20-05:00
{ TimeStamp = 23/02/2019 02:00:00 -05:00
  Data = [|1.0; 2.0; 3.0; 4.0|] }
{ TimeStamp = 23/02/2019 02:00:10 -05:00
  Data = [|5.0; 6.0; 7.0; 8.0|] }
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.58.143