Beginner

Variables and Types in Scheme

Learn about variable bindings, data types, and Scheme's unique numeric tower with practical Docker-ready examples

Scheme is a dynamically typed language, meaning types are attached to values rather than to variable names. There are no type declarations — you bind a name to a value with define, and Scheme tracks what kind of value it is at runtime. This simplicity is intentional: Scheme’s design philosophy prizes orthogonality, and the type system is no exception.

As a Lisp dialect, Scheme treats “variables” as named bindings to values. The default is immutability — define creates a binding you can rebind with set!, but idiomatic Scheme code rarely uses mutation. Instead, you compose functions and pass values forward. Understanding Scheme’s types means understanding what kinds of values you can bind, pass, and return.

Scheme’s type system has one feature that surprises newcomers: its numeric tower. Unlike most languages that offer just integers and floats, Scheme supports exact integers of arbitrary size, exact rationals (like 1/3), inexact floats, and complex numbers — and the language tracks whether a number is exact or inexact as part of its type.

In this tutorial you will see how to bind values with define and let, explore Scheme’s core types, understand the numeric tower, and use type predicates and conversion functions.

Variable Bindings with define

In Scheme, define creates a top-level binding. The syntax is (define name value). Unlike most languages, there is no type annotation — the value carries its own type.

Create a file named variables.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
; Variables and Types in Scheme

