Reflection
Reflection lets a program inspect its own types, classes, functions, and
metadata at runtime. Geblang's reflection surface is built into the language
(typeof, the .type shorthand, instanceof) and the reflect module.
Everything described here works at runtime and behaves identically on both
execution backends (the evaluator used by geblang test and the bytecode VM
used by geblang run and geblang build).
Reflection is what makes Geblang's metadata-driven features possible: decorators are inspectable rather than erased, generics are reified, and frameworks can discover classes, read their fields and decorators, and bind request data to typed handler parameters without any framework syntax in the language itself.
import reflect;
The import is optional (1.16.0): an unshadowed bare reflect.X(...)
dispatches ambiently on both backends. Importing it explicitly remains
the documented style.
Type introspection
typeof(x) returns the runtime type name of any value as a string:
import io;
io.println(typeof(42)); # int
io.println(typeof("hi")); # string
io.println(typeof([1, 2, 3])); # list
io.println(typeof(3.14)); # decimal
Every value also carries a .type shorthand that returns the same name:
let x = 42;
io.println(x.type); # int
For a richer type value (rather than a bare name string), use
reflect.typeOf, which returns a Type value:
io.println(reflect.typeOf(42)); # int
io.println(reflect.typeOf("hi")); # string
io.println(reflect.typeOf([1, 2, 3])); # list
instanceof
instanceof tests whether a value is an instance of a class, a subclass, or an
implementer of an interface:
interface Greeter {
func greet(): string;
}
class Animal {
string name;
func Animal(string name) { this.name = name; }
}
class Dog extends Animal implements Greeter {
func Dog(string name) { parent(name); }
func greet(): string { return "Woof"; }
}
let d = Dog("Rex");
io.println(d instanceof Dog); # true
io.println(d instanceof Animal); # true (subclass)
io.println(d instanceof Greeter); # true (implements)
Reified generics
Generic type parameters are reified, so instanceof can test the element type
of a parameterized collection - and, with the same invariant model, the
recorded bindings of a user generic class instance:
let xs = [1, 2, 3];
io.println(xs instanceof list<int>); # true
io.println(xs instanceof list<string>); # false
class Box<T> {
T value;
func Box(T v) { this.value = v; }
}
let b = Box<string>("hi");
io.println(b instanceof Box<string>); # true
io.println(b instanceof Box<int>); # false
reflect.typeBindings(value) returns a dict mapping each type parameter name to
the concrete type it was bound to. This is how generic containers, validators,
and wrappers discover what they were parameterized with:
class Box<T> {
T value;
func Box(T v) { this.value = v; }
}
let b = Box<int>(5);
io.println(reflect.typeBindings(b)); # {"T": "int"}
Looking things up by name or value
reflect.class, reflect.function, and reflect.module each accept either a
name string or a value of the matching kind, and return the corresponding
reflectable target (or null when nothing resolves):
class Dog {
func Dog() {}
}
let d = Dog();
let byName = reflect.class("Dog"); # look up by name
let byValue = reflect.class(d); # extract the class from an instance
io.println(reflect.className(byName)); # Dog
io.println(reflect.className(byValue)); # Dog
reflect.function(name) returns an inspectable handle for a top-level function.
Pass the handle to reflect.parameters, reflect.returnType,
reflect.decorators, and the other introspection calls below:
func helper(int n, string s = "a"): int { return n; }
let f = reflect.function("helper");
io.println(reflect.returnType(f)); # int
io.println(reflect.parameters(f)); # [{... "name": "n" ...}, {... "name": "s" ...}]
io.println(reflect.function("missing")); # null
Reflecting over imported native modules
reflect.module("name") resolves an imported module by its string name.
reflect.class resolves a native module's class exports using the
module.Class form:
import http;
let request = reflect.class("http.Request");
io.println(reflect.className(request)); # Request
reflect.function also resolves a native module's functions (1.16.0). The
result is a first-class callable, the same value math.sqrt produces as an
expression. Unknown members return null:
import math;
let sqrt = reflect.function("math.sqrt");
io.println(sqrt(16.0)); # 4
io.println(reflect.function("math.nope")); # null
Native functions carry no source-level metadata, so structural calls such as
reflect.parameters or reflect.location report nothing useful for them;
they resolve and call.
Class reflection
Given a class value (from reflect.class, an instance, or a class name used
directly), these calls report its structure. Field and method listings cover the
class's own declared members; inherited members live on the parent class, which
you can reach with reflect.parent.
@immutable
class Animal {
string name;
int legs = 4;
func Animal(string name) { this.name = name; }
func describe(): string { return this.name; }
}
class Dog extends Animal implements Greeter {
?string breed;
func Dog(string name, ?string breed) {
parent(name);
this.breed = breed;
}
func greet(): string { return "Woof"; }
}
reflect.className(target) returns the class name as a string:
io.println(reflect.className(Dog)); # Dog
reflect.fields(class) returns one dict per declared field, each with name,
type, nullable, hasDefault, and decorators:
io.println(reflect.fields(Dog));
# [{"decorators": [], "hasDefault": false, "name": "breed", "nullable": true, "type": "?string"}]
reflect.methods(class) returns the names of the class's own methods:
io.println(reflect.methods(Dog)); # ["greet"]
reflect.constructors(class) returns one entry per constructor overload, each a
list of parameter dicts (see parameter metadata below):
io.println(reflect.constructors(Dog));
# [[{"hasDefault": false, "name": "name", "type": "string", "variadic": false},
# {"hasDefault": false, "name": "breed", "type": "?string", "variadic": false}]]
reflect.parent(class) returns the parent class name as a string, or null
when the class has no parent. reflect.interfaces(class) returns the names of
the interfaces the class implements:
io.println(reflect.parent(Dog)); # Animal
io.println(reflect.parent(Animal)); # null
io.println(reflect.interfaces(Dog)); # ["Greeter"]
Parameter metadata
reflect.parameters(function) returns one dict per parameter. Each entry has
name, type, hasDefault, and variadic. reflect.returnType(function)
returns the declared return type:
func handler(int id, string name = "anon"): string { return name; }
io.println(reflect.parameters(handler));
# [{"hasDefault": false, "name": "id", "type": "int", "variadic": false},
# {"hasDefault": true, "name": "name", "type": "string", "variadic": false}]
io.println(reflect.returnType(handler)); # string
The same shape is produced for class fields (reflect.fields), constructors
(reflect.constructors), and methods, so framework code can walk parameters
uniformly.
Enumerating classes
reflect.classes() takes no arguments and returns a list of every class
declared in the program. Frameworks use it to scan for classes carrying a
particular decorator:
let all = reflect.classes();
io.println(all instanceof list); # true
Instances and fields
reflect.getField(instance, name) reads a named field off an instance, and
returns null when the field is absent (no separate existence probe needed):
let d = Dog("Rex", "Lab");
io.println(reflect.getField(d, "name")); # Rex
io.println(reflect.getField(d, "breed")); # Lab
io.println(reflect.getField(d, "missing")); # null
reflect.setField(instance, name, value) writes a named field. It is permissive
by design: the assignment succeeds even if the field was not declared, which is
what dynamic binding code (PATCH-style partial updates, deserializers) needs.
reflect.setField(d, "name", "Fido");
io.println(d.name); # Fido
Decorators and metadata
Decorators in Geblang are inspectable, not erased. reflect.decorators(target)
returns the decorator metadata attached to a function, class, method, or field.
Each entry carries the decorator name, its positional args, its namedArgs,
the target kind, and the source line / column:
@tag("admin")
func handler(int id): string { return ""; }
io.println(reflect.decorators(handler));
# [{"args": ["admin"], "column": 1, "line": 1, "name": "tag",
# "namedArgs": {}, "overload": 0, "position": 0, "target": "function"}]
reflect.hasDecorator(target, name) reports whether a target carries a named
decorator, and reflect.decorator(target, name) returns that single decorator's
metadata dict (or null when absent):
@immutable
class Widget {
func Widget() {}
}
io.println(reflect.hasDecorator(Widget, "immutable")); # true
io.println(reflect.hasDecorator(Widget, "sealed")); # false
io.println(reflect.decorator(Widget, "immutable"));
# {"args": [], "column": 1, "line": 1, "name": "immutable",
# "namedArgs": {}, "overload": 0, "position": 0, "target": "class"}
A decorator can act as pure metadata for reflection, as a callable wrapper, or both. See Functions and callables for how to define and apply decorators.
Modules
reflect.module("name") resolves an imported module by name. reflect.exports,
reflect.location, and friends accept the resulting module value.
reflect.exports(module) lists the names a user module exports (functions and
classes):
# in mymod.gb:
# export func add(int a, int b): int { return a + b; }
# export func sub(int a, int b): int { return a - b; }
# export class Thing { int n; func Thing(int n) { this.n = n; } }
import mymod;
let m = reflect.module("mymod");
io.println(reflect.exports(m)); # ["Thing", "add", "sub"]
reflect.location(target) returns the source position of a function or class
declaration as {module, line, column}, or null when no position was
recorded:
let cls = reflect.class("Thing");
io.println(reflect.location(cls)["line"]); # the line Thing was declared on
dir: the lightweight companion
dir(value) is a lighter-weight tool for listing the methods available on a
value. It returns a sorted list of names and works on instances, primitives, and
modules:
io.println(dir([1, 2, 3]).contains("push")); # true
io.println(reflect.methods("hi").contains("upper")); # true
reflect.methods and dir overlap for listing callable members; reach for
reflect.methods when you are already inside a reflection workflow, and dir
for quick interactive exploration. See Syntax basics for
the introduction to dir and typeof.
What is and isn't reflectable
- User classes, functions, fields, and methods are fully reflectable: structure, parameters, decorators, and locations are all available.
- Decorators are inspectable metadata, not erased.
- Generics are reified:
instanceof list<int>andreflect.typeBindingsresolve concrete type arguments at runtime. - Native module class exports are reflectable via the
module.Classform (reflect.class("http.Request")). - Native module functions resolve to first-class callables
(
reflect.function("math.sqrt"), 1.16.0). They carry no source-level structure, so parameter and location introspection reports nothing for them. - Field and method listings cover a class's own declared members. Inherited
members live on the parent class, reachable with
reflect.parent.