val thisIsAnInt : int Full name: index.thisIsAnInt
val thisIsAString : string Full name: index.thisIsAString
val optionalTypeAnnotation : bool Full name: index.optionalTypeAnnotation
type bool = System.Boolean Full name: Microsoft.FSharp.Core.bool
val listOfFloats : float list Full name: index.listOfFloats
val arrayOfStrings : string [] Full name: index.arrayOfStrings
val add : a:int -> b:int -> int Full name: index.add
val a : int
val b : int
val sign : num:int -> string Full name: index.sign
val num : int
val fib : n:int -> int Full name: index.fib
val n : int
val printer : f:float -> unit Full name: index.printer
val f : float
val printfn : format:Printf.TextWriterFormat<'T> -> 'T Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
Multiple items module List from Microsoft.FSharp.Collections -------------------- type List<'T> = | ( [] ) | ( :: ) of Head: 'T * Tail: 'T list interface IEnumerable interface IEnumerable<'T> member GetSlice : startIndex:int option * endIndex:int option -> 'T list member Head : 'T member IsEmpty : bool member Item : index:int -> 'T with get member Length : int member Tail : 'T list static member Cons : head:'T * tail:'T list -> 'T list static member Empty : 'T list Full name: Microsoft.FSharp.Collections.List<_>
val filter : predicate:('T -> bool) -> list:'T list -> 'T list Full name: Microsoft.FSharp.Collections.List.filter
val i : float
val map : mapping:('T -> 'U) -> list:'T list -> 'U list Full name: Microsoft.FSharp.Collections.List.map
val iter : action:('T -> unit) -> list:'T list -> unit Full name: Microsoft.FSharp.Collections.List.iter
val value : 'a
val fn : ('a -> 'b)
type User = {FirstName: string; LastName: string; Age: int;} Full name: index.User
User.FirstName: string
Multiple items val string : value:'T -> string Full name: Microsoft.FSharp.Core.Operators.string -------------------- type string = System.String Full name: Microsoft.FSharp.Core.string
User.LastName: string
User.Age: int
Multiple items val int : value:'T -> int (requires member op_Explicit) Full name: Microsoft.FSharp.Core.Operators.int -------------------- type int = int32 Full name: Microsoft.FSharp.Core.int -------------------- type int<'Measure> = int Full name: Microsoft.FSharp.Core.int<_>
val kai : User Full name: index.kai
val cloneOfKai : User Full name: index.cloneOfKai
val olderKai : User Full name: index.olderKai
type Shape = | Rectangle of width: float * length: float | Circle of radius: float | Triangle of float * float * float Full name: index.Shape
union case Shape.Rectangle: width: float * length: float -> Shape
Multiple items val float : value:'T -> float (requires member op_Explicit) Full name: Microsoft.FSharp.Core.Operators.float -------------------- type float = System.Double Full name: Microsoft.FSharp.Core.float -------------------- type float<'Measure> = float Full name: Microsoft.FSharp.Core.float<_>
union case Shape.Circle: radius: float -> Shape
union case Shape.Triangle: float * float * float -> Shape
val rectangle : Shape Full name: index.rectangle
val circle : Shape Full name: index.circle
val triangle : Shape Full name: index.triangle
val whichShape : shape:Shape -> unit Full name: index.whichShape
val shape : Shape
val width : float
val length : float
val radius : float
val side1 : float
val side2 : float
val side3 : float
Multiple items module Option from Microsoft.FSharp.Core -------------------- type Option<'a> = | Some of 'a | None Full name: index.Option<_>
union case Option.Some: 'a -> Option<'a>
union case Option.None: Option<'a>
val validString : string option Full name: index.validString
type 'T option = Option<'T> Full name: Microsoft.FSharp.Core.option<_>
val invalidString : string option Full name: index.invalidString
val words : string list Full name: index.words
val foundWord : string option Full name: index.foundWord
val tryFind : predicate:('T -> bool) -> list:'T list -> 'T option Full name: Microsoft.FSharp.Collections.List.tryFind
val x : string
val missingWord : string option Full name: index.missingWord
val printString : input:Option<string> -> unit Full name: index.printString
val input : Option<string>
val s : string
type ContactUser = {Username: string; Email: string option; PhoneNumber: string option;} Full name: index.ContactUser
ContactUser.Username: string
ContactUser.Email: string option
ContactUser.PhoneNumber: string option
val createUser : username:'a -> emailAddress:'b -> phoneNumber:'c -> ContactUser Full name: index.createUser
val username : 'a
val emailAddress : 'b
val phoneNumber : 'c
type ContactInformation = | Email of string | PhoneNumber of string | EmailAndPhone of string * string Full name: index.ContactInformation
union case ContactInformation.Email: string -> ContactInformation
union case ContactInformation.PhoneNumber: string -> ContactInformation
union case ContactInformation.EmailAndPhone: string * string -> ContactInformation
type SafeContactUser = {Username: string; Contact: ContactInformation;} Full name: index.SafeContactUser
SafeContactUser.Username: string
SafeContactUser.Contact: ContactInformation
val email : ContactInformation Full name: index.email
val phoneNumber : ContactInformation Full name: index.phoneNumber
val emailAndPhone : ContactInformation Full name: index.emailAndPhone
val user1 : SafeContactUser Full name: index.user1
val user2 : SafeContactUser Full name: index.user2
val user3 : SafeContactUser Full name: index.user3
val user4 : SafeContactUser Full name: index.user4
val user5 : SafeContactUser Full name: index.user5
val parseDateTime : dateTime:string -> 'a Full name: index.parseDateTime
val dateTime : string
val getYear : date:System.DateTime -> int Full name: index.getYear
val date : System.DateTime
namespace System
Multiple items type DateTime = struct new : ticks:int64 -> DateTime + 10 overloads member Add : value:TimeSpan -> DateTime member AddDays : value:float -> DateTime member AddHours : value:float -> DateTime member AddMilliseconds : value:float -> DateTime member AddMinutes : value:float -> DateTime member AddMonths : months:int -> DateTime member AddSeconds : value:float -> DateTime member AddTicks : value:int64 -> DateTime member AddYears : value:int -> DateTime ... end Full name: System.DateTime -------------------- System.DateTime() (+0 other overloads) System.DateTime(ticks: int64) : unit (+0 other overloads) System.DateTime(ticks: int64, kind: System.DateTimeKind) : unit (+0 other overloads) System.DateTime(year: int, month: int, day: int) : unit (+0 other overloads) System.DateTime(year: int, month: int, day: int, calendar: System.Globalization.Calendar) : unit (+0 other overloads) System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : unit (+0 other overloads) System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: System.DateTimeKind) : unit (+0 other overloads) System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: System.Globalization.Calendar) : unit (+0 other overloads) System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : unit (+0 other overloads) System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: System.DateTimeKind) : unit (+0 other overloads)
property System.DateTime.Year: int
val date : System.DateTime Full name: index.date
val currentYear : int Full name: index.currentYear
val composedGetYear : (string -> int) Full name: index.composedGetYear
val yearFromComposed : int Full name: index.yearFromComposed
val add : x:int -> y:int -> int Full name: index.add
val x : int
val y : int
val threeParams : firstName:'a -> middleName:'b -> lastName:'c -> unit Full name: index.threeParams
val firstName : 'a
val middleName : 'b
val lastName : 'c
val curriedAdd : (int -> int) Full name: index.curriedAdd
Multiple items val double : (int -> int) Full name: index.double -------------------- type double = System.Double Full name: Microsoft.FSharp.Core.double
val sum : list:int list -> int Full name: index.sum
Multiple items val list : int list -------------------- type 'T list = List<'T> Full name: Microsoft.FSharp.Collections.list<_>
val reduce : reduction:('T -> 'T -> 'T) -> list:'T list -> 'T Full name: Microsoft.FSharp.Collections.List.reduce
val freeSum : (int list -> int) Full name: index.freeSum
val batchesOf : n:int -> (seq<'a> -> seq<seq<'a>>) Full name: index.batchesOf
module Seq from Microsoft.FSharp.Collections
val mapi : mapping:(int -> 'T -> 'U) -> source:seq<'T> -> seq<'U> Full name: Microsoft.FSharp.Collections.Seq.mapi
val i : int
val v : 'a
val groupBy : projection:('T -> 'Key) -> source:seq<'T> -> seq<'Key * seq<'T>> (requires equality) Full name: Microsoft.FSharp.Collections.Seq.groupBy
val fst : tuple:('T1 * 'T2) -> 'T1 Full name: Microsoft.FSharp.Core.Operators.fst
val map : mapping:('T -> 'U) -> source:seq<'T> -> seq<'U> Full name: Microsoft.FSharp.Collections.Seq.map
val snd : tuple:('T1 * 'T2) -> 'T2 Full name: Microsoft.FSharp.Core.Operators.snd
val doubleAndIncrement : x:int -> int Full name: index.doubleAndIncrement
val freeDoubleAndIncrement : (int -> int) Full name: index.freeDoubleAndIncrement
val result : string list * int Full name: index.result
val async : AsyncBuilder Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
val username : string
val replies : string list
type 'T list = List<'T> Full name: Microsoft.FSharp.Collections.list<_>
val count : int
val length : list:'T list -> int Full name: Microsoft.FSharp.Collections.List.length
Multiple items type Async static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool> static member AwaitTask : task:Task -> Async<unit> static member AwaitTask : task:Task<'T> -> Async<'T> static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool> static member CancelDefaultToken : unit -> unit static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>> static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T> static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T> static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T> static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T> static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> static member Ignore : computation:Async<'T> -> Async<unit> static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable> static member Parallel : computations:seq<Async<'T>> -> Async<'T []> static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T static member Sleep : millisecondsDueTime:int -> Async<unit> static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T> static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>> static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>> static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit> static member SwitchToNewThread : unit -> Async<unit> static member SwitchToThreadPool : unit -> Async<unit> static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T> static member CancellationToken : Async<CancellationToken> static member DefaultCancellationToken : CancellationToken Full name: Microsoft.FSharp.Control.Async -------------------- type Async<'T> Full name: Microsoft.FSharp.Control.Async<_>
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:System.Threading.CancellationToken -> 'T
Introduction to F# for greater type safety in .NET
Who here has any prior experience with F#/Functional programming
What is Functional Programming
Focus on data flow
Function Composition over Inheritance
Expressions over Statements
Focus on data flow instead of individual instructions. This gives a greater insight into what elements depend on what data, and in turn allows for easier refactoring in the future. See "What's Functional Programming All About?" by Li Haoyi
Common traits of the Functional paradigm
Higher order functions
High level abstractions
Code reusability
Pure Functions
Easier to reason about
Idempotency
Immutability
Predictability
Thread safety
Less bugs
"If it compiles, it works"
"Pit of Success"
The ball is your project and its architecture
The top of the mountain is a successful project
Takes a lot of hard work and discipline
However, the moment you or someone else on the team makes a mistake, all that hard work is undone
For example, making a database call directly from a Domain Model, instead of going through predefined services and other abstractions
This time, success is at the bottom of the pit
That doesn't mean that success is bad, simply that gravity helps you get there
What that means in practice, is that the F# type system and compiler help you get there
And once your in that pit, it's much easier to stay successful, as trying to get out now takes more effort
It's much more difficult to make mistakes, or to ignore established patterns and abstractions, because the compiler enforces them
What is F#
Functional first, multi-paradigm language
Runs on .Net (and Mono, Xamarin, .Net Core)
First released in 2005 by Microsoft Research
Now belongs to The F# Software Foundation
Open Source
Syntax in a nutshell
ML syntax
Statically Typed with type inference
Whitespace significant
Expression-based
Immutable by default
1:
2:
3:
4:
5:
6:
7:
8:
9:
let thisIsAnInt = 1
let thisIsAString = "This is a string"
let optionalTypeAnnotation : bool = true
let listOfFloats = [ 1.0 ; 2.0 ; 3.0 ]
let arrayOfStrings =
[| "www.zuehlke.com"
"www.duckduckgo.com"
"www.microsoft.com" |]
let declarations are called values instead of variables
Functions
First class functions
Can be passed around like other values
Last expression is the return "statement"
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
let add a b =
a + b
let sign num =
if num > 0 then "positive"
elif num < 0 then "negative"
else "zero"
let rec fib n =
match n with
| 1 -> 1
| 2 -> 1
| n -> fib (n - 1 ) + fib (n - 2 )
Piping
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
let printer f = printfn "Number is: %f " f
[0.0 .. 100.0 ]
|> List . filter (fun i -> i % 2.0 = 0.0 )
|> List . map (fun i -> i ** 2.0 )
|> List . iter printer
// |> List.iter (fun i -> printer i)
let (|> ) value fn =
fn value
Records
Simple aggregates of data
Can be struct or reference types
Has structural equality
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
type User =
{ FirstName : string
LastName : string
Age : int }
let kai = { FirstName = "Kai" ; LastName = "Ito" ; Age = 88 }
let cloneOfKai = { FirstName = "Kai" ; LastName = "Ito" ; Age = 88 }
printfn " %b " (kai = cloneOfKai ) // true
let olderKai = { kai with Age = kai . Age + 1 }
printfn " %i " (olderKai . Age ) // 89
printfn " %i " (kai . Age ) // 88
Discriminated Unions
More powerful enum
Data point that can have multiple different types
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Triangle of float * float * float
let rectangle : Shape = Rectangle (2.0 , 5.0 )
let circle : Shape = Circle 2.5
let triangle : Shape = Triangle (6.1 , 2.0 , 3.7 )
let whichShape (shape : Shape ) =
match shape with
| Rectangle (width , length ) ->
printfn "Rectangle with sides %f %f " width length
| Circle radius ->
printfn "Circle with radius %f " radius
| Triangle (side1 , side2 , side3 ) ->
printfn "Triangle with sides %f %f %f " side1 side2 side3
Benefits of the F# type system
No null
Make illegal states unrepresentable
Use types to represent the domain
Types can also be used to encode business logic
Files and code must be in dependency order
File and code cannot use forward reference. What this means is that code can only reference values and functions written above it. This means the order of files are important.
This may seem really strange at first, but as you get used to this quirk, you quickly learn to appreciate this organization as it becomes incredibly easy to read large projects
The Option type
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
type Option < ' a > =
| Some of ' a
| None
let validString : string option = Some "text"
let invalidString : string option = None
let words = [ "hello" ; "world" ; "from" ; "home" ; ]
let foundWord = words |> List . tryFind (fun x -> x = "world" )
let missingWord = words |> List . tryFind (fun x -> x = "outside" )
printfn "The word is: %s " foundWord
// Compile Error: Type mismatch: Expecting "string" but got "string option"
let printString input =
match input with
| Some s -> printfn "The word is: %s " s
| None -> printfn "Didn't find word"
printString foundWord // The word is: world
printString missingWord // Didn't find word
This is how a lack of value is represented
Many standard library and 3rd party libraries use the Option type as a return value
Why is this useful?
The compiler forces you to handle both cases
Forces you to think about what happens when you get an unexpected result
Making illegal state unrepresentable
Imagine business logic where a User either needs an email address or phone number or both
Required to have at least one of them
1:
2:
3:
4:
5:
6:
7:
type ContactUser = { Username : string ; Email : string option ; PhoneNumber : string option }
let createUser username emailAddress phoneNumber =
// Logic to assign either email address or phone or both
// Error prone
{ Username = "kaiito" ; Email = "kai.ito@zuehlke.com" ; PhoneNumber = "123 456 789" }
F# Type System to the rescue
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
type ContactInformation =
| Email of string
| PhoneNumber of string
| EmailAndPhone of string * string
type SafeContactUser = { Username : string ; Contact : ContactInformation }
let email = Email "kai.ito@zuehlke.com"
let phoneNumber = PhoneNumber "123 456 789"
let emailAndPhone = EmailAndPhone ("kai.ito@zuehlke.com" , "123 456 789" )
let user1 = { Username = "kaiito1" ; Contact = email }
let user2 = { Username = "kaiito2" ; Contact = phoneNumber }
let user3 = { Username = "kaiito3" ; Contact = emailAndPhone }
let user4 = { Username = "kaiito4" ; Contact = null } // Compiler Error
let user5 = { Username = "kaiito5" ; Contact = "someString" } // Compiler Error
When deconstructing the Contact information, automatically know what kind of contact information it is
Assumes happy path
Validation has been done elsewhere
Goes along with pit of success
Dependency Order
Code in State.fs can't reference any code declared in any of the files below it.
This allows for architectural constructs, such as the Onion model, to be type checked by the compiler
Can't accidentally call a database method from the Domain model which is supposed to be pure
"Pit of Success" forced to do things the proper way
Function Composition
Compose multiple functions into one function
Code reusability without verbosity
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
let parseDateTime (dateTime : string ) = DateTime . Parse (dateTime )
let getYear (date : System . DateTime ) = date . Year
let date = parseDateTime "26-04-2020 12:00am"
let currentYear : int = getYear date
printfn "The current year is: %i " currentYear
// The current year is: 2020
let composedGetYear : string -> int = parseDateTime > > getYear
let yearFromComposed : int = composedGetYear "26-04-2020 12:00am"
printfn "The current year from composed function is: %i " yearFromComposed
// The current year from composed function is: 2020
Before I get into a use case for function composition, I'd like to take a look at function currying first
Function Currying and Partial Function Application
Creating new functions by not supplying all parameters
F# curries all functions by default
What does x: int -> y: int -> int
mean
Curried function
1:
2:
let add x y =
x + y
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
let add x =
fun y ->
x + y
let threeParams firstName middleName lastName =
printfn "Full name is: %O , %O , %O " firstName middleName lastName
let threeParams firstName =
fun middleName ->
fun lastName ->
printfn "Full name is: %O , %O , %O " firstName middleName lastName
A curried function is a function created by only supplying a part of the parameters a function expects
This essentially "binds" the given values to those parameters
Works with any number of parameters
Partial Function Application
Using curried functions
Only supply some of the parameters
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
let add x y =
x + y
let curriedAdd = add 3
printfn "Result is: %i " (curriedAdd 5 )
// Result is: 8
let double = (*) 2
[1.. 10 ]
|> List . map double
// [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]
Point-free programming
What happens when you abuse currying and partial function application
Avoid explicitly specifying parameters
Use Higher-order functions everywhere
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
let sum list = List . reduce (+ ) list
let freeSum = List . reduce (+ )
let batchesOf n =
Seq . mapi (fun i v -> i / n , v ) > >
Seq . groupBy fst > >
Seq . map snd > >
Seq . map (Seq . map snd )
[0.. 10 ]
|> batchesOf 2 // [[0; 1]; [2; 3]; [4; 5]; [6; 7]; [8; 9]; [10]]
let doubleAndIncrement x = x * 2 + 1
let freeDoubleAndIncrement = (*) 2 > > (+ ) 1
Both functions have same signature
The first explicitly defines a parameter
The second returns a function that requires a parameter
Demo
functionCurryingDemo.fsx
You use a free monad which allows you to build a monad from any functor
Dependency injection can be done via partial function application
Railway Oriented Programming
Error handling through function composition
Clean control flow
Treat errors as first class citizens
Not just exceptions. Any errors, for example validation messages as well
Problems with the Imperative approach
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
public string UpdateAndSend(string input)
{
if (string .IsNullOrWhitespace(input))
return "empty input" ; // Or should we throw an Exception?
try {
var updatedUser = UpdateUserInDatabase(input);
return ConvertToJson(updatedUser);
}
catch (DatabaseException e) {
logger.log(e.Message);
return "Problem updating user in DB" ; // Or should we rethrow?
}
}
There's no unifying way to handle errors
Sometimes it uses try catch, other times simple conditional statements
Should we throw exceptions, or should we return different failure messages
Forces caller to handle each failure case individually
Let's take a look at our Red Circle to Yellow Star function from earlier
What happens when some kind of error occurs?
We can model our function like a railway switch
We have our input, and we have a happy path, and a sad path
The happy path can cause an error, but the error path can't go back to the happy path
And just like real rail tracks, we can easily combine them
But since we're combining functions, we can easily Compose them like we saw earlier
However, now you might ask, what happens to the end of the error track. Where does that go?
We can expand the input of our functions to take in an error as well
As I mentioned a moment ago, once we are on an error path, we won't be going back to the happy path
Reason being, we will no longer have valid input for the next part in our workflow, which would cause additional errors to occur
We can now expand this model to compose as many functions as we want, creating an entire workflow
Demo
railwayOrientedDemo.fsx
We partially apply the bind function
Railway oriented programming can do much more, for example running multiple tracks in parallel
or using single track functions, or having dead end tracks.
The standard library already includes several helper methods, but there also exists several open source libraries for working with RoP, one such library is called Chessie
Monads
A monad is just a monoid in the category of endofunctors
Chainable wrapper around a data structure that performs an extra operation after each expression
Semicolon at end of statement performs extra action
Monad in Detail
A constructor that wraps a value: the monadic value M
A bind function that accepts a function as its parameter
Applies this function to the internally wrapped value M
Returns the function’s output wrapped as a monad
A return function that simply unwraps the monadic value
JavaScript Promise
1:
2:
3:
4:
5:
const myPromise =
(new Promise(() = > getDataFromBackend())))
.then(response = > callThirdPartyApi(response.username))
.then(response = > updateHtml(response.data))
.catch (error = > console.log(error));
F# Computation Expression
NOT Monads
Can be used to express monads
Here is the async monad, or continuation monad
Computation expressions can also be used to represent iterators, LINQ queries, or generators to name a few examples
1:
2:
3:
4:
5:
6:
7:
8:
9:
let result =
async {
let! (username : string ) = getByIdAsync 1
let! (replies : string list ) = getRepliesByUsernameAsync username
let count = replies |> List . length
return (replies , count )
} |> Async . RunSynchronously
The let bang calls the bind method on the builder, in this case the async builder
Regular let works normally
Demo
Easier to understand with a demo
Lets create a computation expression from scratch
computationExpression.fsx