Skip to content
FutureBlitz

    Install

    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 For
    Output
    Volume: 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)
    Output
    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())
    Output
    > 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[]>>())
    Output
    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
    Output
    {"Volume":80,"Name":"player","Fullscreen":false}
    55 player

    A 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 T repeats its body for each field, with f.Name, f.Get, and f.Set.
    • A loop’s Where lines say what each field’s type must do, and every call is checked against them.
    • For Method m In T goes over Methods, Where m Has A picks them by attribute, and m.Call calls them.
    • Reflection happens at compile time: each field or Method gets its own copy of the loop’s body.
    • Save and Load<T> convert a Struct or Class made of convertible fields to a Value and back, by the same reflection.

    Report an issue with this page or with FutureBlitz on GitHub.