Beer, Meditation & Railway Oriented Programming

By Moko Sharma

[ @MokoSan ]

About Me

  • Graduated
    [ May 2015 ]

  • Developer @ SIG
    [ August 2015 ]

  • Discovered F#
    [ December 2015 ]

  • Became an Enemy of Mutable State
    [ First Null Reference Exception ]

Agenda


  1. ROP

  2. Beer

  3. Suave.IO

  4. Meditation

Railway Oriented Programming

Exceptions, Exceptions Everywhere

What is ROP?

Is a..

Clean,

Functional,

Robust,

Way of Dealing with the Unhappy Path

The Model I

 
                            type Input = ...
                        
 
                            type Result<'TSuccess, 'TFailure> =
                                | Success of 'TSuccess
                                | Failure of 'TFailure
                        

The Model II

Goal: To Combine / Compose 1 Input - 2 Output Functions into a Robust Framework

Combiner 1: Bind [ >>= ]

Converts 1 Input - 2 Output Functions into 2 Input - 2 Output Functions

Combiner 1: Bind [ >>= ]

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

                            let (>>=) switchFunction input =
                                bind switchFunction input 
                        
 
                            let combineROPFunctions  = ropFunction1 >> bind ropFunction2
                            let combineROPFunctions' input = 
                                input 
                                |> ropFunction1 
                                >>= ropFunction2
                        

Combiner 2: Switch [ >=> ]

Combining Many 1 Input - 2 Output Functions Together into One Function

Combiner 2: Switch [ >=> ]

 
                            let switch switchFunction1 switchFunction2 input =
                                match switchFunction1 input with
                                | Success s -> switchFunction2 s
                                | Failure f -> Failure f

                            let (>=>) switchFunction1 switchFunction2 input =
                                switch switchFunction1 switchFunction2 input 
                        
 
                            let combineROPFunctions  = switch ropFunction1 ropFunction2
                            let combineROPFunctions' = ropFunction1 >=> ropFunction2
                        

Adapting Different Functions in this Model

Case 1: Database Write Functions

 
                            let writeToDatabase databaseWrite input = 
                                try
                                    databaseWrite input 
                                    Success input
                                with
                                | ex -> Failure ex.Message
                        

Case 2: Methods that Are Always Successful

 
                            let changeData dataChanger input = 
                                dataChanger input |> Success
                        

Case 3: What Happens in the End?!

 
                            let doubleMap successCallback failureCallback result =
                                match result with
                                | Success s -> successCallback s 
                                | Failure f -> failureCallback f 
                        

An ROP Example : Saving Customer Info to a Database

  • Validate Input

  • Canonicalize Input

  • Save To Database

  • Handle Result

Tired Hands Fun Fun

Timer Callback [ Happy Path ]

  1. Scrape

  2. Compare

  3. Try Alert

 
                            scrape >=> compare >=> tryAlert
                        

Errors / No-Ops [ Unhappy Path ]

  1. Scrape - Web Errors

  2. Compare - I/O Errors

  3. Alert - API Errors

  4. No Difference Found

 
                            type Errors =
                                | WebError of string
                                | IOError  of string
                                | APIError of string
                                | NoDifferenceFound
                        

ROP View

 
                    type BeerInfo = {ScrapeDateTime : Date; Beers : string list}
                    

Libraries Used

Chiron for JSON

Logary for Logging

C# Twilio API

Next Steps

  • Deploy as an Azure Function

  • Manayunk Brewery + Others

  • Distribution of Updates - Machine Learning

Suave.IO

Is a ..

Non-Blocking,

Lightweight,

Functional-First,

F# Web Development Library

Web-Server Function

Web-Server Function

 
                            Request -> Response option
                            Request -> Async< Response option > 
                        

Simplified Suave Types - I

Request

 
                            type RequestType = GET | POST | .. 
type Request = { Route : string; Type : RequestType }

Response

 
                            type StatusCode = int 
type Response = { Content : string; StatusCode : StatusCode }

Simplified Suave Types - II

Context

 
                            type Context = { Request : Request; Response : Response }
                        

Option Based Async Context

 
                            Context -> Async< Context option > 
                        

Simplified Suave Types Combined

 
                            type WebPart = Context -> Async< Context option > 
                        

