-
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, like forEach. Then you can use native clause in wollok.wlk:
method forEach(closure) native
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
}
}
You can see here the real implementation of forEach method
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) { |
But, what happens if you want to map a method with a different name in Java? And what if you want to use a Java reserved word or symbol? Then you can use @NativeMessage annotation, like in < message for WDates:
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.
How should I define a constructor? Well, if you model a wollok wko object is already instantiated and you can jump to next section.
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()
Native objects have 3 possible constructors in WDate.xtend:
new() { ... }
new(WollokObject obj) {
// here you can access wollok object
// (referenced in Wollok as el2001 or hoy)
// in Range, you can access a Wollok reference like start or end...
obj.resolve("start").wollokToJava(Integer) as Integer
new(WollokObject obj, WollokInterpreter interpreter) {
// interpreter allows you to evaluate
// a wollok expression
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.