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:
| |
Several types appear here:
"Scheme"— a string, delimited by double quotes1975— an exact integer3.14159— an inexact real (floating point)#t— the boolean true (#fis 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 forquote)
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:
| |
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 otherlet*— binds names sequentially; each binding can use previous onesletrec— 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:
| |
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
| |
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 variables —
definebinds a name to a value; mutation viaset!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?andinexact?test this, andexact->inexact/inexact->exactconvert between them - Symbols are not strings —
'schemeand"scheme"are distinct types; symbols are interned identifiers used as lightweight data (keys, tags, enum-like values) - Characters have their own type —
#\Ais not the string"A";char->integerandinteger->charconvert 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
letto limit scope; usedefinefor 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
Comments
Loading comments...
Leave a Comment