Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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, in the sense this chapter uses the word, let you control how a value is read or changed instead of letting code touch a field directly. You can:

  • Validate on assignment (no negative ages)
  • Calculate on access (compute age from birth year)
  • Cache computed values
  • Log access for debugging

Zebra has no dedicated property syntax. There is no prop, get, or set keyword, and no paren-less "computed field" declaration — a def that looks like a field access (def age: int, no parentheses) is a parse error. Every pattern in this chapter is an ordinary method: declared with () (def age(): int) and called with () (person.age()). "Property" here describes the pattern — a method that behaves like a field from the caller's point of view — not a language feature. A plain var field, by contrast, is read and assigned with no parentheses at all (person.age, person.age = 30) — see the "Read-only and write-only" section below for how that interacts with the method-based patterns here — see "Read-Only Properties" and "Write-Only Properties" under Common Patterns.


Getters

Simple Getter

# file: 10_getter.zbr
# teaches: computed properties
# chapter: 10-Properties-and-Computed-Values

class Person
    var birth_year: int = 2000
    
    def age(): int
        return 2024 - birth_year
    
    def name_length(): int
        var name = "Alice"
        return name.len

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: int = 0
    var height: int = 0
    
    def area(): int
        return width * height
    
    def perimeter(): int
        return 2 * (width + height)
    
    def is_square(): bool
        return width == height

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: float = 0.0
    
    def deposit(amount: float): bool
        if amount <= 0.0
            return false
        balance = balance + amount
        return true
    
    def withdraw(amount: float): bool
        if amount <= 0.0
            return false
        if amount > balance
            return false
        balance = balance - amount
        return true
    
    def get_balance(): float
        return balance

def main()
    var account = Account()
    account.deposit(100.0)
    print(account.get_balance())  # 100.0

    account.withdraw(25.0)
    print(account.get_balance())  # 75.0

    account.withdraw(100.0)  # Fails (not enough balance)
    print(account.get_balance())  # 75.0 (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: str = ""
    var email: str = ""
    var last_modified: str = ""
    
    def set_username(new_name: str): 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: str): bool
        if not new_email.contains("@")
            return false
        email = new_email
        last_modified = "2024-01-01"  # Update timestamp
        return true

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: List(int) = List()
    
    def sum(): int
        var total = 0
        for num in numbers
            total = total + num
        return total
    
    def average(): float
        if numbers.count() == 0
            return 0.0
        return sum() / numbers.count()
    
    def min_value(): int
        var min = numbers.at(0)
        for num in numbers
            if num < min
                min = num
        return min
    
    def max_value(): int
        var max = numbers.at(0)
        for num in numbers
            if num > max
                max = num
        return max

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: str?   = nil
    var is_connected: bool = false
    
    def get_connection(): str
        if connection == nil
            # Expensive operation: only when needed
            connection = "Connected to DB"
            is_connected = true
        return connection!  # narrowed to str? above; unwrap for the str return type

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 — no second connection attempt
    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: float = 0.0
    
    def fahrenheit(): float
        return celsius * 9.0 / 5.0 + 32.0
    
    def kelvin(): float
        return celsius + 273.15
    
    def set_from_fahrenheit(f: float)
        celsius = (f - 32.0) * 5.0 / 9.0
    
    def set_from_kelvin(k: float)
        celsius = k - 273.15
    
    def is_freezing(): bool
        return celsius <= 0.0
    
    def is_boiling(): bool
        return celsius >= 100.0

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: str = "localhost"
    var port: int = 8080
    var debug: bool = false
    
    def set_host(h: str): bool
        if h.len == 0
            return false
        host = h
        return true
    
    def set_port(p: int): bool
        if p < 1 or p > 65535
            return false
        port = p
        return true
    
    def get_url(): str
        return "http://${host}:${port}"
    
    def set_debug(d: bool)
        debug = d

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: float = 0.0
    
    def get_balance(): float
        return balance  # Can read
    
    def deposit(amount: float)
        balance = balance + amount  # Can modify only via methods
    
    # No setter: direct assignment not allowed

Write-Only Properties

class Password
    var encrypted_password: str = ""
    
    def set_password(plain: 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: int = 0
var user = User()
user.age = -5  # ❌ No validation!

✅ Better:

class User
    var age: int = 0
    def set_age(new_age: int): bool
        if new_age < 0
            return false
        age = new_age
        return true

❌ Mistake: Computing expensive properties every time

class DataSet
    var numbers: List(int) = List()
    
    def sum(): 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: List(int) = List()
    var cached_sum: int? = nil
    
    def sum(): 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: int)
        numbers.add(num)
        cached_sum = nil  # Invalidate cache

❌ Mistake: Side effects in getters

class Logger
    var count: int = 0
    
    def get_count(): int
        count = count + 1  # ❌ Has side effect!
        return count

✅ Better:

class Logger
    var count: int = 0
    
    def get_count(): 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: str = ""
    var balance: float = 0.0
    var interest_rate: float = 0.02
    
    def set_owner(name: str): bool
        if name.len < 2
            return false
        owner = name
        return true
    
    def deposit(amount: float): bool
        if amount <= 0.0
            return false
        balance = balance + amount
        return true
    
    def withdraw(amount: float): 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(): float
        return balance

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: float = 0.0
    
    def set_radius(r: float): bool
        if r <= 0.0
            return false
        radius = r
        return true
    
    def diameter(): float
        return radius * 2.0
    
    def set_diameter(d: float): bool
        if d <= 0.0
            return false
        radius = d / 2.0
        return true
    
    def area(): float
        return 3.14159 * radius * radius
    
    def circumference(): float
        return 2.0 * 3.14159 * radius

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: str = ""
    var email: str = ""
    var age: int = 0
    
    def set_username(name: str): bool
        if name.len < 3 or name.len > 20
            return false
        username = name
        return true
    
    def set_email(addr: str): bool
        if not addr.contains("@") or not addr.contains(".")
            return false
        email = addr
        return true
    
    def set_age(a: int): bool
        if a < 13 or a > 120
            return false
        age = a
        return true
    
    def is_adult(): bool
        return age >= 18
    
    def is_valid(): bool
        return username.len > 0 and email.contains("@") and age > 0

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

  • Zebra has no property syntax — every getter and setter in this chapter is an ordinary method, declared and called with ()
  • 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.