Introduction to F#

Agenda

  • What is F#
  • Code Examples
  • Ecosystem

What is F#

  • ML family
  • .NET Ecosystem
  • Microsoft Research
  • General purpose language
  • Multi-paradigm, but Functional-first
  • Immutability
  • Functions are first class
  • Rich type system
  • Leverages type inference

Why you should care

  • Language assists the developer
  • Lessens mental load
  • Removes whole classes of errors
  • Pragmatic
  • Encourages domain modeling
  • Simplifies refactoring

Example F# Program

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
open System

let isEven x =
  x % 2 = 0

let lineCount file =
  IO.File.ReadAllLines(file)
  |> Array.filter (fun x -> x <> "")
  |> Array.length

[<EntryPoint>]
let main argv =
  let lines = lineCount argv.[0]
  let linesEven = isEven lines 
  printfn "There are %d non-empty lines (Even=%b)" lines linesEven

  0

Variables

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
let ten = 10

let rand = Random()

let loveCoffee = ("Coffee", true)

// Mutable
let mutable mystery = rand.NextDouble()
mystery <- mystery + 30.

Option

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let fiftyFifty = 
  if rand.NextDouble() > 0.5 then Some 100. else None

let populated = fiftyFifty.IsSome

match fiftyFifty with
| Some(x) -> printfn "value: %f" x
| None -> printfn "no value"

Functions

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
let conditionalMultiplier (x: int) (y: int) (z: bool) :int =
  if z then x * y else x

let doubler x = x + x

let makeAdder x = (fun y -> x + y)

let addTwenty = makeAdder 20

let biggerFoo = doubler foo 
let perhapsBiggerFoo = foo 200 true
let fifty = addTwenty 30

Lists, Arrays, and Sequences

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
let tripledList =
  [0..10]
  |> List.map (fun x -> x * 3)
// tripledList : int list = [0; 3; 6; 9; 12; 15; 18; 21; 24; 27; 30]

let tripledArray =
  [| 0..2..10 |]
  |> Array.map (fun x -> sprintf "%d" (x * 3))
// tripledArray : string [] = [|"0"; "6"; "12"; "18"; "24"; "30"|]

let tripledTripledSeq = seq { 
  for i in [0..10] do
    yield (i * 3) }
  |> Seq.map (fun x -> x * 3)
// tripledTripledSeq : seq<int> = seq [0; 9; 18; 27; ...]

Pipelines

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
[0..10]
|> List.filter (fun x -> x % 2 = 0)
|> List.map doubler
|> List.groupBy (fun x -> x % 3)
|> List.iter (fun x -> printfn "%d has %A" (fst x) (snd x))

// Results: 
0 has [0; 12]
1 has [4; 16]
2 has [8; 20]

Sharp Roasters

Types

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
type CoffeeId = int

type Country = | Columbia | Ethiopia | Kenya

type ProcessMethod = 
| Dry
| Wet
| Custom of string

type SearchCriteria = 
| Rating of int
| Description of string
| Method of ProcessMethod

Records

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
type Profile = { Complexity: float; Body: float; Fragrance: float }

type Coffee = { 
  Id: CoffeeId; 
  Country: Country; 
  Farm: string option; 
  ProcessMethod: ProcessMethod; 
  Profile: Profile; 
  Description: string option;
  Cost: decimal } 

Records

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
let coffee1 = {
  Id = 1; 
  Country = Ethiopia; 
  Farm = Some "Cooperative 123"; 
  ProcessMethod = Wet; 
  Profile = { Complexity = 9.; Body = 8.; Fragrance = 8.8 }; 
  Description = Some "Recommended city to city+ roast"; 
  Cost = 6.78m }

let coffee2 = {
  Id = 2
  Country = Columbia;
  Farm = None;
  ProcessMethod = Custom("Washed then fermented");
  Profile = { Complexity = 5.; Body = 5.; Fragrance = 5. };
  Description = None;
  Cost = 5.67m }

let coffee2' = { coffee2 with Cost = 6.01m }

Anonymous Records

