Registered Types
A registered type is a Java data type for which the client has registered
a proxy type with the Joe interpreter. A proxy type is an object
that provides information about the instances of the type.
Most of Joe's standard types, e.g., String
, are implemented in just this
way. This section explains how to define a ProxyType<V>
and register
it with Joe for use.
Defining A Proxy Type
A proxy type is a subclass of ProxyType<V>
, where V
is the proxied type.
For example,
public class StringType extends ProxyType<String> {
public StringType() {
super("String");
}
...
}
By convention, Java proxy type names end with "Type", e.g., StringType
.
- If the type can be subclassed by a scripted Joe
class
, the name should end with "Class", e.g.,TextBuilderClass
. - If the type has no instances, i.e., if it exists only as the owner of
static methods and/or constants, its name should end with "Singleton",
e.g.,
JoeSingleton
.
The proxy type defines the various aspects of the proxy. Most details
are configured in the type's constructor; others involve overriding
various ProxyType
methods.
- The Script-level Type Name
- The Proxied Types
- Type Lookup
- Extending Supertype Proxies
- Stringification
- Static Constants
- Static Methods
- Static Types
- Initializer
- Iterability
- Instance Fields
- Instance Methods
- Nero Fact Conversion
- Installing a Proxy Type
The Script-level Type Name
First, the proxy's constructor defines the script-level type name, which by convention should always begin with an uppercase letter. For example:
public class StringType extends ProxyType<String> {
public StringType() {
super("String");
...
}
...
}
The script-level type name is often the same as the Java class name, but not always.
- Joe's
Number
type is actually a JavaDouble
; it's calledNumber
because there's only one kind of number in Joe. - Joe's
List
type actually maps to two differentList<Object>
types, both under the umbrella of theJoeList
interface. Calling it simplyList
is a kindness to the client.
When the proxy is registered Joe will create a global variable with the
same name as the type, e.g., String
.
The Proxied Types
Second, the proxy must explicitly identify the proxied type or types.
Usually a proxy will proxy the single type V
, but if V
is an interface or
a base class then it might be desirable to explicitly identify the concrete
classes. For example, a Joe String
is just exactly a Java String
.
public class StringType extends ProxyType<String> {
public StringType() {
super("String");
proxies(String.class);
}
...
}
But a Joe List
could be a Java ListValue
or ListWrapper
, both of which
implement the JoeList
interface:
public class ListType extends ProxyType<JoeList> {
public ListType() {
super("List");
proxies(ListValue.class);
proxies(ListWrapper.class);
}
...
}
Type Lookup
At runtime, Joe sees a value and looks up the value's proxy type in its type registry. This section explains how the lookup is done, as it can get complicated.
Joe keeps registered type information in the proxyTable
, a map from
Java Class
objects to Java ProxyType<?>
objects.
-
If value's
Class
is found in theproxyTable
, the proxy is returned immediately. This is the most common case. -
Next, Joe looks in the
proxyTable
for the value's superclass, and so on up the class hierarchy. -
Next, Joe looks in the
proxyTable
for any interfaces implemented by the value's type, starting with the type's ownClass
and working its way up the class hierarchy. -
Finally, if no proxy has been found then an
OpaqueType
proxy is created and registered for the value'sClass
.
Whatever proxy is found, it is cached back into the proxyTable
for the
value's concrete class so that it will be found immediately next time.
NOTE: when looking for registered interfaces, Joe only looks at the interfaces directly implemented by the value's class or its superclasses; it does not check any interfaces that those interfaces might extend. This is intentional, as expanding the search is too likely to lead to false positives and much unintentional comedy. Registered interfaces should be used with great care!
Extending Supertype Proxies
Sometimes it happens that both a Java type and its Java supertype are
registered types; this is the case with Joe's AssertError
and Error
types, which are represented internally by the Java AssertError
and
JoeError
types. In such a case, the subtype's proxy can "inherit" the
supertype's methods via the extendsProxy()
method:
public class AssertErrorProxy extends ProxyType<AssertError> {
public AssertErrorProxy() {
super("AssertError");
proxies(AssertError.class);
extendsProxy(ErrorProxy.TYPE);
}
...
}
Stringification
Joe doesn't use Object::toString
when it needs to convert a value to
a string; instead it calls Joe::stringify
, which allows Joe to customize
the value's script-level string representation.
For example, all Joe numbers are java Doubles
. The default string
representation for doubles includes the fractional part, but Joe includes
the fractional part only when it is non-zero.
For proxied types, Joe::stringify
calls the proxy's stringify
method,
which calls Object::toString
by default. The client can override
the proxy's stringify
method to customize the returned value.
For example, NumberType
's stringify
method removes the decimal part
when it is zero.
@Override
public String stringify(Joe joe, Object value) {
assert value instanceof Double;
String text = ((Double)value).toString();
if (text.endsWith(".0")) {
text = text.substring(0, text.length() - 2);
}
return text;
}
Static Constants
A proxy may define any number of named constants, to be presented at
the script level as properties of the type object. For example,
NumberType
defines several numeric constants. A constant is defined
by its property name and the relevant Joe value using the constant
method.
public class NumberType extends ProxyType<Double> {
public NumberType() {
super("Number");
...
constant("PI", Math.PI);
...
}
...
}
The constant is accessible as Number.PI
.
Static Methods
A proxy may also define any number of static methods, called as properties
of the type object. For example, the String
type defines the
String.join()
method, which
joins the items in the list into a string with a given delimiter,
and the Number
type defines a great many math functions as static methods.
A static method is simply a native function defined
as a property of a list object using the staticMethod
method:
public class StringType extends ProxyType<String> {
public StringType() {
...
staticMethod("join", this::_join);
...
}
...
private Object _join(Joe joe, Args args) {
args.exactArity(2, "join(delimiter, list)");
...
}
}
Static Types
A proxy type can be defined as a kind of library of static constants and
methods. JoeSingleton
is just such a proxy type. There are no
instances of Joe
at the script level, and so there are instance fields or
methods.
In this case, the type can be declared to be a static type. Among other things, this means that attempts to create an instance using the type's initializer will get a suitable error message.
public class JoeSingleton extends ProxyType<Void> {
public JoeSingleton() {
super("Joe");
staticType();
...
}
...
}
Initializer
Most type proxies will provide an initializer function for creating values of the type. The initializer function is named after the type, returns a value of the type, and may take any desired arguments.
For example, the List
type provides this
initializer:
public class ListType extends ProxyType<JoeList> {
public ListType() {
super("List");
...
initializer(this::_init);
...
}
private Object _init(Joe joe, Args args) {
...
// Create a copy of another list.
var other = args.next();
return new ListValue(other);
}
...
}
Iterability
Joe's foreach
statement, and its in
and ni
operators, can iterate over or
search the following kinds of collection values:
- Any Java
Collection<?>
- Any proxied type whose
ProxyType
can produce a list of items for iteration.
To make your registered type iterable, provide an iterable supplier, a
function that accepts a value of your type and returns a Collection<?>
.
public class MyProxyType extends ProxyType<MyType> {
public MyProxyType() {
super("MyType");
...
iterableSupplier(this::_iterables);
...
}
private Object _iterables(Joe joe, MyType value) {
return value.getItems();
}
}
Instance Fields
A proxy type can expose a value's properties as read-only fields at the script
level using the field
method. For example, suppose MyType
has an id
property and the client wishes to expose that property as a read-only field
rather than as an instance method:
public class MyProxyType extends ProxyType<MyType> {
public MyProxyType() {
super("MyType");
...
field(this::_id);
...
}
private Object _id(Joe joe, MyType value) {
return value.getId();
}
}
This technique is appropriate if all instances of the type have the same fields, and the fields are all read-only, which is the usual case.
In unusual cases it is necessary to override the following ProxyType
methods:
getFieldNames(Object value)
get(Joe joe, Object value, String propertyName)
set(Joe joe, Object value, String fieldName, Object other)
This is an advanced move; see the com.wjduquette.joe.types.FactType
for an
example.
Instance Methods
Most type proxies will define one or more instance methods for values of the
type. For example, String
and
List
provide a great many instance methods.
An instance method is like a native function, but has a different
signature because it is bound to a value of the proxied type.
For example, here is the implementation of
the String
type's length()
method.
public class StringType extends ProxyType<String> {
public StringType() {
...
method("length", this::_length);
...
}
...
private Object _length(String value, Joe joe, Args args) {
args.exactArity(0, "length()");
return (double)value.length();
}
}
Each instance method takes an initial argument that receives the bound value.
The argument's type is of type V
, as defined in the proxy's extends
clause.
Otherwise, this is nothing more than a native function, and it is implemented
in precisely the same way.
Nero Fact Conversion
Scripted values that are to be used as inputs to
Nero rule sets must first be converted to Fact
values.
The conversion done automatically, but the type must support the conversion by
overriding the ProxyType
's isFact(Joe, Object)
and toFact(Joe, Object)
methods.
isFact(Joe, Object)
indicates whether the object (a value of typeV
) can be converted into aFact
.toFact(Joe, Object)
actually does the conversion.
If a ProxyType
defines script-visible instance fields
using the ProxyType::field
method, then isFact
and toFact
are defined
automatically. toFact
will produce a RecordFact
whose relation is the type
name and whose fields are the type's visible fields.
However, proxy types are free to override these, to provide Facts
in some
other way.
Installing a Proxy Type
Proxy types are installed using the Joe::installType
method (or by
including them in a Joe Package):
var joe = new Joe();
joe.installType(new MyType());
Installation creates the MyType
object, and registers the type so that
MyType
's instance methods can be used with all values of MyType
.
If a proxy type has no dynamic data, i.e., if it need not be configured with
application-specific data in order to do its job, it is customary to create
a single instance that can be reused with any number of instances of Joe
.
This is the case for most of the proxies in Joe's standard library. For example,
public class StringType extends ProxyType<String> {
public static final StringType TYPE = new StringType();
...
}
This can then be installed as follows:
var joe = new Joe();
joe.installType(StringType.TYPE);