Beginner

Operators in Scheme

Learn how Scheme handles arithmetic, comparison, logical, and equality operators using uniform prefix notation and its exact numeric tower

In most languages, operators are special syntax: 2 + 3 reads left to right, and a precedence table decides whether 2 + 3 * 4 means 14 or 20. Scheme throws all of that away. There are no operators in the syntactic sense — +, <, and = are ordinary procedures, and you call them exactly like any other procedure, in prefix position inside parentheses.

This is a direct consequence of Scheme’s heritage as a minimalist Lisp. Everything is an S-expression of the form (operator operand ...), so (+ 2 3) is a procedure call where + happens to be the procedure. Because they are just procedures, the arithmetic and comparison “operators” are variadic(+ 1 2 3 4 5) is perfectly valid — and there is no precedence to memorize. The parentheses make evaluation order explicit and unambiguous.

In this tutorial you’ll work through Scheme’s arithmetic procedures (including its exact rational number tower), its comparison and logical operations, and the family of equality predicates that distinguishes Scheme from most languages. Along the way you’ll see why prefix notation and the “only #f is false” rule make Scheme’s expression model unusually consistent.

Arithmetic Operators

The four arithmetic procedures +, -, *, and / accept any number of arguments. A standout feature is Scheme’s numeric tower: dividing two integers that don’t divide evenly yields an exact rational, not a truncated or floating-point result.

Create a file named arithmetic.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
;; Arithmetic procedures use prefix notation: (op operand ...)
(display (+ 3 4))        ; addition
(newline)
(display (- 10 3))       ; subtraction
(newline)
(display (* 6 7))        ; multiplication
(newline)
(display (/ 20 4))       ; division (divides evenly -> exact integer)
(newline)

;; They are variadic - any number of arguments
(display (+ 1 2 3 4 5))
(newline)
(display (* 2 3 4))
(newline)

;; A single argument has special meaning
(display (- 5))          ; negation
(newline)
(display (/ 4))          ; reciprocal
(newline)

;; The numeric tower: exact rational arithmetic, no rounding
(display (/ 1 3))        ; stays exact as a fraction
(newline)
(display (+ 1/2 1/4))    ; rational literals work too
(newline)

;; Integer division procedures
(display (quotient 17 5))   ; truncated quotient
(newline)
(display (remainder 17 5))  ; remainder (sign follows dividend)
(newline)
(display (modulo -7 3))     ; modulo (sign follows divisor)
(newline)

;; Other common numeric procedures
(display (expt 2 10))    ; exponentiation
(newline)
(display (sqrt 16))      ; exact when the result is exact
(newline)
(display (abs -42))      ; absolute value
(newline)
(display (max 3 7 2 9 1))
(newline)
(display (min 3 7 2 9 1))
(newline)

The key surprises for newcomers: (/ 4) returns 1/4 (single-argument division is reciprocal), (- 5) negates, and (/ 1 3) produces the exact fraction 1/3 rather than 0.333.... The difference between remainder and modulo only shows with negative numbers — remainder takes the sign of the dividend, modulo takes the sign of the divisor.

Comparison Operators