1: 
2: 
3: 
4: 
5: 
let store = {| 
  Name = "Rise and Grind Coffee"; 
  Rating = 4.7 |}

printfn "%s is rated %2.1f" store.Name store.Rating

Classes

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
type BehmorRoaster (deviceId: int, description: string) =
  let mutable running = true
  
  member __.DeviceId = deviceId 
  member __.Description = description

  member __.StartRoast (coffee: Coffee, profile: RoastProfile) =
    running <- true
    true
  
  member __.StopRoast () =
    running <- false 
    true

  member __.Status () = sprintf "Status: %b" running
  
  override __.ToString () = sprintf "Behmor (%s)" __.Description

Classes

1: 
2: 
3: 
let roaster = BehmorRoaster (9600, "1600AB")
printfn "roaster: %A" (roaster.StartRoast coffee1 profile10)
printfn "roaster: %A" (roaster.Status())

Match

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
let friendlyProcessMethod (method: ProcessMethod) = 
  match method with
  | Wet -> "Wet processed"
  | Dry -> "Dry processed"
  | Custom(s) -> sprintf "It was custom processed. Details: %s" s 
  // | _ -> sprintf "Catch-all" // Unnecessary

let friendlySource (country: Country) (farm: string option) =
  match (country, farm) with
  | Ethiopia, Some(farm') -> sprintf "Ethopian - %s" farm' 
  | country', Some(farm') -> sprintf "%A (%s)" country' farm' 
  | country', None        -> sprintf "%A" country'

MailBox Processor

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
let batchProcessor = MailboxProcessor<Coffee * RoastProfile>.Start (fun inbox ->
  let rec messageLoop (batchCount: int) = async {
    let! (coffee, profile) = inbox.Receive()
    printfn "Handling batch %d (%A, %A)" batchCount coffee profile
    roaster.StartRoast coffee profile |> ignore
    return! messageLoop(batchCount + 1)
  }
  messageLoop 0)

batchProcessor.Post (coffee1, profile10)
batchProcessor.Post (coffee1, profile30)

Async

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
let queryMaker roaster query = async {
  // Long-running query
  let results = RoasterLib.query roaster query
  return results 
}

let queuedBatches = queryMaker behmor "count queued"
let completeBatches = queryMaker behmor "count complete"

// ---

let queued = queuedBatches |> Async.RunSynchronously
printfn "Queued batches: %d" queued

let allBatches = 
  [queuedBatches; completeBatches]
  |> Async.Parallel
  |> Async.RunSynchronously
printfn "All batches: %A" allBatches

Units of Measure

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
[<Measure>] type Pound
[<Measure>] type Kilogram

let batch1Size = 10<Pound>
let batch2Size = 10<Kilogram>
//let allBatches = batch1Size + batch2Size // Error

let kilogramPerPound = 0.4535924<Kilogram/Pound>
let batch3Size = 10.<Pound> * kilogramPerPound

Type providers

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
(* batch_template.csv
BatchId,Date,CoffeeId,RoastProfileId,Pounds
100,2020-01-02,1,20,1
101,2020-01-03,1,30,0.9
*)

open FSharp.Data
type CsvBatch = CsvProvider<"batch_template.csv">
  
let batches = CsvBatch.Load(dataFile)
for row in batches.Rows do
  printfn "row: %d %A %d %d" row.BatchId row.Date row.CoffeeId row.Pounds

Computation expressions

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
let run state x = let f = x in f state
let initState = run []
let processBatch x = fun (s: int list) -> (x, x::s)

type BatchTrackingBuilder() =
  member __.Bind (x, f) =
    (fun state ->
      let (result: int), state = run state x
      run state (f result))
  member __.Return x = fun s -> (x, s)

Computation expressions

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
let batch = BatchTrackingBuilder()

let batchResults = 
  batch {
      let! batch1 = processBatch 100
      let! batch2 = processBatch 200
      let! batch3 = processBatch 300
      return batch1 + batch2 + batch3
  } |> initState

printfn "Batch Results: %A" batchResults 

// Results:
Batch Results: (600, [300; 200; 100])

Additional Syntax

Functions - Partial Application

