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:
function mapObject(obj, transform):
result = {}
for field in keys(obj):
value = get(obj, field)
set(result, field, transform(field, value))
return resultNow we can do things like:
// 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:
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:
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 deepComparing Objects
To compare objects field by field:
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:
| Approach | When | Character |
|---|---|---|
| Runtime Reflection | At execution time | Dynamic, flexible, discovers runtime state |
| Static Analysis | At compile/build time | Fast, type-safe, explicit |
Runtime Reflection
Runtime reflection examines the program while it runs:
// 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:
// TypeScript decorator (analyzed at compile time)
@serializable
class User {
name: string
age: number
}
// The decorator triggers code generation
// that creates serialization methodsStatic 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
| Runtime | Static | |
|---|---|---|
| Flexibility | High---handles unknown types | Low---only known types |
| Performance | Slower---inspection at runtime | Faster---no runtime cost |
| Safety | Less---errors at runtime | More---errors at compile time |
| Debugging | Harder---behavior determined dynamically | Easier---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."
// 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:
// 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