Web-Part Combinators

 
                    let combine webPart1 webPart2 context = 
                        async {
                            let! firstResult = webPart1 context
                            match firstResult with
                            | Some f -> return! webPart2 f 
                            | None   -> return None
                        }

                    let (>=>) combine 
                    

ROP View

A Suave Web-Part Example

 
                            let OK content context : WebPart =
                                let response = { Content : content; StatusCode = 200 }
                                { context with Response = response } 
                                |> Some
                                |> async.Return
                        

Combined:

 
                        ( OK "All" ) >=> ( OK "Your" ) >=> ( OK "Base" ) ...
                        

Filters

 
                            let iff condition context = 
                                if condition then 
                                    context |> Some |> async.Return
                                else 
                                    None |> async.Return

                            let GET = iff ( fun context -> context.Request.Type = GET )
                            let path inputPath = 
                                iff ( fun context -> context.Request.Route = inputPath ) 

                            GET  >=> path "/somePath" >=> OK "GET"
                            POST >=> path "/somePath" >=> OK "POST" 
                        

Remember..


                            type RequestType = GET | POST | .. 
                            type Request     = { Route : string; Type : RequestType }
                        

Choosing Web-Parts


                    let rec choose webParts context : WebPart option = async {
                        match webParts with
                        | [] -> return None
                        | x :: xs -> 
                            let! result = x context 
                            match result with 
                            | Some r -> return! Some r 
                            | None   -> choose xs context
                    }
                    

From Web-Parts to Web Applications


                    let app : WebPart = choose [  
                        GET  >=> path "/resource1" >=> OK "r1: GET Success"
                        POST >=> path "/resource1" >=> OK "r1: POST Success"
                        path "/resource2" >=> choose [
                            GET >=> OK "r2: GET Success"
                            POST >=> OK "r2: POST Success"
                        ]
                    ]

                    startWebServer defaultConfig app
                    

This

Is

Meditation Fun Fun

Meditation Fun Fun

Is a Meditation Timer and Journal SPA

Rest Resource


                        type RestResource< 'TModel > = {
                            Name     : string
                            GetAll   : unit    -> seq<'TModel>
                            GetById  : int     -> 'TModel option 
                            IsExists : int     -> bool 
                            Create   : 'TModel -> 'TModel
                            Update   : int     -> 'TModel -> 'TModel
                            Delete   : int     -> unit
                        }
                        

Creating WebParts from Rest Resources

Api.Controller.Common


                        
                        let getWebPartFromRestResource resource : WebPart = 
                            ...

                            let handleResource requestError = function 
                                | Some r -> r |> Jsonize
                                | None   -> requestError

                            let getResourceById = 
                                resource.GetById >> handleResource ( NOT_FOUND "Resource Not Found" )

                            GET >=> pathScan resourceIdPath getResourceById
                        

Journals

Data Model


                            type Journal = { Title : string; 
                                             DateOfMeditation: DateTime; 
                                             Content : string }
                        

Database Connectivity


                                let connectionString = "..." 
                                type sql = 
                                    SqlDataProvider< ConnectionString = connectionString >

                                let dbContext = sql.GetDataContext()
                                let dbo       = dbContext.Dbo
                            

Journal CRUD

Selecting Specific Rows


                        ...
                        let journalDbo = query {
                                for j in dbo.Journals do
                                where j.Id = id
                                select j
                                headOrDefault
                            }
                        

Creating New Journals


                            let newJournalDbo = dbo.Journals.Create()
                            newJournalDbo.Title <- "Meditation - 06/07/2017"
                        

Deleting Journals


                            journalDbo.Delete()
                        

Combining Model and Controller I

Api.Controller.Journal


                        let journalWebPart = getWebPartFromRestResource {
                            Name       = "journals"
                            GetAll     = getAllJournals 
                            GetById    = getJournalById
                            Create     = createNewJournal
                            Update     = updateJournal 
                            UpdateById = updateJournalWithId
                            Delete     = deleteJournal 
                            IsExists   = isJournalExists 
                        }
                        

App


                        let api = choose [ journalWebPart; ]
                        startWebServer defaultConfig api 
                        

Next Steps

  • Multiple Users + User Authentication
  • Dockerize
  • Deploy via Azure
  • Integrate with the FitBit Api

Talk Inspired by:

Scott Wlaschin's Railway Oriented Programming

Tamizhvedhan's F# Applied

Tired Hand's Awesome Beer Selection

/r/30DaySit

Thank you!

Questions?