Introspection
Introspection, also known as reflection, is the ability for a script to
query metadata about its types and values. This section discusses Joe's
introspection features, which are available via the methods of the
Joe
singleton.
Finding a value's type
Given any value whatsoever, Joe.typeOf(value)
will return its type.
Given the type, type.name()
will return the type's name.
Scoped vs. Unscoped Types
Most Joe types are defined in the global environment as a variable with the type's name whose value is the type itself.
- E.g., the standard type
String
is referenced by the global variableString
.
A Joe class is defined in the scope that contains the class
declaration. If
class Thing {...}
appears at global scope, it will define a global
variable called Thing
. If class Thing {...}
appears in a local scope,
e.g., in a function body, it will define a variable called Thing
in that
scope.
A type defined in a scope is called a scoped type.
But consider this case:
function makeValue() {
class MyType { ... }
return MyType();
}
var theValue = makeValue();
The function makeValue
creates a class called MyType
in its own scope and
then returns an instance of the class. The variable MyType
goes out of
scope when makeValue
returns; the type can no longer be accessed "by name"
in any scope in the script. MyType
is now said to be unscoped.
And yet, theValue
still has type MyType
:
// Prints "MyType"
println(Joe.typeOf(theValue).name());
Similarly, just as a client can extend Joe with a binding for a scoped type
(in exactly the same way as standard types like String
and List
), a
client can also create values of named-but-unscoped types.
Unscoped Types and Pattern Matching
Joe's pattern matching feature provides several ways to match on a value's type. In each case, the type is matched by its name rather than by strict equality of type objects, precisely because the value's type might be unscoped: the type's name is known to the value and to the script author, but isn't assigned to any convenient variable.
In other words,
if (thing ~ Thing{#id: id}) {
...
}
will match if Joe.typeOf(thing).name() == "Thing"
. Whether type Thing
's
variable is in-scope is irrelevant.
Field Names
The Joe.getFieldNames(value)
method will return a list of the value's fields.
For example,
class Thing {
method init(id, name) {
this.id = id;
this.name = name;
}
}
// Prints ["id", "name"]
println(Joe.getFieldNames(Thing));
If the given value is a Joe type, Joe.getFieldNames()
will return the
names of the type's static fields, not the names of its instance fields.
It may seem odd to call Joe.getFieldNames()
on a value to discover the
value's fields, rather than on the value's type; but Joe types don't
generally know the names of their instances' fields. Consider the
following example:
class Person {
method init(name) {
this.name = name;
}
}
var fred = Person("Fred");
fred.favoriteFlavor = "chocolate";
fred.lastYearsFavorite = "vanilla";
Every Person
is assigned a name
in the initializer; but the script
author has decided to add two additional fields to Person
fred
.
A script can add any number of fields to any class instance, fields
other instances of that type won't have.
In fact, the Person
type doesn't even know that every Person
has
a name
. The init()
method could be written to assign a value to
this.name
for some persons but not others.
Thus, there's no way to query type Person
to find the field names of its
instances.