cv.cm

Coding · Programming languages

F# tutorial for beginners

6 min read

Short answer

F# is a functional-first, statically typed language for .NET. It infers most types for you, uses indentation instead of braces, and makes immutable data, pattern matching and the pipeline operator |> the normal way to write code, while still giving full access to .NET libraries. Install the .NET SDK and run a script with dotnet fsi hello.fsx.

What F# is and who uses it

F# was designed by Don Syme at Microsoft Research and is now an open-source language maintained by Microsoft and the F# Software Foundation community. It belongs to the ML family of languages (like OCaml) and runs on .NET on Windows, macOS and Linux.

Teams use F# for financial modeling, data analysis, domain-heavy business logic, web back ends and scripting. Because it compiles to the same .NET intermediate language as C#, an F# project can use any NuGet package and can be called from C# code.

Install F# and run Hello, world

Install the .NET SDK from Microsoft. F# is included, so no separate install is needed. For quick experiments, write a script file with the .fsx extension and run it with F# Interactive (dotnet fsi).

// hello.fsx
printfn "Hello, world!"
dotnet fsi hello.fsx
# Hello, world!
dotnet new console -lang "F#" -o HelloApp
cd HelloApp
dotnet run

The second set of commands creates a full console project with a .fsproj file and Program.fs. In a project, the order of files listed in the .fsproj matters: a file can only use code from files above it.

Values and basic types

let binds a name to a value. Bindings are immutable unless you mark them mutable, and then you change them with <-, not =.

TypeExampleNotes
int4232-bit integer
float3.1464-bit floating point (a .NET double)
decimal19.99mExact decimal, good for money
string"hello"Immutable .NET string
char'a'Single character
booltrue, falseLogical values
unit()"No value", like void
let name = "Ana"
let age = 30
let mutable count = 0
count <- count + 1
printfn "%s is %d, count = %d" name age count
// Ana is 30, count = 1
let total = float age + 0.5   // explicit conversion needed

F# never converts numbers implicitly: 1 + 2.0 is a compile error. Convert with functions such as float, int and string.

Control flow and functions

if is an expression that returns a value, so both branches must have the same type. Loops exist (for, while), but most code uses collection functions instead.

let sign n =
    if n > 0 then "positive"
    elif n < 0 then "negative"
    else "zero"
for i in 1 .. 3 do
    printfn "%d squared is %d" i (i * i)
let add x y = x + y          // int -> int -> int
let add5 = add 5             // partial application
printfn "%d" (add5 10)       // 15
let square = fun x -> x * x
[1; 2; 3; 4]
|> List.filter (fun x -> x % 2 = 0)
|> List.map square
|> printfn "%A"              // [4; 16]

Arguments are separated by spaces, not commas. Every function takes one argument at a time (currying), so add 5 returns a new function. The pipeline operator x |> f means f x and lets you write data transformations from top to bottom. Indentation is significant: the body of a function or loop must be indented under it.

Collections, records and options

CollectionLiteralNotes
List[1; 2; 3]Immutable linked list, the default choice
Array[| 1; 2; 3 |]Fixed size, mutable elements, fast indexing
Sequenceseq { 1 .. 10 }Lazy, computed on demand
MapMap ["a", 1; "b", 2]Immutable dictionary
Setset [1; 2; 2]Immutable set of unique values
type Person = { Name: string; Age: int }
let ana = { Name = "Ana"; Age = 30 }
let older = { ana with Age = 31 }       // copy with one change
let people = [ ana; { Name = "Ben"; Age = 17 } ]
let adult = people |> List.tryFind (fun p -> p.Age >= 18)
match adult with
| Some p -> printfn "First adult: %s" p.Name
| None -> printfn "No adults"
// First adult: Ana

Records are immutable types with named fields and structural equality. F# has no null for its own types; a missing value is an Option: Some value or None. Functions like List.tryFind return an option, and the compiler makes you handle both cases.

Signature features: discriminated unions and pattern matching

A discriminated union describes a value that is exactly one of several named cases, each with its own data. match checks the cases, and the compiler warns you if you forget one.

type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
let area shape =
    match shape with
    | Circle r -> System.Math.PI * r * r
    | Rectangle (w, h) -> w * h
[ Circle 1.0; Rectangle (2.0, 3.0) ]
|> List.map area
|> List.iter (printfn "%.2f")
// 3.14
// 6.00

Other well-known features are type inference (you rarely write type annotations), units of measure (float<m> cannot be added to float<s>), and computation expressions such as task { ... } and async { ... } for asynchronous code.

Error handling

For expected failures, idiomatic F# returns a Result: Ok value or Error info. For unexpected failures, .NET exceptions are available with try ... with, failwith and raise.

let parseAge (s: string) =
    match System.Int32.TryParse(s) with
    | true, n when n >= 0 -> Ok n
    | true, _ -> Error "Age must not be negative"
    | false, _ -> Error "Not a number"
match parseAge "abc" with
| Ok n -> printfn "Age %d" n
| Error msg -> printfn "Error: %s" msg
// Error: Not a number
let safeDivide a b =
    try Some (a / b)
    with :? System.DivideByZeroException -> None
printfn "%A" (safeDivide 10 0)   // None

TryParse has an out parameter in .NET; F# turns it into a tuple (success, value) that you can match directly.

A small complete program: word frequencies

let text = "the cat and the hat and the bat"
text.Split(' ')
|> Array.countBy id
|> Array.sortByDescending snd
|> Array.truncate 2
|> Array.iter (fun (word, n) -> printfn "%s %d" word n)
// the 3
// and 2

Array.countBy id groups equal words and returns (word, count) pairs, snd takes the second element of each pair for sorting, and the result is printed with a lambda that destructures the tuple.

Where to go next

  • Microsoft Learn: the F# documentation, including "Tour of F#" and the language reference.
  • Practice in dotnet fsi, where you can paste code and see results immediately; end input with ;;.
  • Learn the List, Array, Seq, Option and Result modules; most daily code is built from them.
  • Look at F# web frameworks and libraries on NuGet once the basics are comfortable.

FAQ

Is F# only for Windows?

No. F# runs on .NET, which is cross-platform, so you can build and run F# programs on Windows, macOS and Linux.

What is the difference between F# and C#?

Both run on .NET and can use the same libraries. C# is object-oriented first with functional features added, while F# is functional first, with immutable data, type inference and pattern matching as defaults.

What does |> mean in F#?

x |> f is the pipeline operator and means f x. It passes the value on the left as the last argument of the function on the right, so chains of transformations read from top to bottom.

Is F# related to OCaml?

Yes. F# belongs to the ML family and its core syntax is close to OCaml, but it is built for .NET, uses significant indentation by default, and has its own features such as computation expressions.

All coding tutorials