Reflection in Practice

A mirror is useful only if you do something with what you see. Reflection becomes powerful when we move beyond mere examination to transformation, filtering, and comparison. The meta-level becomes a workshop.

Transforming Objects

Reflection enables powerful transformations. Let's create a function that applies a transformation to every field:

python
    function mapObject(obj, transform):
        result = {}
        for field in keys(obj):
            value = get(obj, field)
            set(result, field, transform(field, value))
        return result

Now we can do things like:

python
    // Double all numeric values
    stats = { health: 100, attack: 25, defense: 15 }
    
    boosted = mapObject(stats, (field, value) => value * 2)
    // { health: 200, attack: 50, defense: 30 }
    
    // Prefix all string values
    piece = { type: "knight", color: "white" }
    
    labeled = mapObject(piece, (field, value) => field + "=" + value)
    // { type: "type=knight", color: "color=white" }

Filtering Fields

Select only certain fields from an object:

python
    function pick(obj, fieldNames):
        result = {}
        for field in fieldNames:
            if hasField(obj, field):
                set(result, field, get(obj, field))
        return result
    
    function omit(obj, fieldNames):
        result = {}
        for field in keys(obj):
            if field not in fieldNames:
                set(result, field, get(obj, field))
        return result
    fullKnight = { type: "knight", color: "white", position: "g1", 
                   hasMoved: false, capturedPieces: 3 }
    
    // Just the essential fields
    essential = pick(fullKnight, ["type", "color", "position"])
    // { type: "knight", color: "white", position: "g1" }
    
    // Everything except internal tracking
    publicInfo = omit(fullKnight, ["hasMoved", "capturedPieces"])
    // { type: "knight", color: "white", position: "g1" }

Deep Cloning

Reflection enables recursive operations on nested structures:

python
    function deepClone(obj):
        if isPrimitive(obj):
            return obj
        
        if isArray(obj):
            return map(obj, deepClone)
        
        result = {}
        for field in keys(obj):
            value = get(obj, field)
            set(result, field, deepClone(value))
        return result
    gameState = {
        board: {
            pieces: [
                { type: "king", position: "e1" },
                { type: "queen", position: "d1" }
            ]
        },
        turn: "white",
        moveCount: 0
    }
    
    copy = deepClone(gameState)
    // Complete independent copy, arbitrarily deep

Comparing Objects

To compare objects field by field:

python
    function deepEqual(a, b):
        if isPrimitive(a) and isPrimitive(b):
            return a == b
        
        if isArray(a) and isArray(b):
            if length(a) != length(b): return false
            for i in range(length(a)):
                if not deepEqual(a[i], b[i]): return false
            return true
        
        keysA = keys(a)
        keysB = keys(b)
        if not setEqual(keysA, keysB): return false
        
        for field in keysA:
            if not deepEqual(get(a, field), get(b, field)):
                return false
        return true

\marginnote{Equality is subtle. Does {a: 1, b: 2} equal {b: 2, a: 1}? With reflection, we decide---and implement once.}

Two Flavors of Reflection

Reflection can happen at different times:

\textwidth
ApproachWhenCharacter
Runtime ReflectionAt execution timeDynamic, flexible, discovers runtime state
Static AnalysisAt compile/build timeFast, type-safe, explicit

Runtime Reflection

Runtime reflection examines the program while it runs:

python
    // JavaScript
    keys = Object.keys(user)           // ["name", "age", "email"]
    hasAge = "age" in user             // true
    
    // Python
    attrs = dir(user)                  // list of all attributes
    value = getattr(user, "name")      // get attribute by string name
    
    // Java
    methods = user.getClass().getMethods()
    field = user.getClass().getDeclaredField("name")
    field.setAccessible(true)
    value = field.get(user)

Runtime reflection is essential when:

  • Working with data whose structure is unknown until runtime (JSON from an API)
  • Building generic frameworks (ORMs, serializers, dependency injection)
  • Implementing plugins that load dynamically

Static Analysis

Static analysis examines the program before it runs---during compilation or build:

wollok
    // TypeScript decorator (analyzed at compile time)
    @serializable
    class User {
        name: string
        age: number
    }
    
    // The decorator triggers code generation
    // that creates serialization methods

Static analysis is preferred when:

  • Performance is critical (no runtime overhead)
  • Type safety matters (errors caught at compile time)
  • The structure is known at build time

The Trade-off

\textwidth
RuntimeStatic
FlexibilityHigh---handles unknown typesLow---only known types
PerformanceSlower---inspection at runtimeFaster---no runtime cost
SafetyLess---errors at runtimeMore---errors at compile time
DebuggingHarder---behavior determined dynamicallyEasier---can see generated code

Reflection and Types

Strongly-typed languages have an uneasy relationship with reflection. Types say "this object has exactly these fields." Reflection says "let me see what fields exist at runtime."

python
    // TypeScript: the type says there's a 'name' field
    interface User {
        name: string
        age: number
    }
    
    // But reflection can access fields by string
    function getField(obj: any, field: string): any {
        return obj[field]
    }
    
    // Type safety is lost
    getField(user, "nmae")  // typo not caught!

Some languages bridge this gap with safer reflection APIs:

python
    // Rust: reflection is minimal but type-safe
    // Most "reflection" happens at compile time via macros
    
    #[derive(Debug)]  // generates debug printing at compile time
    struct Knight {
        position: String,
        has_moved: bool,
    }

When to Reflect

Reflection is powerful but not free. Use it when:

Avoid it when:

  • Writing frameworks that must handle arbitrary types
  • Building tools (debuggers, serializers, ORMs)
  • Implementing dynamic features (plugin systems, scripting)
  • The alternative is massive code duplication

We've seen reflection as examination and transformation. But what if we could intercept operations as they happen---watching every field access, every modification? The next chapter introduces proxies: invisible wrappers that observe and control.

  • A direct approach works
  • Type safety is paramount
  • Performance is critical
  • The code becomes harder to understand