Skip to content

Developing a Wollok Class

javierfernandes edited this page Jul 12, 2016 · 17 revisions

So, I want to define a new class. What should I do?

Define a Wollok class/object

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

Wollok classes, references and methods

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

Native methods

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).

Native classes in WDK Lib

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

Native methods in depth

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.

Constructors

How should I define a constructor? Well, if you model a wollok wko object is already instantiated and you can jump to next section.

Wollok class

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
     

Conversions between Java and Wollok

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()

Wollok -> Java

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.

Java -> Wollok

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> {

Manual conversion from Wollok to Java

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.

Best practices

  • 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.
Clone this wiki locally