Skip to content

Developing a Wollok Class

Fernando Dodino edited this page Aug 17, 2016 · 17 revisions

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

Define a Wollok class/object

Edit lang.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 lang.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. Then you can use native clause in lang.wlk:

method forEach(closure) 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

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
	}

	
}

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.

Native methods in depth

Every wollok "native" method has its corresponding native implementation in Java/Xtend/etc.

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

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

A Wollok class should define its own constructors. Let's take a look into WDate wollok definition in lang.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()

Access to Wollok Object

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

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.

Native Objects

In the same fashion you can also define native objects

Here is the Wollok side: lang.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)

Caveats about natives

There are a couple of limitations or warning when working with natives objects / classes

  • Native methods cannot be overriden // TODO:

Appendix

Syntax Graph

The following image shows the full graph for Wollok language syntax based on the grammar definition in xtext format (similar to EBNF)

wollok-syntax-graph.png

Clone this wiki locally