-
Notifications
You must be signed in to change notification settings - Fork 16
Developing a Wollok Class
So, I want to define a new class. What should I do?
Edit wollok.wlk file in project org.uqbar.project.wollok.lib, source folder src. Let's suppose you want to define a Range class. Choose a name, like Range. Then you have to think
- whether you will use a native class behind
- or if you want to define a pure Wollok class/object
The latter option is easy to implement... Go to wollok.wlk and write your definition
/**
* @since 1.3
*/
class Range {
const start
const end
constructor(_start, _end) { start = _start ; end = _end }
method map(closure) {
const l = []
self.forEach{e=> l.add(closure.apply(e)) }
return l
}
override method internalToSmartString(alreadyShown) = start.toString() + ".." + end.toString()
}
Here you define
- a Wollok class (or object)
- references named start & end
- methods like map, internalToSmartString & a 2-params constructor
Certain methods are awkward to implement. Then you can use native clause in wollok.wlk:
method aNativeMethod() native
A method without a body and that uses the native keyword. But it also has a regular Wollok method. This is a native class. One that has at least one native method. So where's that method's implementation/code ? For that we have the java side
There should be a corresponding Range class in WDK with the same package in java/xtend (see below).
Project org.uqbar.project.wollok.lib has several packages
- wollok.lang
- wollok.lib
There you can code a Range class, in Java/Xtend/any JVM language.
package wollok.lang has this definition of Range.xtend
/**
*
* @author jfernandes
*/
class Range extends AbstractJavaWrapper<IntegerRange> {
...
def void forEach(WollokObject proc) {
val c = (proc.getNativeObject(CLOSURE) as Closure)
initWrapped.forEach[e| c.doApply(e.javaToWollok) ]
}
def initWrapped() {
if (wrapped == null)
wrapped = new IntegerRange(obj.resolve("start").wollokToJava(Integer) as Integer, obj.resolve("end").wollokToJava(Integer) as Integer)
wrapped
}
}
Notice that the java class resolution is based on a naming convention. The wollok class full name is "wollok.classes.natives.MyNative", therefore Wollok expects to find a java class with that exact name, somewhere in the classpath.
Every time someone instantiates the MyNative wollok class in a Wollok program, the interpreter will also instantiate one of such java classes. So you can also store state in the native object.
Every wollok "native" method has its corresponding native implementation in Java/Xtend/etc.
wollok.wlk - Wollok Range class | Range.xtend |
---|---|
method forEach(closure) native |
def void forEach(WollokObject proc) { |
Now there are some situations where the java-side method cannot have the same name as the Wollok side. For example because of java reserved words, or in case you want to expose a message with a symbol like "+", etc. For these cases there's a special annotations @NativeMessage("...") you can use in the java-side that tells Wollok which message is this method implementing.
wollok.wlk - Wollok WDate class | WDate.xtend |
---|---|
method <(_aDate) native |
@NativeMessage("<") def lessThan(WDate aDate) {
|
NativeMessage annotation needs the name of the wollok method, then Wollok Interpreter is able to find the correct native method implementation for a wollok object.
Just to clarify: if you don't change the wollok method name, you don't need to use @NativeMessage.
A Wollok class should define its own constructors. Let's take a look into WDate wollok definition in wollok.wlk:
class Date {
constructor()
constructor(_day, _month, _year) { self.initialize(_day, _month, _year) }
method initialize(_day, _month, _year) native
So, users can instantiate wollok dates these ways:
const el2001 = new Date(4, 5, 2001)
const hoy = new Date()
Sometimes the native class might need access to the Wollok object instance, because there could be state or behaviour there defined in Wollok. For that there's a convention-over-configuration based on constructors.
A native Java class might have one of the following constructors
- new(): empty constructor, won't give you access to the object
- new(WollokObject): Wollok will pass you the Wollok object instance upon instantiation
- new(WollokObject, WollokInterpreter): in case you also need the interpreter instance.
Here is a small example showing access from java to the Wollok side.
Wollok side
class MyNativeWithAccessToObject {
var initialValue = 42
method lifeMeaning() native
method newDelta(d) native
method initialValue() {
return initialValue
}
}
Then xtend-side
class MyNativeWithAccessToObject {
const WollokObject obj
var delta = 100
new(WollokObject obj) {
self.obj = obj
}
def lifeMeaning() {
// accessing initialValue Wollok reference
delta + (obj.resolve("initialValue") as WollokInteger).wrapped
}
def newDelta(int newValue) {
delta = newValue
}
}
When you pass a Wollok Date, how is it converted to a WDate object?
You have to check WollokJavaConversions.xtend file, which handles bidirectional conversion in two methods:
- wollokToJava()
- convertJavaToWollok()
Lets see wollokToJava method:
def static Object wollokToJava(Object o, Class<?> t) {
if(o == null) return null
if(t.isInstance(o)) return o
if(t == Object) return o
if (o.isNativeType(CLOSURE) && t == Function1)
return [Object a|((o as WollokObject).getNativeObject(CLOSURE) as Function1).apply(a)]
if (o.isNativeType(INTEGER) && (t == Integer || t == Integer.TYPE))
return ((o as WollokObject).getNativeObject(INTEGER) as JavaWrapper<Integer>).wrapped
...
if (o.isNativeType(DATE)) {
return (o as WollokObject).getNativeObject(DATE)
}
...
throw new RuntimeException('''Cannot convert parameter "«o»" of type «o.class.name» to type "«t.simpleName»""''')
}
Here you convert a Wollok Date into a WDate, the native type in Java (or Xtend, it makes no difference).
Where do you put DATE, INTEGER, DOUBLE and other constants? In WollokSDK.xtend:
public static val DATE = "wollok.lang.Date"
There is also another important definition: DefaultObjectNativeFactory.xtend
class DefaultNativeObjectFactory implements NativeObjectFactory {
// static public as a temporary "cut the refactor" method
public static val Map<String, String> transformations = #{
OBJECT -> "wollok.lang.WObject",
...
NUMBER -> "wollok.lang.WNumber",
STRING -> "wollok.lang.WString",
BOOLEAN -> "wollok.lang.WBoolean",
DATE -> "wollok.lang.WDate"
}
Here you map a Wollok Date into a wollok.lang.WDate object.
And now we implement a multiple dispatch method in WollokJavaConversions ...
def static WollokObject javaToWollok(Object o) {
if(o == null) return null
convertJavaToWollok(o)
}
def static dispatch WollokObject convertJavaToWollok(LocalDate o) { evaluator.newInstanceWithWrapped(DATE, o) }
- What is evaluator? A message (sent to self/this) that returns an interpreter evaluator
- What does the evaluator do? Creates a new instance of a Wollok Date, wrapping a LocalDate inside.
- You must conform your class with an AbstractJavaWrapper, so you don't need to implement this method.
class WDate extends AbstractJavaWrapper<LocalDate> {
Let's take a look Range class, forEach method
def void forEach(WollokObject proc) {
val c = (proc.getNativeObject(CLOSURE) as Closure)
initWrapped.forEach[e| c.doApply(e.javaToWollok) ]
}
You can convert a parameter manually, from WollokObject to Closure in this case, but it's a heavy process to do every time you receive an argument. So automatic conversion in WollokJavaConversions is the recommended way.
- If you want a new Class, check if it is not already developed under a different name.
- Try to define a Wollok implementation of a class, unless limitations are obvious
- If benefits exceed costs, implement a native method. Try to delegate native implementation to Java or Xtends objects.
- Never return a native object as a result of a native method 🔥
def plusDays(int days) {
wrapped.plusDays(days)
new WDate(wrapped) // Don't do this
}
Instead, return a java object 👍
def plusDays(int days) {
wrapped.plusDays(days) // this returns a LocalDate, which will be converted in WollokJavaConversions
}
- Don't try to return another type of object in a constructor method. For example, if you are instantiating a Range, don't return a list.
In the same fashion you can also define native objects
Here is the Wollok side: wollok.wlk
package lib {
object console {
method println(obj) native
method readLine() native
method readInt() native
}
}
And then the native part is coded in xtend
wollok/lib/ConsoleObject.xtend
package wollok.lib
//...
class ConsoleObject {
val reader = new BufferedReader(new InputStreamReader(System.in))
def println(Object obj) {
WollokInterpreter.getInstance().getConsole().logMessage("" + obj);
}
def readLine() {
reader.readLine
}
def readInt() {
val line = reader.readLine
Integer.parseInt(line)
}
}
Notice that the convention for the name is pretty similar to the one for classes. The only difference is that the Java class simple name must comply with the convention:
$objectName.firstLetterUppercase + "Object"
(This console sample is actually one of the Wollok built-in objects part of the library)
There are a couple of limitations or warning when working with natives objects / classes
- Native methods cannot be overriden // TODO:
The following image shows the full graph for Wollok language syntax based on the grammar definition in xtext format (similar to EBNF)