154
Railway Oriented Programming A functional approach to error handling What do railways have to do with programming?

Railway Oriented Programming

Embed Size (px)

DESCRIPTION

(Video of these slides here http://fsharpforfunandprofit.com/rop) (My response to "this is just Either" here: http://fsharpforfunandprofit.com/rop/#monads) Many examples in functional programming assume that you are always on the "happy path". But to create a robust real world application you must deal with validation, logging, network and service errors, and other annoyances. So, how do you handle all this in a clean functional way? This talk will provide a brief introduction to this topic, using a fun and easy-to-understand railway analogy.

Citation preview

Page 1: Railway Oriented Programming

Railway Oriented Programming A functional approach to error handling

What do railways have to do with programming?

Page 2: Railway Oriented Programming

Railway Oriented Programming A functional approach to error handling

Scott Wlaschin

@ScottWlaschin

fsharpforfunandprofit.com

FPbridge.co.uk

...but OCaml and Haskell are very similar.

Examples will be in F#...

Page 3: Railway Oriented Programming

Overview

Topics covered:

• Happy path programming

• Straying from the happy path

• Introducing "Railway Oriented Programming"

• Using the model in practice

• Extending and improving the design

Page 4: Railway Oriented Programming

Happy path programming

Implementing a simple use case

Page 5: Railway Oriented Programming

A simple use case

Receive request

Validate and canonicalize request

Update existing user record

Send verification email

Return result to user

type Request = { userId: int; name: string; email: string }

"As a user I want to update my name and email address"

Page 6: Railway Oriented Programming

Imperative code

string ExecuteUseCase() { var request = receiveRequest(); validateRequest(request); canonicalizeEmail(request); db.updateDbFromRequest(request); smtpServer.sendEmail(request.Email); return "Success"; }

Page 7: Railway Oriented Programming

Functional flow

let executeUseCase = receiveRequest >> validateRequest >> canonicalizeEmail >> updateDbFromRequest >> sendEmail >> returnMessage

F# left-to-right composition operator

Page 8: Railway Oriented Programming

Straying from the happy path...

What do you do when

something goes wrong?

Page 9: Railway Oriented Programming

Straying from the happy path

Page 10: Railway Oriented Programming

“A program is a spell cast over a

computer, turning input

into error messages”

Page 11: Railway Oriented Programming

“A program is a spell cast over a

computer, turning input

into error messages”

Page 12: Railway Oriented Programming

Straying from the happy path

Name is blank Email not valid

Receive request

Validate and canonicalize request

Update existing user record

Send verification email

Return result to user

User not found Db error

Authorization error Timeout

"As a user I want to update my name and email address"

type Request = { userId: int; name: string; email: string }

- and see sensible error messages when something goes wrong!

Page 13: Railway Oriented Programming

Imperative code with error handling

string UpdateCustomerWithErrorHandling() { var request = receiveRequest(); validateRequest(request); canonicalizeEmail(request); db.updateDbFromRequest(request); smtpServer.sendEmail(request.Email) return "OK"; }

Page 14: Railway Oriented Programming

Imperative code with error handling

string UpdateCustomerWithErrorHandling() { var request = receiveRequest(); var isValidated = validateRequest(request); if (!isValidated) { return "Request is not valid" } canonicalizeEmail(request); db.updateDbFromRequest(request); smtpServer.sendEmail(request.Email) return "OK"; }

Page 15: Railway Oriented Programming

Imperative code with error handling

string UpdateCustomerWithErrorHandling() { var request = receiveRequest(); var isValidated = validateRequest(request); if (!isValidated) { return "Request is not valid" } canonicalizeEmail(request); var result = db.updateDbFromRequest(request); if (!result) { return "Customer record not found" } smtpServer.sendEmail(request.Email) return "OK"; }

Page 16: Railway Oriented Programming

Imperative code with error handling

string UpdateCustomerWithErrorHandling() { var request = receiveRequest(); var isValidated = validateRequest(request); if (!isValidated) { return "Request is not valid" } canonicalizeEmail(request); try { var result = db.updateDbFromRequest(request); if (!result) { return "Customer record not found" } } catch { return "DB error: Customer record not updated" } smtpServer.sendEmail(request.Email) return "OK"; }

Page 17: Railway Oriented Programming

Imperative code with error handling

string UpdateCustomerWithErrorHandling() { var request = receiveRequest(); var isValidated = validateRequest(request); if (!isValidated) { return "Request is not valid" } canonicalizeEmail(request); try { var result = db.updateDbFromRequest(request); if (!result) { return "Customer record not found" } } catch { return "DB error: Customer record not updated" } if (!smtpServer.sendEmail(request.Email)) { log.Error "Customer email not sent" } return "OK"; }

Page 18: Railway Oriented Programming

Imperative code with error handling

string UpdateCustomerWithErrorHandling() { var request = receiveRequest(); var isValidated = validateRequest(request); if (!isValidated) { return "Request is not valid" } canonicalizeEmail(request); try { var result = db.updateDbFromRequest(request); if (!result) { return "Customer record not found" } } catch { return "DB error: Customer record not updated" } if (!smtpServer.sendEmail(request.Email)) { log.Error "Customer email not sent" } return "OK"; }

Page 19: Railway Oriented Programming

Functional flow with error handling

let updateCustomer = receiveRequest >> validateRequest >> canonicalizeEmail >> updateDbFromRequest >> sendEmail >> returnMessage

Before

Page 20: Railway Oriented Programming

Functional flow with error handling

let updateCustomerWithErrorHandling = receiveRequest >> validateRequest >> canonicalizeEmail >> updateDbFromRequest >> sendEmail >> returnMessage

Does this look familiar?

Don't believe me? Stay tuned! After

Page 21: Railway Oriented Programming

Designing the unhappy path

Page 22: Railway Oriented Programming

Request/response (non-functional) design

Request

Response

Validate Update Send Request handling service

Request

Response

Validate Update Send

Request handling service

Request

Errors Response

Validate Update Send

Request handling service

Imperative code can return early

Page 23: Railway Oriented Programming

Data flow (functional) design

Response Validate Update Send A single function representing the use case Request

Request Response Validate Update Send

A single function representing the use case

Request

Errors

Success Response

Validate Update Send

Error Response

A single function representing the use case

Q: How can you bypass downstream functions when an error happens?

Page 24: Railway Oriented Programming

Functional design

How can a function have more than one output?

type Result = | Success | ValidationError | UpdateError | SmtpError

Request

Errors

Success Validate Update Send

Failure

A single function representing the use case

Page 25: Railway Oriented Programming

Functional design

How can a function have more than one output?

type Result = | Success | Failure

Request

Errors

Success Validate Update Send

Failure

A single function representing the use case

Page 26: Railway Oriented Programming

Functional design

How can a function have more than one output?

type Result<'TEntity> = | Success of 'TEntity | Failure of string

Request

Errors

Success Validate Update Send

Failure

A single function representing the use case

Page 27: Railway Oriented Programming

Functional design

Request

Errors

Success Validate Update Send

Failure

A single function representing the use case

• Each use case will be equivalent to a single function

• The function will return a sum type with two cases:

"Success" and "Failure".

• The use case function will be built from a series of smaller functions,

each representing one step in a data flow.

• The errors from each step will be combined into a single "failure" path.

Page 28: Railway Oriented Programming

How do I work with errors

in a functional way?

Page 29: Railway Oriented Programming

Monad dialog

Page 30: Railway Oriented Programming
Page 31: Railway Oriented Programming
Page 32: Railway Oriented Programming
Page 33: Railway Oriented Programming
Page 34: Railway Oriented Programming
Page 35: Railway Oriented Programming
Page 36: Railway Oriented Programming
Page 37: Railway Oriented Programming
Page 38: Railway Oriented Programming
Page 39: Railway Oriented Programming
Page 40: Railway Oriented Programming
Page 41: Railway Oriented Programming
Page 42: Railway Oriented Programming
Page 43: Railway Oriented Programming
Page 44: Railway Oriented Programming
Page 45: Railway Oriented Programming
Page 46: Railway Oriented Programming
Page 47: Railway Oriented Programming
Page 48: Railway Oriented Programming

v

Page 49: Railway Oriented Programming
Page 50: Railway Oriented Programming
Page 51: Railway Oriented Programming
Page 52: Railway Oriented Programming
Page 53: Railway Oriented Programming
Page 54: Railway Oriented Programming
Page 55: Railway Oriented Programming
Page 56: Railway Oriented Programming
Page 57: Railway Oriented Programming
Page 58: Railway Oriented Programming
Page 59: Railway Oriented Programming
Page 60: Railway Oriented Programming
Page 61: Railway Oriented Programming
Page 62: Railway Oriented Programming
Page 63: Railway Oriented Programming
Page 64: Railway Oriented Programming

Monads are confusing?

Monads are not really that confusing

http://bit.ly/monad-paper

Page 65: Railway Oriented Programming

Railway oriented programming This has absolutely nothing to do with monads.

Page 66: Railway Oriented Programming

A railway track analogy

The Tunnel of Transformation Function

apple -> banana

Page 67: Railway Oriented Programming

A railway track analogy

Function 1

apple -> banana

Function 2

banana -> cherry

Page 68: Railway Oriented Programming

A railway track analogy

>> Function 1

apple -> banana Function 2

banana -> cherry

Page 69: Railway Oriented Programming

A railway track analogy

New Function

apple -> cherry

Can't tell it was built from smaller functions!

Page 70: Railway Oriented Programming

An error generating function

Request Success Validate

Failure

let validateInput input = if input.name = "" then Failure "Name must not be blank" else if input.email = "" then Failure "Email must not be blank" else Success input // happy path

Page 71: Railway Oriented Programming

Introducing switches

Success!

Failure

Input ->

Page 72: Railway Oriented Programming

Connecting switches

Validate UpdateDb on success

bypass

Page 73: Railway Oriented Programming

Connecting switches

Validate UpdateDb

Page 74: Railway Oriented Programming

Connecting switches

Validate UpdateDb SendEmail

Page 75: Railway Oriented Programming

Connecting switches

Validate UpdateDb SendEmail

Page 76: Railway Oriented Programming

The two-track model in practice

Page 77: Railway Oriented Programming

Composing switches

Validate UpdateDb SendEmail

Page 78: Railway Oriented Programming

Composing switches

Validate UpdateDb SendEmail

Page 79: Railway Oriented Programming

Composing switches

Validate UpdateDb SendEmail

>> >>

Composing one-track functions is fine...

Page 80: Railway Oriented Programming

Composing switches

Validate UpdateDb SendEmail

>> >>

... and composing two-track functions is fine...

Page 81: Railway Oriented Programming

Composing switches

Validate UpdateDb SendEmail

... but composing switches is not allowed!

Page 82: Railway Oriented Programming

Composing switches

Validate

Two-track input Two-track input

Validate One-track input Two-track input

Page 83: Railway Oriented Programming

Building an adapter block

Two-track input

Slot for switch function

Two-track output

Page 84: Railway Oriented Programming

Building an adapter block

Two-track input Two-track output

Validate Validate

Page 85: Railway Oriented Programming

let adapt switchFunction = fun twoTrackInput -> match twoTrackInput with | Success s -> switchFunction s | Failure f -> Failure f

Building an adapter block

Two-track input Two-track output

Page 86: Railway Oriented Programming

let adapt switchFunction = fun twoTrackInput -> match twoTrackInput with | Success s -> switchFunction s | Failure f -> Failure f

Building an adapter block

Two-track input Two-track output

Page 87: Railway Oriented Programming

let adapt switchFunction = fun twoTrackInput -> match twoTrackInput with | Success s -> switchFunction s | Failure f -> Failure f

Building an adapter block

Two-track input Two-track output

Page 88: Railway Oriented Programming

let adapt switchFunction = fun twoTrackInput -> match twoTrackInput with | Success s -> switchFunction s | Failure f -> Failure f

Building an adapter block

Two-track input Two-track output

Page 89: Railway Oriented Programming

Bind as an adapter block

Two-track input Two-track output

let bind switchFunction = fun twoTrackInput -> match twoTrackInput with | Success s -> switchFunction s | Failure f -> Failure f

bind : ('a -> TwoTrack<'b>) -> TwoTrack<'a> -> TwoTrack<'b>

Page 90: Railway Oriented Programming

Bind as an adapter block

Two-track input Two-track output

let bind switchFunction twoTrackInput = match twoTrackInput with | Success s -> switchFunction s | Failure f -> Failure f

bind : ('a -> TwoTrack<'b>) -> TwoTrack<'a> -> TwoTrack<'b>

Page 91: Railway Oriented Programming

name50

Bind example

let nameNotBlank input = if input.name = "" then Failure "Name must not be blank" else Success input let name50 input = if input.name.Length > 50 then Failure "Name must not be longer than 50 chars" else Success input let emailNotBlank input = if input.email = "" then Failure "Email must not be blank" else Success input

nameNotBlank

emailNotBlank

Page 92: Railway Oriented Programming

Bind example

nameNotBlank (combined with) name50 (combined with) emailNotBlank

nameNotBlank name50 emailNotBlank

Page 93: Railway Oriented Programming

Bind example

bind nameNotBlank bind name50 bind emailNotBlank

nameNotBlank name50 emailNotBlank

Page 94: Railway Oriented Programming

Bind example

bind nameNotBlank >> bind name50 >> bind emailNotBlank

nameNotBlank name50 emailNotBlank

Page 95: Railway Oriented Programming

Bind example

let validateRequest = bind nameNotBlank >> bind name50 >> bind emailNotBlank // validateRequest : TwoTrack<Request> -> TwoTrack<Request>

validateRequest

Page 96: Railway Oriented Programming

Bind example

let (>>=) twoTrackInput switchFunction = bind switchFunction twoTrackInput let validateRequest twoTrackInput = twoTrackInput >>= nameNotBlank >>= name50 >>= emailNotBlank

validateRequest

Page 97: Railway Oriented Programming

Bind doesn't stop transformations

FunctionB

type TwoTrack<'TEntity> = | Success of 'TEntity | Failure of string

FunctionA

Page 98: Railway Oriented Programming

Composing switches - review

Validate UpdateDb SendEmail

Validate UpdateDb SendEmail

Page 99: Railway Oriented Programming

Comic Interlude

What do you call a

train that eats toffee? I don't know, what do you

call a train that eats toffee?

A chew,

chew train!

Page 100: Railway Oriented Programming

More fun with railway tracks...

Working with other functions

Page 101: Railway Oriented Programming

More fun with railway tracks...

Fitting other functions into this framework:

• Single track functions

• Dead-end functions

• Functions that throw exceptions

• Supervisory functions

Page 102: Railway Oriented Programming

Converting one-track functions

Fitting other functions into this framework:

• Single track functions

• Dead-end functions

• Functions that throw exceptions

• Supervisory functions

Page 103: Railway Oriented Programming

Converting one-track functions

// trim spaces and lowercase let canonicalizeEmail input = { input with email = input.email.Trim().ToLower() }

canonicalizeEmail

Page 104: Railway Oriented Programming

Converting one-track functions

UpdateDb SendEmail Validate

canonicalizeEmail

Won't compose

Page 105: Railway Oriented Programming

Converting one-track functions

Two-track input

Slot for one-track function

Two-track output

Page 106: Railway Oriented Programming

Converting one-track functions

Two-track input Two-track output

Canonicalize Canonicalize

Page 107: Railway Oriented Programming

Converting one-track functions

Two-track input Two-track output

let map singleTrackFunction = fun twoTrackInput -> match twoTrackInput with | Success s -> Success (singleTrackFunction s) | Failure f -> Failure f

map : ('a -> 'b) -> TwoTrack<'a> -> TwoTrack<'b>`

Single track function

Page 108: Railway Oriented Programming

Converting one-track functions

Two-track input Two-track output

let map singleTrackFunction = bind (singleTrackFunction >> Success)

map : ('a -> 'b) -> TwoTrack<'a> -> TwoTrack<'b>

Single track function

Page 109: Railway Oriented Programming

Converting one-track functions

UpdateDb SendEmail Validate

canonicalizeEmail

Will compose

Page 110: Railway Oriented Programming

Converting dead-end functions

Fitting other functions into this framework:

• Single track functions

• Dead-end functions

• Functions that throw exceptions

• Supervisory functions

Page 111: Railway Oriented Programming

Converting dead-end functions

let updateDb request = // do something // return nothing at all

updateDb

Page 112: Railway Oriented Programming

Converting dead-end functions

SendEmail Validate UpdateDb

Won't compose

Page 113: Railway Oriented Programming

Converting dead-end functions

One-track input

Slot for dead end function

One-track output

Page 114: Railway Oriented Programming

Converting dead-end functions

One-track input One-track output

let tee deadEndFunction oneTrackInput = deadEndFunction oneTrackInput oneTrackInput

tee : ('a -> unit) -> 'a -> 'a

Dead end function

Page 115: Railway Oriented Programming

Converting dead-end functions

SendEmail Validate

UpdateDb

Will compose

Page 116: Railway Oriented Programming

Functions that throw exceptions

Fitting other functions into this framework:

• Single track functions

• Dead-end functions

• Functions that throw exceptions

• Supervisory functions

Page 117: Railway Oriented Programming

Functions that throw exceptions

One-track input Two-track output

SendEmail SendEmail

Add try/catch to handle timeouts, say

Looks innocent, but might throw an exception

Page 118: Railway Oriented Programming

Functions that throw exceptions

Even Yoda recommends

not to use exception

handling for control flow:

Guideline: Convert exceptions into Failures

"Do or do not, there is

no try".

Page 119: Railway Oriented Programming

Supervisory functions

Fitting other functions into this framework:

• Single track functions

• Dead-end functions

• Functions that throw exceptions

• Supervisory functions

Page 120: Railway Oriented Programming

Supervisory functions

Two-track input Two-track output

Slot for one-track function for Success case

Slot for one-track function for Failure case

Page 121: Railway Oriented Programming

Putting it all together

Page 122: Railway Oriented Programming

Putting it all together

Validate

UpdateDb

SendEmail

Canonicalize

Input

Output??

Page 123: Railway Oriented Programming

Putting it all together

Validate

UpdateDb

SendEmail

Canonicalize

returnMessage

Input Output

let returnMessage result = match result with | Success obj -> OK obj.ToJson() | Failure msg -> BadRequest msg

Page 124: Railway Oriented Programming

Putting it all together - review

The "two-track" framework is a useful approach for

most use-cases.

You can fit most functions into this model.

Page 125: Railway Oriented Programming

Putting it all together - review

The "two-track" framework is a useful approach for

most use-cases.

let updateCustomer = receiveRequest >> validateRequest >> updateDbFromRequest >> sendEmail >> returnMessage

let updateCustomer = receiveRequest >> validateRequest >> updateDbFromRequest >> sendEmail >> returnMessage

Let's look at the code -- before and after adding error handling

Page 126: Railway Oriented Programming

Comic Interlude

Why can't a steam

locomotive sit down? I don't know,

why can't a steam

locomotive sit down?

Because it

has a tender

behind!

Page 127: Railway Oriented Programming

Extending the framework:

• Designing for errors

• Parallel tracks

• Domain events

Page 128: Railway Oriented Programming

Designing for errors

Unhappy paths are requirements too

Page 129: Railway Oriented Programming

Designing for errors

let validateInput input = if input.name = "" then Failure "Name must not be blank" else if input.email = "" then Failure "Email must not be blank" else Success input // happy path

type TwoTrack<'TEntity> = | Success of 'TEntity | Failure of string

Using strings is not good

Page 130: Railway Oriented Programming

Designing for errors

let validateInput input = if input.name = "" then Failure NameMustNotBeBlank else if input.email = "" then Failure EmailMustNotBeBlank else Success input // happy path

type TwoTrack<'TEntity> = | Success of 'TEntity | Failure of ErrorMessage

type ErrorMessage = | NameMustNotBeBlank | EmailMustNotBeBlank

Special type rather than string

Page 131: Railway Oriented Programming

Designing for errors

let validateInput input = if input.name = "" then Failure NameMustNotBeBlank else if input.email = "" then Failure EmailMustNotBeBlank else if (input.email doesn't match regex) then Failure EmailNotValid input.email else Success input // happy path

type ErrorMessage = | NameMustNotBeBlank | EmailMustNotBeBlank | EmailNotValid of EmailAddress

Add invalid email as data

Page 132: Railway Oriented Programming

Designing for errors

type ErrorMessage = | NameMustNotBeBlank | EmailMustNotBeBlank | EmailNotValid of EmailAddress // database errors | UserIdNotValid of UserId | DbUserNotFoundError of UserId | DbTimeout of ConnectionString | DbConcurrencyError | DbAuthorizationError of ConnectionString * Credentials // SMTP errors | SmtpTimeout of SmtpConnection | SmtpBadRecipient of EmailAddress

Documentation of everything that can go wrong --

And it's type-safe documentation that can't go

out of date!

Page 133: Railway Oriented Programming

Designing for errors

type ErrorMessage = | NameMustNotBeBlank | EmailMustNotBeBlank | EmailNotValid of EmailAddress // database errors | UserIdNotValid of UserId | DbUserNotFoundError of UserId | DbTimeout of ConnectionString | DbConcurrencyError | DbAuthorizationError of ConnectionString * Credentials // SMTP errors | SmtpTimeout of SmtpConnection | SmtpBadRecipient of EmailAddress

Documentation of everything that can go wrong --

And it's type-safe documentation that can't go

out of date!

As we develop the code, we can build up a complete list of everything that could go wrong

Page 134: Railway Oriented Programming

Designing for errors – converting to strings

No longer works – each case must now be explicitly converted to a string

returnMessage

let returnMessage result = match result with | Success _ -> "Success" | Failure msg -> msg

Page 135: Railway Oriented Programming

Designing for errors – converting to strings

let returnMessage result = match result with | Success _ -> "Success" | Failure err -> match err with | NameMustNotBeBlank -> "Name must not be blank" | EmailMustNotBeBlank -> "Email must not be blank" | EmailNotValid (EmailAddress email) -> sprintf "Email %s is not valid" email // database errors | UserIdNotValid (UserId id) -> sprintf "User id %i is not a valid user id" id | DbUserNotFoundError (UserId id) -> sprintf "User id %i was not found in the database" id | DbTimeout (_,TimeoutMs ms) -> sprintf "Could not connect to database within %i ms" ms | DbConcurrencyError -> sprintf "Another user has modified the record. Please resubmit" | DbAuthorizationError _ -> sprintf "You do not have permission to access the database" // SMTP errors | SmtpTimeout (_,TimeoutMs ms) -> sprintf "Could not connect to SMTP server within %i ms" ms | SmtpBadRecipient (EmailAddress email) -> sprintf "The email %s is not a valid recipient" email

Each case must be converted to a string – but this is only needed once, and only at the last step.

All strings are in one place, so translations are easier.

returnMessage

(or use resource file)

Page 136: Railway Oriented Programming

Designing for errors - review

type ErrorMessage = | NameMustNotBeBlank | EmailMustNotBeBlank | EmailNotValid of EmailAddress // database errors | UserIdNotValid of UserId | DbUserNotFoundError of UserId | DbTimeout of ConnectionString | DbConcurrencyError | DbAuthorizationError of ConnectionString * Credentials // SMTP errors | SmtpTimeout of SmtpConnection | SmtpBadRecipient of EmailAddress

Documentation of everything that can go wrong.

Type-safe -- can't go out of date!

Page 137: Railway Oriented Programming

Parallel tracks

Page 138: Railway Oriented Programming

Parallel validation

nameNotBlank name50 emailNotBlank

Problem: Validation done in series. So only one error at a time is returned

Page 139: Railway Oriented Programming

Parallel validation

nameNotBlank

name50

emailNotBlank

Split input

Combine output

Now we do get all errors at once!

... But how to combine?

Page 140: Railway Oriented Programming

Combining switches

+

Trick: if we create an operation that combines pairs into a new switch, we can repeat to combine as many

switches as we like.

Page 141: Railway Oriented Programming

Combining switches

+ +

Trick: if we create an operation that combines pairs into a new switch, we can repeat to combine as many

switches as we like.

Page 142: Railway Oriented Programming

Combining switches

+ +

Trick: if we create an operation that combines pairs into a new switch, we can repeat to combine as many

switches as we like.

Page 143: Railway Oriented Programming

Domain events

Communicating information to

downstream functions

Page 144: Railway Oriented Programming

Events are not errors

Validate

UpdateDb

SendEmail

Tell CRM that

email was sent

Page 145: Railway Oriented Programming

Events are not errors

Validate

UpdateDb

SendEmail

Tell CRM that

email was sent

type MyUseCaseMessage = | NameMustNotBeBlank | EmailMustNotBeBlank | EmailNotValid of EmailAddress // database errors | UserIdNotValid of UserId // SMTP errors | SmtpTimeout of SmtpConnection // Domain events | UserSaved of AuditInfo | EmailSent of EmailAddress * MsgId

Page 146: Railway Oriented Programming

Events are not errors

Validate

UpdateDb

SendEmail

Tell CRM that

email was sent

type MyUseCaseMessage = | NameMustNotBeBlank | EmailMustNotBeBlank | EmailNotValid of EmailAddress // database errors | UserIdNotValid of UserId // SMTP errors | SmtpTimeout of SmtpConnection // Domain events | UserSaved of AuditInfo | EmailSent of EmailAddress * MsgId

type TwoTrack<'TEntity> = | Success of 'TEntity * Message list | Failure of Message list

Page 147: Railway Oriented Programming

Comic Interlude

Why can't a train

driver be electrocuted? I don't know,

why can't a train driver

be electrocuted?

Because he's not

a conductor!

Page 148: Railway Oriented Programming

Some topics not covered...

... but could be handled

in an obvious way.

Page 149: Railway Oriented Programming

Topics not covered

• Errors across service boundaries

• Async on success path (instead of sync)

• Compensating transactions (instead of two phase commit)

• Logging (tracing, app events, etc.)

Page 150: Railway Oriented Programming

Summary

A recipe for handling errors in a

functional way

Page 151: Railway Oriented Programming

Recipe for handling errors in a functional way

type TwoTrack<'TEntity> = | Success of 'TEntity * Message list | Failure of Message list

Validate UpdateDb SendEmail

type Message = | NameMustNotBeBlank | EmailMustNotBeBlank | EmailNotValid of EmailAddress

Page 153: Railway Oriented Programming

I don’t always have errors...

Page 154: Railway Oriented Programming

I don’t always have errors...

Railway Oriented Programming

@ScottWlaschin

fsharpforfunandprofit.com

FPbridge.co.uk

http://bit.ly/rop-example