Value Classes
A value class is an immutable object — like a record or struct. Once created, its slots cannot change. "Mutations" return a new object with the change applied.
Use Value subclass: for data that travels between methods without needing
concurrency or identity. Value subclass: enforces immutability at compile
time — direct slot assignment (self.x := ...) is a compile error. Use
Object subclass: when you want the same immutable style with snapshot
semantics but are comfortable with runtime rather than compile-time enforcement.
Examples: coordinates, money amounts, colours, dates, config records.
Defining a value class
Value subclass: Point
field: x = 0
field: y = 0
// Read-only access (auto-generated for each slot too, but you can override):
x => self.x
y => self.y
// A method that computes from slots:
distanceSquared => (self.x * self.x) + (self.y * self.y)
// Methods return new instances for "mutation":
translateBy: dx and: dy =>
Point x: (self.x + dx) y: (self.y + dy)
// String representation:
printString => "Point({self.x}, {self.y})"
Construction forms
Three ways to create a value object — all equivalent:
TestCase subclass: Ch10ValueClasses
testDefaultConstruction =>
// new — all slots get declared defaults
p := Point new
self assert: p x equals: 0
self assert: p y equals: 0
testMapConstruction =>
// new: with a map — missing keys keep defaults
p := Point new: #{#x => 3, #y => 4}
self assert: p x equals: 3
self assert: p y equals: 4
testKeywordConstruction =>
// Keyword constructor — preferred, auto-generated from slot names
p := Point x: 3 y: 4
self assert: p x equals: 3
self assert: p y equals: 4
testPartialConstruction =>
// Only provide some slots — rest use defaults
p := Point new: #{#x => 7}
self assert: p x equals: 7
self assert: p y equals: 0
Functional setters (with*:)
Each slot gets an auto-generated withSlotName: method.
It returns a NEW object with that one slot changed; the original is untouched.
testWithSetter =>
p := Point x: 1 y: 2
p2 := p withX: 10
self assert: p2 x equals: 10
self assert: p2 y equals: 2 // y preserved
self assert: p x equals: 1 // original unchanged
testChainedWithSetters =>
p := Point new
p2 := (p withX: 5) withY: 7
self assert: p2 x equals: 5
self assert: p2 y equals: 7
Immutability
testImmutableByDefault =>
// Direct slot mutation is a compile-time error inside Value subclass: methods.
// At runtime, fieldAt:put: raises immutable_value:
p := Point x: 1 y: 2
self
should: [p fieldAt: #x put: 99]
raise: #immutable_value
Value equality
Two value objects with the same class and slot values are equal (=:=).
testValueEquality =>
p1 := Point x: 3 y: 4
p2 := Point x: 3 y: 4
self assert: p1 =:= p2
testValueInequality =>
p1 := Point x: 3 y: 4
p2 := Point x: 9 y: 9
self deny: p1 =:= p2
testWithSetterEquality =>
p := Point x: 1 y: 2
self assert: (p withX: 10) =:= (Point x: 10 y: 2)
Using methods
testDistanceSquared =>
p := Point x: 3 y: 4
self assert: p distanceSquared equals: 25
testTranslate =>
p := Point x: 1 y: 2
p2 := p translateBy: 3 and: -1
self assert: p2 x equals: 4
self assert: p2 y equals: 1
self assert: p x equals: 1 // original unchanged
testPrintString =>
p := Point x: 3 y: 4
self assert: p printString equals: "Point(3, 4)"
Value objects in collections
testValueObjectsInArray =>
points := #[Point x: 1 y: 1, Point x: 2 y: 2, Point x: 3 y: 3]
xs := points collect: [:p | p x]
self assert: xs equals: #[1, 2, 3]
testSelectOnValueObjects =>
points := #[Point x: 1 y: 1, Point x: 2 y: 2, Point x: 3 y: 3]
big := points select: [:p | p x > 1]
self assert: big size equals: 2
testInjectOnValueObjects =>
points := #[Point x: 1 y: 0, Point x: 2 y: 0, Point x: 3 y: 0]
sumX := points inject: 0 into: [:acc :p | acc + p x]
self assert: sumX equals: 6
Reflection
testFieldAccess =>
p := Point x: 3 y: 4
self assert: (p fieldAt: #x) equals: 3
self assert: (p fieldAt: #y) equals: 4
testClass =>
p := Point x: 3 y: 4
self assert: p class equals: Point
testSuperclass =>
self assert: Point superclass equals: Value
A more real-world example: Money
Value subclass: Money
field: amount = 0
field: currency = #USD
add: other =>
other currency =:= self.currency
ifFalse: [^self error: "Currency mismatch"]
Money amount: (self.amount + other amount) currency: self.currency
printString => "{self.amount} {self.currency}"
TestCase subclass: Ch10MoneyExample
testMoneyAddition =>
a := Money amount: 10 currency: #USD
b := Money amount: 5 currency: #USD
c := a add: b
self assert: c amount equals: 15
self assert: c currency equals: #USD
testMoneyImmutable =>
a := Money amount: 10 currency: #USD
b := Money amount: 5 currency: #USD
a add: b
self assert: a amount equals: 10 // unchanged
testMoneyPrintString =>
m := Money amount: 42 currency: #EUR
self assert: m printString equals: "42 EUR"
Exercises
1. Functional update chain. Create a Point x: 0 y: 0 and use chained
withX: and withY: to produce Point(5, 10). Verify the original point
is still Point(0, 0).
Hint
p := Point x: 0 y: 0
p2 := (p withX: 5) withY: 10
// p2 is Point(5, 10), p is still Point(0, 0)
Each with*: method returns a new Point — the original is never modified.
2. Value equality. Create two Points with the same coordinates using
different construction forms (x:y: and new:). Are they =:=?
Hint
p1 := Point x: 3 y: 4
p2 := Point new: #{#x => 3, #y => 4}
p1 =:= p2 // => true
Value objects compare by slot values, not identity.
3. Currency mismatch. Create two Money values — one USD and one EUR — and
try to add them. What error do you get? How would you handle it with on:do:?
Hint
usd := Money amount: 10 currency: #USD
eur := Money amount: 5 currency: #EUR
[usd add: eur] on: Error do: [:e | e message]
// => "Currency mismatch"
The add: method checks that currencies match and raises an error if they don't.