SRFI-9: Record Types
Table of Contents
-
- 0.1. Tutorial: Comparing Record Types in Guile, Clojure, Python, and TypeScript
- 0.2. Related Languages and Record Types
- 0.3. SRFI-9 in 2026: Implementation Status
- 1. Related
0.1. Tutorial: Comparing Record Types in Guile, Clojure, Python, and TypeScript
0.1.1. Objective:
0.1.2. Contents:
- Introduction to Record Types
- Guile SRFI-9 Records
- Clojure
defrecord - Python Data Classes
- TypeScript Classes
- Comparison and Summary
0.1.3. Part 1: Introduction to Record Types
Duration: 5 minutes
0.1.4. Part 2: Guile SRFI-9 Records
Duration: 20 minutes
0.1.4.1. Example:
(use-modules (srfi srfi-9))
(define-record-type <employee>
(make-employee name age salary)
employee?
(name employee-name)
(age employee-age set-employee-age!)
(salary employee-salary set-employee-salary!))
(define fred (make-employee "Fred" 45 20000.00))
(employee? fred) ;; #t
(employee-age fred) ;; 45
(set-employee-salary! fred 25000.00) ;; pay rise
0.1.5. Part 3: Clojure defrecord
Duration: 20 minutes
0.1.5.1. Example:
(defrecord Employee [name age salary])
(def fred (->Employee "Fred" 45 20000.00))
(:age fred) ;; 45
(assoc fred :salary 25000.00) ;; pay rise, creates a new record with updated salary
- Explanation:
defrecorddefines a new record typeEmployee.->Employeeis the constructor.:ageis the accessor.associs used to create a new record with updated fields (immutable update).
0.1.6. Part 4: Python Data Classes
Duration: 20 minutes
0.1.6.1. Example:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
age: int
salary: float
fred = Employee("Fred", 45, 20000.00)
print(fred.age) # 45
fred.salary = 25000.00 # pay rise
print(fred)
- Explanation:
@dataclassis used to define a data class.- Fields are defined with type annotations.
- Fields can be accessed and modified directly.
0.1.7. Part 5: TypeScript Classes
Duration: 20 minutes
0.1.7.1. Example:
class Employee {
constructor(public name: string, public age: number, public salary: number) {}
setSalary(newSalary: number) {
this.salary = newSalary;
}
}
let fred = new Employee("Fred", 45, 20000.00);
console.log(fred.age); // 45
fred.setSalary(25000.00); // pay rise
- Explanation:
classdefines a class with a constructor.- Fields are defined in the constructor.
- Methods can be added for field modifications.
0.1.8. Part 6: Comparison and Summary
Duration: 25 minutes
0.1.8.1. Syntax Comparison
Guile (SRFI-9):
(define-record-type <employee> ...)Clojure (
defrecord):(defrecord Employee [name age salary])Python (data classes):
@dataclass class Employee:TypeScript (classes):
class Employee { constructor(public name: string, public age: number, public salary: number) {} }
0.1.8.2. Field Access and Modification
Guile:
(employee-age fred) (set-employee-salary! fred 25000.00)Clojure:
(:age fred) (assoc fred :salary 25000.00)Python:
fred.age fred.salary = 25000.00TypeScript:
fred.age fred.setSalary(25000.00)
0.1.8.3. Immutability
- Guile: Mutable by default, can be made immutable with
define-immutable-record-type. - Clojure: Immutable by default, use
assocfor updates. - Python: Mutable by default, use
frozen=Truein@dataclassfor immutability. - TypeScript: Mutable by default, use techniques like
Readonlyfor immutability.
0.1.9. Summary
- Guile (SRFI-9): Strong syntax for functional programming, requires explicit accessors and modifiers.
- Clojure (
defrecord): Emphasizes immutability and functional updates. - Python (data classes): Simple and flexible, easy to use with direct field access.
- TypeScript (classes): Combines object-oriented and functional programming features, supports methods for field manipulation.
0.1.10. Conclusion
0.2. Related Languages and Record Types
0.2.1. Rust Structs
Duration: 20 minutes
0.2.1.1. Example:
struct Employee {
name: String,
age: u32,
salary: f64,
}
fn main() {
let mut fred = Employee {
name: String::from("Fred"),
age: 45,
salary: 20000.00,
};
println!("{}", fred.age); // 45
fred.salary = 25000.00; // pay rise
}
- Explanation:
structdefines a new struct type.- Fields are defined with types.
- Fields can be accessed and modified directly if mutable.
0.2.2. Haskell Data Types
Duration: 20 minutes
0.2.2.1. Example:
data Employee = Employee {
name :: String,
age :: Int,
salary :: Float
} deriving (Show)
let fred = Employee "Fred" 45 20000.00
age fred -- 45
fred {salary = 25000} -- pay rise, creates a new record with updated salary
- Explanation:
datakeyword defines a new record type.- Fields are defined with types.
- Records are immutable, creating a new record for updates.
0.2.3. Swift Structs
Duration: 20 minutes
0.2.3.1. Example:
struct Employee {
var name: String
var age: Int
var salary: Double
}
var fred = Employee(name: "Fred", age: 45, salary: 20000.00)
print(fred.age) // 45
fred.salary = 25000.00 // pay rise
- Explanation:
structdefines a new struct type.- Fields are defined with types.
- Fields can be accessed and modified directly.
0.2.4. Elm Records
Duration: 20 minutes
0.2.4.1. Example:
type alias Employee =
{ name : String
, age : Int
, salary : Float
}
fred : Employee
fred =
{ name = "Fred"
, age = 45
, salary = 20000.00
}
fred.age -- 45
{ fred | salary = 25000 } -- pay rise, creates a new record with updated salary
- Explanation:
type aliasdefines a new record type.- Fields are defined with types.
- Records are immutable, creating a new record for updates.
0.2.5. JavaScript (ES6 Classes)
Duration: 20 minutes
0.2.5.1. Example:
class Employee {
constructor(name, age, salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
setSalary(newSalary) {
this.salary = newSalary;
}
}
let fred = new Employee("Fred", 45, 20000.00);
console.log(fred.age); // 45
fred.setSalary(25000.00); // pay rise
console.log(fred);
- Explanation:
classdefines a new class type.- Fields are defined in the constructor.
- Methods can be added for field modifications.
- Person
class Person { constructor(name, age) { this.name = name; this.age = age; } } class Employee extends Person { constructor(name, age, salary) { super(name, age); // Call parent constructor this.salary = salary; } } let bob = new Employee("Bob", 30, 50000); console.log(bob.name); // "Bob" (Inherited from Person) console.log(bob.salary); // 50000 - Point
class Point { constructor(x, y) { Object.defineProperty(this, 'x', { value: x, writable: false }); Object.defineProperty(this, 'y', { value: y, writable: false }); } } let p1 = new Point(3, 4); // p1.x = 5; // Throws an error, as 'x' is not writable console.log(p1); - Color
class Color { constructor(red, green, blue) { this.red = red; this.green = green; this.blue = blue; } equals(otherColor) { return ( this.red === otherColor.red && this.green === otherColor.green && this.blue === otherColor.blue ); } } let c1 = new Color(255, 0, 0); // Red let c2 = new Color(255, 0, 0); // Red let c3 = new Color(0, 255, 0); // Green console.log(c1.equals(c2)); // true console.log(c1.equals(c3)); // false
0.2.6. Comparison and Summary
0.2.6.1. Syntax Comparison
Rust:
struct Employee { name: String, age: u32, salary: f64, }Haskell:
data Employee = Employee { name :: String, age :: Int, salary :: Float }Swift:
struct Employee { var name: String var age: Int var salary: Double }Elm:
type alias Employee = { name : String, age : Int, salary : Float }JavaScript (ES6):
class Employee { constructor(name, age, salary) { this.name = name; this.age = age; this.salary = salary; } }
0.2.6.2. Field Access and Modification
Rust:
fred.age fred.salary = 25000.00Haskell:
age fred fred {salary = 25000}Swift:
fred.age fred.salary = 25000.00Elm:
fred.age { fred | salary = 25000 }JavaScript (ES6):
fred.age fred.setSalary(25000.00)
0.2.6.3. Immutability
- Rust: Mutable by default, but can enforce immutability.
- Haskell: Immutable by default.
- Swift: Mutable by default, use
letfor immutability. - Elm: Immutable by default.
- JavaScript: Mutable by default, use libraries like Immutable.js for immutability.
0.2.7. Summary
- Rust Structs: Provide a balance between performance and safety with strong typing and mutability control.
- Haskell Data Types: Emphasize immutability and functional programming.
- Swift Structs: Flexible and easy to use with both mutability and immutability options.
- Elm Records: Strongly typed and immutable by default, suited for functional programming.
- JavaScript (ES6 Classes): Object-oriented approach with flexible mutability, widely used in web development.
0.3. SRFI-9 in 2026: Implementation Status
SRFI-9 ("Defining Record Types") was finalized in 1999 and is one of the most universally adopted SRFIs. It is included verbatim in R7RS-small (Section 5.5), making it a de facto baseline for portable Scheme code.
0.3.1. Native Support Matrix (April 2026)
Two rows named the wrong mechanism, checked by running each claim locally. Racket CS: plain =(require racket/base)= does not expose =define-record-type= at all (confirmed: "unbound identifier"); =racket/base= only provides =struct=, and the R7RS record form requires =#lang r7rs= with =(import (scheme base))= instead. Chez Scheme: stock/Petite Chez 10.3.0 has no =(scheme base)= library ("library (scheme base) not found") -- R7RS record support is not built in without the separate =chez-srfi=/R7RS-library tooling, and Chez's own native =define-record-type= uses R6RS clause syntax, which rejects the SRFI-9-style constructor spec used elsewhere in this note.
| Implementation | SRFI-9 Status | Notes |
|---|---|---|
| Guile 3.0.x | Native (built-in) | (use-modules (srfi srfi-9)) or R7RS import |
| Chez Scheme | Partial | Native define-record-type is R6RS-syntax, not SRFI-9; R7RS (scheme base) needs separate library support |
| Racket CS | Via library | #lang r7rs + (import (scheme base)); plain racket/base has struct, not define-record-type |
| Chibi-Scheme | Native (built-in) | Full R7RS-small; define-record-type in core |
| Chicken 5 | Native (built-in) | Included in (chicken base) |
| Gambit | Native (built-in) | R7RS mode via #!r7rs declaration |
| Kawa (JVM) | Native (built-in) | Maps to Java class generation |
| MIT Scheme | Via SRFI loader | (load-option 'SRFI-9) |
| Bigloo | Partial | (define-struct ...) is the idiomatic alternative |
0.3.2. Key Points
- Portability: Code using
define-record-typeper SRFI-9 / R7RS-small is portable across all major actively-maintained Scheme implementations as of 2026. - Extensions: Guile adds
define-immutable-record-typebeyond the SRFI-9 spec. Chicken provides(chicken record-types)with additional introspection. These are implementation-specific extensions and not portable. - SRFI-131 (ERR5RS Records): Provides a compatible extension adding inheritance. Adopted by Chibi and Kawa; Guile supports it via a compatibility module.
- R7RS-large: The Red Edition includes SRFI-9 as its record-type primitive. Future ballots may add inheritance via SRFI-131 or a derivative.
0.3.3. Minimal Portable Example (R7RS-small)
Ran this exact code, unmodified, in Guile 2.2.7 with no preceding import: it fails with "Unbound variable: define-record-type". The "Works in: Guile" claim only holds once =(use-modules (srfi srfi-9))= (or an R7RS =(import (scheme base))=) precedes the form, as in the Part 2 example above -- the code as literally written here is not a complete, runnable program in Guile.
;; Portable R7RS-small syntax, but needs an import to run:
;; (use-modules (srfi srfi-9)) in Guile, (import (scheme base)) under R7RS
(define-record-type <point>
(make-point x y)
point?
(x point-x)
(y point-y))
(define origin (make-point 0 0))
(define p1 (make-point 3 4))
(point? origin) ;; => #t
(point-x p1) ;; => 3
(point-y p1) ;; => 4
;; Euclidean distance (no mutation needed -- purely functional)
(define (point-distance a b)
(let ((dx (- (point-x b) (point-x a)))
(dy (- (point-y b) (point-y a))))
(sqrt (+ (* dx dx) (* dy dy)))))
(point-distance origin p1) ;; => 5.0
1. Related
- Scheme: Notes and Examples – the language the SRFI is for
- Typed Languages (Summer 2016) – the typed languages compared here
- Effective TypeScript: Strategies and Patterns – TypeScript's structural records