Registered Types
A registered type is a Java data type for which Joe has been provided
a proxy type: an object that provides information about 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, a Java proxy types have names ending
with "Type", as in StringType
. If the type can be subclassed by a scripted
Joe class
, then the proxy type's name should end in "Class", as in
TextBuilderClass
. If the type has no instances, but exists only as the
owner of static methods and/or constants, it should end in "Singleton",
as in the JoeSingleton
class.
The constructor defines the various aspects of the proxy:
- The Script-level Type Name
- The Proxied Types
- Type Lookup
- The Supertype
- Stringification
- Iterability
- Static Constants
- Static Methods
- Initializer
- Static Types
- Instance Methods
- Installing a Proxy Type
The Script-level Type Name
First, the proxy 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, 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!
The Supertype
Sometimes it happens that both a type and its supertype are registered types;
this is the case with Joe's Error
and AssertError
types. In such a case,
the subtype's proxy can "inherit" the supertype's methods via the supertype()
method:
public class AssertErrorProxy extends ProxyType<AssertError> {
public AssertErrorProxy() {
super("AssertError");
proxies(AssertError.class);
extendsProxy(ErrorProxy.TYPE);
}
...
}
Stringification
When Joe converts a value to a string, it does so using a function called
Joe::stringify
. If the value belongs to a proxied type, Joe::stringify
calls the proxy's stringify()
method, which returns the result of the
values toString()
by default.
To change how a value is stringified at the script level, override
ProxyType::stringify
. For example, NumberType
ensures that
integer-valued numbers are displayed without a trailing .0
:
@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;
}
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 value that implements the
JoeIterable
interface - Any proxied type whose
ProxyType
can produce a list of items for iteration.
To make your registered type iterable, provide an iterables supplier, a
function that accepts a value of your type and returns a Collection<?>
.
public class MyProxyType extends ProxyType<MyType> {
public MyProxyType() {
super("MyType");
...
iterables(myValue -> myValue.getItems());
...
}
...
}
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.
public class NumberType extends ProxyType<Double> {
public NumberType() {
super("Number");
...
constant("PI", Math.PI);
...
}
...
}
In this case, 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.
A static method is simply a native function defined as a property of a list object.
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)");
...
}
}
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 new list given the arguments ...
return new ListValue(list);
}
Static Types
A proxy type can be defined as a kind of library of static constants and
methods. The NumberType
is just such a proxy. Numbers have no methods
in Joe, and they do not need an initializer as they can be typed directly
into a script.
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 NumberType extends ProxyType<Double> {
public NumberType() {
super("Number");
staticType();
proxies(Double.class);
...
}
...
}
In the case of the Number
type, the proxy still proxies an actual data type.
This need not be the case. It is common to define a static type as a named
library of functions with no related Java type. For example, consider
a library to convert between Joe values and JSON strings. It might look
like this:
public class JSONSingleton extends ProxyType<Void> {
public JSONSingleton() {
super("JSON");
staticType();
staticMethod("toJSON", this::_toJSON);
staticMethod("fromJSON", this::_fromJSON);
...
}
...
}
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 quite similar to a native function, but
an instance method is bound to a value of the proxied type, and so has a
slightly different signature. 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 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.
Installing a Proxy Type
Proxy types are installed using the Joe::installType
method:
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 all 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);