← Back to Course Index
Lesson 3: Functions & Composition
Master pure functions and reusable code patterns
Learning Objectives
- Understand Nix functions as first-class values
- Write functions with positional and named parameters
- Use default parameters and parameter sets
- Compose functions into complex expressions
- Understand higher-order functions
Functions in Nix
Functions are pure—they always return the same output for the same input, with no side effects. This is Nix's superpower for reproducibility.
Function syntax: parameter: body or { param1, param2 }: body
Simple Functions
Single-Parameter Functions
add5 = x: x + 5;
add5 3 # Evaluates to 8
Multiple Parameters
Nix functions take one parameter at a time. Multiple parameters are curried:
add = x: y: x + y;
add 3 4 # Evaluates to 7
# Or use named parameters for clarity:
add2 = { x, y }: x + y;
add2 { x = 3; y = 4; } # Evaluates to 7
Default Parameters
# With defaults, using '?'
greet = { name ? "friend", greeting ? "Hello" }:
"${greeting}, ${name}!";
greet { } # "Hello, friend!"
greet { name = "alice"; } # "Hello, alice!"
greet { name = "bob"; greeting = "Hi"; } # "Hi, bob!"
Practical Function Patterns
Building a Package Definition
mkPackage = { name, version, dependencies ? [] }:
{
inherit name version dependencies;
fullName = "${name}-${version}";
description = "Package: ${name}";
};
pkg = mkPackage {
name = "my-app";
version = "1.0.0";
dependencies = [ "nodejs" "python3" ];
};
pkg.fullName # "my-app-1.0.0"
inherit keyword: inherit name is shorthand for name = name. Saves typing in attribute sets.
Function Composition
Combine functions to build complex logic:
double = x: x * 2;
addTen = x: x + 10;
compose = f: g: x: f (g x); # Compose two functions
result = compose double addTen 5; # (5 + 10) * 2 = 30
Higher-Order Functions
Map (Transform Lists)
# Apply a function to every element
map = f: list: builtins.map f list;
names = [ "alice" "bob" "charlie" ];
greetings = map (name: "Hello, ${name}!") names;
# [ "Hello, alice!" "Hello, bob!" "Hello, charlie!" ]
Filter (Select Elements)
numbers = [ 1 2 3 4 5 6 ];
evens = builtins.filter (n: n % 2 == 0) numbers;
# [ 2 4 6 ]
Fold (Reduce to a Single Value)
numbers = [ 1 2 3 4 5 ];
sum = builtins.foldl' (acc: n: acc + n) 0 numbers;
# 15
Using with and let for Clarity
let
double = x: x * 2;
triple = x: x * 3;
numbers = [ 1 2 3 ];
in
builtins.map (n: double n + triple n) numbers
# [ 5 10 15 ] (each element: 2x + 3x = 5x)
Try It Yourself
Interactive Exercise
Open the Nix REPL:
nix repl
Define and call a simple function:
nix-repl> square = x: x * x
nix-repl> square 5
25
Create a function with named parameters and defaults:
nix-repl> greet = { name ? "world" }: "Hello, ${name}!"
nix-repl> greet {}
"Hello, world!"
nix-repl> greet { name = "alice"; }
"Hello, alice!"
Map over a list:
nix-repl> numbers = [ 1 2 3 4 5 ]
nix-repl> builtins.map (x: x * 2) numbers
[ 2 4 6 8 10 ]
Chain operations:
nix-repl> data = [ 1 2 3 4 5 ]
nix-repl> evens = builtins.filter (n: n % 2 == 0) data
nix-repl> doubled = builtins.map (n: n * 2) evens
nix-repl> doubled
[ 4 8 ]
Challenge: Build a Function Library
Create reusable utility functions. Write these in the REPL:
let
# Function to greet a person
greetPerson = { name, title ? "Friend" }: "Hello, ${title} ${name}!";
# Function to check if number is even
isEven = n: n % 2 == 0;
# Filter and greet
people = [ { name = "alice"; } { name = "bob"; title = "Dr"; } ];
in
builtins.map (p: greetPerson p) people
Key Takeaways
- Functions are values that can be passed around and composed
- Pure functions always produce the same output for the same input
- Named parameters with defaults make functions more usable
- Higher-order functions (map, filter, fold) are how you work with lists in Nix
- Composition is how you build complex logic from simple pieces