Coding · Programming languages
F# tutorial for beginners
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 runThe 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 =.
| Type | Example | Notes |
|---|---|---|
| int | 42 | 32-bit integer |
| float | 3.14 | 64-bit floating point (a .NET double) |
| decimal | 19.99m | Exact decimal, good for money |
| string | "hello" | Immutable .NET string |
| char | 'a' | Single character |
| bool | true, false | Logical 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 neededF# 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
| Collection | Literal | Notes |
|---|---|---|
| List | [1; 2; 3] | Immutable linked list, the default choice |
| Array | [| 1; 2; 3 |] | Fixed size, mutable elements, fast indexing |
| Sequence | seq { 1 .. 10 } | Lazy, computed on demand |
| Map | Map ["a", 1; "b", 2] | Immutable dictionary |
| Set | set [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: AnaRecords 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.00Other 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) // NoneTryParse 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 2Array.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,OptionandResultmodules; 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.