- F# From the ground up tutorial↗
- http://www.fssnip.net/ ↗
- F# Core Docs↗
- There are a lot of examples of how to do things in the "Expert F#"↗ book
- Libs:
- General Tips
- Fable Tips
- Fable Bindings Examples
- Strings
- Collections
- Records
- Optional Parameters
- Errors & Exceptions
- Try/Catch
- Option/Result
- Async Fable
- Async
- Timers
- IO
- Processes
- Computation Expressions
- JSON
- Debugging
- Types
- Fixing Cyclic Dependencies
- FSX Scripts
- Converting C# to F#
- Using LINQ in F#
- Working WIth Null
- Server
- Logging
- Music
- SQLite
- 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 thenchmod +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 thenopen 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
- Namespaces:
Browser.*
,Core.JS.*
andCore.JsInterop.*
. Technically theCore
namespaces start withFable.*
, 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.
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"
- https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-stringmodule.html
- https://fsprojects.github.io/FSharpPlus/reference/fsharpplus-string.html
- https://github.com/microsoft/fsharplu/blob/main/FSharpLu/Text.fs
- String ToLower
- Check For Empty String
- Convert string to boolean
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)
- Use
System.Boolean.Parse
to convert string"true"
and"false"
too boolean.
let s = String.Join(", ", [|"Hello"; "World!"|])
- https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections.html
- When using Seq and you want to check if its length is 0, use
Seq.isEmpty
instead. Since sequences are lazy, using .Length you have to evaluate the whole sequence, whereas if you use Seq.isEmpty, you only have to check the first item. - If you want a mutable list, you can use
ResizeArray
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>
- There is a helper library for
ResizeArray
that gives you a nice CE: https://github.com/brianrourkeboll/FSharp.Collections.Builders#resizearray - If you want a mutable dictionary, use
Dictionary
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 toDictionary<string, obj>()
- Getting record key names and values:
- https://stackoverflow.com/questions/16976673/looping-through-f-record-like-javascript-object
- https://stackoverflow.com/questions/63418908/count-members-of-a-record-type
- https://stackoverflow.com/questions/59421595/is-there-a-way-to-get-record-fields-by-string-in-f
- https://stackoverflow.com/questions/58979038/get-record-type-labels-as-a-list-of-strings
nameof Unchecked.defaultof<Record>.fieldName
- https://forums.fsharp.org/t/is-there-a-better-way-to-get-the-names-of-record-fields/3603/2
- 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)
let tryDivide (x:decimal) (y:decimal) =
try
Ok (x/y)
with
| :? DivideByZeroException as ex -> Error ex
let upgradeCustomer customer =
customer
|> getPurchases
|> Result.map tryPromoteToVip
|> Result.bind increaseCreditIfVip
- F# Exception Handling Docs
- https://www.udemy.com/course/fsharp-from-the-ground-up/learn/lecture/22825911#overview
- https://sachabarbs.wordpress.com/2014/04/17/f19-exceptions/
- Raise Exception
- Pattern Match on Errors
- https://demystifyfp.gitbook.io/fstoolkit-errorhandling/
- To make sure the custom error includes your error message: https://stackoverflow.com/questions/6370462/how-to-throw-exceptions-with-meaningful-messages-in-f
raise (Exception(sprintf "Invalide name format: %s" s))
[<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
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
- 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
}
- Also by opening
Fable.Core
you can gain access toAsync.AwaitPromise
andAsync.StartAsPromise
: https://stackoverflow.com/a/54451205/3458681 - Parallel async in fable: https://github.com/fable-compiler/fable-fetch/blob/605ca7bb5b4e49cb3c8bf1a92dbb0bfa7c53981f/tests/FetchTests.fs#L88
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 otherAsync.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
}
- Use
task
if you want things to run straight away (like promises in JS). Useasync
for deferred execution (like Fluture) async { expression }
- Using this syntax, the expression is some computation that will run asynchronously. This means it will not block the current computation/thread.
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 oflet!
:
let foo t =
async {
printfn "%s" t
}
let caller2 id =
async {
do! foo "Hello"
}
async
does not result in its own thread. By default async is a event-loop style programming. Meaning you can create thousands ofasync
and allasync
can still be handled by a single-thread. The idea is that anasync
only has non-blocking operations. Instead of blocking a whole thread and let a thread "wait", the control is given to some other code that can run in the meantime on the same thread. Async are important because they are even more "lighter" than threads. You can create thousands of asyncs without a problem. But creating thousand of threads would be a big problem, and probably even hurt performance instead of making things faster.- From the comments on this page: https://fsharpforfunandprofit.com/posts/concurrency-async-and-parallel/
- You should only call
Async.RunSynchronously
once, at the edge of your program. - For
Async.Parallel
, because asynchronous computations execute independently of program flow, there is no defined order in which they print their information and finish executing. The computations will be scheduled in parallel, but their order of execution is not guaranteed. - Manual parallel:
// Manual parallel // NOTE: THIS DOESNT WORK THE WAY I THINK IT DOES!. DO TESTS! let parallel2 (job1, job2) = async { // Start both tasks let! task1 = Async.StartChild job1 let! task2 = Async.StartChild job2 // Wait for both to finish let! res1 = task1 let! res2 = task2 return (res1, res2) } // manual parallel when using tasks task { let thing1 = task { () } let thing2 = task { () } do! thing1 do! thing2 }
- You can also use https://github.com/TheAngryByrd/IcedTasks#parallelasync
- You can also use
Task.WhenAll()
for parallel: https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.whenall?view=net-7.0 - but i think you need the result of each task to be the same type?
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}"
}
- Also https://medium.com/@dorlugasigal/net-9s-upcoming-feature-task-wheneach-f9e7fc403e48
Async.Start
:
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
- Beware when using
Async.Start
that errors are not propigated to the main thread - https://stackoverflow.com/questions/55693931/why-does-async-start-seemingly-propagate-uncatchable-exceptions/55694256
- "A try cannot catch an exception in an async when executed with Async.Start because they fork in different threads. If you want to catch it you can do so with Async.RunSynchronously or within another async"
- https://medium.com/@eulerfx/f-async-guide-eb3c8a2d180a#0772
- Cancelling and onCancel: https://dev.to/askpt/cancellation-tokens-in-f-28gh
- Returning
Result
instead of throwing an error: https://stackoverflow.com/questions/53506325/- You can also use
asyncResult
:
let doSomethingAsync x = asyncResult { try let! a = testSomethingAsync x return a * 2 with ex -> return! AsyncResult.error "no no no" } // then calling it let foo () = async { let! result = doSomethingAsync 42 return match result with | Ok thing -> thing | Error error -> error }
- You can also use
- Run a program/service that you want to stay running and not exit immediately
// 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()
- If you want to use a recursive function, only
async
is supported (nottask
)- https://stackoverflow.com/questions/78401360/is-it-possible-to-do-tail-recursion-with-tasks
- https://discord.com/channels/196693847965696000/196695876054286336/1281121292283678731
- If you want to use recursion with a
task
, you will have to convert it to use a while loop
- Although F# provides some abilities to start an asynchronous computation on the current thread (or explicitly not on the current thread), asynchrony generally is not associated with a particular threading strategy.
!while
should be available in the next .Net: https://devblogs.microsoft.com/dotnet/simplifying-fsharp-computations-with-the-new-while-keyword/- Promise.race with F#:
- Articles & Stack Overflow Questions:
- https://stackoverflow.com/questions/6793774/chaining-asynchronous-functions-in-f
- https://stackoverflow.com/questions/76445536/how-to-execute-an-array-of-tasks-sequentially
- https://stackoverflow.com/questions/15825183/f-using-async-parallel-to-run-2-tasks-in-parallel
- https://stackoverflow.com/questions/76614159/how-to-start-and-await-multiple-i-o-operations-at-the-same-time-without-wasting
- https://stackoverflow.com/questions/76655868/how-to-start-many-asynct-and-wait-for-them-one-at-a-time
- https://learn.microsoft.com/en-us/dotnet/fsharp/tutorials/async
- https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-based-asynchronous-programming
- https://www.compositional-it.com/news-blog/task-vs-async/
- https://tomasp.net/blog/csharp-async-gotchas.aspx/
- https://www.youtube.com/watch?v=AHOU1_nXR40
- https://www.youtube.com/watch?v=SHLxuEkP3kE
- https://www.youtube.com/watch?v=1ymHmM-CNoU
- https://www.youtube.com/watch?v=np1j4sLxIH4
- https://www.youtube.com/watch?v=oe0Mn8aAGZE
- https://www.youtube.com/watch?v=JHMXn1TEj18
- https://www.youtube.com/watch?v=drrGRGgfS4o
- https://medium.com/@dagbrattli/asynchronicity-in-f-eb4c952f0035
- https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-fsharpasync.html
- https://learn.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-6#task-
- https://sachabarbs.wordpress.com/2014/05/14/f27-asynchronous-workflows/
- https://dev.to/amoondria/f-exception-handling-in-asynchronous-context-14kd
- https://www.youtube.com/watch?v=u2SlQ5WdL2k&list=PL-nSd-yeckKhsHa6Hr90oYjfFHGiRbPF4&index=1
- https://medium.com/@eulerfx/f-async-guide-eb3c8a2d180a
- https://stackoverflow.com/questions/55693931/
- https://dev.to/askpt/cancellation-tokens-in-f-28gh
- https://www.youtube.com/watch?v=q3Ies_XTo_s&t=1288s
- http://www.fssnip.net/tags/async
- https://www.pluralsight.com/courses/building-async-api-aspdotnet-core
- https://www.pluralsight.com/courses/getting-started-with-asynchronous-programming-dotnet
- https://blog.darklang.com/optimizing-tasks-in-fsharp/
- http://tomasp.net/blog/async-csharp-differences.aspx/
- https://fsharpforfunandprofit.com/posts/concurrency-async-and-parallel/
- https://mushedresearch.com/articles/down-the-rabbit-hole-concurrency
- https://stackoverflow.com/questions/77856670/how-to-catch-error-from-within-async-block-in-f
- https://stackoverflow.com/questions/tagged/f%23+asynchronous
- Alt Async Libs:
- https://www.ilkayilknur.com/a-new-modern-timer-api-in-dotnet-6-periodictimer
- https://www.youtube.com/watch?v=J4JL4zR_l-0
- https://fsharpforfunandprofit.com/posts/concurrency-reactive/
- https://learn.microsoft.com/en-us/dotnet/api/system.threading.timer
- https://learn.microsoft.com/en-us/dotnet/api/system.timers.timer
- NOTE:
Timer.timers
is apparently bad and not thread safe. It is recommended to use something else.
- NOTE:
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
}
- There is a built in constant:
__SOURCE_DIRECTORY__
that points to the file's directory and__SOURCE_FILE__
which is the current files name. - https://blog.tunaxor.me/blog/2021-12-16-Building-a-webpack-alternative-in-fsharp.html
- https://dev.to/tunaxor/doing-some-io-in-f-4agg
- https://dev.to/tunaxor/doing-some-io-in-f-2-3b6d
// 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://www.fssnip.net/5f/title/Async-File-Crawl
- https://github.com/microsoft/fsharplu/blob/main/FSharpLu/File.fs
- Zip a file
- File watcher: https://gist.github.com/Darkle/dae6deabaebc388cacde4125049260a5#file-server-sent-events-file-watcher-fs
- https://dev.to/tunaxor/making-http-requests-in-f-1n0b
- https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-webextensions.html
- http://www.fssnip.net/a7/title/Send-HTTP-POST-request
- https://www.encora.com/insights/making-http-requests-in-f
- https://youtu.be/6YZ4-sfLi-M?t=103
- https://fsprojects.github.io/FSharp.Data/library/Http.html
- https://fsprojects.github.io/FSharp.Data/library/HtmlParser.html
- Download a file: https://flurl.dev/docs/fluent-http/
- https://github.com/bezzad/Downloader
- https://learn.microsoft.com/en-us/dotnet/api/?term=Process
- https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process
- https://alexn.org/blog/2020/12/06/execute-shell-command-in-fsharp/
- https://github.com/CaptnCodr/Fli
- https://github.com/microsoft/fsharplu/blob/main/FSharpLu/Diagnostics.fs - (it says Diagnostics but it has code on starting/killing processes)
- The bang (
!
) unwraps the effect using the Bind function - Theres a chapter called "Computation Expressions" in https://leanpub.com/essential-fsharp
- Creating your own custom expressions:
- https://www.reddit.com/r/fsharp/comments/15ag6id/understanding_customoperations_in_computation/
- https://www.youtube.com/watch?v=7AOxGvp9tlg
- https://discord.com/channels/196693847965696000/196695876054286336/1232064224369578125
- https://usergroup.tv/videos/computation-expressions-and-how-to-abuse-them/
- https://en.wikibooks.org/wiki/F_Sharp_Programming/Computation_Expressions
- https://www.youtube.com/watch?v=Z8G9eXnuYSk
- https://github.com/dawedawe/bobkonf2024/blob/main/talk/slides.md
- https://www.youtube.com/watch?v=qzvRjUdFF8Q
- https://sleepyfran.github.io/blog/posts/fsharp/ce-in-fsharp/
- 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 thelaunch.json
but that feels like it might be asking for trouble.
- Alternatively, you can set
- Option 1:
- Open the "F# Solution Explorer"
- Click the green arrow
- Option 2:
- Create the file
.vscode/launch.json
in your project root directory - Open the
.vscode/launch.json
file in vscode - Click the button bottom right that says "Add Configuration..."
- Type in ".net" and then you probably want to select ".NET: Launch Executable file (Console)"
- Change the
program
key value to match what is in the${workspaceFolder}/bin/Debug/
folder. e.g.${workspaceFolder}/bin/Debug/net8.0/MyApp.dll
- Go to the debugger view in vscode sidebar and click the green arrow.
- It will say "Could not find the task 'build'.". You should click the "Configure Task" button and select Build.
- Change the
label
key (in the.vscode/tasks.json
file that was just created) to "build" - Note: you can add env args with the
"env": {}
key
- Create the file
- https://www.udemy.com/course/fsharp-from-the-ground-up/learn/lecture/22825573#overview
- https://gist.github.com/TheAngryByrd/910eca81b3c3ee7018695d8c7d88e859
type PersonId = private PersonId of Guid with
static member toString (PersonId x) = string x
static member fromString = Guid.tryParse >> Option.map PersonId
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
let a = 5
a.GetType() = typeof<int>
- 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'
- There is an F# lang suggestion to add this to F#:
- 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"
- Create a
build.fsx
file. - run
chmod +x build.fsx
- 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
- Don't forget that you can now use something like ChatGPT to help converting C# code to F#
- https://github.com/knocte/2fsharp/blob/master/csharp2fsharp.md
- When you encounter a C# api with
+=
like this:
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
- https://stackoverflow.com/questions/76678747/how-can-i-call-a-c-sharp-function-that-takes-an-actiont-from-f
- https://stackoverflow.com/questions/27866159/f-pattern-match-on-c-sharp-classes
- https://github.com/demystifyfp/FsToolkit.ErrorHandling/blob/8b5309736182b7c2a0cf9f8b579e778ea6ec9191/src/FsToolkit.ErrorHandling/Option.fs#L200-L216
- https://discord.com/channels/196693847965696000/196695876054286336/1234039656958656613
- https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/nullable-value-types
- https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/symbol-and-operator-reference/nullable-operators
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
- But note that when auto-binding query params, the query params may be null. So it's prolly best to not use auto-binding for query string params and to use the option mentioned below:
ctx.Request.Query.Item("foo") |> Seq.tryHead
- It looks like there are issues using optional query parameters in F#: https://stackoverflow.com/questions/28160230/using-optional-query-parameters-in-f-web-api-project
- If you want the route function to live in another file, you can do the following:
// 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
- Note: for some reason some api stuff wont show up in intellisense/autocomplete, but they still work fine (e.g.
AddEndpointFilter
) - https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-8.0
- More info about minimal api in F#: https://blog.tunaxor.me/blog/2023-07-20-oh-no-I-typed-dotnet-new-web-with-fsharp.html
- Logging request execution time: https://discord.com/channels/196693847965696000/386379055827779595/1230175500404523118
- fsharp/fslang-suggestions#1131 (comment)
- Creating your own logger with arbitray params using overloading: https://github.com/En3Tho/FSharpExtensions/blob/2595d1e8eb58e8bc653653a609d027576b077a3f/FSharpExtensions/En3Tho.FSharp.Extensions.Logging/ILoggerExtensions.fs
- You can also use
ParamArray
: - https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/parameters-and-arguments#parameter-arrays
- https://stackoverflow.com/questions/674319/
type Trace =
// https://stackoverflow.com/questions/674319/
static member Log(message: string, [<ParamArray>] args: 'T array) =
traceLogger.Trace(message, args)
- Logging request execution time: https://discord.com/channels/196693847965696000/386379055827779595/1230175500404523118
- 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
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
}
- Static connections are not thread safe, so you should create a new connection every time you need to access/query the DB.
- Pooling (enabled by default) helps to help make this performant. See: https://softwareengineering.stackexchange.com/questions/142065/
SQLitePCL.raw.sqlite3_update_hook
is available if you want to log SQLite stuff in dev.
let conn = (DB.conn ()).Handle
let myTraceFun = delegate_trace (fun thing1 thing2 -> ())
SQLitePCL.raw.sqlite3_trace (conn, myTraceFun, null)
- There are also other ones like
sqlite3_update_hook
available: dotnet/efcore#13827 (comment) - Get data from db:
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) |} |]