Compile-time code, 3 of 4
Reflection
Go over a type's fields and Methods at compile time, to write code that works for many Structs and Classes.
What you’ll build
Three generic tools that work for a Struct or Class without being told its fields: Changes, which
lists the fields that differ between two values; Clear, which sets every field to its type’s zero
value; and RunAll, which calls every Method marked @Command. Then you’ll see how Load and Save
use the same reflection to turn a Struct into JSON and back.
Going over the fields
For Field f: F In T repeats its body once for each field of T, in order. The compiler makes a
copy of the body for each field, in which F is that field’s type, so the loop costs nothing
when the program runs: there’s no run-time reflection. f.Name is the field’s name, and
f.Get(value) reads it.
Import std.core
Import std.collections
Struct Settings
Field Volume: Int = 80
Field Name: String = "player"
Field Fullscreen: Bool
End Struct
Function Changes<T>(before: T, after: T): String[]
Local changes: String[] = []
For Field f: F In T
Where F: Equatable & Printable
If f.Get(before) != f.Get(after)
changes.Add("{f.Name}: {f.Get(before)} -> {f.Get(after)}")
End If
End For
Return changes
End Function
Local saved = Settings()
Local edited = saved
edited.Volume = 40
edited.Fullscreen = True
For change In Changes(saved, edited)
Print(change)
End ForVolume: 80 -> 40 Fullscreen: False -> True
The body is checked once, with F as a type parameter. Its Where line says what each field’s
type must do: here, be compared with != and shown in a string. A call is checked against it,
so Changes on a type with a field that isn’t Printable is an error that names the field.
Setting fields
f.Set(ByRef value, x) changes a field of a variable passed ByRef. With Where F: Defaultable, every field’s type has F.DefaultValue(), its zero value: 0, "", or False
here, whatever the field’s own default in the Struct is.
Import std.core
Struct Settings
Field Volume: Int = 80
Field Name: String = "player"
Field Fullscreen: Bool
End Struct
Function Clear<T>(ByRef value: T)
For Field f: F In T
Where F: Defaultable
f.Set(ByRef value, F.DefaultValue())
End For
End Function
Local s = Settings()
s.Fullscreen = True
Clear(ByRef s)
Print(s.Volume, s.Name == "", s.Fullscreen)0 True False
A ReadOnly field can’t be set this way, and neither can one the loop’s code couldn’t assign by
name, like another file’s Private field.
Going over the Methods
For Method m In T goes over a Struct’s or Class’s Methods that take nothing, give nothing, and
throw nothing, but not Destroy, or Unsafe, ByRef, or Async Methods. A Where m Has Command
line picks only those with the Command attribute, and m.Call(target) calls each one.
Import std.core
Attribute Command
Class Console
@Command
Method Hello()
Print("hello")
End Method
@Command
Method Version()
Print("1.0")
End Method
Method Hidden()
Print("not a command")
End Method
End Class
Function RunAll<T>(target: T)
For Method m In T
Where m Has Command
Print("> {m.Name}")
m.Call(target)
End For
End Function
RunAll(Console())> Hello hello > Version 1.0
A typed loop, For Method m: M In T with Where M: Func(String): Int, goes over the Methods
whose type is that Func type instead, and m.Call(target, "text") passes their arguments.
For Property p: P In T goes over Properties, which it reads with p.Get(value); there’s no Set.
Type names
TypeName<T>() gives a type’s name as a String, known at compile time, with its type arguments:
Import std.core
Import std.collections
Struct Settings
Field Volume: Int
End Struct
Print(TypeName<Settings>())
Print(TypeName<Map<String, Int[]>>())Settings Map<String, Int32[]>
Int is a name for Int32, so the second line shows Int32.
Load and Save
The built-ins Save(x) and Load<T>(value) convert between a value and std.core’s Value, which
std.data’s formats read and write. They take numbers, Bools, Strings, Enums, T?s, arrays,
Map<String, V>s, and Structs and Classes made of these, which they go through field by field
with the same reflection, so a Struct needs no code of its own to become JSON. A Func, a tuple,
a Ptr, a Task, and an Interface’s object can’t be converted, and the compiler says so.
Import std.core
Import std.data
Struct Settings
Field Volume: Int = 80
Field Name: String = "player"
Field Fullscreen: Bool
End Struct
Print(Json.Write(Save(Settings())))
Try
Local loaded = Try Load<Settings>(Try Json.Read("{{\"Volume\": 55}}"))
Print(loaded.Volume, loaded.Name)
Catch e As Error
PrintError(e.Message)
End Try{"Volume":80,"Name":"player","Fullscreen":false}
55 playerA field the JSON doesn’t have keeps its default, so Name is still "player". A value of the
wrong kind is a ParseError that says where it is.
What you learned
For Field f: F In Trepeats its body for each field, withf.Name,f.Get, andf.Set.- A loop’s
Wherelines say what each field’s type must do, and every call is checked against them. For Method m In Tgoes over Methods,Where m Has Apicks them by attribute, andm.Callcalls them.- Reflection happens at compile time: each field or Method gets its own copy of the loop’s body.
SaveandLoad<T>convert a Struct or Class made of convertible fields to aValueand back, by the same reflection.
The rules are in the language reference.
Report an issue with this page or with FutureBlitz on GitHub.