Skip to content

Instantly share code, notes, and snippets.

@Darkle
Last active September 12, 2024 04:48
Show Gist options
  • Save Darkle/c445f11bf1b6d94fe6667b8c186753d0 to your computer and use it in GitHub Desktop.
Save Darkle/c445f11bf1b6d94fe6667b8c186753d0 to your computer and use it in GitHub Desktop.
How To Do Things In F#

Elsewhere:

Here

General Tips

  • Start a new project: dotnet new console -lang "F#" -o src/App
  • A nice hack to get a quick fsx script going is to add #!/usr/bin/env -S dotnet fsi to the top of the script and then chmod +x myscript.fsx. Then you can just run it with ./myscript.fsx
  • Load a library into dotnet fsi with #r "nuget: Newtonsoft.Json, 9.0.1";;. And then open Newtonsoft.Json;;
  • If you are looking to use a pre-release build of a package and its not available on nuget but is available on https://www.myget.org/, then check the "Package history" section (down the bottom) for pre-release builds, and use like so: dotnet add package EdgeDB.Net.Driver --version 1.2.3-build-167 --source https://www.myget.org/F/edgedb-net/api/v3/index.json
  • When using the debugger in VSCode, the debugger console (and conditional breakpoints) only support C# code and not F# code
  • .Net Configuration In Depth
  • dotnet list package --include-transitive is very handy
  • You can check the outdated packages with dotnet list package --outdated
  • To ignore/suppress a warning, you can either add #nowarn "901" to the code file, or add <NoWarn>($NoWarn);fs0901</NoWarn> inside an MSBuild PropertyGroup in the .fsproj file

Fable Tips

  • Namespaces: Browser.*, Core.JS.* and Core.JsInterop.*. Technically the Core namespaces start with Fable.*, but you can usually omit that.
  • I had to add <DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference> to the <PropertyGroup> and <PackageReference Include="FSharp.Core" Version="7.0.300" /> to the <ItemGroup> in the project .fsproj file to get rid of an error with string interpolation in the vscode ionide extension (eg when you use $"asd {1}")
  • Feliz supports custom html attributes via prop.custom("attrName", "attrVal")
  • dotnet list package --include-transitive is very handy
  • Feliz and Fable Remoting both add around 5 seconds to the initial build time (incremental compilation is near instantaneous though). This can be annoying as you usually end up Ctrl+C-ing and restarting the fable build a lot when doing js dev. Best thing todo is not use Feliz or Fable Remoting.

Fable Bindings Examples

type Signal<'T> =
  abstract value: 'T with get, set

type ReadonlySignal<'T> =
  abstract value: 'T with get

