10: Properties and Computed Values
Audience: All Time: 90 minutes Prerequisites: 07-Classes You'll learn: Getters, setters, computed properties, lazy initialization, encapsulation
The Big Picture
Properties let you control how fields are accessed and modified. Instead of letting code directly access person.age, you can: - Validate on assignment (no negative ages) - Calculate on access (compute age from birth year) - Cache computed values - Log access for debugging
Getters
Simple Getter
// file: 10_getter.zbr
// teaches: computed properties // chapter: 10-Properties-and-Computed-Values
class Person var birth_year as int = 2000
def age as int return 2024 - birth_year
def name_length as int var name = "Alice" return name.len
class Main shared def main var person = Person() person.birth_year = 1990 print person.age() // 34 print person.name_length() // 5
Derived Properties
// file: 10_derived.zbr
// teaches: deriving values from fields // chapter: 10-Properties-and-Computed-Values
class Rectangle var width as int = 0 var height as int = 0
def area as int return width * height
def perimeter as int return 2 * (width + height)
def is_square as bool return width == height
class Main shared def main var rect = Rectangle() rect.width = 10 rect.height = 10
print "Area: ${rect.area()}" // 100 print "Perimeter: ${rect.perimeter()}" // 40 print "Square: ${rect.is_square()}" // true
Setters (Validation)
Controlling Field Assignment
// file: 10_setter_validation.zbr
// teaches: setters with validation // chapter: 10-Properties-and-Computed-Values
class Account var balance as float = 0.0
def deposit(amount as float) as bool if amount <= 0.0 return false balance = balance + amount return true
def withdraw(amount as float) as bool if amount <= 0.0 return false if amount > balance return false balance = balance - amount return true
def get_balance as float return balance
class Main shared def main var account = Account() account.deposit(100.0) print account.get_balance() // 100
account.withdraw(25.0) print account.get_balance() // 75
account.withdraw(100.0) // Fails (not enough balance) print account.get_balance() // 75 (unchanged)
Setter with Side Effects
// file: 10_setter_effects.zbr
// teaches: setters with side effects // chapter: 10-Properties-and-Computed-Values
class User var username as str = "" var email as str = "" var last_modified as str = ""
def set_username(new_name as str) as bool if new_name.len < 3 return false username = new_name last_modified = "2024-01-01" // Update timestamp return true
def set_email(new_email as str) as bool if not new_email.contains("@") return false email = new_email last_modified = "2024-01-01" // Update timestamp return true
class Main shared def main var user = User() if user.set_username("alice") print "Username set"
if user.set_email("alice@example.com") print "Email set" print "Last modified: ${user.last_modified}"
Computed Properties
// file: 10_computed.zbr
// teaches: expensive computed properties // chapter: 10-Properties-and-Computed-Values
class DataSet var numbers as List(int) = List()
def sum as int var total = 0 for num in numbers total = total + num return total
def average as float if numbers.count() == 0 return 0.0 return sum / numbers.count()
def min_value as int var min = numbers.at(0) for num in numbers if num < min min = num return min
def max_value as int var max = numbers.at(0) for num in numbers if num > max max = num return max
class Main shared def main var data = DataSet() data.numbers.add(10) data.numbers.add(20) data.numbers.add(30) data.numbers.add(40)
print "Sum: ${data.sum()}" // 100 print "Average: ${data.average()}" // 25 print "Min: ${data.min_value()}" // 10 print "Max: ${data.max_value()}" // 40
Lazy Initialization
// file: 10_lazy_init.zbr
// teaches: lazy initialization // chapter: 10-Properties-and-Computed-Values
class Database var connection as str? = nil var is_connected as bool = false
def get_connection as str if connection == nil # Expensive operation: only when needed connection = "Connected to DB" is_connected = true return connection
class Main shared def main var db = Database()
# Connection not created yet print "Is connected: ${db.is_connected()}" // false
# Access connection (now it's created) var conn = db.get_connection() print conn // Connected to DB
# Already exists var conn2 = db.get_connection() print conn2 // Connected to DB
Real World: Temperature Converter
// file: 10_temperature.zbr
// teaches: properties in realistic scenarios // chapter: 10-Properties-and-Computed-Values
class Temperature var celsius as float = 0.0
def fahrenheit as float return celsius * 9.0 / 5.0 + 32.0
def kelvin as float return celsius + 273.15
def set_from_fahrenheit(f as float) celsius = (f - 32.0) * 5.0 / 9.0
def set_from_kelvin(k as float) celsius = k - 273.15
def is_freezing as bool return celsius <= 0.0
def is_boiling as bool return celsius >= 100.0
class Main shared def main var temp = Temperature() temp.celsius = 25.0
print "Celsius: ${temp.celsius}" print "Fahrenheit: ${temp.fahrenheit()}" print "Kelvin: ${temp.kelvin()}" print "Freezing: ${temp.is_freezing()}" print "Boiling: ${temp.is_boiling()}"
temp.set_from_fahrenheit(98.6) print "Body temp in Celsius: ${temp.celsius}"
Real World: Configuration
// file: 10_config.zbr
// teaches: configuration management // chapter: 10-Properties-and-Computed-Values
class Config var host as str = "localhost" var port as int = 8080 var debug as bool = false
def set_host(h as str) as bool if h.len == 0 return false host = h return true
def set_port(p as int) as bool if p < 1 or p > 65535 return false port = p return true
def get_url as str return "http://${host}:${port}"
def set_debug(d as bool) debug = d
class Main shared def main var config = Config() config.set_host("example.com") config.set_port(443) config.set_debug(true)
print config.get_url() // http://example.com:443 print "Debug: ${config.debug}" // true
Common Patterns
Read-Only Properties
class BankAccount
var balance as float = 0.0
def get_balance as float return balance // Can read
def deposit(amount as float) balance = balance + amount // Can modify only via methods
# No setter: direct assignment not allowed
Write-Only Properties
class Password
var encrypted_password as str = ""
def set_password(plain as str) # Encrypt and store encrypted_password = "encrypted:" + plain
# No getter: can't read it back
If you're new to programming
> A getter is a method that retrieves a value (often computing it on the fly). > > A setter is a method that stores a value (often with validation). > > Computed properties are values you calculate rather than store. Like age = current_year - birth_year. > > Lazy initialization means "don't create something expensive until someone actually asks for it."
Common Mistakes
> ❌ Mistake: Exposing internal fields directly > >
> class User > var age as int = 0 > var user = User() > user.age = -5 # ❌ No validation! > > > ✅ Better: > > class User > var age as int = 0 > def set_age(new_age as int) as bool > if new_age < 0 > return false > age = new_age > return true >
> ❌ Mistake: Computing expensive properties every time > >
> class DataSet > var numbers as List(int) = List() > > def sum as int # Recalculates every call > var total = 0 > for num in numbers > total = total + num > return total # O(n) every time! > > > ✅ Better (if called often): > > class DataSet > var numbers as List(int) = List() > var cached_sum as int? = nil > > def sum as int > if cached_sum == nil > var total = 0 > for num in numbers > total = total + num > cached_sum = total > return cached_sum > > def add_number(num as int) > numbers.add(num) > cached_sum = nil # Invalidate cache >
> ❌ Mistake: Side effects in getters > >
> class Logger > var count as int = 0 > > def get_count as int > count = count + 1 # ❌ Has side effect! > return count > > > ✅ Better: > > class Logger > var count as int = 0 > > def get_count as int > return count # ✅ Pure getter, no side effects > > def log_access > count = count + 1 # Explicit method for side effects >
Exercises
Exercise 1: Bank Account Properties
Create a bank account with validated setters:
Solution
class BankAccount
var owner as str = "" var balance as float = 0.0 var interest_rate as float = 0.02
def set_owner(name as str) as bool if name.len < 2 return false owner = name return true
def deposit(amount as float) as bool if amount <= 0.0 return false balance = balance + amount return true
def withdraw(amount as float) as bool if amount > balance return false balance = balance - amount return true
def apply_interest var interest = balance * interest_rate balance = balance + interest
def get_balance as float return balance
class Main shared def main var account = BankAccount() account.set_owner("Alice") account.deposit(1000.0) account.apply_interest() print "Balance: ${account.get_balance()}"
Exercise 2: Circle Properties
Create a circle class with radius and diameter properties:
Solution
class Circle
var radius as float = 0.0
def set_radius(r as float) as bool if r <= 0.0 return false radius = r return true
def diameter as float return radius * 2.0
def set_diameter(d as float) as bool if d <= 0.0 return false radius = d / 2.0 return true
def area as float return 3.14159 radius radius
def circumference as float return 2.0 3.14159 radius
class Main shared def main var circle = Circle() circle.set_radius(5.0)
print "Radius: ${circle.radius}" print "Diameter: ${circle.diameter()}" print "Area: ${circle.area()}" print "Circumference: ${circle.circumference()}"
circle.set_diameter(20.0) print "New radius: ${circle.radius}"
Exercise 3: User Profile with Validation
Create a user profile with validated properties:
Solution
class UserProfile
var username as str = "" var email as str = "" var age as int = 0
def set_username(name as str) as bool if name.len < 3 or name.len > 20 return false username = name return true
def set_email(addr as str) as bool if not addr.contains("@") or not addr.contains(".") return false email = addr return true
def set_age(a as int) as bool if a < 13 or a > 120 return false age = a return true
def is_adult as bool return age >= 18
def is_valid as bool return username.len > 0 and email.contains("@") and age > 0
class Main shared def main var user = UserProfile() if user.set_username("alice_wonder") print "Username set" if user.set_email("alice@example.com") print "Email set" if user.set_age(25) print "Age set"
print "Is adult: ${user.is_adult()}" print "Is valid: ${user.is_valid()}"
Next Steps
- → 11-Nil-Tracking — Advanced safety with properties - → 14-Contracts — Enforce property invariants - 🏋️ Project-1-CLI-Tool — Use configuration properties
Key Takeaways
- Getters compute values from fields - Setters validate before storing - Computed properties derive from other data - Lazy initialization defers expensive work - Control access to prevent invalid states - Encapsulation protects class invariants
Next: Head to Part 3 and 11-Nil-Tracking for Zebra's safety features.