Functions in Scheme
Learn how functions work in Scheme - definitions, lambda, closures, recursion, tail calls, and higher-order functions with Docker-ready examples
In most languages, functions are a feature. In Scheme, functions are the whole point. As a member of the Lisp family with a deeply functional core, Scheme treats procedures as first-class values: they can be stored in variables, passed as arguments, returned from other functions, and created on the fly. There is no separate notion of “methods” or “subroutines” — everything is a procedure, and a procedure is just a value like any number or string.
Scheme also pioneered two ideas that shape how you write functions: lexical scoping (a function remembers the environment where it was defined) and proper tail calls (recursion is as cheap as a loop). Together these mean you rarely reach for mutable state or for loops — you compose small functions and let recursion do the iterating.
In this tutorial you’ll define functions, write anonymous lambda procedures, capture state with closures, accept optional arguments, write both naive and tail-recursive functions, and pass functions to other functions. Every concept builds on the S-expression syntax you already met in Hello World.
Defining Functions
A function definition uses define with the parameter list folded into the name. The body is implicitly a begin block, and the value of the last expression is returned — there is no return keyword.
Create a file named functions.scm:
| |
A few things to notice:
(define (square x) ...)is exactly equivalent to(define square (lambda (x) ...)). The named form is just convenient sugar.- No
return— the last expression’s value is the result.ifis an expression, so(if (= n 0) 1 ...)is the value. name . restis the variadic syntax. Everything after the dot collects extra arguments into a list, which we use here to fake a default value.make-adderreturns a function. Because of lexical scoping, the returned lambda keeps a live reference ton— that captured binding is a closure.
Higher-Order Functions
The real power of first-class functions shows up when functions consume and produce other functions. Scheme’s standard library leans on this heavily with map, filter, and friends.
Create a file named higher_order.scm:
| |
Key ideas at work here:
apply-twicereceivesfand simply calls it. Functions are passed by name with no special syntax —incis a value.composereturns a lambda.double-then-incis a new function manufactured at runtime.mapandfilterare higher-order functions built into Scheme. They replace the explicit loops you’d write in an imperative language.applytakes a function and a list of arguments, calling the function as if the list elements were passed individually —(apply + '(1 2 3 4 5))is(+ 1 2 3 4 5).
Running with Docker
Run both files with GNU Guile using the official image:
| |
Expected Output
Running functions.scm:
square 6 = 36
sum-of-squares 3 4 = 25
cube 3 = 27
Hello, World!
Welcome, Scheme!
add-10 5 = 15
factorial 5 = 120
factorial-iter 10 = 3628800
Running higher_order.scm:
apply-twice inc 0 = 2
double-then-inc 5 = 11
map double = (2 4 6 8 10)
filter even? = (2 4)
sum (apply +) = 15
Key Concepts
- Functions are first-class values — they can be named, passed, returned, and built at runtime; there is no separate “method” or “subroutine” concept.
defineandlambdaare the same tool —(define (f x) ...)is sugar for(define f (lambda (x) ...)).- No
returnstatement — a function’s value is its last expression, andif/condare expressions that produce values. - Closures come from lexical scoping — a returned lambda keeps the bindings it was defined with, which is how
make-adderremembersn. - Recursion replaces loops — and with a tail call (like the named
letinfactorial-iter), Scheme guarantees constant stack space, so recursion is as efficient as iteration. - Variadic functions use the dotted
name . restparameter to collect extra arguments into a list, enabling optional-argument patterns. - Higher-order functions like
map,filter,apply, and your owncomposeare the idiomatic way to transform data — compose small functions instead of writing imperative loops.
Running Today
All examples can be run using Docker:
docker pull weinholt/guile:latest
Comments
Loading comments...
Leave a Comment