Comparison procedures return a boolean (#t or #f). Like the arithmetic procedures, they are variadic — and this is genuinely useful: (< 1 2 3 4) checks that the whole sequence is strictly increasing in a single call.

Create a file named comparison.scm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;; Comparison procedures return #t or #f
(display (= 5 5))      ; numeric equality
(newline)
(display (< 3 7))
(newline)
(display (> 3 7))
(newline)
(display (<= 4 4))
(newline)
(display (>= 10 2))
(newline)

;; Variadic comparison checks an entire chain at once
(display (< 1 2 3 4))  ; strictly increasing?
(newline)
(display (< 1 2 2 4))  ; the 2 2 breaks strict ordering
(newline)
(display (= 5 5 5))    ; all equal?
(newline)

;; There is no "!=" operator - negate = with not
(display (not (= 3 4)))
(newline)

Note that = is specifically for numbers. Scheme has no !=; you express “not equal” by wrapping a comparison in not. The variadic chaining means (< 1 2 2 4) is #f because 2 < 2 fails somewhere in the chain.

Logical Operators

and, or, and not handle boolean logic, but and and or are special forms, not ordinary procedures — they short-circuit, evaluating only as many arguments as needed. They also return useful values, not just booleans: and returns its last operand when everything is truthy, and or returns the first truthy operand it finds.

Scheme’s truthiness rule is famously strict: only #f is false. Every other value — including 0 and the empty list '() — counts as true.

Create a file named logical.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
;; and / or / not for boolean logic
(display (and #t #t))
(newline)
(display (and #t #f))
(newline)
(display (or #f #t))
(newline)
(display (not #f))
(newline)

;; and returns the LAST value when all operands are truthy
(display (and 1 2 3))
(newline)
;; or returns the FIRST truthy value
(display (or #f 5 10))
(newline)

;; Only #f is false - everything else is truthy
(display (if 0 "true" "false"))   ; 0 is truthy!
(newline)
(display (if '() "true" "false")) ; empty list is truthy!
(newline)

;; Identity values for the empty forms
(display (and))   ; and with no args
(newline)
(display (or))    ; or with no args
(newline)

Because and returns its last value, (and config-loaded? user-name) can be used to retrieve user-name only when the configuration is present — a common Scheme idiom that replaces null checks. The empty forms (and) and (or) return their identity elements, #t and #f respectively.

Equality Predicates

Where most languages have one ==, Scheme offers a family of equality predicates, each suited to a different job. Choosing the right one matters: eq? tests object identity, eqv? adds reliable comparison for numbers and characters, and equal? does a deep structural comparison that works on lists and strings.

Create a file named equality.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
;; = compares numbers (note: 2 and 2.0 are numerically equal)
(display (= 2 2.0))
(newline)

;; eqv? - identity, reliable for numbers, chars, and symbols
(display (eqv? 'a 'a))
(newline)
(display (eqv? 2 2))
(newline)

;; equal? - deep structural equality (lists, strings, vectors)
(display (equal? '(1 2 3) '(1 2 3)))
(newline)
(display (equal? "hello" "hello"))
(newline)

;; eq? - pointer identity; always reliable for symbols
(display (eq? 'foo 'foo))
(newline)

;; Dedicated string comparison procedures
(display (string=? "abc" "abc"))
(newline)
(display (string<? "abc" "abd"))   ; lexicographic ordering
(newline)

;; String "concatenation" is a procedure, not an operator
(display (string-append "Hello, " "World!"))
(newline)

A useful rule of thumb: use equal? when comparing compound data like lists and strings, = when comparing numbers, and eq?/eqv? for symbols and identity checks. Strings even have their own comparison procedures (string=?, string<?, and friends) for lexicographic ordering, and string-append plays the role that a + concatenation operator does elsewhere.

Precedence and Nesting

Because operators are just procedures wrapped in parentheses, Scheme has no operator precedence at all. There is nothing to memorize and no ambiguity — the parentheses dictate exactly what evaluates first. Translating infix math into Scheme is purely a matter of nesting.

Create a file named operators.scm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
;; Scheme has NO precedence rules - parentheses decide everything

;; infix (2 + 3) * 4  ->
(display (* (+ 2 3) 4))
(newline)

;; infix 2 + 3 * 4  ->
(display (+ 2 (* 3 4)))
(newline)

;; infix ((10 - 2) / 4) + (3 * 5)  ->
(display (+ (/ (- 10 2) 4) (* 3 5)))
(newline)

;; Mixing arithmetic and comparison: 3 < 6
(display (< (+ 1 2) (* 2 3)))
(newline)

;; Combining comparison and logical operators
(display (and (> 5 3) (< 2 10)))
(newline)

The two contrasting expressions show the point precisely: (* (+ 2 3) 4) is 20 while (+ 2 (* 3 4)) is 14. No precedence table is needed because the structure is already in the parentheses.

Running with Docker

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

# Run each example
docker run --rm -v $(pwd):/app -w /app weinholt/guile:latest guile --no-auto-compile arithmetic.scm
docker run --rm -v $(pwd):/app -w /app weinholt/guile:latest guile --no-auto-compile comparison.scm
docker run --rm -v $(pwd):/app -w /app weinholt/guile:latest guile --no-auto-compile logical.scm
docker run --rm -v $(pwd):/app -w /app weinholt/guile:latest guile --no-auto-compile equality.scm
docker run --rm -v $(pwd):/app -w /app weinholt/guile:latest guile --no-auto-compile operators.scm

Expected Output

Running arithmetic.scm:

7
7
42
5
15
24
-5
1/4
1/3
3/4
3
2
2
1024
4
42
9
1

Running comparison.scm:

#t
#t
#f
#t
#t
#t
#f
#t
#t

Running logical.scm:

#t
#f
#t
#t
3
5
true
true
#t
#f

Running equality.scm:

#t
#t
#t
#t
#t
#t
#t
#t
Hello, World!

Running operators.scm:

20
14
17
#t
#t

Key Concepts

  • Operators are procedures+, <, and = are ordinary procedures called in prefix position, not special syntax.
  • No precedence rules — parentheses make evaluation order explicit, so (* (+ 2 3) 4) is unambiguous with nothing to memorize.
  • Variadic by default(+ 1 2 3 4 5) and chained comparisons like (< 1 2 3 4) work because the procedures accept any number of arguments.
  • Exact numeric tower(/ 1 3) produces the exact fraction 1/3, not a floating-point approximation; integers stay exact unless you introduce a float.
  • quotient, remainder, and modulo handle integer division, with remainder following the dividend’s sign and modulo following the divisor’s.
  • Only #f is false0, "", and '() are all truthy, which trips up programmers coming from other languages.
  • and/or return values and short-circuit — they yield the last (or first truthy) operand, enabling them to stand in for conditional value selection.
  • Pick the right equality predicate= for numbers, equal? for deep structural comparison of lists and strings, eq?/eqv? for identity and symbols.

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