1: 
2: 
3: 
4: 
let adder x y = x + y
let addTen = adder 10

let forty = addTen 30

Functions - Recursion

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
let rec printEven (x: int list) =
  match x with
  | h::t -> if h % 2 = 0 then printfn "%d" h
            printEven t
  | [] -> ()

printfn "Evens: "
printEven [0..10]

let factorial x = 
  let rec factorial' a x = 
    if x = 0 || x = 1
    then a 
    else factorial' (a * x) (x - 1)
  factorial' 1 x

printfn "factorial: %d" (factorial 10)

try..with

1: 
2: 
3: 
4: 
5: 
6: 
try
  let foo =  1 / 0
  printfn "foo = %d" foo
with
  | :? DivideByZeroException -> printfn "Error"
  | ex -> printfn "%s" (ex.Message);

Classes - Part 2 - Interfaces

1: 
2: 
3: 
4: 
type IRoaster =
  abstract member StartRoast: Coffee * RoastProfile -> bool
  abstract member StopRoast: unit -> bool
  abstract member Status: unit ->  string

Classes - Part 2 - Class

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
type BehmorRoaster (deviceId: int, description: string) =
  let mutable running = true
  
  member __.DeviceId = deviceId 
  member __.Description = description 
  member __.Ping () = sprintf "Pong" 
  override __.ToString () = sprintf "Behmor (%s)" __.Description

  interface IRoaster with
    member __.StartRoast (coffee: Coffee, profile: RoastProfile) =
      running <- true
      true
    
    member __.StopRoast () =
      running <- false 
      true

    member __.Status () =
      sprintf "Status: %b" running

Classes - Part 2 - Usage

1: 
2: 
3: 
4: 
5: 
6: 
let behmor = BehmorRoaster (9600, "1600AB")
printfn "Ping .. %s" (behmor.Ping())

let roaster = behmor :> IRoaster
printfn "roaster: %A" (roaster.StartRoast coffee1 profile1)
printfn "roaster: %A" (roaster.Status())

Match - Active patterns

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
let (|Wet|_|) (processMethodPattern: string) =
  if processMethodPattern.ToLower().Contains "water" then Some Wet else None

let (|Dry|_|) (processMethodPattern: string) =
  if processMethodPattern.ToLower().Contains "dried" then Some Dry else None

match "Water was used" with
| Wet -> printfn "Is wet processed" 
| Dry -> printfn "Is dry processed"
| _ -> printfn "Undetermined process method" 

Modules

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
module RoasterLib = 
  type RoastProfileId = int
  type RoastProfileStep = { Temp: float; FanLevel: float; Duration: int }
  type RoastProfile = { Id: RoastProfileId; Steps: RoastProfileStep [] }
  type BatchId = int
  type Batch = { Id: BatchId; Coffee: Coffee; Pounds:float;  Time: float }

  let startRoast (roaster: IRoaster) (batch: Batch) =
    let success = roaster.StartRoast batch.Coffee batch.RoastProfile
    printfn "Starting roast #%d for coffee: %A (%b)" batch.Id batch.Coffee success

Scripting

File: archive.fsx

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
#load "codesuji.os.fsx"

open System
open Codesuji.Os

let tarString dir =
  sprintf "tar -cvf %s.tar %s" dir dir

[ "docs"; "photos" ]
|> List.map (fun dir -> exec "/home/jeff/data" (tarString dir))
|> printfn "%A"

Ecosystem

  • Windows, Mac, Linux
  • DotNetCore, .NET Framework, Mono
  • Package Managers: nuget, paket
  • Editors: VSCode + Ionide, Jetbrains Rider, Vim, VS
  • Testing: Expecto, FSCheck
  • F# Make: Fake

Notable endeavors

(Not a complete list)

  • Web: Fable, Elmish
  • Webserver: Giraffe, Saturn, Suave
  • SAFE stack
  • Mobile: React Native, Xamarin
  • F# compiler services
  • FsAutoComplete (FSAC)
  • FSharpPlus
  • FParsec
  • FsReveal

Learn more

Get Started

Download dot.net

1: 
dotnet new console -lang f# -n Foo