Beginner

Variables and Types in Dylan

Learn about variables, data types, type declarations, and constants in Dylan with practical Docker-ready examples

Dylan is a dynamically typed language with a strong type system — values carry their types at runtime, and the language prevents unsafe coercions between incompatible types. What makes Dylan’s approach distinctive is that type declarations are optional annotations, not requirements. You can write fully dynamic code with no type information at all, or add type constraints where you want documentation, safety, or performance optimization.

Dylan’s type system reflects its dual heritage from Common Lisp and ALGOL. Type names are surrounded by angle brackets — <integer>, <string>, <float> — a convention borrowed from CLOS (Common Lisp Object System). Every value is an object, and every type is a class in Dylan’s object hierarchy rooted at <object>. This uniformity means integers, strings, functions, and classes themselves are all first-class objects.

In this tutorial you will explore variable declaration with let and define variable, Dylan’s core types (integers, floats, strings, booleans, characters, and symbols), type annotations with the :: syntax, constants, and type checking at runtime.


Local Variables with let

Local variables in Dylan are introduced with let. A let binding is lexically scoped — it exists only within the body where it is defined. Dylan uses let for immutable local bindings (the default) and define variable for module-level mutable variables.

Create a file named variables.dylan:

 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
Module: hello

// Local variables with let (immutable within their scope)
let greeting = "Hello from Dylan";
let year = 1992;
let pi-approx = 3.14159;

format-out("greeting = %s\n", greeting);
format-out("year = %d\n", year);
format-out("pi ≈ %=\n", pi-approx);

// Type-annotated variables using :: syntax
let language :: <string> = "Dylan";
let version :: <integer> = 2025;
let ratio :: <float> = 1.618;

format-out("\nlanguage = %s\n", language);
format-out("version = %d\n", version);
format-out("ratio = %=\n", ratio);

// Multiple value binding with let
let (quotient, remainder) = truncate/(17, 5);
format-out("\n17 / 5 = %d remainder %d\n", quotient, remainder);

// Nested scope
let outer = "outer value";
begin
  let inner = "inner value";
  format-out("\n%s and %s\n", outer, inner);
end;
format-out("outer is still: %s\n", outer);

Running with Docker

1
2
3
4
5
# Pull the Dylan Docker image
docker pull codearchaeology/dylan:latest

# Run the variables example
docker run --rm -v $(pwd):/app codearchaeology/dylan:latest

Expected Output

greeting = Hello from Dylan
year = 1992
pi ≈ 3.14159

language = Dylan
version = 2025
ratio = 1.618

17 / 5 = 3 remainder 2

outer value and inner value
outer is still: outer value

Module-Level Variables and Constants

Dylan distinguishes between local bindings (let) and module-level definitions. define variable creates a mutable module-level variable, while define constant creates an immutable binding. Module-level definitions are visible throughout the module.

Create a file named variables_module.dylan:

 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
Module: hello

// Module-level mutable variables
define variable *counter* :: <integer> = 0;
define variable *app-name* :: <string> = "CodeArchaeology";

// Module-level constants (cannot be reassigned)
define constant $pi :: <float> = 3.14159265;
define constant $max-retries :: <integer> = 5;
define constant $app-version = "1.0.0";

format-out("App: %s v%s\n", *app-name*, $app-version);
format-out("Pi = %=\n", $pi);
format-out("Max retries = %d\n", $max-retries);

// Mutable variables can be reassigned with :=
format-out("\nCounter starts at: %d\n", *counter*);
*counter* := *counter* + 1;
format-out("After increment: %d\n", *counter*);
*counter* := *counter* + 1;
format-out("After another increment: %d\n", *counter*);

// Naming conventions:
// *earmuffs* for mutable module variables (borrowed from Lisp)
// $dollar-prefix for constants
// kebab-case for everything (Dylan convention)

define variable *greeting* :: <string> = "Hello";
define constant $separator = ", ";

format-out("\n%s%s%s!\n", *greeting*, $separator, "World");
*greeting* := "Greetings";
format-out("%s%s%s!\n", *greeting*, $separator, "Dylanista");

Running with Docker

1
docker run --rm -v $(pwd):/app codearchaeology/dylan:latest

Expected Output

App: CodeArchaeology v1.0.0
Pi = 3.14159265
Max retries = 5

Counter starts at: 0
After increment: 1
After another increment: 2

Hello, World!
Greetings, Dylanista!

Core Types and Type Checking

Dylan’s type hierarchy is rooted at <object>. All built-in types use the angle-bracket naming convention. The language provides runtime type checking through instance?, which tests whether a value belongs to a given type.

Create a file named variables_types.dylan:

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
Module: hello

// Integers: arbitrary precision
let small :: <integer> = 42;
let negative :: <integer> = -17;
let big = 1000000 * 1000000;

format-out("=== Integers ===\n");
format-out("small = %d\n", small);
format-out("negative = %d\n", negative);
format-out("big = %d\n", big);

// Floating point numbers
let pi :: <float> = 3.14159;
let tiny :: <float> = 0.001;
let sci = 6.022e23;

format-out("\n=== Floats ===\n");
format-out("pi = %=\n", pi);
format-out("tiny = %=\n", tiny);
format-out("scientific = %=\n", sci);

// Strings
let name :: <string> = "Dylan";
let empty :: <string> = "";
let message = concatenate("Hello, ", name, "!");

format-out("\n=== Strings ===\n");
format-out("name = %s\n", name);
format-out("empty = \"%s\"\n", empty);
format-out("message = %s\n", message);
format-out("length of name = %d\n", name.size);

