Groovy - Build Custom Syntax Using Groovy Domain-Specific Language and Perform Delegation
Repository
https://github.com/JetBrains/intellij-community
https://github.com/gradle/gradle
https://github.com/apache/groovy
What Will I Learn?
- Create a project using Gradle Build Tool
- Common powerful features of Groovy programming language
- Groovy Closure (An anonymous block of codes)
- How to delegate an object upon a Closure
- You will learn about Groovy Domain-Specific Language (DSL)
Requirements
- The latest version of Java™ SE Development Kit 8
- The latest version of Gradle Build Tool
- The latest stable version of Apache Groovy
- IntelliJ IDEA Community Edition
- Basic knowledge of Java SE
And this is my current system states:
I myself recommend you to install both JDK version before the system module is released, 8 and the latest one, 10. This is because there are still many platforms, including Groovy that have not migrated to the module system since JDK 9. Or there are several libraries on JDK 8, while in JDK 9 until the latest one is no longer included in the default runtime images. This is due to modularity. So, the latest runtime images only includes some limited packages in the java.base
module. You can still run Groovy properly using the latest JDK, as long as you have to manually add required modules that are no longer available into your custom runtime images.
Difficulty
- Intermediate
Tutorial Contents
Many Java developers have difficulty in developing software which relies on fast delivery of results, especially the Startup. The common issues are the syntax that is too static and its characteristic as statically typed programming language. So, in the case of productivity Java can't be superior against Python, Ruby or Node.js (JavaScript on server-side). Of course since the birth of JDK 7, the issues are out of date and things are getting better right now, because there are already many new and stable JVM-based programming languages that are awesome not only in increasing productivity, but also flexibility, mildness on the developer side in interpreting source codes and the most important point, the ecosystem becomes more solid. Related to this tutorial, I will present the coolest one, Groovy.
Why Have to Use Groovy?
- Groovy is open-source.
- Groovy is both a statically and dynamically (or event optionally) typed of both a programming and scripting language.
- Groovy supports both the current popular programming paradigm, object orientated and functional easily.
- Easy to learn and flexible syntax (You'll love this one), like Ruby.
- The ecosystem uses Groovy, such as Gradle, Spock, Grails and many others.
- Smooth Java and third-parties libraries integration.
- Easy authoring your own DSL, especially for readability of your codes.
The Disadvantages of Groovy
- As the same common problems as other dynamic languages, tracing a bug will be more difficult and some miracles just happen normally at runtime even though you were not warned by the compiler.
But Groovy, they can be minimized by using the@groovy.transform.TypeCheked
annotation, so that unexpected things and bugs can be prevented prematurely at compile time. Like a coin, if you see one side, you can't see the other side. So, when using this annotation for the sake of early prevention, then at the same time you will lose a lot of Groovy's powerful features. Not at all, please refer here for more details. - Groovy can access every members of class, even they are private. Therefore, the access modifiers become as if they are meaningless to Groovy. For Java lovers, things like this are not easily acceptable.
Groovy Quick Guide
Here are limited important idioms only to cover this tutorial. If you are experienced in Groovy, just skip this section.
- The
return
keyword is optional. So, any block of codes, such as method, function or closure will always return something (at leastnull
) implicitly even you don't intend to return any value. Which one? The last statement that can produce a value. - Groovy supports both an anonymous block of codes lambda expression and closure. You should use Closure because of its ability which is more powerful and flexible using delegation. It can be created easily, by
{ }
which means a closure that returnsnull
. - Semi-colon is optional by following the rule of one statement per line. But if more statements per line, it has to appear as separator.
- Closure can be executed by:
- Implicitly like method invocation, or
- Explicitly invoke
call
method.
def closure1 = { } def closure2 = { null } closure1() //(1) closure2.call() //(2) assert closure1() == closure2.call() //The same result
- By default, you can access an argument of closure without explicitly declaring a parameter using the
it
keyword. But this is no longer valid if the parameters are more than one.def pow = { it * it } //Equivalent to: pow = { it -> it * it } assert pow(2) == 4
- Parameter of code blocks can have a default value.
def multi(double a, double b = 1) { a * b } //The second parameter b has 1 as default value def sum = { int a, int b = 0 -> a + b } //The second parameter of this closure has 0 as default value assert multi(5) == sum(3, 2)
- Parentheses of codes block invocation is optional. But for several cases they must be present to eliminate obscurity.
def append = { String prefix, Closure getText -> prefix + getText().toString() } def result = append 'Lom', { 'bok' } println result //It will print: Lombok println append('Lom', { 'bok' }) //Closure of "append" has to invoke with parentheses, because it pass to "print" method as an argument.
- Groovy supports any illegal characters except dollar sign (
$
) and line separator for naming a block of codes by surrounding it using any quotes that form a literal string, but not interpolation.def 'sum or concat two obj'(a, b) { a + b }
- If there is any block of codes that has closure as its parameter in the final position, then it can be placed outside of the parentheses.
def append(String prefix, Closure getText) { prefix + getText() } append('Lom') { 'bok' } //or append 'Lom', { 'bok' } //or even without comma as argument separator def append(Closure<String> getPrefix, Closure getSuffix) { getPrefix() + getSuffix().toString() } append { 'Lom' } { 'bok' }
- Any class, method/function, interface without declaring any access modifier by default is a public. So, how to apply package scope visibility? By annotate it using
@PackageScope
annotation. - Any field without declaring any access modifier by default is a property. This means that it becomes private and has both a public getter and setter implicitly.
- A getter can be invoked like accessing its field directly and a setter can be invoked like assigning a value directly to its field.
class Person {
String name
}
def murez = new Person()
murez.name = 'Murez Nasution'
//Equivalent to: murez.setName('Murez Nasution')
println murez.name
//Equivalent to: println(murez.getName())
Overview
If you have a some experiences, you will realize that Groovy does not support the do-while
loop as Java can do. Groovy does not officially explain the reason, but we can easily analyze it as follows:
Suppose you have an interface,
interface Clause {
While(boolean condition)
}
and a function named Do
that accept a closure as an argument,
Clause Do(Closure loop) {
new Clause() {
@Override
def "while"(boolean condition) {
condition
}
}
}
then you can easily invoke that function as,
Do {
. . .
} While(true)
The above statement will be ambiguous against a do-while
loop. It becomes clear why it is not supported. So, I will raise this case to apply the custom syntax to replace a loop using Groovy DSL.
Create a Groovy Project using the Gradle Build Tool
This project will be named as Lombok which provides DSL for iteration over the Java collections which presents a do-while
loop. There are two ways to perform this initial creation,
Via IDE
- Open your IntelliJ IDEA
- On the Welcome to IntelliJ IDEA dialog, select Create New Project.
Then New Project dialog will appear. - On the left panel, select Gradle and then check Groovy. Make sure to choose JDK 8 on the Project SDK and click Next.
- Now, enter
com.murez
as the GroupId andLombok
as the ArtifactId. About version, just ignore it.
- Choose the option of Use local gradle distribuion, then IDEA will automatically scan the Gradle home directory for you if GRADLE_HOME already exists in your environment variables. And again make sure to choose JDK 8 on the Gradle JVM, then click Next.
- My target directory is
D:\Murez\Project\JVM\Groovy
. So, this is my Project location,
and at last click Finish.
If successful, the results will be as follows,
This is still incomplete, because there is no Gradle wrapper. - Finally, open the Terminal by pressing
Alt + F12
or by accessing View ⇢ Tool Windows ⇢ Terminal, and executing the following command,gradle wrapper
and thengradle build
.
The final state of the Lombok project can be seen as follows,
Via Gradle Command
- Open your Command Prompt
- Move to the target directory, mine is
D:\Murez\Project\JVM\Groovy
. - Create Lombok directory and again move after it.
- Now, execute gradle command:
gradle init --type groovy-library
.
- Open your IntelliJ IDEA, then close any active project.
- On the Welcome to IntelliJ IDEA dialog, select Import Project.
- Browse to
D:\Murez\Project\JVM\Groovy\Lombok
and click OK. - Next, choose Import project from external model and then click Gradle. Click Next.
- Still in the current dialog window, instead of Use local gradle distribution, you should choose Use default gradle wrapper. And finally, click Finish.
The result will be the same as using the IDE method as before. It's just that by using the gradle command,
init gradle [options]
, we have got the gradle wrapper at once and also the Groovy class sample in thesrc\main\groovy
directory and also the sample test in thesrc\test\groovy
.
Design Custom Loop Syntax
Here are some basic patterns that will be applied to the customized loop syntax.
- Loop over a range of index, which means that it starts from zero to
n > 0
.Loop.go { //statements } until positiveNumber
- Loop over a condition as long as it's always true.
Loop.go { //statements } along { condition }
- Loop along an object of Java collections, such as iterable, iterator and map.
Loop.go { //statements } along collections
We will not be able to use the do
and while
words, because they have been used as the preserved keywords. Instead, we use go
and until
/along
, because in my opinion they are simple and easy to remember.
If you are still confused, remember or refresh to section Groovy Quick Guide point 6 that parentheses are optional. Actually they are all equivalent as follows in Java code,
Closure statements = { /* statements */ };
Loop.go(statements).until(positiveNumber);
Loop.go(statements).along({ condition });
Loop.go(statements).along(collection);
Sometimes we need a state, which is an object that is always modified against every element in the collection along the loop. If the object has been modified in the first iteration, then it will be re-passed as an argument to the second iteration, and so on. Everything is OK without having to change our last syntax as follows,
int i = 0
Loop.go { //(1)
println "i = $i | it = $it"
++i
} until 5
//It will be the same as the following, but without declaring any integer variables
Loop.go { //(2)
if(it == null) //(3)
it = 0
println it
++it
} until 5
In case (2
), the performance of loop will decrease because condition (3
) will always be executed along the loop, whereas it is only needed in the first iteration. So, we provide a parameter at the go
method which can accept an initial object that will be used as a state. We make the parameter have a default value to support backward compatibility. So that loop (2
) will be as follows,
Loop.go(0) {
println it
++it
} until 5
So far our custom syntax is good, but I still have to demonstrate delegation of closure to you. So, we will try to cover the following issue,
def list = [ 'Murez', 'Nasution' ]
int n = list.size()
def builder = new StringBuilder('[')
for(int i = -1; ++i < n; ) {
builder.append('"').append(list.get(i)).append('"')
builder.append(',')
}
builder.append(']')
assert builder.toString() == '["Murez","Nasution",]'
println "${ builder.toString() } can't be parsed to JSON"
The resulting string of JSON array is still wrong, because there is a comma that is not expected in the last element. So, to solve this issue we will expand our syntax to be as follows,
Loop.go {
next { } //(1)
{
//the main statements
//can access _key and _val. (2)
}
} until positiveNumber
def list = [ 'Murez', 'Nasution' ]
Loop.go(new StringBuilder('[')) { //(3)
next { it.append(',') }
{
int index = _key()
it.append('"').append(list(index)).append('"')
return it
}
} along list
The following is the explanation.
- The
next
is a callback method to set a closure which will be called if there's a next element relative to this current cursor, and another one as main closure containing the main statements. It also can be invoked like this,
In the second form like this, we can no longer supply the main statements in the scope of the outer closure. Instead, we must move them to the scope of the inner one which is the second argument on thenext({ }, { /*main statements*/ }) //or next { }, { /*main statements*/ } //or next({ }) { /*main statements*/ } //and finally next { } { /*main statements*/ }
next
method.
Why can this happen? Actually this is a form of contract. The user defines the actions against the loop by supplying it as an argument to the//Instead of as follows Loop.go(new StringBuilder('[')) { next { it.append(',') } { //nothing to do } int index = _key() it.append('"').append(list(index)).append('"') it } along list //You should define it as follows Loop.go(new StringBuilder('[')) { next { it.append(',') } { int index = _key() it.append('"').append(list(index)).append('"') it } } along list
next
method. However, since the user has declared his contract, it has not been accepted as long as the provider has not executed the outer closure.
Maybe you will be confused with this, but I'm sure you will understand after observing the implementation. The important thing I want to convey is the first form and the second one is the outer closure execution that has different result and interpretation.boolean invoked = false def setter = { action -> println 'I am setter and has already invoked' action() invoked = true } def outer = { setter { println 'I am a contract' } } println invoked //Still false, which means that the contract is still not accepted outer() //By invoke the outer closure, setter has been called and a contract has already accepted println invoked //Finally, true
In the first form, the outer closure is the main action that will be executed over all of the iterations. So that in the first execution, it has started the first iteration.
Whereas in the second form, the outer closure is not the main action. So that in the first execution, only to get a contract, i.e. the main action and the next one. And actually, have not started any iteration at all. - We will also provide an index by accessing the
_key
field and elements in the collection by accessing the_val
field. - Examples of correct use of the second form of loop.
Implement Custom Syntax Using Groovy DSL
On the Project tab, right click on
groovy
directory and choose: New ⇢ Groovy Class. Then a new window will appear.
Enter
com.murez.util.Loop
and hit OK.And the following is the source codes of
Loop
class,
package com.murez.util
import static groovy.lang.Closure.DELEGATE_FIRST as X
/**
* @author Murez Nasution
*/
class Loop {
static final STOP = new Object()
static go(def init = null, Closure loop) { new Loop(init, loop) }
private Closure loop, next
private v, k = 0
private init
private test = 'No! I am private.'
private Loop(def init, Closure loop) {
this.init = init
this.loop = loop
}
def until(int last) {
if(init == STOP) return STOP
def o
try { o = 'do'() }
catch(NullPointerException | NoSuchLoopException ignored) { return }
catch(e) { throw e }
if(k < last) {
if(next) {
for(; ++k < last; ) o = loop next(o)
} else
for(; ++k < last; ) o = loop o
}
}
def along(Closure<Boolean> condition) {
if(init == STOP) return STOP
def o
try { o = 'do'() }
catch(NullPointerException | NoSuchLoopException ignored) { return }
catch(e) { throw e }
if(condition) {
if(!next) {
for(; condition(++k); ) o = loop o
} else
for(; condition(++k); ) o = loop next(o)
}
}
def along(Iterable collection) {
if(init == STOP) return STOP
def i, o
try {
i = collection.iterator()
if(!(o = i.hasNext())) return o
v = i.next()
o = 'do'()
}
catch(NullPointerException | NoSuchLoopException ignored) { return }
catch(e) { throw e }
if(next) {
for(; i.hasNext(); o = loop o) {
o = next o
++k
v = i.next()
}
} else
for(; i.hasNext(); o = loop o) {
++k
v = i.next()
}
o
}
def along(Map entryPairs, Closure<Boolean> ctrl = null) {
if(init == STOP) return STOP
def o, i
try {
i = entryPairs.entrySet().iterator()
if(!(o = i.hasNext())) return o
set i
o = 'do'() }
catch(NullPointerException | NoSuchLoopException ignored) { return }
catch(e) { throw e }
if(next) {
for(; i.hasNext(); o = loop o) {
o = next o
set i
}
} else
for(; i.hasNext(); o = loop o) set i
o
}
private void set(Iterator<Map.Entry> i) {
def e = i.next()
k = e.key
v = e.value
}
private 'do'() {
try { loop.resolveStrategy = X }
catch(e) { throw e }
def args = [ flag: true ]
loop.delegate = new Delegator({ k }, { v }, {
nextAction, mainAction ->
next = nextAction
args.main = mainAction
args.flag = null
})
def o = loop init
if(!args.flag) {
if(!args.main) throw new NoSuchLoopException()
loop = args.main as Closure
loop init
} else o
}
private final class Delegator {
Closure<Void> next
Closure _key, _val
private Delegator(key, val, setter) {
_key = key
_val = val
next = setter
}
}
private final class NoSuchLoopException extends RuntimeException {
private NoSuchLoopException() {
super('No action to be performed')
}
}
}
As seen, the next
method has never been defined, but user can use it without any error at runtime. This is because we have delegated an object created from the Delegator
class to the target closure, which is main closure (in this class I named it as a loop
). When a program does not find any variables or methods that have never been defined, it will try to look for it first to the delegated object. If the object never exists, then an error will occur.
We can also specify the delegation strategy by giving a delegation constant via the resolveStrategy
property. Here I use DELEGATE_FIRST
which means that if there are two definitions that are the same wherever the program can find it, the highest priority is the delegated object.
Test
It's like creating a Groovy class before, but instead of choosing a class you should choose Groovy Script and be named as com.murez.Main
. With Groovy script, you can write any codes directly without having to define the class first, like JavaScript.
Since we use the Gradle build tool, there are two ways to run the main program, i.e. via the IDE or Gradle. Well, running via an IDE is normal, just focus on the script or class that has the public static main(String[])
method and press the Ctrl + Shift + F10
. But if via gradle, we have to change the build.gradle
file a bit by adding the application
plugin and setting the mainClassName
property to the class name that will be executed as the main program, as follows,
plugins {
id 'groovy'
id 'application'
}
mainClassName = 'com.murez.Main'
sourceCompatibility = 1.8
targetCompatibility = 1.8
version = '0.0.1-SNAPSHOT'
group = 'com.murez'
repositories {
jcenter()
}
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.4.15'
testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
}
Finally, just execute gradle run
if you have installed Gradle on the local machine and with the same version or if not, you should use Gradle Wrapper gradlew run
command (this is the recommended way) on the Terminal and gradle will show the output.
Now my project can be run easily by anyone just by executing gradlew run
command, even if they don't have Gradle installed on the local machine. Everything is solved over the network and make sure you have internet access.
Finally, the following is a summary of the test
- Basic Loop
Loop.go { println 'Just print once' } along { false } println '___________' Loop.go { println _key() } until 5 println '___________' int i = 0 Loop.go { println "i = $i | index = ${ _key() }" ++i } along { i < 5 }
- Traverse the Collections
def list = [ 'Murez', 'Nasution', 'Apache Groovy' ] Loop.go { println "index = ${ _key() } | value = ${ _val() }" } along list println '_____________________________________' println() def map = [ name: 'Murez Nasution', email: 'murez.nasution@gmail', contact: 963852741 ] Loop.go { println "key = ${ _key() } | value = ${ _val() }" } along map
- Build String of JSON
def person = [ name: 'Murez Nasution', email: 'murez.nasution@gmail', contact: 963852741 ] def json = Loop.go person.size() > 0? new StringBuilder('{') : Loop.STOP, { next { it.append ',' } { it.append '"' append _key() append '"' append ':' def value if((value = _val()) == null || value instanceof Number) it.append value else it.append '"' append value append '"' } } along person append '}' print json
- Something You Might Like. :-D
As I explained earlier, Groovy can access private variables. But I still have some tricks to hide it and will not be discussed now.
Thank you!
Proof of Work Done
https://github.com/murez-nst/JVM-Based/tree/master/Groovy/Lombok
Thank you for your contribution.
While I liked the content of your contribution, I would still like to extend few advices for your upcoming contributions:
Looking forward to your upcoming tutorials.
Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.
To view those questions and the relevant answers related to your post, click here.
Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]
Thank you for your review, @portugalcoin!
So far this week you've reviewed 1 contributions. Keep up the good work!
Hi @murez-nst! We are @steem-ua, a new Steem dApp, computing UserAuthority for all accounts on Steem. We are currently in test modus upvoting quality Utopian-io contributions! Nice work!
Hi @murez-nst, I'm @checky ! While checking the mentions made in this post I noticed that @packagescope doesn't exist on Steem. Maybe you made a typo ?
If you found this comment useful, consider upvoting it to help keep this bot running. You can see a list of all available commands by replying with
!help
.Hey @murez-nst
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!
Want to chat? Join us on Discord https://discord.gg/h52nFrV.
Vote for Utopian Witness!