Skip to content
F# Programming Language Guide — Functional-First .NET Language

F# Programming Language Guide — Functional-First .NET Language

DodaTech Updated Jun 7, 2026 11 min read

F# is a functional-first programming language on the .NET platform that combines the expressiveness of functional programming with the power and reach of the .NET ecosystem — enabling concise, correct, and performant code.

What You’ll Learn

  • F# syntax and functional-first approach
  • Immutability by default and type inference
  • Discriminated unions and pattern matching
  • Async workflows for asynchronous programming
  • .NET interop and Visual Studio integration

Why It Matters

F# brings the rigor of functional programming to the .NET ecosystem — used for financial modeling, data science, web services, and cross-platform applications. DodaZIP uses F# for its compression algorithm configuration system where immutable data prevents subtle threading bugs. Financial institutions like Morgan Stanley and Credit Suisse use F# for trading systems that require both correctness and performance. F# runs anywhere .NET runs — Windows, Linux, macOS, Docker, and cloud — and integrates seamlessly with C# and .NET libraries.

Learning Path

    flowchart LR
  A[F# Basics<br/>You are here] --> B[Functions & Types]
  B --> C[Pattern Matching & DU]
  C --> D[Async & .NET Interop]
  D --> E[Build a Real Application]
  

Your First F# Program

// hello.fsx
printfn "Hello, F#!"

// F# uses significant whitespace (like Python)
let name = "World"
printfn "Hello, %s!" name
dotnet fsi hello.fsx
# Hello, F#!
# Hello, World!

Immutability by Default

Variables in F# are immutable by default — a core functional principle.

// Immutable binding
let x = 10
// x <- 20  // ERROR: cannot reassign

// Mutable variable (explicit)
let mutable y = 10
y <- 20  // OK

// Shadowing (creating a new binding with same name)
let z = 5
let z = z + 3  // new z = 8, original z is unchanged

// Collection immutability
let numbers = [1; 2; 3; 4; 5]
let doubled = numbers |> List.map (fun n -> n * 2)
let filtered = numbers |> List.filter (fun n -> n % 2 = 0)

printfn "Original: %A" numbers   // [1; 2; 3; 4; 5]
printfn "Doubled: %A" doubled    // [2; 4; 6; 8; 10]
printfn "Evens: %A" filtered     // [2; 4]
Original: [1; 2; 3; 4; 5]
Doubled: [2; 4; 6; 8; 10]
Evens: [2; 4]

Functions and Type Inference

F# uses type inference — the compiler deduces types from usage.

// Simple function
let add x y = x + y

// Function with type annotation
let multiply (x: int) (y: int) : int = x * y

// Lambda (anonymous function)
let square = fun x -> x * x

// Pipeline operator |>
let result = 5 |> square |> add 10
printfn "Result: %d" result  // 35

// Function composition
let doubleThenAdd1 = (fun x -> x * 2) >> (fun x -> x + 1)
printfn "Composed: %d" (doubleThenAdd1 5)  // 11

// Partial application
let add5 = add 5
printfn "Partial: %d" (add5 10)  // 15

// Recursive function (needs rec keyword)
let rec factorial n =
    if n <= 1 then 1
    else n * factorial (n - 1)

printfn "Factorial 6: %d" (factorial 6)  // 720
Result: 35
Composed: 11
Partial: 15
Factorial 6: 720

Discriminated Unions

Discriminated unions (DUs) model data that can take different forms — one of the most powerful F# features.

// Simple discriminated union
type PaymentMethod =
    | Cash
    | Card of string
    | Crypto of address: string * amount: float

// Using the DU
let payment1 = Cash
let payment2 = Card "Visa-1234"
let payment3 = Crypto ("0xabc123", 0.05)

// Option type (built-in DU): Some | None
let safeDivide x y =
    if y = 0 then None
    else Some (x / y)

// Result type (built-in DU): Ok | Error
type DivisionError = DivisionByZero | NegativeInput
let safeDivide2 x y =
    if y = 0 then Error DivisionByZero
    elif x < 0 then Error NegativeInput
    else Ok (x / y)

printfn "%A" (safeDivide 10 2)  // Some 5
printfn "%A" (safeDivide 10 0)  // None

Pattern Matching

Pattern matching is F#’s Swiss Army knife — it works with DUs, lists, tuples, and more.

// Match on discriminated union
let describePayment payment =
    match payment with
    | Cash -> "Paying with cash"
    | Card issuer -> sprintf "Paying with %s card" issuer
    | Crypto (addr, amt) -> sprintf "Crypto: %f from %s" amt addr

printfn "%s" (describePayment (Card "Mastercard-5678"))
// Paying with Mastercard-5678 card

// Match on list patterns
let describeList lst =
    match lst with
    | [] -> "Empty"
    | [x] -> sprintf "One item: %d" x
    | [x; y] -> sprintf "Two items: %d, %d" x y
    | head :: tail -> sprintf "Head: %d, Tail length: %d" head (tail.Length)

printfn "%s" (describeList [1; 2; 3])  // Head: 1, Tail length: 2

// Active patterns (custom pattern matching)
let (|Even|Odd|) n =
    if n % 2 = 0 then Even else Odd

let describeNumber n =
    match n with
    | Even -> sprintf "%d is even" n
    | Odd -> sprintf "%d is odd" n

printfn "%s" (describeNumber 42)  // 42 is even
Paying with Mastercard-5678 card
Head: 1, Tail length: 2
42 is even

Async Workflows

F# has built-in async support through computation expressions.

open System.Net.Http
open System.IO

// Async workflow
let fetchUrlAsync (url: string) = async {
    use client = new HttpClient()
    let! response = client.GetAsync(url) |> Async.AwaitTask
    let! content = response.Content.ReadAsStringAsync() |> Async.AwaitTask
    return content
}

// Run multiple async operations in parallel
let fetchAllAsync urls =
    urls
    |> List.map fetchUrlAsync
    |> Async.Parallel
    |> Async.RunSynchronously

// Async file processing
let processFileAsync (path: string) = async {
    let! content = File.ReadAllTextAsync(path) |> Async.AwaitTask
    let wordCount = content.Split(' ') |> Array.length
    return (path, wordCount)
}

let files = ["file1.txt"; "file2.txt"; "file3.txt"]
let results = files |> List.map processFileAsync |> Async.Parallel |> Async.RunSynchronously

for (path, count) in results do
    printfn "%s: %d words" path count

.NET Interop

F# interoperates with C# and any .NET library seamlessly.

open System
open System.Collections.Generic
open System.Text.RegularExpressions

// Use .NET collections
let dict = Dictionary<string, int>()
dict.Add("one", 1)
dict.Add("two", 2)
dict.Add("three", 3)

// Use .NET regex
let pattern = @"\d+"
let regex = Regex(pattern)
let matches = regex.Matches("abc123def456")
for m in matches do
    printfn "Found: %s" m.Value
// Found: 123
// Found: 456

// Call C# libraries
open System.Net
let client = WebClient()
let html = client.DownloadString("https://example.com")
printfn "Downloaded %d characters" html.Length

// Use System.Linq (via F# Seq module)
let numbers = [1..100]
let evens = numbers |> Seq.filter (fun n -> n % 2 = 0) |> Seq.toList
let squared = numbers |> Seq.map (fun n -> n * n) |> Seq.take 5 |> Seq.toList

Common Mistakes

1. Forgetting rec for recursive functions

F# assumes functions can’t call themselves unless you add rec. let rec factorial n = ... — without rec, you get a reference error.

2. Confusing = with <-

= creates an immutable binding. <- assigns to a mutable value. let x = 5 vs x <- 10 (requires let mutable x = 5 first).

3. Not handling all DU cases in match

match payment with
| Cash -> ...
// ERROR: incomplete match  Card and Crypto not handled

F# enforces exhaustive matching. Use _ as catch-all if needed.

4. Mixing up list types

[1; 2; 3] is an F# list (linked list). [|1; 2; 3|] is an array. ResizeArray<int>() is a mutable .NET List. They have different performance characteristics and APIs.

5. Incorrect indentation

F# uses significant whitespace. Wrong indentation causes unexpected parsing errors. Use 4 spaces consistently.

6. Forgetting ! for async

let! within async { } awaits an async operation inside a computation expression. Without it, you get the async object, not the result. Don’t confuse let! with regular let.

Practice Questions

  1. What makes F# immutable by default? All bindings are immutable unless explicitly marked mutable. Collections are immutable by default (lists, maps, sets). This eliminates entire categories of bugs from unexpected mutation.

  2. What are discriminated unions? Types that can take one of several named cases, each optionally carrying data. Like enums with payloads. They enable exhaustive pattern matching and precise domain modeling.

  3. How does F# pattern matching differ from C# switch? F# pattern matching is more powerful: exhaustive (compiler checks), works with DUs, lists, tuples, active patterns, and supports guards and destructuring. C# switch has become more powerful but is still more limited.

  4. What is the pipe operator |>? x |> f passes x as the last argument to f. It chains operations left-to-right: data |> process |> analyze |> display. Makes code read in the order of operations.

  5. How does F# handle async differently from C#? F# uses computation expressions: async { let! result = fetchAsync() ... }. Tasks are explicit and composable. F# async is a cold model (starts only when run). C# async/await uses hot tasks (start as soon as created).

Challenge: Write an F# script that reads a CSV file, parses it into a list of records using a discriminated union for error handling, processes the data (filter, sort, aggregate), and writes the result to a new CSV file.

Mini Project — Markdown to HTML Converter

open System.Text.RegularExpressions

type InlineElement =
    | Text of string
    | Bold of string
    | Italic of string
    | Code of string
    | Link of text: string * url: string

type BlockElement =
    | Heading of level: int * content: string
    | Paragraph of elements: InlineElement list
    | CodeBlock of language: string * code: string
    | UnorderedList of items: string list
    | OrderedList of items: string list

let parseInline (text: string) : InlineElement list =
    let regex = Regex(@"(?s)(\*\*(.+?)\*\*)|(\*(.+?)\*)|(`(.+?)`)|\[(.+?)\]\((.+?)\)")
    let matches = regex.Matches(text)
    if matches.Count = 0 then [Text text]
    else
        [ for m in matches do
            if m.Groups.[2].Success then yield Bold m.Groups.[2].Value
            elif m.Groups.[4].Success then yield Italic m.Groups.[4].Value
            elif m.Groups.[6].Success then yield Code m.Groups.[6].Value
            elif m.Groups.[8].Success then yield Link (m.Groups.[8].Value, m.Groups.[9].Value) ]

let renderInline element =
    match element with
    | Text t -> t
    | Bold t -> sprintf "<strong>%s</strong>" t
    | Italic t -> sprintf "<em>%s</em>" t
    | Code c -> sprintf "<code>%s</code>" c
    | Link (text, url) -> sprintf "<a href=\"%s\">%s</a>" url text

let renderBlock block =
    match block with
    | Heading (lvl, content) ->
        sprintf "<h%d>%s</h%d>" lvl content lvl
    | Paragraph elements ->
        let inner = elements |> List.map renderInline |> String.concat ""
        sprintf "<p>%s</p>" inner
    | CodeBlock (lang, code) ->
        let langAttr = if lang <> "" then sprintf " class=\"language-%s\"" lang else ""
        sprintf "<pre><code%s>%s</code></pre>" langAttr code
    | UnorderedList items ->
        let lis = items |> List.map (sprintf "<li>%s</li>") |> String.concat "\n"
        sprintf "<ul>\n%s\n</ul>" lis
    | OrderedList items ->
        let lis = items |> List.map (sprintf "<li>%s</li>") |> String.concat "\n"
        sprintf "<ol>\n%s\n</ol>" lis

let parseMarkdown (md: string) : BlockElement list =
    let lines = md.Split('\n') |> Array.toList
    let rec parseLines lines acc =
        match lines with
        | [] -> acc |> List.rev
        | line :: rest when line.StartsWith("# ") ->
            parseLines rest (Heading (1, line.Substring(2)) :: acc)
        | line :: rest when line.StartsWith("## ") ->
            parseLines rest (Heading (2, line.Substring(3)) :: acc)
        | line :: rest when line.StartsWith("- ") ->
            let items = line.Substring(2) :: (rest |> List.takeWhile (fun l -> l.StartsWith("- ")) |> List.map (fun l -> l.Substring(2)))
            let remaining = rest |> List.skipWhile (fun l -> l.StartsWith("- "))
            parseLines remaining (UnorderedList items :: acc)
        | line :: rest when line = "" ->
            parseLines rest acc
        | line :: rest ->
            parseLines rest (Paragraph (parseInline line) :: acc)
    parseLines lines []

let convertMarkdownToHtml (md: string) =
    let blocks = parseMarkdown md
    let body = blocks |> List.map renderBlock |> String.concat "\n\n"
    sprintf "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>Converted Markdown</title>\n</head>\n<body>\n%s\n</body>\n</html>" body

// Example usage
let markdown = """
# Hello F#

This is **bold** and *italic* text with `inline code`.

- Item one
- Item two
- Item three
"""

let html = convertMarkdownToHtml markdown
printfn "%s" html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Converted Markdown</title>
</head>
<body>
<h1>Hello F#</h1>

<p>This is <strong>bold</strong> and <em>italic</em> text with <code>inline code</code>.</p>

<ul>
<li>Item one</li>
<li>Item two</li>
<li>Item three</li>
</ul>
</body>
</html>

FAQ

Is F# a good first functional language?
Yes. F# has excellent tooling (VS Code + Ionide), runs on .NET (huge library ecosystem), and has a gentler learning curve than Haskell. The type inference means you write less type annotations than C# while getting strong type safety.
Can F# replace C#?
Yes, in many scenarios. F# can do everything C# can do, often with less code. Many .NET shops use both — F# for business logic and data processing, C# for UI and legacy code. The two languages compile to the same IL and interoperate freely.
Does F# work on Linux?
Yes. .NET 6+ is fully cross-platform. F# works on Windows, Linux, and macOS. The Ionide extension for VS Code provides a full IDE experience on any platform.
What is F# used for in production?
Financial modeling, data science, web APIs (Giraffe, Saturn), machine learning (ML.NET), scripting, and configuration management. Companies like Jet.com, Microsoft, and Morgan Stanley use F# in production.
How does F# compare to Python?
F# has stronger type safety, better performance (compiled vs interpreted), and built-in async. Python has a larger ecosystem and more libraries. F# type providers let you consume data sources (SQL, JSON, CSV) with compiler-checked schemas.
What are type providers?
F#-specific feature that generates types at compile time from external data sources. A SQL type provider creates types matching your database schema — querying the database becomes type-checked code. No other .NET language has this.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro