Skip to content

F# Guide — Functions: First-Class Citizens in F#

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Functions in F# are first-class values that can be passed as arguments to other functions, returned as results, and composed together to build complex operations from simple, reusable building blocks.

What You'll Learn

  • Defining functions with let bindings
  • Type annotations and type inference
  • Partial application and currying
  • Higher-order functions
  • Function composition and pipelines

Why It Matters

Functions are the fundamental unit of abstraction in F#. Understanding first-class functions, partial application, and composition enables concise, reusable, and testable code. Durga Antivirus Pro uses function composition for its analysis pipeline.

Real-World Use

F#'s function composition is used in data processing pipelines, web request handling, and financial calculations where operations are naturally chained.

flowchart LR
    A["Functions"] --> B["Definition"]
    B --> C["Currying"]
    C --> D["HOFs"]
    D --> E["Composition"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Defining Functions

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

// With type annotations
let multiply (x: int) (y: int): int = x * y

// Single argument
let square x = x * x

// No arguments (unit)
let greet () = printfn "Hello!"

Type Inference

// F# infers types from usage
let add x y = x + y
// val add: x: int -> y: int -> int

let concat a b = a + b
// val concat: a: string -> b: string -> string

let genericAdd x y = x + y  // Error: ambiguous

Currying and Partial Application

// All F# functions are curried by default
let add x y = x + y

// Partial application
let addFive = add 5
let result = addFive 3  // 8

// Real example
let multiply x y = x * y
let double = multiply 2
let triple = multiply 3

[1; 2; 3] |> List.map double  // [2; 4; 6]

Higher-Order Functions

// Function as parameter
let applyTwice f x = f (f x)
let result = applyTwice (fun x -> x * 2) 5  // 20

// Function as return value
let createAdder x = fun y -> x + y
let add10 = createAdder 10
add10 5  // 15

Pipelines

// Forward pipe operator |>
let result = 5 |> square |> add 10 |> multiply 2

// Without pipes (nested calls)
let result2 = multiply 2 (add 10 (square 5))

// Pipes make the data flow clear
let processed =
    data
    |> List.filter (fun x -> x > 0)
    |> List.map square
    |> List.sum

Function Composition

// Composition operator >>
let addThenDouble = add 5 >> multiply 2
addThenDouble 3  // (3 + 5) * 2 = 16

// Reverse composition <<
let doubleThenAdd = multiply 2 >> add 5
doubleThenAdd 3  // (3 * 2) + 5 = 11

Anonymous Functions

// Lambda syntax
List.map (fun x -> x * x) [1; 2; 3]

// Multiple arguments
List.fold (fun acc x -> acc + x) 0 [1; 2; 3]

// Pattern matching in lambda
List.choose (fun x -> if x > 0 then Some x else None) [-1; 0; 1; 2]

Common Mistakes

1. Forgetting partial application order

List.map f [1;2] applies f to each element. List.map takes the function first, then the list.

2. Missing parentheses for unit

Functions with no arguments need (): let f () = ... not let f = ....

3. Confusing pipe and composition

|> pipes a value into a function. >> composes two functions. They serve different purposes.

4. Overusing lambdas

If a lambda just calls a named function, use partial application: List.map (add 5) not List.map (fun x -> add 5 x).

5. Ignoring generic type constraints

Some operations (like +) are not generic. You may need to specify types or use inline functions.

Practice Questions

1. What does currying mean in F#? All functions take one argument and return another function (or value). Multiple arguments are simulated through nested functions.

2. What does the pipe operator |> do? It passes the left operand as the last argument to the right operand function, enabling readable data flow.

3. When would you use >> vs |>? >> composes two functions into one. |> applies a value to a function. Use >> to build new functions, |> to Process data.

Challenge: Write a pipeline that filters even numbers, squares them, and sums the result using only function composition and pipes.

FAQ

{{< faq question="Are F# functions pure by default?" >}} No. F# is not purely functional. Functions can have side effects. Immutability is the default, but mutability is available when needed. {{< /faq >}}

{{< faq question="What is the difference between let and let rec?" >}} let defines a function that cannot call itself. let rec defines a recursive function that can reference itself. {{< /faq >}}

{{< faq question="Can functions be nested?" >}} Yes. F# supports nested functions that capture variables from the enclosing scope (closures). {{< /faq >}}

{{< faq question="What is inline in F#?" >}} inline tells the compiler to inline the function at the call site, enabling generic arithmetic and avoiding function call overhead. {{< /faq >}}

{{< faq question="How do I define generic functions?" >}} F# infers generic types automatically. Use explicit type parameters with 'T notation for complex cases: let identity (x: 'T) = x. {{< /faq >}}

Mini Project

Build a data processing pipeline using function composition:

let processData =
    List.filter (fun x -> x > 0)
    >> List.map (fun x -> x * 2)
    >> List.sum

let data = [-5; 1; 2; -3; 4]
let result = processData data  // (1+2+4)*2 = 14

What's Next

Now that you understand functions, explore immutable data and how F# applies immutability by default.

Topic Description Link
F# Immutable Data Immutability in F# {{< ref "05-immutable-data" >}}
F# Pattern Matching Pattern matching basics {{< ref "06-pattern-matching" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro