Skip to content
Tuples and Type Inference
step 1/7

Reading — step 1 of 7

Learn

~3 min readLists, Recursion, IO

F# inherits ML's strong type inference — types are usually deduced from usage. Tuples are first-class, common for returning multiple values without defining a record.

Tuples

let pair = (1, "hello")              // int * string
let triple = (1, "a", true)          // int * string * bool

// Destructure:
let (n, s) = pair
printfn "%d %s" n s

Tuple types use *: int * string reads as "int and string." Note: * is NOT multiplication here — it's the tuple type constructor.

fst and snd

fst (1, 2)                            // 1
snd (1, 2)                            // 2

Work only on PAIRS (2-tuples). For larger tuples, destructure with patterns.

Returning multiple values

let divmod a b = (a / b, a % b)

let (q, r) = divmod 17 5
printfn "%d remainder %d" q r

Clean way to return multiple values without defining a record. Common in F#.

Type inference

F# infers types from usage:

let add x y = x + y                   // inferred: int -> int -> int
let greet name = "Hello, " + name     // inferred: string -> string

let generic x = x                     // inferred: 'a -> 'a (polymorphic)

Add explicit annotations only when needed (ambiguity, documentation):

let add (x: int) (y: int) : int = x + y
let greet (name: string) : string = "Hello, " + name

Type inference quirks

Sometimes F# can't decide:

let upper s = s.ToUpper()
// Error: Lookup on object of indeterminate type — could be string, char[], etc.

Fix with annotation:

let upper (s: string) = s.ToUpper()

F#'s inference is left-to-right; sometimes the compiler hasn't seen enough to decide. Explicit types resolve.

Generic functions

let swap (x, y) = (y, x)              // 'a * 'b -> 'b * 'a
let identity x = x                     // 'a -> 'a
let compare a b =
    if a > b then 1
    elif a < b then -1
    else 0
// 'a -> 'a -> int when 'a : comparison

F# auto-generalizes — uses type variables ('a, 'b) when the function works for any type with the right operations.

The constraint when 'a : comparison (or others like equality, :>SomeType) appears when needed.

Inline functions

For numeric functions that should work on multiple number types:

let inline double x = x * x
// 'a -> 'a when 'a : (static member ( * ) : 'a * 'a -> 'a)

double 5                              // int → int
double 3.14                           // float → float

inline enables "statically resolved type parameters" — special inference for arithmetic over multiple types. Used in libraries like FSharp.Core.

Tuples vs records

// Tuple (positional, anonymous):
let point = (3.0, 4.0)

// Record (named, more readable):
type Point = { X: float; Y: float }
let point = { X = 3.0; Y = 4.0 }

Tuples for short-lived multi-value returns. Records for domain types with clear names. Don't pass tuples around in public APIs — readers can't tell what each component means.

Common mistakes

  • * confusion in tuple typesint * int is the type of pairs of ints, NOT multiplication.
  • Tuples beyond 3-4 elements — readability collapses. Use a record.
  • Forgetting type inference is left-to-right — the compiler may need annotations earlier than you expect.
  • Using fst/snd on triples — they only work on pairs. For larger tuples, destructure.
  • Mixing F# tuples with C# Tuple<T,U> — they're different types. F# uses System.Tuple under the hood but the F# tuple syntax is preferred.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…