// Characters
let ch :: <character> = 'A';
let space :: <character> = ' ';

format-out("\n=== Characters ===\n");
format-out("ch = %c\n", ch);
format-out("char code of 'A' = %d\n", as(<integer>, ch));

// Booleans: #t and #f
let yes :: <boolean> = #t;
let no :: <boolean> = #f;

format-out("\n=== Booleans ===\n");
format-out("yes = %=\n", yes);
format-out("no = %=\n", no);

// Symbols: interned names, written with #"name" syntax
let status = #"active";
let color = #"red";

format-out("\n=== Symbols ===\n");
format-out("status = %=\n", status);
format-out("color = %=\n", color);
format-out("same symbol? %=\n", status == #"active");

// Type checking with instance?
format-out("\n=== Type Checking ===\n");
format-out("42 is <integer>? %=\n", instance?(42, <integer>));
format-out("3.14 is <float>? %=\n", instance?(3.14, <float>));
format-out("\"hi\" is <string>? %=\n", instance?("hi", <string>));
format-out("'A' is <character>? %=\n", instance?('A', <character>));
format-out("#t is <boolean>? %=\n", instance?(#t, <boolean>));
format-out("42 is <number>? %=\n", instance?(42, <number>));
format-out("42 is <object>? %=\n", instance?(42, <object>));

Running with Docker

1
docker run --rm -v $(pwd):/app codearchaeology/dylan:latest

Expected Output

=== Integers ===
small = 42
negative = -17
big = 1000000000000

=== Floats ===
pi = 3.14159
tiny = 0.001
scientific = 6.022e+23

=== Strings ===
name = Dylan
empty = ""
message = Hello, Dylan!
length of name = 5

=== Characters ===
ch = A
char code of 'A' = 65

=== Booleans ===
yes = #t
no = #f

=== Symbols ===
status = #"active"
color = #"red"
same symbol? #t

=== Type Checking ===
42 is <integer>? #t
3.14 is <float>? #t
"hi" is <string>? #t
'A' is <character>? #t
#t is <boolean>? #t
42 is <number>? #t
42 is <object>? #t

Type Conversion

Dylan uses the generic function as for type conversions. The pattern is as(<target-type>, value), which returns the value converted to the target type when a valid conversion exists.

Create a file named variables_conversion.dylan:

 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
Module: hello

// Integer / float conversions
let n :: <integer> = 42;
let f :: <float> = 3.7;

format-out("=== Number Conversions ===\n");
format-out("42 as float: %=\n", as(<float>, n));
format-out("3.7 truncated: %d\n", truncate(f));
format-out("3.7 floored: %d\n", floor(f));
format-out("3.7 ceiling: %d\n", ceiling(f));
format-out("3.7 rounded: %d\n", round(f));

// String / number conversions
format-out("\n=== String Conversions ===\n");
let num-str = "42";
let parsed = string-to-integer(num-str);
format-out("\"%s\" as integer: %d\n", num-str, parsed);
format-out("42 as string: \"%s\"\n", integer-to-string(42));
format-out("255 in hex: \"%s\"\n", integer-to-string(255, base: 16));
format-out("8 in binary: \"%s\"\n", integer-to-string(8, base: 2));

// Character conversions
format-out("\n=== Character Conversions ===\n");
let ch :: <character> = 'A';
format-out("'A' as integer: %d\n", as(<integer>, ch));
format-out("65 as character: %c\n", as(<character>, 65));

// Uppercase / lowercase
let lower = 'a';
format-out("'a' uppercased: %c\n", as-uppercase(lower));
format-out("'A' lowercased: %c\n", as-lowercase(ch));

// String case conversion
let text = "Hello Dylan";
format-out("\n=== String Case ===\n");
format-out("uppercase: %s\n", as-uppercase(text));
format-out("lowercase: %s\n", as-lowercase(text));

Running with Docker

1
docker run --rm -v $(pwd):/app codearchaeology/dylan:latest

Expected Output

=== Number Conversions ===
42 as float: 42.0
3.7 truncated: 3
3.7 floored: 3
3.7 ceiling: 4
3.7 rounded: 4

=== String Conversions ===
"42" as integer: 42
42 as string: "42"
255 in hex: "FF"
8 in binary: "1000"

=== Character Conversions ===
'A' as integer: 65
65 as character: A

'a' uppercased: A
'A' lowercased: a

=== String Case ===
uppercase: HELLO DYLAN
lowercase: hello dylan

Key Concepts

  • Dynamic typing with optional annotations — Variables don’t require type declarations, but adding :: <type> provides documentation, catches errors earlier, and enables compiler optimization
  • let for locals, define variable for module scopelet creates lexically scoped immutable bindings; define variable creates mutable module-level variables that can be reassigned with :=
  • define constant for immutable bindings — Constants use the $prefix naming convention and cannot be reassigned after initialization
  • Angle-bracket type names — All types use <angle-brackets>: <integer>, <string>, <float>, <boolean>, <character>, <object> — inherited from CLOS
  • Everything is an <object> — Dylan’s type hierarchy is rooted at <object>; every value (including numbers and booleans) is a first-class object
  • instance? for runtime type checking — Test whether a value belongs to a type with instance?(value, <type>), returning #t or #f
  • as for type conversion — The generic function as(<target-type>, value) handles all standard type conversions; invalid conversions signal an error at runtime
  • Naming conventions matter*earmuffs* for mutable module variables, $dollar for constants, and kebab-case for all identifiers — these conventions are strongly followed in the Dylan community

Running Today

All examples can be run using Docker:

docker pull codearchaeology/dylan:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining