Groovy - Build Custom Syntax Using Groovy Domain-Specific Language and Perform Delegation

in #utopian-io6 years ago (edited)

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

And this is my current system states:
Untitled 01.png

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.

  1. The return keyword is optional. So, any block of codes, such as method, function or closure will always return something (at least null) implicitly even you don't intend to return any value. Which one? The last statement that can produce a value.
  2. 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 returns null.
  3. 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.
  4. 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
    
  5. 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
    
  6. 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)
    
  7. 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.
    
  8. 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 }
    
  9. 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'
    }
    
  10. 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.
  11. 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.
  12. 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,

  1. 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.
      Untitled 02.png
    • Now, enter com.murez as the GroupId and Lombok as the ArtifactId. About version, just ignore it.
      Untitled 03.png
    • 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.
      Untitled 04.png
    • My target directory is D:\Murez\Project\JVM\Groovy. So, this is my Project location,
      Untitled 05.png
      and at last click Finish.
      If successful, the results will be as follows,
      Untitled 06.png
      This is still incomplete, because there is no Gradle wrapper.
    • Finally, open the Terminal by pressing Alt + F12 or by accessing ViewTool WindowsTerminal, and executing the following command, gradle wrapper and then gradle build.
      The final state of the Lombok project can be seen as follows,
      Untitled 07.png

  2. 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.
      Untitled 08.png
    • 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 the src\main\groovy directory and also the sample test in the src\test\groovy.

Design Custom Loop Syntax


Here are some basic patterns that will be applied to the customized loop syntax.

  1. Loop over a range of index, which means that it starts from zero to n > 0.
    Loop.go {
     //statements
    } until positiveNumber
    
  2. Loop over a condition as long as it's always true.
    Loop.go {
     //statements
    } along { condition }
    
  3. 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.

  1. 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,
    next({  }, { /*main statements*/ })
    
    //or
    next {  }, {
         /*main statements*/
    }
    
    //or
    next({  }) {
         /*main statements*/
    }
    
    //and finally
    next {  } {
         /*main statements*/
    }
    
    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 the next method.
    //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
    
    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 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.
    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
    
    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.
    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.
  2. We will also provide an index by accessing the _key field and elements in the collection by accessing the _val field.
  3. Examples of correct use of the second form of loop.

Implement Custom Syntax Using Groovy DSL


  1. On the Project tab, right click on groovy directory and choose: NewGroovy Class. Then a new window will appear.
    Untitled 09.png

  2. Enter com.murez.util.Loop and hit OK.

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

  1. 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 }
    
    Untitled 10.png
  2. 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
    
    Untitled 11.png
  3. 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
    
    Untitled 12.png
  4. Something You Might Like. :-D
    Untitled 13.png
    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

Sort:  

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:

  • Nice work on the explanations of your code, although adding a bit more comments to the code can be helpful as well.
  • The tutorial is a bit extensive, it would be better to have split the tutorial in parts.

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!

Coin Marketplace

STEEM 0.28
TRX 0.13
JST 0.033
BTC 62875.13
ETH 3030.71
USDT 1.00
SBD 3.67