Control Flow
Introduction to Control Flow in Common Lisp
Control flow refers to the order in which statements in a program are executed. It determines the path the program takes based on conditions and other factors. Common Lisp provides a rich set of control flow forms, offering flexibility and expressiveness in structuring your code. Unlike some languages that rely heavily on structural indentation, Common Lisp uses parentheses ()
to define code blocks, making the logical structure explicit.
This tutorial will introduce you to the fundamental control flow concepts in Common Lisp and guide you through the most important control flow forms. We will cover conditional execution, looping, and non-local exits, providing examples and explanations to help you understand how to effectively use these tools.
Key Control Flow Concepts:
- Conditional Execution: Executing different code blocks based on whether a condition is true or false.
- Iteration (Looping): Repeating a block of code multiple times, either a fixed number of times or until a certain condition is met.
- Non-Local Exits: Mechanisms for transferring control to a different part of the program, bypassing the normal sequential execution.
Table of Contents:
Here's a breakdown of the control flow forms we will explore:
1. Conditional Forms
if
: Basic conditional execution.when
: Execute a block only if a condition is true.unless
: Execute a block only if a condition is false.cond
: General conditional execution with multiple clauses.case
: Conditional execution based on comparing a key to specific values.typecase
: Conditional execution based on the type of a value.ecase
: Error-signaling variant ofcase
.etypecase
: Error-signaling variant oftypecase
.
2. Looping Forms
loop
: The most versatile looping construct, offering various clauses for control.- Simple iteration
for
clauses (iterating over sequences, numbers, etc.)while
anduntil
clauses (conditional termination)collect
,sum
,count
,minimize
,maximize
(accumulation)
do
: Parameterized iteration with variable updates.dotimes
: Iterating a fixed number of times.dolist
: Iterating over the elements of a list.
3. Non-Local Exits
block
andreturn-from
: Named blocks for exiting from nested expressions.catch
andthrow
: Dynamic non-local exits, transferring control to acatch
block.
4. Other Control Flow Constructs
go
andtagbody
: Low-level control flow (generally discouraged in favor of higher-level forms).
By the end of this tutorial, you will be able to write more complex and structured Common Lisp programs using the appropriate control flow mechanisms. We will provide clear examples and explanations for each form, allowing you to quickly grasp these essential concepts.
1. Conditional Forms in Common Lisp
Conditional forms allow you to execute different parts of your code based on whether certain conditions are true or false. Common Lisp offers several conditional forms, each suited for different situations.
1.1 if
: Basic Conditional Execution
The if
form is the most fundamental conditional. It takes three arguments:
- Test: An expression that is evaluated to determine truth.
- Then: An expression to be evaluated if the test is true.
- Else: An optional expression to be evaluated if the test is false.
(if (< 5 10) ; Test: Is 5 less than 10?
(print "5 is less than 10") ; Then: Execute this if true
(print "5 is not less than 10")) ; Else: Execute this if false
Output:
"5 is less than 10"
If the else
clause is omitted and the test is false, if
returns nil
.
(if (> 5 10)
(print "5 is greater than 10")) ; No else clause
Evaluation result:
NIL
1.2 when
: Execute a Block if True
The when
form is a simplified if
that executes a block of code only if the test is true. It takes a test and any number of expressions to be executed if the test is true. If the test is false, when
returns nil
.
(let ((x 15))
(when (> x 10)
(print "x is greater than 10")
(print (* x 2)))) ; Multiple expressions in the 'then' block
Output:
"x is greater than 10"
30
1.3 unless
: Execute a Block if False
unless
is the opposite of when
. It executes a block of code only if the test is false.
(let ((x 5))
(unless (> x 10)
(print "x is not greater than 10")
(print (* x 2))))
Output:
"x is not greater than 10"
10
1.4 cond
: General Conditional Execution
The cond
form provides a more general way to handle multiple conditions. It takes a list of clauses, where each clause has a test and a sequence of expressions to be executed if the test is true. The first clause whose test evaluates to true is executed, and the rest are skipped.
(let ((grade 85))
(cond ((>= grade 90) (print "A"))
((>= grade 80) (print "B"))
((>= grade 70) (print "C"))
((>= grade 60) (print "D"))
(t (print "F")))) ; The 't' clause acts as a default (else)
Output:
"B"
The t
clause is often used as the last clause to provide a default action if none of the previous tests are true.
1.5 case
: Conditional Execution Based on a Key
The case
form compares a key to a set of keylists. It's useful when you want to check for equality against specific values.
(let ((fruit 'apple))
(case fruit
((apple) (print "It's an apple!"))
((banana orange) (print "It's a banana or an orange!"))
(otherwise (print "It's some other fruit."))))
Output:
"It's an apple!"
If the key matches a key in a keylist, the corresponding expressions are executed. The otherwise
keyword acts as a default case.
1.6 typecase
: Conditional Execution Based on Type
The typecase
form checks the type of a value and executes the corresponding code.
(let ((value 10))
(typecase value
(integer (print "It's an integer!"))
(string (print "It's a string!"))
(t (print "It's something else."))))
Output:
"It's an integer!"
1.7 ecase
and etypecase
: Error-Signaling Variants
ecase
and etypecase
are similar to case
and typecase
, respectively, but they signal an error if the key or type doesn't match any of the provided clauses. This is useful for ensuring that all possible cases are handled.
(let ((fruit 'grape))
(ecase fruit
((apple) (print "It's an apple!"))
((banana) (print "It's a banana!"))))
This will signal an error because grape
is not in any of the keylists. Similarly, etypecase
will signal an error if the value's type does not match any of the specified types.
These conditional forms provide a comprehensive set of tools for controlling the flow of execution in your Common Lisp programs. Choosing the right form depends on the specific logic you need to implement. if
, when
, and unless
are suitable for simple conditions. cond
is more general for multiple conditions. case
and typecase
provide efficient ways to check against specific values or types, with ecase
and etypecase
adding error checking for robustness.
2. Looping Forms in Common Lisp
Looping constructs allow you to repeat a block of code multiple times. Common Lisp provides several powerful looping forms, with loop
being the most versatile.
2.1 loop
: The Versatile Looping Construct
The loop
macro is extremely flexible and can handle a wide variety of looping needs. It uses various clauses to control the loop's behavior.
2.1.1 Simple Iteration
The simplest form of loop
creates an infinite loop. You must provide a way to exit the loop using return
or a similar construct.
(loop
(print "This will print forever unless we stop it!")
(return)) ; Exit the loop immediately
This will print the message only once because of the return
.
2.1.2 for
Clauses: Iteration with Variables
The for
clause introduces loop variables and specifies how they are updated in each iteration.
- Iterating over a range of numbers:
(loop for i from 0 to 4 ; Iterate from 0 to 4 (inclusive)
do (print i))
Output:
0
1
2
3
4
You can use from
, to
, below
(exclusive upper bound), by
(step), downfrom
, downto
for different iteration patterns.
(loop for i from 10 downto 1 by 2
do (print i))
Output:
10
8
6
4
2
- Iterating over a list:
(loop for item in '(a b c d) ; Iterate over the elements of the list
do (print item))
Output:
A
B
C
D
2.1.3 while
and until
Clauses: Conditional Termination
while
continues the loop as long as a condition is true. until
continues the loop until a condition becomes true.
(let ((x 0))
(loop while (< x 5) ; Loop while x is less than 5
do (print x)
(incf x))) ; Increment x
Output:
0
1
2
3
4
(let ((x 0))
(loop until (> x 5) ; Loop until x is greater than 5
do (print x)
(incf x)))
Output:
0
1
2
3
4
5
2.1.4 Accumulation Clauses: collect
, sum
, count
, minimize
, maximize
These clauses allow you to accumulate results during the loop.
collect
: Collects the results into a list.
(loop for i from 1 to 5
collect (* i i)) ; Collect the squares of numbers from 1 to 5
Output:
(1 4 9 16 25)
sum
: Sums the results.
(loop for i from 1 to 5
sum i) ; Sum the numbers from 1 to 5
Output:
15
count
: Counts how many times a condition is true.
(loop for i from 1 to 10
count (evenp i)) ; Count the even numbers from 1 to 10
Output:
5
minimize
andmaximize
: Find the minimum or maximum value.
(loop for i in '(3 1 4 1 5 9 2 6)
minimize i)
Output:
1
2.2 do
: Parameterized Iteration
The do
form provides more explicit control over loop variables and their updates. It takes a list of variable specifications, a termination test, and a body of code.
(do ((i 0 (1+ i)) ; Initialize i to 0, update it by adding 1 in each iteration
(j 10 (- j 2))) ; Initialize j to 10, update it by subtracting 2
((> i 5) (print "Loop finished!")) ; Termination test: exit when i > 5
(format t "i: ~d, j: ~d~%" i j))
Output:
i: 0, j: 10
i: 1, j: 8
i: 2, j: 6
i: 3, j: 4
i: 4, j: 2
i: 5, j: 0
"Loop finished!"
2.3 dotimes
: Iterating a Fixed Number of Times
dotimes
is a convenient form for iterating a specific number of times.
(dotimes (i 5) ; Iterate 5 times, i will take values from 0 to 4
(format t "Iteration ~d~%" i))
Output:
Iteration 0
Iteration 1
Iteration 2
Iteration 3
Iteration 4
2.4 dolist
: Iterating over a List
dolist
provides a simple way to iterate over the elements of a list.
(dolist (item '(apple banana cherry)) ; Iterate over the list
(format t "Fruit: ~a~%" item))
Output:
Fruit: APPLE
Fruit: BANANA
Fruit: CHERRY
These looping forms offer different levels of control and are suited for various situations. loop
is the most powerful and flexible, while do
, dotimes
, and dolist
provide more specialized and often more concise ways to express common looping patterns.
3. Non-Local Exits in Common Lisp
Non-local exits provide a way to transfer control from one part of a program to another, bypassing the usual sequential flow. Common Lisp offers two primary mechanisms for this: block
/return-from
for lexical exits and catch
/throw
for dynamic exits.
3.1 block
and return-from
: Lexical Exits
block
creates a named block of code. return-from
exits that block, returning a specified value. These are lexically scoped, meaning return-from
can only exit a block
that is lexically visible (within the same function or a containing function).
(defun my-function (x)
(block my-block ; Define a block named my-block
(loop for i from 1 to 10
do (if (> (* x i) 50)
(return-from my-block (* x i))) ; Exit the block if the condition is met
(print (* x i)))
"Loop completed without exceeding 50")) ; This is returned if the loop finishes normally
(print (my-function 5)) ; Output: 50
(print (my-function 2)) ; Output: "Loop completed without exceeding 50"
In the first call to my-function
(with x = 5
), the loop reaches i = 10
, where (* x i)
becomes 50. The return-from
exits the my-block
and the function immediately returns 50.
In the second call (with x = 2
), the loop completes without the condition in the if
being met. The function then proceeds to the last expression in the block
and returns the string "Loop completed without exceeding 50".
You can also use return
as a shorthand for (return-from nil ...)
which will return from the innermost enclosing block
with the name nil
(which is implicitly created by many forms like loop
, progn
, let
, etc.):
(loop
(print "Inside the loop")
(return "Exiting the loop")) ; Equivalent to (return-from nil "Exiting the loop")
3.2 catch
and throw
: Dynamic Exits
catch
establishes a catch point with a specific tag. throw
transfers control to the nearest dynamically enclosing catch
with a matching tag, unwinding the call stack as needed. These are dynamically scoped, meaning throw
can exit any catch
that is currently active on the call stack, regardless of the lexical structure.
(defun nested-calculations (x y)
(catch 'my-tag ; Establish a catch point with the tag 'my-tag
(print "Starting calculations...")
(let ((result (* x (nested-helper y))))
(print "Calculations completed.") ; This might not be reached
result)))
(defun nested-helper (z)
(if (< z 0)
(throw 'my-tag "Negative input not allowed!") ; Throw to the 'my-tag catch
(* z 2)))
(print (nested-calculations 5 3)) ; Output: 30, "Starting calculations...", "Calculations completed."
(print (nested-calculations 5 -1)) ; Output: "Starting calculations...", "Negative input not allowed!"
In the first call to nested-calculations
, nested-helper
returns 6, and the multiplication and final print occur.
In the second call, nested-helper
is called with -1
. The throw
form is executed, transferring control directly to the catch
form in nested-calculations
, with the value "Negative input not allowed!". The "Calculations completed." print is skipped.
catch
and throw
provide a powerful way to handle exceptional situations or implement complex control flow patterns, but they should be used judiciously, as excessive use can make code harder to understand.
4. Other Control Flow Constructs
4.1 go
and tagbody
: Low-Level Control Flow
tagbody
establishes a block of code with labels (tags). go
transfers control to a specific tag within the same tagbody
. These are the most primitive control flow mechanisms in Common Lisp and are generally discouraged in favor of higher-level constructs like loop
, do
, cond
, and block
/return-from
.
(tagbody
start
(print "Starting...")
(let ((x (read)))
(if (zerop x)
(go end))) ; Go to the 'end' tag if x is zero
(print "Not zero!")
(go start) ; Go back to the 'start' tag
end
(print "Ending."))
This code creates a loop that prompts the user for input. If the input is zero, the program jumps to the end
tag and exits. Otherwise, it prints "Not zero!" and loops back to the beginning.
While go
and tagbody
provide a basic level of control, they can easily lead to spaghetti code that is difficult to read and maintain. The higher-level control flow forms provide better structure and readability, so they should be preferred in most cases.
This concludes the introduction to control flow in Common Lisp. By understanding these forms, you can write more complex and well-structured programs. Remember to use the most appropriate control flow construct for each situation to keep your code clear and maintainable.