Functions in Standard ML
Learn how functions work in Standard ML - definitions, recursion, currying, partial application, higher-order functions, and closures with Docker-ready examples
Functions are the heart of Standard ML. As a functional language, SML treats functions as first-class values: they can be bound to names, passed as arguments, returned from other functions, and stored in data structures. There are no “methods” or “subroutines” in the object-oriented sense — every piece of behavior is a function, and even operators like + are functions underneath.
Two ideas set SML functions apart from imperative languages. First, every function takes exactly one argument and returns exactly one value; what looks like a multi-argument function is really a chain of single-argument functions, a technique called currying. Second, SML’s Hindley-Milner type inference figures out the type of each function for you, so you rarely write type annotations even though the language is statically typed.
This tutorial covers defining and calling functions, recursion (which replaces loops in functional code), higher-order functions like map and foldl, currying with partial application, and closures that capture their surrounding environment. Each example is a complete, runnable program.
Defining and Calling Functions
Functions are introduced with the fun keyword. Parameters are separated by spaces — both in the definition and at the call site — not wrapped in parentheses and commas.
Create a file named functions.sml:
| |
Notice that add 3 4 applies add to two arguments with nothing but spaces. The ^ operator concatenates strings, and Int.toString converts an integer to its textual form so it can be printed.
Recursion Instead of Loops
Pure functional code has no mutable loop counters, so iteration is expressed with recursion. SML makes recursion natural by combining it with pattern matching: you list one clause per case, separated by |.
Create a file named recursion.sml:
| |
The first version of factorial builds up a chain of pending multiplications. The factTail version carries a running result in an accumulator, so the recursive call is the last thing it does — a tail call that SML compilers turn into an efficient loop.
Higher-Order Functions
Because functions are values, they can be passed to and returned from other functions. The Basis Library provides map, List.filter, and foldl for processing lists without writing explicit recursion. You can pass either a named function or an anonymous one written with fn ... => ....
Create a file named higher_order.sml:
| |
applyTwice increment 5 computes increment (increment 5), which is 7. The combination of small reusable functions and higher-order operators is the functional alternative to writing loops by hand.
Currying and Partial Application
Every SML function of “several arguments” is actually a curried chain of single-argument functions. The type int -> int -> int reads as “a function that takes an int and returns a function that takes an int and returns an int.” This means you can apply a function to some of its arguments and get back a specialized function — partial application.
Create a file named currying.sml:
| |
addFive and addTen are closures: they remember the value (5 and 10) that was in scope when they were created. makeCounter shows closures capturing a mutable ref, giving each counter its own private, persistent state — calling tick three times yields 1, 2, 3.
Local Functions and Scope
The let ... in ... end construct introduces bindings — including helper functions — that are visible only inside the expression. Names bound at the top level with val or fun are visible to every definition that follows them.
Create a file named scope.sml:
| |
The helper parity exists only within describe; referencing it elsewhere would be a compile error. Meanwhile taxRate is a global binding that addTax closes over. Using let to hide helpers keeps the top-level namespace clean and signals intent.
Running with Docker
| |
SML/NJ also prints version banners and inferred type information for each binding. The lines below are the program output produced by the print calls.
Expected Output
functions.sml:
square 5 = 25
add 3 4 = 7
Hello, Standard ML!
recursion.sml:
factorial 5 = 120
sumList [1,2,3,4] = 10
factTail (6, 1) = 720
higher_order.sml:
applyTwice increment 5 = 7
doubled = [2, 4, 6, 8]
evens = [2, 4, 6]
total = 15
currying.sml:
addFive 3 = 8
addTen 7 = 17
tick () = 1
tick () = 2
tick () = 3
scope.sml:
sumOfSquares 3 4 = 25
7 is odd
addTax 100 = 108
Key Concepts
fundefines functions, and arguments are applied with spaces (add 3 4), not parenthesized comma lists.- Type inference deduces each function’s type, so annotations like
(name : string)are optional even in this statically-typed language. - Recursion replaces loops; pair it with pattern matching (
fun f [] = ... | f (x::xs) = ...) to handle each case cleanly. - Tail recursion with an accumulator lets the compiler reuse stack space, the functional equivalent of an iterative loop.
- Higher-order functions like
map,List.filter, andfoldltake other functions — named or anonymous (fn x => ...) — as arguments. - Currying means every multi-argument function is a chain of one-argument functions, enabling partial application (
add 5yields a new function). - Closures capture variables from their defining scope, including mutable
refcells, which gives functions private persistent state. let ... in ... endscopes local values and helper functions so they stay invisible to the rest of the program.
Running Today
All examples can be run using Docker:
docker pull eldesh/smlnj:latest
Comments
Loading comments...
Leave a Comment