Functions in F#
Learn how to define and use functions in F# - parameters, recursion, higher-order functions, currying, closures, and composition with Docker-ready examples
Functions are the beating heart of F#. As a functional-first language, F# treats functions as first-class values: they can be bound to names, passed as arguments, returned from other functions, partially applied, and composed together to build larger behavior from small pieces. There is no special “function declaration” ceremony — a function is just a value that happens to take an argument.
This functional foundation shapes everything about how you write F#. Where an imperative language reaches for loops and mutable counters, F# reaches for recursion and higher-order functions like List.map and List.fold. Where an object-oriented language wires behavior into methods on objects, F# composes plain functions with the pipe (|>) and composition (>>) operators.
In this tutorial you’ll learn how to define functions with the let keyword, how F#’s type inference figures out parameter and return types for you, and how recursion replaces loops. Then we’ll explore the features that make functional programming powerful: higher-order functions, currying and partial application, closures, and function composition.
Defining and Calling Functions
In F#, you define a function with let, list its parameters separated by spaces, and use = to introduce the body. There are no parentheses around the parameter list and no return keyword — the last expression evaluated is the return value.
Create a file named functions.fsx:
| |
Notice how square and add need no type annotations — F# infers that they work on int because of the * and + operations. The parentheses in calls like (square 5) group the expression so its result is passed as a single argument; F# uses spaces, not commas, to separate arguments.
Recursion
F# discourages mutable loop variables, so recursion is the natural way to repeat work. A function that calls itself must be marked with the rec keyword — this is required and the compiler will tell you if you forget it.
Create a file named functions_recursion.fsx:
| |
The sumTo example shows an important pattern: the inner loop function carries an accumulator, so the recursive call is the very last thing it does. This tail recursion lets the F# compiler reuse the stack frame, avoiding stack overflow even for large inputs — the functional equivalent of a for loop.
Higher-Order Functions, Currying, and Composition
This is where F# shines. Functions can take other functions as arguments and return new functions. Because all F# functions are curried by default, supplying fewer arguments than a function expects gives you back a new function waiting for the rest — this is partial application.
Create a file named functions_higher_order.fsx:
| |
A few things to highlight: applyTwice double 3 evaluates double (double 3), doubling twice. add10 is add with its first argument fixed at 10. The pipe operator turns nested calls inside-out into a readable top-to-bottom data flow, and >> glues two functions into a single new one where the output of the first becomes the input of the second.
Running with Docker
These are F# script files, so we run them with F# Interactive (dotnet fsi) inside the official .NET SDK image — no local install required.
| |
Expected Output
Running functions.fsx:
square 5 = 25
add 3 4 = 7
Hello, F#!
circleArea 2.0 = 12.566360
Running functions_recursion.fsx:
factorial 5 = 120
fib 10 = 55
sumTo 100 = 5050
Running functions_higher_order.fsx:
applyTwice double 3 = 12
add10 5 = 15
sumOfSquares = 55
addThenDouble 5 = 30
counter: 1 2 3
Key Concepts
- Functions are values —
let name args = bodydefines a function exactly the waylet x = 5defines a value. There is noreturnkeyword; the last expression is the result. - Type inference — F# usually infers parameter and return types from how they’re used, so explicit annotations are optional and reserved for clarity or disambiguation.
recis required for recursion — a function that calls itself must be declared withlet rec. Use an accumulator to make recursion tail-recursive and stack-safe, replacing imperative loops.- Everything is curried — multi-argument functions are really chains of single-argument functions, which makes partial application (
add 10) free and natural. - Higher-order functions — functions can take and return other functions;
List.map,List.filter, andList.foldare the idiomatic alternatives to writing loops. - Closures — an inner function captures variables from its enclosing scope, letting you build stateful helpers like counters.
- Pipe and compose —
|>feeds a value through a sequence of transformations for readable data flow, while>>combines functions into a single reusable function.
Running Today
All examples can be run using Docker:
docker pull mcr.microsoft.com/dotnet/sdk:9.0
Comments
Loading comments...
Leave a Comment