Skip to content
Volume F · The TensorCore.si Library

F#

Functional-first programming on .NET.

F# is a functional-first language on .NET. It combines concise syntax, strong type inference and immutable data with full access to the .NET ecosystem. It is popular in finance, data science and domain modelling, and it runs anywhere .NET does.

Prologue

Why learn F#?

F# gives you the safety of functional programming with the libraries and tooling of .NET.

  • Concise: type inference means less code than C# for the same logic.
  • Correct: discriminated unions and pattern matching make illegal states hard to write.
  • Data and ML: works with ML.NET, TorchSharp and interactive notebooks.
Prologue

Set up and run your first program

Install the .NET SDK, which includes F#. Run scripts with dotnet fsi or create projects with dotnet new.

terminal
dotnet --version
dotnet fsi hello.fsx
dotnet new console -lang F# -o MyApp
hello.fsx
let name = "Ada"
printfn "Hello, AI! Welcome to F#, %s." name
Tip.fsx files are scripts; .fs files belong to projects. Indentation defines blocks.
Chapter 1

Values and types

let binds immutable values. Types are inferred, but you can annotate them. Use mutable only when you really need to change a value.

values.fsx
let layers = 32
let lr = 3e-4
let model = "llama3.1"
let fp16GB (p: float) = p * 2.0 / 1e9
printfn "%s: %d layers, %.1f GB" model layers (fp16GB 8.03e9)
let mutable steps = 0
steps <- steps + 1
Chapter 2

Functions and pipelines

Functions are values. The pipe operator |> passes a value through a chain of functions, which makes data transformations easy to read.

functions.fsx
let square x = x * x
let isEven x = x % 2 = 0

[1 .. 10]
|> List.filter isEven
|> List.map square
|> List.sum
|> printfn "Sum of even squares: %d"

let params layers width = 12L * int64 layers * int64 width * int64 width
let params4096 = params 32        // partial application
printfn "%d" (params4096 4096)
Chapter 3

Lists, arrays and maps

Lists are immutable linked lists, arrays are fast and mutable, and Map is an immutable dictionary. List comprehensions use [ for ... ].

collections.fsx
let vram = Map [ "H100", 80; "H200", 141; "RTX 5090", 32 ]
let big = [ for KeyValue(name, gb) in vram do if gb >= 80 then yield name ]
let total = vram |> Map.values |> Seq.sum
printfn "%A, total %d GB" big total
let weights = Array.init 5 (fun i -> float i * 0.1)
printfn "%A" weights
Chapter 4

Records and discriminated unions

Records hold data; discriminated unions model choices. Pattern matching makes the compiler check that every case is handled.

types.fsx
type Gpu = { Name: string; Vram: int }

type Device =
    | Cpu
    | Gpu of Gpu
    | Tpu of version: int

let describe device =
    match device with
    | Cpu -> "CPU"
    | Gpu g when g.Vram >= 80 -> sprintf "data center GPU %s" g.Name
    | Gpu g -> sprintf "GPU %s (%d GB)" g.Name g.Vram
    | Tpu v -> sprintf "TPU v%d" v

[ Cpu; Gpu { Name = "H100"; Vram = 80 }; Tpu 5 ] |> List.iter (describe >> printfn "%s")
Chapter 5

Options, results and async

Option replaces null and Result models success or failure. task { } blocks write asynchronous code that reads like normal code.

async.fsx
let tryParse (s: string) =
    match System.Int32.TryParse s with
    | true, v -> Some v
    | _ -> None

printfn "%A %A" (tryParse "42") (tryParse "abc")

open System.Net.Http
let fetchLength (url: string) = task {
    use client = new HttpClient()
    let! body = client.GetStringAsync url
    return body.Length
}
printfn "%d" (fetchLength "https://example.com").Result
Part II · AI with F#

AI with F#

F# works with the .NET AI stack: ML.NET for classic ML, TorchSharp (PyTorch bindings) for deep learning, ONNX Runtime for inference, and Microsoft.Extensions.AI or Semantic Kernel for LLM apps. Polyglot Notebooks let you explore data interactively.

The essential libraries

