Introduction to F#
Mikhail Smal https://mikhail.dev Win a ticket to Update Conference 2019!
https://forms.gle/Z9EBCmSG877BBQMg7 Introduction to F#
Mikhail Smal https://mikhail.dev
F#
▪ Multiparadigm programming language ▪ Functional first ▪ Strongly and statically typed ▪ Open-source ▪ Cross-platform thanks to .NET Core ▪ Compiles to JavaScript thanks to Fable ▪ Scripting language ▪ Awesome community Where to use
▪ Web development with SAFE ▪ Saturn – web-server framework ▪ Azure – cloud provider ▪ Fable – F# -> JS compiler ▪ Elmish – Model-View-Update implementation ▪ Serverless (FaaS) ▪ Mobile development (Xamarin, React Native) Tooling
▪ Visual Studio ▪ Visual Studio Code (Ionide extension) ▪ JetBrains Rider ▪ MonoDevelop ▪ vim-fsharp Basic syntax Values
// int let age = 30 // inferred
// int let age : int = 31 // explicit
// string let age = string age // totally different type
// unit let nothing = () Functions
// 'a -> 'a -> 'a let add x y = x + y // indentation instead of curly brackets
// int -> int let addTen = add 10 // partial application
// int let result = addTen 2 // 12
// ('a * 'b) -> ('a -> 'b -> 'c) -> 'c let exec (x, y) f = f x y let result1 = exec (100, 10) (-) // 90 let result2 = exec ("Hello", "developers") (sprintf "%s, %s!") // ”Hello, developers!” let (>>!) = exec let result3 = ("Hello", "devs") >>! (sprintf "%s, %s!") let result4 = ("Hello", "devs") ||> (sprintf "%s, %s!") Functions with types let add (x : int) (y : int) : int = x + y let printAll<'a> (collection : 'a seq) = Seq.iter (fun x -> printfn "%A" x) collection printAll
let ``Function throws an exception when receives ¯\_(ツ)_/¯`` () = ... Type system type [
// 5 characters starting with T type PhoneCode = PhoneCode of string // 10000 < code < 100000 type TabletCode = TabletCode of int type GadgetCode = | PhoneCode of PhoneCode | TabletCode of TabletCode type Gadget = { Code : GadgetCode Name : GadgetName Weight : Gramms ScreenSize : Inches } Restrictions
// 5 characters starting with T type PhoneCode = private PhoneCode of string let phoneCode = PhoneCode "T1234" // Won't compile let (PhoneCode code) = phoneCode // Won't work in other modules type Option<'a> = | Some of 'a | None module PhoneCode = let create (s : string) = if not (isNull s) && s.StartsWith("T") && s.Length = 5 then Some (PhoneCode s) else None
let value (PhoneCode code) = // Works only in one module code Pattern matching open FSharp.Data.UnitSystems.SI.UnitSymbols type Direction = North | South | East | West type Weather = | Cold of temperature:float
// int -> float let foo x = ...
// float -> string let bar x = ...
// int -> string let fooBar = foo >> bar Pipelines let data = [(true, 100); (true, 200); (false, -10)] data |> List.filter fst |> List.sortByDescending snd |> List.take 1 |> List.map string |> List.head Computation expressions let squares = seq { for i in 1..10 do yield i * i } let ints = seq { yield! [1;2;3] } Computation expressions let option1 = Some 1 let option2 = Some 2 let result = let result = optional { option1 let! value1 = option1 |> Option.bind (fun value1 -> let! value2 = option2 option2 return value1 + value2 |> } Option.bind (fun value2 -> Some (value1 + value2) ) ) Saturn CEs let app = application { pipe_through endpointPipe
use_router topRouter url "http://0.0.0.0:8085/" memory_cache use_static "static" use_gzip } let ctrl = controller { index someIndexHandler add someAddHandler show someShowHandler edit someEditHandler } Railway-oriented programming Imperative code string ExecuteUseCase() { var request = receiveRequest(); validateRequest(request); canonicalizeEmail(request); db.UpdateDbFromRequest(request); smtpServer.SendEmail(request.Email); return "Success"; } Functional flow let executeUseCase = receiveRequest >> validateRequest >> canonicalizeEmail >> updateDbFromRequest >> sendEmail >> returnMessage Imperative code with error handling string UpdateCustomerWithErrorHandling() { var request = receiveRequest(); validateRequest(request); canonicalizeEmail(request); db.UpdateDbFromRequest(request); smtpServer.SendEmail(request.Email);
return "OK"; } 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"; } 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"; } 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"; } 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"; } Functional flow with error handling Before let executeUseCase = receiveRequest >> validateRequest >> canonicalizeEmail >> updateDbFromRequest >> sendEmail >> returnMessage Functional flow with error handling After let updateCustomerWithErrorHandling = receiveRequest >> validateRequest >> canonicalizeEmail >> updateDbFromRequest >> sendEmail >> returnMessage Result type type Result<'Success,'Failure> = | Ok of 'Success | Error of 'Failure Validation function
Request Validate Success
Failure let validateInput input = if input.name = "" then Error "Name must not be blank" elif input.email = "" then Error "Email must not be blank" else Ok input // Success Switch Switches composition
Validate UpdateDb on success
bypass Switches composition
Validate UpdateDb Switches composition
Validate UpdateDb SendEmail Switches composition Switches composition One track composition Two-track composition Wrong composition Adapter block
let bind switchFunction = fun twoTrackInput -> match twoTrackInput with | Ok s -> switchFunction s | Error f -> Error f
val bind : ('a -> Result<'b,'c>) -> Result<'a,'c> -> Result<'b,'c> Bind example
nameNotBlank (combined with) name50 (combined with) emailNotBlank
// Input -> Result Bind example
bind nameNotBlank bind name50 bind emailNotBlank
// (bind nameNotBlank): Result -> Result Bind example bind nameNotBlank >> bind name50 >> bind emailNotBlank Bind example
let validateRequest = bind nameNotBlank >> bind name50 >> bind emailNotBlank
// validateRequest: Result -> Result Bind example let (>>=) twoTrackInput switchFunction = bind switchFunction twoTrackInput
let validateRequest twoTrackInput = twoTrackInput >>= nameNotBlank >>= name50 >>= emailNotBlank Further topics
▪ Single track functions ▪ Dead-end functions ▪ Functions that throw exceptions ▪ Supervisory function ▪ Parallel tracks Asynchronous programming .NET
▪ Thread ▪ AutoResetEvent ▪ BackgroundWorker ▪ IAsyncResult ▪ Task Async F#
▪ Async<‘T> type ▪ Async module ▪ async { } computation expression Async example
// string -> Async
▪ Must be explicitly started ▪ Easier to combine and sequence Async example let getTotalFileBytes path = async { let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask return bytes.Length } let total = arrayOfPaths |> Array.map getTotalFileBytes |> Async.Parallel |> Async.RunSynchronously |> Array.sum Asynchronous programming in F# IS NOT an abstraction for multi-threading Nested workflow let nestedWorkflow = async { printfn "Starting parent" let! childWorkflow = Async.StartChild sleepWorkflow
// give the child a chance and then keep working do! Async.Sleep 100 printfn "Doing something useful while waiting "
// block on the child let! result = childWorkflow
// done printfn "Finished parent" }
// run the whole workflow Async.RunSynchronously nestedWorkflow MailboxProcessor Basic example let printerAgent = MailboxProcessor.Start(fun inbox->
// the message processing function let rec messageLoop() = async {
// read a message let! msg = inbox.Receive()
// process a message printfn "message is: %s" msg
// loop to top return! messageLoop() }
// start the loop messageLoop() ) printerAgent.Post "hello" printerAgent.Post "hello again" printerAgent.Post "hello a third time" Demo Type providers Type providers
▪ Based on external data source ▪ Provide types, properties, and methods ▪ Generate Provided Types ▪ Instant autocomplete Demo Fable Fable
▪ F# to JavaScript compiler ▪ Generates JavaScript which you could be proud of ▪ Easy JavaScript interop (use NPM packages!) ▪ Supports most of the F# core library ▪ Online REPL Demo Scott Wlaschin
https://www.fsharpforfunandprofit.com/ QUESTIONS Competition results Mikhail Smal
https://mikhail.dev
Workshop: Web development in F# with SAFE stack
2 November 2019