let signal (value: 'T) : Signal<'T> = importMember "@preact/signals"
let computed (callBack: unit -> 'T) : ReadonlySignal<'T> = importMember "@preact/signals"
let useComputed (callBack: unit -> 'T) : ReadonlySignal<'T> = importMember "@preact/signals"
let useSignal (value: 'T) : Signal<'T> = importMember "@preact/signals"
let effect (callBack: unit -> unit) : unit -> unit = importMember "@preact/signals"
let batch (callBack: unit -> unit) : unit = importMember "@preact/signals"
let proxy (thing: 'T) : 'T = importMember "valtio"
let useSnapshot (thing: 'T) : 'T = importMember "valtio"
let snapshot (thing: 'T) : 'T = importMember "valtio"
let watch (get: 'T -> 'T) : unit = importMember "valtio/utils"
let subscribeKey (state: 'T1, prop: string, value: 'T2 -> unit) : unit = importMember "valtio/utils"
let subscribe (thing: 'T1, callback: 'T2 -> unit) : unit -> unit = importMember "valtio"

Strings

Check For Empty String

let isEmptyString str =
    (System.String.IsNullOrWhiteSpace str) 

let isEmptyString str = 
    (System.String.IsNullOrEmpty str)
    
let isNotEmptyString str =
    (System.String.IsNullOrWhiteSpace str) |> not

let isNotEmptyString str = 
    not (System.String.IsNullOrEmpty str)    

Convert string to boolean

  • Use System.Boolean.Parse to convert string "true" and "false" too boolean.

Convert array to string

let s = String.Join(", ", [|"Hello"; "World!"|])

Collections

let ra = ResizeArray<string>()
ra.Add("hello")

// convert it to an F# list using either Seq.toList
Seq.toList ra // now list<string>
// or List.ofSeq
List.ofSeq ra // now list<string>
let thing = Dictionary<string, string>()
thing.Add("foo", "bar")
thing.Add("baz", "merp")
  • Note: If you need to add different types to a Dictionary (e.g. for then converting to js json), you can set the type to Dictionary<string, obj>()

Records

Optional Parameters

  • You can only have optional parameters in type class members.
  • e.g.
type Foo =
  static member doThing(?postId: string, ?title: string) =
    let postId = defaultArg postId ""
    let title = defaultArg title ""
    printfn "%s %s" postId title
  • If you want to have a type class member that has optional parameters that then calls another member with optional parameters, you need to call it like this: Foo.doThing (?postId = postId, ?title = title)
  • e.g.
type Foo =
  static member doThing(?postId: string, ?title: string) =
    let postId = defaultArg postId ""
    let title = defaultArg title ""
    printfn "%s %s" postId title

  static member callingDoThing(?postId: string, ?title: string) =
    Foo.doThing (?postId = postId, ?title = title)

Try/Catch

TryCatch with Result

let tryDivide (x:decimal) (y:decimal) =
  try
    Ok (x/y)
  with
    | :? DivideByZeroException as ex -> Error ex

Option/Result

Pipe Option/Result

let upgradeCustomer customer =
    customer 
    |> getPurchases 
    |> Result.map tryPromoteToVip 
    |> Result.bind increaseCreditIfVip

Errors & Exceptions

Raise Exception

raise (Exception(sprintf "Invalide name format: %s" s))

Custom Exceptions

[<Serializable>]
type MyException(msg: string) =
  inherit Exception(msg)
  
// I think you can also do:
type MyException<'a> (reason: string, thing: 'a) =
    inherit System.Exception (reason) 
    
[<Serializable>]
exception MyException of string

[<Serializable>]
exception Non200HTTPStatus of string * Response    

Pattern Match on Errors

let main argv =
    if argv.Length = 1 then
        let filePath = argv[0]
        let fileExists = File.Exists filePath

        if fileExists then
            printfn "Processing %s" filePath

            try
                Summary.summarize filePath
                0
            with
            | :? FormatException as e ->
                printfn "Error: %s" e.Message
                printfn "The file was not in the expected format"
                1
            | :? IOException as e ->
                printfn "Error: %s" e.Message
                printfn "The file is open in another program"
                1
            | _ as e ->
                printfn "Unexpected Error: %s" e.Message
                1
        else
            printfn "File not found '%s'" filePath
            2
    else
        printfn "Please specify a file"
        3

Async Fable

  • When doing async in fable, you can do:
    promise {
        let! res = fetch "http://fable.io" []
        let! txt = res.text()
        // Access your resource here
        Browser.console.log txt
    }

Async

  • Async.RunSynchronously is only used in your entry file and only if your entry file has async it needs to do specifically when the app starts up (eg starting a server). In non-entry files, you would use one of the other Async.start-thing methods anywhere you need to start async (can be just a one off, doesnt need to be a chain of async things).
async {
  let x = functionCallOrValue // normal value binding
  let! x = functionCallOrValue // awaits the Async to complete
  
  use x = functionCallOrValue // normal value binding with automatic dispose
  use! x = functionCallOrValue // awaits the Async to complete with automatic dispose
  
  do // call operation that returns unit
  do! // Await async operation that returns unit
  
  match functionCallOrValue with // normal match
  match! functionCallOrValue with // awaits the Async to complete, then match
  
  return // wraps a value in Async: e.g. async { return 41} -> Async<int>
  return! // returns that is already Async without wrapping it
}
let myAsyncFunc id = 
  async {
    return id + 1
  }

let caller id = 
  async {
    let! r =  myAsyncFunc id
    return r + 17
  }
  • If you dont need the return value of the async expression, you can use do! instead of let!:
let foo t = 
    async { 
        printfn "%s" t 
    }

let caller2 id = 
    async { 
        do! foo "Hello" 
    }
task {
    // here Db.Async.exec returns a task
    let insertPosts = conn |> Db.newCommand sqlForPosts |> Db.Async.exec

    let updateJoinTable = conn |> Db.newCommand sqlForJoinTable |> Db.Async.exec

    let! _ = Task.WhenAll([| insertPosts; updateJoinTable |])
}
  • Running async things one at a time (sequentially):
async {
    for item in 1..10 do
        do! Async.Sleep 1000
        printfn $"item: {item}"
}
let throwAsync = async { failwith "I was not caught!" }

let catchAsync = async {
    try 
        do! throwAsync
    with _-> printfn "caught inside async!"
}

[<EntryPoint>]
let main argv =
    try throwAsync |> Async.RunSynchronously 
    with _ -> printfn "caught outside!"
    try catchAsync |> Async.Start 
    with _ -> printfn "I did not catch it either!"
    System.Console.Read() |> ignore
    printfn "finishing!"
    0
let throwAsync = async { failwith "I was not caught!" }

let catchAsync = async { do! throwAsync }

let catchAsync2 =
    async {
        try
            do! catchAsync
        with _ ->
            printfn "caught inside async22222!"
    }

[<EntryPoint>]
let main argv =
    try
        throwAsync |> Async.RunSynchronously
    with _ ->
        printfn "caught outside!"

    try
        catchAsync2 |> Async.Start
    with _ ->
        printfn "I did not catch it either!"

    System.Console.Read() |> ignore
    printfn "finishing!"
    0
// Doing it this way as opposed to a while loop makes it run on a background thread.
let rec loop () = async { return! loop () }

loop () |> Async.RunSynchronously

or

Process.GetCurrentProcess().WaitForExit()

Timers

let timer = new Timers.Timer(1000)
timer.Enabled <- true
timer.Elapsed.Add(fun (eventArgs: ElapsedEventArgs) -> ())  
  • Using PeriodicTimer:
  // Will not overlap as PeriodicTimer pauses while the code is being run
  let timer = new PeriodicTimer(TimeSpan.FromSeconds(30))

  task {
    // run straight away on first run
    let mutable tik = true

    while tik do
      do! startProcessingImages ()
      let! nextTik = timer.WaitForNextTickAsync()
      tik <- nextTik
  }
  
  //Or
  
  task {
    use timer = new PeriodicTimer(TimeSpan.FromSeconds(15))
    let! token = Async.CancellationToken

    while not token.IsCancellationRequested do
      let! tick = timer.WaitForNextTickAsync()

      if tick then
        do! recurringTask()
  }
  
// Or
let timer = new PeriodicTimer(TimeSpan.FromMinutes(60))

task {
  // uncomment this line if you also want it to run straight away as well
  // do! doThing ()
  while! timer.WaitForNextTickAsync() do
    do! doThing ()
}
|> ignore  
let rec loop (interval:int) doTheWork =
    async {
        do! Async.Sleep interval
        doTheWork()
        return! loop interval doTheWork
    }

Async.Start(loop 1000 f)
let periodicWork (delay: TimeSpan) (ct: CancellationToken) work onError = task {
    while not ct.IsCancellationRequested do
        try
            do! Task.Delay (delay, ct)

            work ct
            // or do! work ct
        with
            | :? TaskCanceledException -> ()
            | e -> onError e
}

IO

File IO

// This would be an alternate to the normal way
let readFile path = // string -> seq<string>
    seq { 
        use reader = new StreamReader(File.OpenRead(path))
        while not reader.EndOfStream do
            reader.ReadLine() 
    }

HTTP

DB

Shelling Out

Processes

Computation Expressions

JSON

Debugging

  • Note: When using the debugger in VSCode, the debugger console (and conditional breakpoints) only support C# code and not F# code
  • Note: the project needs to be built first as the debugger attaches to the built .dll file.
    • Alternatively, you can set "requireExactSource": false to the launch.json but that feels like it might be asking for trouble.
  • Option 1:
    1. Open the "F# Solution Explorer"
    2. Click the green arrow
  • Option 2:
    1. Create the file .vscode/launch.json in your project root directory
    2. Open the .vscode/launch.json file in vscode
    3. Click the button bottom right that says "Add Configuration..."
    4. Type in ".net" and then you probably want to select ".NET: Launch Executable file (Console)"
    5. Change the program key value to match what is in the ${workspaceFolder}/bin/Debug/ folder. e.g. ${workspaceFolder}/bin/Debug/net8.0/MyApp.dll
    6. Go to the debugger view in vscode sidebar and click the green arrow.
    7. It will say "Could not find the task 'build'.". You should click the "Configure Task" button and select Build.
    8. Change the label key (in the .vscode/tasks.json file that was just created) to "build"
    9. Note: you can add env args with the "env": {} key
  • https://www.udemy.com/course/fsharp-from-the-ground-up/learn/lecture/22825573#overview
  • https://gist.github.com/TheAngryByrd/910eca81b3c3ee7018695d8c7d88e859

Types

Add methods to types

type PersonId = private PersonId of Guid with
  static member toString (PersonId x) = string x
  static member fromString = Guid.tryParse >> Option.map PersonId

Custom type examples

type ProductCode = string

type HttpError = { headers: HttpResponseHeaders; statusCode: HttpStatusCode; feedUrl: string }

//An alternative to a record is to use a "named" tuple.
type Pet =
    | Cat of name: string * meow: string
    | Dog of name: string * bark: string

let main =
    let cat = Cat("Whiskers", "Meow!")

    match cat with
    | Cat(name, meow) ->
        printfn "Cat Name: %s" name
        printfn "Cat Meow: %s" meow
    | Dog(name, _) ->
        printfn "This is not a cat, it's a dog named %s" name

How to check a values type

let a = 5
a.GetType() = typeof<int>

Emulate Typescript's literal types

  • F# doesn't have literal types, but you can try to emulate them like so:

In typescript:

type RockPaperScissors =
  | 'R'
  | 'P'
  | 'S'

In F#:

[<RequireQualifiedAccess>]
type ImageSize =
    | ``800``
    | ``1080``
    | ``1280``
    | original

type ShippingMethod = 
    | ``XRQ - TRUCK GROUND`` = 1
    | ``ZY - EXPRESS`` = 2
    | ``OVERSEAS - DELUXE`` = 3
    | ``OVERNIGHT J-FAST`` = 4
    | ``CARGO TRANSPORT 5`` = 5

//alternatively
type RockPaperScissors =
  | Rock
  | Paper
  | Scissors

  static member create =
    function
    | 'R' -> Some Rock
    | 'P' -> Some Paper
    | 'S' -> Some Scissors
    | _ -> None

  static member value =
    function 
    | Rock -> 'R'
    | Paper -> 'P'
    | Scissors -> 'S'

RequireQualifiedAccess

  • This makes it so that you need to specify the full type name
type MyU =
    | Case1
    | None
    | Case3
    
match thing with
| Case1 -> printfn "Case1"
| Case3 -> printfn "Case3"
| None -> printfn "None"

[<RequireQualifiedAccess>]
type MyU =
    | Case1
    | None
    | Case3
    
match thing with
| MyU.Case1 -> printfn "Case1"
| MyU.Case3 -> printfn "Case3"
| MyU.None -> printfn "None"

Box/Unbox

Fixing Cyclic Dependencies

FSX Scripts

  1. Create a build.fsx file.
  2. run chmod +x build.fsx
  3. Add below:
#!/usr/bin/env -S dotnet fsi
// The ‘-S’ option instructs ‘env’ to split the single string into multiple arguments

// You can also load other modules into the script. They can be from other .fsx or .fs files.
#load "build-foo.fsx"

// Skip the first 2 args as they are just a .dll thing and the file name.
let argv = System.Environment.GetCommandLineArgs()[2..]

// If you import another module, dont forget to open it
open foo


printfn "Hello, these are the args this script received: %A" argv
printfn "This is a val from another module %A" x

Converting C# to F#

evt.MessageReceived += (object sender, EventSourceMessageEventArgs e) => Console.WriteLine($"{e.Event} : {e.Message}");

you usually change it to either:

evt.MessageReceived.Add(fun data -> printfn "data received: %A" data)

or to

evt.add_MessageReceived(fun data -> printfn "data received: %A" data)
  • Something like this:
new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
           Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
    RequestPath = "/StaticFiles"
}

would look like this in F#:

new StaticFileOptions(
  FileProvider =
    new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
  RequestPath = "/StaticFiles"
)
  • C# method options:
var context = await browser.NewContextAsync(new() { UserAgent = "My User Agent" });

would look like this in F#:

let! context = browser.NewContextAsync(BrowserNewContextOptions(UserAgent = "My User Agent"))
  • Converting C# classes to F# type classes:

C#

public class HostApplicationLifetimeEventsHostedService : IHostedService
{
    private readonly IHostApplicationLifetime _hostApplicationLifetime;

    public HostApplicationLifetimeEventsHostedService(
        IHostApplicationLifetime hostApplicationLifetime)
        => _hostApplicationLifetime = hostApplicationLifetime;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
        _hostApplicationLifetime.ApplicationStopping.Register(OnStopping);
        _hostApplicationLifetime.ApplicationStopped.Register(OnStopped);

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;

    private void OnStarted()
    {
        // ...
    }

    private void OnStopping()
    {
        // ...
    }

    private void OnStopped()
    {
        // ...
    }
}

to F#:

type internal LifetimeEventsHostedServices(appLifetime: IHostApplicationLifetime) =

  let _appLifetime = appLifetime

  let onStarted () = =
    // Do stuff
    ()

  let onStopping () =
    // Do stuff
    ()

  let onStopped () = =
    // Do stuff
    ()

  interface IHostedService with

    member this.StartAsync(cancellationtoken: CancellationToken) =
      _appLifetime.ApplicationStarted.Register(Action onStarted) |> ignore
      _appLifetime.ApplicationStopping.Register(Action onStopping) |> ignore
      _appLifetime.ApplicationStopped.Register(Action onStopped) |> ignore
      Task.CompletedTask

    member this.StopAsync(cancellationtoken: CancellationToken) = Task.CompletedTask

Using LINQ in F#:

Working with null:

Server

  • dotnet new web --no-https --exclude-launch-settings --language F#
    • You can delete the .json files it creates.
  • Using the new dotnet minimal api:
open System
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Hosting

type Todo = { Name: string; IsComplete: bool }

[<EntryPoint>]
let main args =
    let builder = WebApplication.CreateBuilder(args)
    let app = builder.Build()

    app.MapGet("/", Func<string>(fun () -> "Hello World!")) |> ignore
    app.MapGet("/{id}", Func<HttpContext, int, string>(fun ctx id -> $"The id is {id}")) |> ignore
    
    // Will be sent back as json
    app.MapGet("/api/todo", Func<HttpContext, Todo>(fun ctx -> { Name = "todo1"; IsComplete = true })) |> ignore

    app.Run()

    0 // Exit code
  • As mentioned here, you can auto-bind query params like so:
server.MapGet(
  "/foo/{id}",
  Func<HttpContext, 
  int, // this is id from "/foo/{id}"
  int, // this is query param merp
  string, // this is query param foo
  string // this last one is the return type
  >(fun ctx id merp foo ->
    $"The id is {id}. The merp is {merp}. The foo is {foo}")
) |> ignore
// Then it would be called via: http://localhost:3000/foo/10?merp=33&foo=hello
// In your server file:
sever.MapGet("/foo", Func<HttpContext, Task<string>>(foo)) |> ignore

// In another file:
let foo (ctx: HttpContext) =
    task {
      try
        let maybePostId = ctx.Request.Query.Item("postId") |> Seq.tryHead
        //..
        let bodyStream = new StreamReader(ctx.Request.Body)
        let! bodyData = bodyStream.ReadToEndAsync()        
        //...
      with e ->
        //...
    }
  • Or alternatively, you can do:
// In your server file:
sever.MapGet("/foo", foo) |> ignore

// In another file:
let foo =
  Func<HttpContext, Task<string>>(fun ctx ->
    task {
      try
        let maybePostId = ctx.Request.Query.Item("postId") |> Seq.tryHead
        //..
        let bodyStream = new StreamReader(ctx.Request.Body)
        let! bodyData = bodyStream.ReadToEndAsync()        
        //...
      with e ->
        //...
    })
  • Send text as html:
  server.MapGet(
    "/",
    Func<HttpContext, IResult>(fun ctx ->
      Results.Content(
        $"""<div style="margin:10px;"></div>""",
        "text/html"
      ))
  ) |> ignore
  
// Or this
server.MapGet(
  "/",
  Func<HttpContext, HttpResults.ContentHttpResult>(fun ctx ->
    TypedResults.Text(
      content =
        $"""<div style="margin:10px;"></div>""",
      contentType = "text/html"
    ))
) |> ignore
  • Handle a form:
server.MapPost(
  "/",
  Func<HttpContext, Task<string>>(fun ctx ->
    task {
      let! formData = ctx.Request.ReadFormAsync(ctx.RequestAborted)
      // https://stackoverflow.com/questions/48188934/why-is-stringvalues-used-for-request-query-values
      let _, names = formData.TryGetValue("name")
      let name = names |> Seq.tryHead |> Option.defaultValue "No value"

      let _, ages = formData.TryGetValue("age")
      let age = ages |> Seq.tryHead |> Option.defaultValue "No value"

      return $"""Post data name: {name}, age: {age}. """
    })
)
|> ignore

Logging

type Trace =
  // https://stackoverflow.com/questions/674319/
  static member Log(message: string, [<ParamArray>] args: 'T array) =
    traceLogger.Trace(message, args)

Music

  • A hacky way to play some music with .Net on linux:
let playSoundOnError () =
  let startInfo = ProcessStartInfo()
  startInfo.FileName <- "/usr/bin/env"
  startInfo.ArgumentList.Add("-S")
  startInfo.ArgumentList.Add("bash")
  startInfo.ArgumentList.Add("-c")
  startInfo.ArgumentList.Add("paplay /usr/share/sounds/LinuxMint/stereo/dialog-question.wav")
  use p = new Process()
  p.StartInfo <- startInfo
  p.Start() |> ignore

SQLite

type DB =
  static member conn() =
    let sqlConString = new SqliteConnectionStringBuilder()
    sqlConString.DataSource <- "./foo.db"

    let sqLiteConnection = new SqliteConnection(sqlConString.ConnectionString)
    sqLiteConnection.Open()
    sqLiteConnection

  // Call this on shutdown of app
  static member ClearAllPools() =
    Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools()

// In another file
task {
  // Important to use `use` so .net disposes of connection.
  use conn = DB.conn()
  use sqlCommand = conn.CreateCommand()
  sqlCommand.CommandText <- "SELECT * FROM FOO"
  //..so on
}      
let conn = (DB.conn ()).Handle
let myTraceFun = delegate_trace (fun thing1 thing2 -> ())
SQLitePCL.raw.sqlite3_trace (conn, myTraceFun, null)
let sqlCommand = sqLiteConnection.CreateCommand()
sqlCommand.CommandText <- """SELECT name from sqlite_master WHERE type = "table"; """
let dr = sqlCommand.ExecuteReader()

let results =
    [| while dr.Read() do
           {| tableName = dr.GetString(0) |} |]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment