← Back to DevBytes

Functional Programming in F#

What is Functional Programming in F#?

Functional programming is a paradigm where computation is treated as the evaluation of mathematical functions, avoiding mutable state and side effects. F# is a functional-first language that runs on .NET, meaning it encourages and makes it natural to write code using functions, immutable data, and declarative patterns. In F#, you compose programs by defining pure functions, applying transformations to immutable data structures, and leveraging a powerful type system to model domains safely.

Unlike object-oriented languages that center on classes and mutable objects, F# puts functions and data transformations at the forefront. Everything is an expression that returns a value, and values are immutable by default. This leads to code that is easier to reason about, test, and parallelize. Functional programming in F# isn’t just a style—it’s baked into the language design, from discriminated unions and pattern matching to automatic currying and type inference.

Why Functional Programming Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Adopting functional programming with F# brings several concrete benefits:

These qualities make functional programming a natural fit for data processing, financial systems, web APIs, and any domain where correctness and maintainability are critical.

How to Use Functional Programming in F#

Below are the key techniques and concepts you’ll use daily when writing functional F# code.

Immutability by Default

In F#, every value bound with let is immutable. If you need mutation, you must explicitly use mutable. This nudges you toward safer, immutable designs.

// Immutable value – cannot be changed
let x = 10
// x <- 20  // error: 'x' is not mutable

// Explicit mutable value
let mutable y = 10
y <- 20  // allowed

// Immutable collections
let numbers = [1; 2; 3; 4; 5]   // List
let doubled = numbers |> List.map (fun n -> n * 2)
// numbers remains unchanged

First-Class Functions

Functions are values in F#. You can assign them to variables, pass them as arguments, return them from other functions, and store them in data structures. This enables powerful abstraction and composition.

let add x y = x + y
let add5 = add 5       // partial application
let result = add5 3    // 8

// Function as parameter
let applyFunc (f: int -> int) (value: int) = f value
let double x = x * 2
applyFunc double 10    // 20

// Returning a function
let multiplyBy factor =
    fun x -> x * factor

let triple = multiplyBy 3
triple 4               // 12

Higher-Order Functions and Pipelines

Higher-order functions like List.map, List.filter, and List.fold replace explicit loops. The forward pipeline operator |> feeds the result of one expression as input to the next, producing clean, left-to-right data flows.

let numbers = [1..10]

let evenSquaresSum =
    numbers
    |> List.filter (fun n -> n % 2 = 0)
    |> List.map (fun n -> n * n)
    |> List.sum

// evenSquaresSum = 4 + 16 + 36 + 64 + 100 = 220

Pattern Matching

Pattern matching deconstructs values, branches on shapes, and extracts data. It works with options, lists, discriminated unions, tuples, and even literals. The compiler exhaustiveness check ensures you handle every possible case.

// Matching on option
let describeOption opt =
    match opt with
    | Some value -> sprintf "Got %A" value
    | None -> "Nothing"

// Matching on discriminated union
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

let myCircle = Circle 5.0
area myCircle  // 78.5398...

Discriminated Unions and Option Types

Discriminated unions model choices that are mutually exclusive. The Option type (some, none) safely represents the presence or absence of a value, replacing null. You can define your own unions, such as Result for error handling.

type Result<'a, 'e> =
    | Ok of 'a
    | Error of 'e

let divide x y =
    if y = 0 then Error "Division by zero"
    else Ok (x / y)

// Usage with pattern matching
match divide 10 0 with
| Ok value -> printfn "Result: %d" value
| Error msg -> printfn "Error: %s" msg

Function Composition

The composition operators >> (forward) and << (backward) let you glue functions together without needing to define intermediate variables. Composition works naturally because functions are pure and have one input and one output.

let square x = x * x
let negate x = -x
let squareThenNegate = square >> negate
let result = squareThenNegate 5  // -25

// Backward composition
let negateThenSquare = negate << square
negateThenSquare 5  // also -25

Currying and Partial Application

All F# functions are curried by default—they take arguments one at a time. Partial application fixes some arguments, yielding a new function with fewer parameters. This simplifies customization and reuse.

let multiply x y = x * y
let double = multiply 2
let triple = multiply 3

double 7  // 14
triple 7 // 21

// Can also pipe into a partially applied function
[1..5]
|> List.map (multiply 10)
// [10; 20; 30; 40; 50]

Best Practices

Conclusion

Functional programming in F# offers a powerful, expressive way to build software. By embracing immutability, treating functions as first-class values, and leveraging pattern matching and discriminated unions, you can write code that is easier to understand, test, and maintain. The techniques covered here form the foundation of idiomatic F#—start applying them in your daily work to unlock cleaner architecture, fewer defects, and a more enjoyable development experience.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles