Skip to content

LISP - Functions as Return Values

In Lisp, functions are first-class citizens. This means a function is a data type, just like a number, string, or list. This principle allows for powerful programming paradigms, including the ability for a function to return another function as its result.

A first-class function has the following capabilities:

  • Can be stored in variables: (let ((my-func #'+)) ...)
  • Can be passed as an argument: (mapcar #'1+ '(1 2 3))
  • Can be returned from another function: This is the focus of this chapter.

A function that either takes another function as an argument, or returns a function, is known as a higher-order function. Functions like mapcar, reduce, and remove-if are common built-in higher-order functions. A function that returns a function is often called a function factory.

When a function returns another function, the returned function doesn’t just contain code; it also captures the lexical environment in which it was created. This combination of a function and its captured environment is called a closure. The closure ‘remembers’ the variables from its creation scope, even after the outer function has finished executing.

Let’s create a higher-order function, make-adder, that acts as a factory. It takes a number n and returns a new function (a closure) that adds n to its own argument.

;; A higher-order function that returns a function.
(defun make-adder (n)
"This is a function factory. It creates and returns a new function
that will add N to any number given to it."
(lambda (y)
(+ n y))) ; The returned lambda CLOSES OVER the variable 'n'.
;; --- Using the function factory ---
;; Create a specific "add-5" function.
(let ((add-5 (make-adder 5)))
;; `add-5` is now a function we can call.
;; We use `funcall` to invoke a function stored in a variable.
(print (funcall add-5 10)) ; -> 15
(print (funcall add-5 100)); -> 105
)
;; We can create another adder on the fly.
(let ((add-10 (make-adder 10)))
(print (funcall add-10 10)) ; -> 20
)

When you execute the code, it returns the following result:

15
105
20
  • Customizable Behavior: You can create generic functions and customize them by returning specialized versions, reducing code duplication.
  • Stateful Functions: A closure can be used to maintain state between calls without using global variables or objects. A classic example is a counter function that ‘remembers’ its last value.
  • Delayed Execution (Callbacks): In event-driven programming (like GUIs or asynchronous networking), you often need to define what should happen later when an event occurs. Closures are perfect for creating these callback functions, as they can capture the necessary context to be used when the event finally fires.
(defun make-counter ()
(let ((count 0))
(lambda ()
(incf count)))) ; incf increments the 'count' variable
(let ((counter-a (make-counter))
(counter-b (make-counter)))
(print (funcall counter-a)) ; -> 1
(print (funcall counter-a)) ; -> 2
(print (funcall counter-b)) ; -> 1 (A separate closure with its own 'count')
(print (funcall counter-a)) ; -> 3
)

Each call to make-counter creates a new closure with its own private count variable, demonstrating how closures encapsulate both behavior and state.