/groovy-comprehension

Primary LanguageGroovyApache License 2.0Apache-2.0

groovy-comprehension

Overview

groovy-comprehension Groovy extension module provides simple list comprehension functionality similar to that of Haskell, Scala or Python. Let's look at simple example.

import groovyx.comprehension.keyword.select

assert select(x*2) {
    x: [1,2,3]
} == [2, 4, 6]

Where x is a variable which covers each of the values in list [1,2,3]. And whole of the select (..) {...} expression emits values of x*2 for each x as list.

In the other words, above code represent a list which are represented roughly with following math notation:

Follwing example uses two variables x and y.

import groovyx.comprehension.keyword.select

assert select([x,y]) {
    x: [1,2,3]
    y: [5,6,7]
} == [[1,5], [1,6], [1,7], [2,5],[2,6],[2,7], [3,5],[3,6],[3,7]]

In this case, all combinations for all value of x and y are generated. You can also use groovy's range expression to specify sequential values.

assert select([x,y]) {
    x: 1..3
    y: 5..7
} == [[1,5], [1,6], [1,7], [2,5],[2,6],[2,7], [3,5],[3,6],[3,7]]

Guard and Auto Guard

You can specify guard clause to filter values.

assert select([x,y]) {
    x: 1..3
    guard(x % 2 == 0)  // x is 2
    y: 5..7
    guard(y % 2 == 1) // y is 5 or 7
} == [[2,5], [2,7]]

Previous code can be written like following:

assert select([x,y]) {
    x: 1..3
    x % 2 == 0
    y: 5..7
    y % 2 == 1
} == [[2,5], [2,7]]

When the expression in comprehension returns boolean value at runtime, you can ommit explicit specifing guard.

Above code are roughly correspondes to following math expression:

You can specify guard clause to filter values.

where is just alias of whare.

assert select([x,y]) {
    x: 1..3
    where(x % 2 == 0)  // x is 2
    y: 5..7
    where(y % 2 == 1) // y is 5 or 7
} == [[2,5], [2,7]]

Import and Change Keyword

This comprehension fanctionality is enabled only when explicitly import the class groovyx.comprehension.keyword.select. So existing code which uses select identifier is safe as far as you don't import the class.

You can change the comprehension keyword select to other word by using import as.

import groovyx.comprehension.keyword.select as foreach
def list = foreach(n) { n:1..10 }

In this case, foreach can be used to specify comprehension instead of select.

Explicit Yield

This form

def list = select(n) { n:1..10 }

Is semantically equals to following:

def list = select { n:1..10; yield(n) }

where yield is same meaning of that in Scala, and return of Haskell aka unit function.

Examples

Pythagorean Numbers

You can calcurate numbers a, b and c which satisfies the equation , where a is equal or less then 10.

assert select {
    a: 1..10
    b: 1..a
    c: a..a+b
    a**2 + b**2 == c**2 // auto guard
    yield("(a=$a,b=$b,c=$c)")
} == ["(a=4,b=3,c=5)", "(a=8,b=6,c=10)"]

Pythagorean Numbers(Infinite Stream Version)

On the Java SE 8, you can use infinite lazy stream of java.util.stream.Stream in comprehension.

import groovyx.comprehension.keyword.select
import static java.util.stream.Collectors.toList

assert select ([a,b,c]) {
         a: iterate(1,{it+1})
         b: iterate(1,{it+1}).limit(a-1)
         c: iterate(a,{it+1}).limit(b)
         a**2 + b**2 == c**2
       }.skip(100).findFirst().get() == [144, 108, 180]

Verbal Arithmetic

Try to solve following verbal arithmetic.

   SEND
+) MORE
~~~~~~~~~~
  MONEY

Where alphabet S, E, N, D .. are correspond to one decimal digit different from each other. You can solve above verbal arithmetic using comprehension:

import groovyx.comprehension.keyword.select

def digits = 0..9
select("""\
  $S$E$N$D
+)$M$O$R$E
 $M$O$N$E$Y
""") {
    S:digits-0
    M:digits-S-0
    E:digits-S-M
    O:digits-S-M-E
    N:digits-S-M-E-O
    R:digits-S-M-E-O-N
    D:digits-S-M-E-O-N-R
    Y:digits-S-M-E-O-N-R-D
    (S*1000+E*100+N*10+D) + (M*1000+O*100+R*10+E) == (M*10000+O*1000+N*100+E*10+Y)
}.each { println it }

above code emit following:

  9567
+)1085
 10652

You can simplify the code using list-comprehension. Supply possible values for each variables (S,M,E ...) and add constraint that should be sutisfied, you can get the answer.

How to use

Through Maven Repository

groovy-comprehension jar are published at jcenter. So with gradle 1.7 or later:

apply plugin: 'groovy'

repositories {
    jcenter() // specify jcenter
}

dependencies {
    groovy 'org.codehaus.groovy:groovy-all:2.2.1'
    compile 'org.jggug.kobo:groovy-comprehension:0.3'
//  compile 'org.jggug.kobo:groovy-comprehension:0.3:java8'
    testCompile 'junit:junit:4.11'
}