;; Top-level bindings with define
(define language "Scheme")
(define year 1975)
(define pi 3.14159)
(define elegant? #t)
(define creator 'sussman)

(display "Language: ") (display language) (newline)
(display "Created: ") (display year) (newline)
(display "Pi: ") (display pi) (newline)
(display "Elegant? ") (display elegant?) (newline)
(display "Creator symbol: ") (display creator) (newline)

;; Characters - written with the #\ prefix
(define initial #\S)
(display "Initial: ") (display initial) (newline)

;; Lists - a fundamental Scheme data structure
(define langs '(scheme lisp racket))
(display "Languages: ") (display langs) (newline)

;; Local bindings with let
(let ((radius 5)
      (pi-approx 3.14159))
  (display "Circle area: ")
  (display (* pi-approx radius radius))
  (newline))

Several types appear here:

  • "Scheme" — a string, delimited by double quotes
  • 1975 — an exact integer
  • 3.14159 — an inexact real (floating point)
  • #t — the boolean true (#f is false)
  • 'sussman — a symbol, a lightweight interned name
  • #\S — a character, written with the #\ prefix
  • '(scheme lisp racket) — a list of symbols (the ' is shorthand for quote)

The let form at the end introduces local bindings scoped to its body. Both radius and pi-approx exist only inside that let expression and are invisible to code outside it.

Scheme’s Numeric Tower

Scheme’s numeric tower is one of its most distinctive features. Most languages draw a hard line between integers and floats — Scheme goes further by distinguishing exact numbers (whose value is mathematically precise) from inexact numbers (which are floating-point approximations).

Create a file named variables_numeric.scm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
; Scheme's Numeric Tower

;; Exact integers - arbitrary precision
(define big 1000000000000)
(display "Big integer: ") (display big) (newline)

;; Exact rationals - unique to the Lisp numeric tower
(define one-third 1/3)
(display "One third: ") (display one-third) (newline)
(display "1/3 + 1/6 = ") (display (+ 1/3 1/6)) (newline)

;; Inexact floats (IEEE 754)
(define approx-e 2.71828)
(display "Approx e: ") (display approx-e) (newline)

;; Exactness predicates
(display "1/3 exact? ") (display (exact? one-third)) (newline)
(display "2.71828 exact? ") (display (exact? approx-e)) (newline)

;; Convert exact to inexact when you need a decimal representation
(display "1/3 as float: ") (display (exact->inexact one-third)) (newline)

The key insight: 1/3 is not division — it is a literal exact rational number. Arithmetic on exact rationals stays exact. (+ 1/3 1/6) produces 1/2, not 0.4999.... When you genuinely need a floating-point approximation, exact->inexact converts it.

The predicate exact? returns #t for integers and rationals, #f for floats. This distinction propagates through arithmetic: mixing an exact and inexact value yields an inexact result.

Local Bindings and Type Predicates

Beyond define, Scheme provides three forms for local bindings:

  • let — binds names simultaneously; bindings cannot refer to each other
  • let* — binds names sequentially; each binding can use previous ones
  • letrec — for mutually recursive local definitions

Every Scheme type has a corresponding predicate — a function returning #t or #f — whose name ends in ?. These are the canonical way to test a value’s type.

Create a file named variables_predicates.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
; Local Bindings and Type Predicates

;; let - all bindings are independent
(let ((x 10)
      (y 20))
  (display "Sum: ") (display (+ x y)) (newline))

;; let* - each binding can reference earlier ones
(let* ((base 5)
       (squared (* base base))
       (cubed (* squared base)))
  (display "5^2 = ") (display squared) (newline)
  (display "5^3 = ") (display cubed) (newline))

;; Type predicates
(display (number? 42)) (newline)
(display (string? "hello")) (newline)
(display (symbol? 'foo)) (newline)
(display (boolean? #f)) (newline)
(display (char? #\a)) (newline)
(display (pair? '(1 2 3))) (newline)
(display (null? '())) (newline)
(display (procedure? display)) (newline)

;; Type conversions
(display (number->string 42)) (newline)
(display (number->string 255 16)) (newline)
(display (string->number "100")) (newline)
(display (string->number "ff" 16)) (newline)
(display (symbol->string 'lambda)) (newline)
(display (char->integer #\A)) (newline)
(display (integer->char 97)) (newline)

Note that pair? returns #t for any non-empty list — in Scheme, lists are built from pairs (cons cells), so every non-empty list is a pair. The empty list '() is its own type tested with null?.

string->number returns #f when the conversion fails — there is no exception thrown. This is idiomatic Scheme: failure is a value, not an error.

Running with Docker

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

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

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

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

Expected Output

Running variables.scm:

Language: Scheme
Created: 1975
Pi: 3.14159
Elegant? #t
Creator symbol: sussman
Initial: S
Languages: (scheme lisp racket)
Circle area: 78.53975

Running variables_numeric.scm:

Big integer: 1000000000000
One third: 1/3
1/3 + 1/6 = 1/2
Approx e: 2.71828
1/3 exact? #t
2.71828 exact? #f
1/3 as float: .3333333333333333

Running variables_predicates.scm:

Sum: 30
5^2 = 25
5^3 = 125
#t
#t
#t
#t
#t
#t
#t
#t
42
ff
100
255
lambda
65
a

Key Concepts

  • Bindings, not variablesdefine binds a name to a value; mutation via set! exists but is avoided in idiomatic Scheme in favor of passing new values through function calls
  • Dynamic, strong typing — types belong to values, not names; Scheme never silently coerces between unrelated types (unlike JavaScript’s ==)
  • Exact vs inexact numbers — Scheme’s numeric tower distinguishes mathematically exact values (integers, rationals) from floating-point approximations; exact? and inexact? test this, and exact->inexact/inexact->exact convert between them
  • Symbols are not strings'scheme and "scheme" are distinct types; symbols are interned identifiers used as lightweight data (keys, tags, enum-like values)
  • Characters have their own type#\A is not the string "A"; char->integer and integer->char convert between characters and their code points
  • Predicate naming convention — every type check in Scheme ends with ?: number?, string?, null?, procedure?; this is a language-wide convention, not just a library style
  • let vs define — use let to limit scope; use define for top-level or procedure-internal definitions; let* when bindings depend on each other
  • The empty list is a type'() (null) is its own distinct value, not false, not zero; null? is the only reliable way to test for it

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