Intermediate

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
;; functions.scm — Functions in Scheme

;; Basic definition: (define (name params...) body...)
(define (square x)
  (* x x))

;; Multiple parameters, calling other functions
(define (sum-of-squares a b)
  (+ (square a) (square b)))

;; Anonymous function (lambda) bound to a name with define
;; (define (cube x) ...) is just sugar for this form
(define cube
  (lambda (x) (* x x x)))

;; "Optional" arguments via a rest parameter (the dotted tail)
;; rest collects any extra arguments into a list
(define (greet name . rest)
  (let ((greeting (if (null? rest) "Hello" (car rest))))
    (string-append greeting ", " name "!")))

;; Closure: make-adder returns a lambda that remembers n
;; This is lexical scoping in action — n stays alive in the returned function
(define (make-adder n)
  (lambda (x) (+ x n)))

(define add-10 (make-adder 10))

;; Recursion: the classic factorial (naive version)
(define (factorial n)
  (if (= n 0)
      1
      (* n (factorial (- n 1)))))

;; Tail recursion with a named let — the accumulator carries the result
;; Scheme guarantees this runs in constant stack space (proper tail calls)
(define (factorial-iter n)
  (let loop ((i n) (acc 1))
    (if (= i 0)
        acc
        (loop (- i 1) (* acc i)))))

;; Exercise every procedure
(display "square 6 = ") (display (square 6)) (newline)
(display "sum-of-squares 3 4 = ") (display (sum-of-squares 3 4)) (newline)
(display "cube 3 = ") (display (cube 3)) (newline)
(display (greet "World")) (newline)
(display (greet "Scheme" "Welcome")) (newline)
(display "add-10 5 = ") (display (add-10 5)) (newline)
(display "factorial 5 = ") (display (factorial 5)) (newline)
(display "factorial-iter 10 = ") (display (factorial-iter 10)) (newline)

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. if is an expression, so (if (= n 0) 1 ...) is the value.
  • name . rest is the variadic syntax. Everything after the dot collects extra arguments into a list, which we use here to fake a default value.
  • make-adder returns a function. Because of lexical scoping, the returned lambda keeps a live reference to n — 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
;; higher_order.scm — Functions as values

;; A function that takes another function as an argument
(define (apply-twice f x)
  (f (f x)))

;; Function composition: returns a brand new function
(define (compose f g)
  (lambda (x) (f (g x))))

(define (double x) (* x 2))
(define (inc x) (+ x 1))

;; Build a new function out of two existing ones
(define double-then-inc (compose inc double))

(define nums '(1 2 3 4 5))

;; Pass a named function as an argument
(display "apply-twice inc 0 = ")
(display (apply-twice inc 0))
(newline)

;; Use the composed function
(display "double-then-inc 5 = ")
(display (double-then-inc 5))
(newline)

;; map applies a function to every element, returning a new list
(display "map double = ")
(display (map double nums))
(newline)

;; filter keeps elements for which the predicate returns #t
(display "filter even? = ")
(display (filter even? nums))
(newline)

;; apply spreads a list as arguments — here, summing the list
(display "sum (apply +) = ")
(display (apply + nums))
(newline)

Key ideas at work here:

  • apply-twice receives f and simply calls it. Functions are passed by name with no special syntax — inc is a value.
  • compose returns a lambda. double-then-inc is a new function manufactured at runtime.
  • map and filter are higher-order functions built into Scheme. They replace the explicit loops you’d write in an imperative language.
  • apply takes 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:

1
2
3
4
5
6
7
8
# Pull the Guile Scheme image
docker pull weinholt/guile:latest

# Run the core functions example
docker run --rm -v $(pwd):/app -w /app weinholt/guile:latest guile --no-auto-compile functions.scm

# Run the higher-order functions example
docker run --rm -v $(pwd):/app -w /app weinholt/guile:latest guile --no-auto-compile higher_order.scm

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.
  • define and lambda are the same tool(define (f x) ...) is sugar for (define f (lambda (x) ...)).
  • No return statement — a function’s value is its last expression, and if/cond are expressions that produce values.
  • Closures come from lexical scoping — a returned lambda keeps the bindings it was defined with, which is how make-adder remembers n.
  • Recursion replaces loops — and with a tail call (like the named let in factorial-iter), Scheme guarantees constant stack space, so recursion is as efficient as iteration.
  • Variadic functions use the dotted name . rest parameter to collect extra arguments into a list, enabling optional-argument patterns.
  • Higher-order functions like map, filter, apply, and your own compose are 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
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining