次の方法で共有


Two Track Coding (ROP) For Dummies – Part 1

Hello World!

This is a pull from my blog located here: https://indiedevspot.azurewebsites.net/2015/01/19/two-track-coding-rop-for-dummies-part-1/

So if you made it to this article, that means you probably have seen this already.  If you haven’t, you really need to.  That is really step one to this whole thing.  Scott wrote a fantastic article there and I finally understand how it works after weeks of studying and coding and working on a new F# project.  This article series is meant to take what Scott and others have written and break it down such that mere mortals and dummies like myself can grasp on to.

Before we get too far, I need to explain a little bit about what Two Track Coding, Railway Oriented Programming (or for short here on out ROP) is and why it is great.  Coding without ROP yields code that can have tons of if/then/else, try/catch nests and is an absolute bear to read, write, understand, debug and also ensure all cases are caught.  Go to https://fsharpforfunandprofit.com/rop/ for more.

 

What building the ROP infrastructure into your code provides is code that is extremely succinct, and looks as though you are simply making function calls, but with all of the nesting, branching and error catching logic built in.  Not only this, but it provides the same exact output type for every single one of your functions, which allows you to write some very powerful code and is the premise in which this is all built.

You can download the entirety of the script file for experimentation here: https://onedrive.live.com/redir?resid=478A64BC5718DA47\!299\&authkey=\!ALbqUC1LcqWQevo\&ithint=file%2cfsx

The Concepts

There are a few key scenarios to cover.

1.  Bind: Validation of a single input parameter across multiple functions.  For example: String is not Empty and String is not over 255 Characters.

2.  Applicatives: Validation of an entire Type.  For example: Nerd {FirstName, LastName}.  We want to ensure neither are empty or over 255 characters. (Maps to a GetSingle)

3.  Options: Validation of a sequence of Types.  For example: List[NerdA; NerdB].  Ensure both Nerds are acceptable. (Maps to a GetAll)

Bind

So lets just start off here with a short code sample on what this looks like after the ROP infrastructure is set up and then explain the various components of it.

1 2 3 4 let CreateFirstName(s:string)=     s |> FirstNameNotBlank         >>= FirstNameNotOver255Characters         >>= NameDoesNotContainATSymbol

So, just to be thorough, lets cover all of the basis.

  1. This is a function that takes a single string parameter, executes 3 functions and returns the result of the function NameDoesNotContainATSymbol.
  2. s |> FirstNameNotBlank means take s, our input, and pipe it into the function, FirstNameNotBlank.
  3. >>= is our own custom defined infix operator.  Where infix operator simply means a computational operator that goes between two values. the addition symbol (+) is an example of an infix operator. (3 + 2) = 5.
  4. Errors are built into each of the smaller functions comprising this function, meaning that this function as a whole has errors and branching built in.

So next stop, lets look at one of the comprising functions to see how it is built.  Every function at this “bottom tier” is built in the same fashion.  Single value in Result out.

1 2 3 4 let FirstNameNotOver255Characters(s:string)=     if s.Length < 256     then Success s     else Failure [FirstNameMustNotBeOver255Characters]

So this looks straight forward, but this Success/Failure thing, these are not defined in the F# language, where did these come from, and what about this funky thing in the brackets?

  1. Success and Failure are of the discriminated union type Result<’TSuccess> (we will look at this closer)
  2. This is where “two track” comes from.  One track for Success and one track for failure.  Notice in Failure that it has a very specific message.  Failure itself is a discriminated union as well.

Discriminated Unions

Lets just look at how they are built.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type FailureMessage =     //Nerd Data Type Errors     | FirstNameMustNotBeBlank     | FirstNameMustNotBeOver255Characters     | LastNameMustNotBeBlank     | LastNameMustNotBeOver255Characters     | NamesCannontContainATSymbol     | PhoneMustNotBeBlank     | PhoneMustBe7Characters     | EmailAddressMustNotBeBlank     | EmailAddressMustNotBeOver255Characters     | AddressForeignKeyMustExist     | DefaultFailure     | PhoneNumberWrong   ///Result is either a Success or a Failure with a list of errors type Result<'TSuccess> =     | Success of 'TSuccess     | Failure of FailureMessage list

So lets start with the simpler of the two union types: FailureMessage.  This is simply defining all of the options in the type FailureMessage, very similar to enum in C#.  These are all of the possible failures that can occur in the application.

The union type Result<’TSuccess> can be of type Success or Failure.  The of keyword indicates that that potential ‘enum’ value is of a specific type.  So here if a Result is a Success, the result must be of the same type of the success.  For example: Result<string> Success is of type string.  This is due to the generics definition and its use after the keyword of <GenericDefinition>.

Failure is a list type, of FailureMessage.  This means, that if you have a result of Failure, it is “wrapping” a list of failure messages.

So in summary, a Result really can be thought of as Success<genericType> or Failure<listofmessages> but as they are a discriminated union, this allows a function to return either or as the same type.  This is extremely powerful as you will soon see.

Finally to Bind and the Infix Operator

Bind and the infix operator.  Recall from the first example >>= is our infix version of bind.  What this is really doing is converting our function from val x -> Result<t> to Result<t> -> Result<u>.  So instead of a value in and a result out, it is a result in and a result out.  This is what allows us to do “functional composition” or simply piping outputs of functions into the input of the next function.

Show me the code!

1 2 3 4 5 6 7 let bind f i =     match i with         | Success s -> f s         | Failure f -> Failure f   let (>>=) i f =     bind f i

So lets break it down…

  1. The function we defined “bind” takes f, a function, which is val -> Result<t> and I, which is Result<a>.  So normal function and result in, result<b> out.  Remember, Result is simply Success or Failure wrapping values or Failure Messages.
  2. If i is a Success Result type, we pull the value out of the generic and execute the input function against that.  Example: Success<string(“hello”)>, Function: printf.  This would choose the success path and attempt to printfn “hello”.   One of the best articles I’ve seen on this can be found here.
  3. If i is a Failure type, we simply keep going down the failure path with the current list of messages.  During bind, this is typically only a single message and the first failure encountered. (We will discuss why a list of messages later.)
  4. Finally, let (>>=) i f = bind f i, simply states that >>= is an infix operator, where i is on the left, f is on the right and we map that to bind.

I wrote bind and the infix as such to demonstrate a point, however, I would normally write it as such below…

1 2 3 4 5 6 let bind i f =     match i with         | Success s -> f s         | Failure f -> Failure f   let (>>=) = bind

Bind Summary

So lets finally review the first Bind code snippet one more time to fully understand.

1 2 3 4 let CreateFirstName(s:string)=     s |> FirstNameNotBlank         >>= FirstNameNotOver255Characters         >>= NameDoesNotContainATSymbol

So we have a one track function that takes a single parameter, and pipes it through several other one track functions, which contain Success/Failure branching logic.  Using the bind infix operator, we can transform any one track function into a two track function.  In this example, s:string gets piped through the entirety of all functions on either the success or failure track.  We also covered all of the basic functional concepts to understand how this works.

This works great for single parameter validation, such as create First Name, but I want to create a Nerd, or several Nerds.  For that, we must build upon these stepping stones in subsequent parts of this series.

Again, you can download the entire script file here: https://onedrive.live.com/redir?resid=478A64BC5718DA47\!299\&authkey=\!ALbqUC1LcqWQevo\&ithint=file%2cfsx