ML.NETClassic machine learning for .NET.
TorchSharpPyTorch (libtorch) bindings for .NET.
ONNX RuntimeFast inference for ONNX models.
Microsoft.Extensions.AIUnified LLM client abstractions.

Gradient descent from scratch

Fit y = 3x + 0.5 with arrays and a fold.

gradient.fsx
let n = 100
let xs = Array.init n (fun i -> -1.0 + 2.0 * float i / float (n - 1))
let ys = xs |> Array.map (fun x -> 3.0 * x + 0.5)

let step (w, b) _ =
    let errs = Array.map2 (fun x y -> w * x + b - y) xs ys
    let gw = 2.0 / float n * Array.sum (Array.map2 (*) errs xs)
    let gb = 2.0 / float n * Array.sum errs
    (w - 0.1 * gw, b - 0.1 * gb)

let w, b = Seq.fold step (0.0, 0.0) [1 .. 500]
printfn "w = %.2f, b = %.2f" w b
Part III · Build an AI agent

Build an AI agent in F#

System.Net.Http.Json and System.Text.Json are built into .NET, so a tool-calling loop needs no extra packages.

Your goal→Model plans→Tool call→Your code runs the tool→Result back to model→Final answer
agent.fsx
open System.Net.Http
open System.Net.Http.Json
open System.Text.Json
open System.Text.Json.Nodes

let http = new HttpClient()
let url = "http://localhost:11434/v1/chat/completions"
let tools = JsonNode.Parse """[{"type":"function","function":{"name":"get_weather","description":"Weather for a city",
  "parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}]"""
let messages = JsonArray()
messages.Add(JsonObject(dict [ "role", JsonValue.Create "user" :> JsonNode
                               "content", JsonValue.Create "Should I take an umbrella in Lahore today?" ]))

let getWeather (city: string) = sprintf "It is 24°C and sunny in %s." city

let rec loop round =
    if round > 0 then
        let body = JsonObject(dict [ "model", JsonValue.Create "llama3.1" :> JsonNode
                                     "messages", messages.DeepClone(); "tools", tools.DeepClone() ])
        let res = http.PostAsJsonAsync(url, body).Result.Content.ReadFromJsonAsync<JsonNode>().Result
        let msg = res["choices"][0]["message"]
        messages.Add(msg.DeepClone())
        match msg["tool_calls"] with
        | :? JsonArray as calls when calls.Count > 0 ->
            for c in calls do
                let args = JsonNode.Parse(c["function"]["arguments"].GetValue<string>())
                let reply = JsonObject(dict [ "role", JsonValue.Create "tool" :> JsonNode
                                              "tool_call_id", JsonValue.Create(c["id"].GetValue<string>())
                                              "content", JsonValue.Create(getWeather (args["city"].GetValue<string>())) ])
                messages.Add reply
            loop (round - 1)
        | _ -> printfn "%s" (msg["content"].GetValue<string>())

loop 5

For production apps, Microsoft.Extensions.AI handles tool calling for you across many providers.

Run it locallyInstall Ollama, run ollama pull llama3.1, and keep it running. The code talks to its OpenAI-compatible API at http://localhost:11434/v1, so the same program also works with vLLM, llama.cpp server or a cloud API by changing the URL, model name and adding an API key.
Part IV · Your path

F# learning roadmap

Tick items off as you go. Your progress is saved in this browser.

Beginner

Intermediate

Advanced

Part IV · Your path

Projects to build

Budget trackerModel income and spending with records and unions.
Data explorerAnalyse a CSV in a Polyglot Notebook.
LLM tool runnerAn agent that calls typed F# functions.
Reference

F# cheat sheet

TaskSyntax
Valuelet x = 5
Functionlet add a b = a + b
Lambdafun x -> x * 2
Pipexs |> List.map f
Matchmatch x with | Some v -> v | None -> 0
Record{ Name = "H100"; Vram = 80 }
Reference

Common mistakes

  • Indentation is significant; mixing tabs and spaces causes errors.
  • File order matters in projects: a file can only use code defined above it.
  • = compares values in expressions; use <- to assign to a mutable.
Reference

Where to go next

On TensorCore.si: C# volume, OCaml volume.

Keyboard: [ previous language · ] next language · / search the library