If you are using gradle version older then 1.7, instaead of jcenter() specify:

repositories {
    maven {
        url "http://jcenter.bintray.com/"
    }
}

Get the Jar by Hand

Download jars from here and make JVM classpath reach it. For example, specify -cp option to the jar or put the jar into ~/.groovy/lib.

Grape/@Grab

You can use Groovy Grape's @Grab annotation:

@Grab("org.jggug.kobo:groovy-comprehension:0.3")
import groovyx.comprehension.keyword.select

Groovy 2.2 or later support JCenter as their standard grab resolver, you don't have to specify @GrabResolver annotation.

When you want to use Java 8 streams in comprehension, specifiy java8 as classifier:

@Grab("org.jggug.kobo:groovy-comprehension:0.3:java8")
import groovyx.comprehension.keyword.select

select(n) { n:1..10 }.each{
  println it
}

To use java 8 stream in comprehension, of cource you have to run groovy on java 8 jre/jdk JVM.

### Known Problems

As far as tried with groovy 2.2/2.3b, MacOS X 10.9, extenstion method looks sometimes doesn't work on Java 7/8 JVM. When you get following exception sometimes/always:

~~ ~~Caught: groovy.lang.MissingMethodException: No signature of method: groovy.lang.IntRange.bind() is applicable for argument types: (sample1_2$_run_closure1) values: [sample1_2$_run_closure1@5587f3a]~~ ~~ :~~ ~~

~~Workaround is to download jars from here statically to ~/.groovy/lib.~~

Same problem encounters when using timyates's groovy-stream, so I think this is not the BUG of this module. This issues are reported GROOVY-6446 and GROOVY-6447. please vote! Those issues are fixed. Thanks!

The Conversion

This feature is implemented with global AST transformation and groovy extension method.

This code

import groovyx.comprehension.keyword.select

def list = select("(a=$a,b=$b,c=$c)") {
   a: 1..10
   b: 1..a
   c: a..a+b
   a**2 + b**2 == c**2
}

is converted to follwing with ComprehensionTransformation:

    public java.lang.Object run() {
        java.lang.Object list = (1..10).bind({ java.lang.Object a ->
            delegate.autoGuard((1.. a )).bind({ java.lang.Object b ->
                delegate.autoGuard(( a .. a + b )).bind({ java.lang.Object c ->
                    delegate.autoGuard( a ** 2 + b ** 2 == c ** 2).bind({ java.lang.Object $$0 ->
                        delegate.yield("(a=$a,b=$b,c=$c)")
                    })
                })
            })
        })
        list == ['(a=4,b=3,c=5)', '(a=8,b=6,c=10)']
        this.println(list)
    }

You can desable autoGuard facility by specifying system property "groovy.comprehension.autoGuard" to not "true". In the case of specify -Dgroovy.comprehension.autoGuard=false with groovy or JVM, following code explicitry using guard(you can't ommit guard in this case):

import groovyx.comprehension.keyword.select

def list = select("(a=$a,b=$b,c=$c)") {
   a: 1..10
   b: 1..a
   c: a..a+b
   guard(a**2 + b**2 == c**2)
}

is converted to:

    public java.lang.Object run() {
        java.lang.Object list = (1..10).bind({ java.lang.Object a ->
            (1.. a ).bind({ java.lang.Object b ->
                ( a .. a + b ).bind({ java.lang.Object c ->
                    this.guard( a ** 2 + b ** 2 == c ** 2).bind({ java.lang.Object $$0 ->
                        delegate.yield("(a=$a,b=$b,c=$c)")
                    })
                })
            })
        })
    }

Monad comprehension

Not only list and stream, any class which have follwing instance method can be used in comprehension.

  • bind(Closure c)
  • yield(x)
  • autoGuard(boolean exp)

Those methods are needed on the meaning of duck typing (It is enough to have method but extends/implement perticuler class/interface). But as for convenience, class groovyx.comprehension.monad.MonadPlus is available.

Because of this MonadPlus provices default guard, autoGuard methods, your class which extends MonadPlus class are available in comprehension if you define following methods.

  • bind(Closure c)
  • yield(x)
  • mzero()

Maybe Monad Example

Just an example, define Maybe monad and use it with comprehension.

import groovyx.comprehension.keyword.select;
import groovyx.comprehension.monad.MonadPlus

@Newify([Just,Nothing])
class MaybeMonadTest extends GroovyTestCase {
    void test01() {
        assert (select {
                    Just(1)
                    Just(3)
                    Nothing()
                    Just(7)
                }) == Nothing()
    }
    void test02() {
        assert (select {
                    Just(1)
                    Just(3)
                    Just(4)
                    Just(7)
                }) == Just(7)
    }
    void test03() {
        assert (((Just(1) >> Just(3)) >> Nothing()) >> Just(7)) == Nothing()
    }
}

TODO

  • Support Set and Map
  • Make static type checking complient
  • Implement monadic combinator parser library using this comprehension