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]]
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]]
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
.
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.
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)"]
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]
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.
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/"
}
}
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
.
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!
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)")
})
})
})
})
}
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()
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()
}
}
- Support Set and Map
- Make static type checking complient
- Implement monadic combinator parser library using this comprehension