6.10. Reflection
The standard library provides two levels of reflection traits: Reflect and Inspect.
Reflect automatic - shallow metadata every type carries for free
Inspect opt-in via #[derive(Inspect)] - rich field/method description
The split into two levels controls binary size while still giving every type, for free, what is essential for any slots, sum types, and REPL auto-printing.
6.10.1. Reflect - automatically implemented trait
Reflect is minimal type introspection attached automatically to every type. Without the user writing #[derive(Reflect)], the compiler synthesizes an impl for every type.
trait Reflect {
// static - called directly on the type
fn type_name() -> str;
fn size() -> usize;
fn align() -> usize;
// instance - runtime dispatch on any slots / sum types
fn type_name_of(self) -> str;
fn size_of(self) -> usize;
}
i32::type_name() == "i32"
i32::size() == 4
i32::align() == 4
let x: any = 42i32;
x.type_name_of() == "i32" // runtime type tag check
let v: i32 | f64 = pick();
v.type_name_of() // "i32" or "f64" - sum type variant check
Being automatic, it requires no user code. The compiler already knows the type_name, size, and align of every type and synthesizes them at zero cost.
Reflect is provided by the compiler for every type; there is no impl in source.
- A handwritten
impl Reflect for Tis an error. The boundT: Reflectalways holds. - If a type has its own method of the same name, such as
size()/type_name(), that method hides the reflection method. - Names (
type_name) carry no module path (List<i64>,Config). The static name of a sum type is written in canonical order (2.6.4) (f64 | i32, withnonelast). Instance forms such astype_name_of(self)report the member held at runtime (for a sum) or the contents of anany. rok::core::reflect::cast<T>(value: any) -> T | noneextracts ananyasTby the same rules asset_field.rok::core::reflect::methods_of<T>() -> List<MethodInfo>is the list ofT's inherent methods.
6.10.2. Inspect - opt-in trait
Inspect is rich reflection opted into with #[derive(Inspect)]. It exposes field names, dynamic field access and modification, and method descriptions.
trait Inspect: Reflect {
fn field_names() -> List<str>;
fn get_field(self, name: str) -> any | none; // private fields too
fn set_field(ref self, name: str, value: any) -> bool; // whether it succeeded
fn methods() -> List<MethodInfo>;
}
struct MethodInfo {
name: str;
arity: usize; // number of arguments excluding self
is_static: bool; // a method that does not take self
arg_types: List<str>; // argument type names excluding self
return_type: str;
}
get_fieldreturns the field's value asany, ornonefor a nonexistent name. Private fields can be read too.set_fieldassigns and returnstruewhen the value has exactly the same type as the field, or when an integer of a different integer type fits without loss (u.set_field("id", 42)into au64field). Otherwise it does not assign and returnsfalse. There are no other conversions (e.g.i32→f64).methods()returns only the inherent methods of the type (excluding trait methods), sorted by name. Methods of a genericimplshow types as written (T).- Derived on an
enum, there are no fields (field_names()is an empty list,get_fieldisnone,set_fieldisfalse); onlymethods()is meaningful.
Usage example:
#[derive(Inspect)]
struct Config { host: str; port: i32; }
let c = Config { host: "localhost", port: 8080 };
Config::field_names() // ["host", "port"]
c.get_field("port") // 8080 inside an any
c.set_field("port", 9090) // true (success)
c.set_field("xxx", 1) // false (no such field)
Inspect is opt-in because its metadata must be stored in the binary. Attaching it to every type automatically would impose too large a binary size cost.
#[derive(Inspect)] is implemented by the meta block #[impl_derive(Inspect)] in rok::core::reflect, a pattern in which the meta-system itself implements a standard library trait (stdlib 2.12.3).
6.10.3. REPL / Jupyter auto-printing
When the REPL prints the result of the last expression, it combines Reflect.type_name_of and Debug (9.7.7).
> let x = 42i32;
> x
i32: 42
> [1, 2, 3]
List<i32>: [1, 2, 3]
> Config { host: "h", port: 80 }
Config: Config { host: "h", port: 80 }
The output format is:
{type_name_of(value)}: {value:?}
For a type without Debug, only the type name is printed. This behavior is used by REPL and Jupyter cell sessions (9.7).
6.10.4. No global typeof / sizeof functions
Introspection that other languages expose through keywords such as typeof or sizeof is provided in rok only as trait methods.
// rok - method calls
T::size()
x.type_name_of()
// other languages - keywords
sizeof(T) // C / C++
typeof(x) // JS / TS
The reason is that this adds no language syntax. The single Reflect trait covers the entire introspection surface.
6.10.5. Dispatch on any slots and sum types
any slots and sum types of the form T1 | T2 determine the actual type at runtime, so the type_name_of call is important.
fn handle(x: any) {
if x.type_name_of() == "i32" { handle_int(x); }
else if x.type_name_of() == "str" { handle_str(x); }
}
fn classify(v: i32 | f64) -> str {
match v {
n: i32 => "integer",
f: f64 => "float",
}
}
The compiler desugars type-test patterns (v: T => ...) into Reflect tag checks.
6.10.6. Relation between the levels
As the notation Inspect: Reflect shows, Inspect has Reflect as a supertrait. A type implementing Inspect therefore always implements Reflect too - trivially, since Reflect is automatic anyway.
every type Reflect (automatic, zero cost)
+ #[derive(Inspect)] Inspect+Reflect (opt-in, metadata embedded)