GEP-3

Author:


Metadata
Number

GEP-3

Title

Command Expression based DSL

Version

2

Type

Feature

Target

Groovy 1.8 or 2.0

Status

Final

Comment

Included in Groovy and has been further enhanced

Leader

Jochen "blackdrag" Theodorou

Created

2009-06-30

Last modification 

2018-10-12

Abstract

Since Groovy 1.0 Groovy supports command expressions. These are method calls without parenthesizing the arguments. This would be in theory a nice base for DSLs, but our command expressions are too limited, because we were not able to find easy rules on how to handle multiple arguments. This proposal now tries to close the gap by defining the evaluation order and meaning of those arguments. The concept is very near to what Scala allows, but is not equal for historic reasons.

Rationale

Current Command Expression

Examples of current command expressions are:

Command expression Meaning

foo a1

foo (a1)

foo {c}

foo ({c})

foo m {c}

foo (m({c})

foo a1, a2

foo (a1, a2)

foo k1:v1, a2, k2:v2

foo ([k1:v1, k2:v2], a2)

foo k1:m{c}

foo ([k1:m({c})])

Examples of current command expressions, that are not allowed:

Command expression Possible meanings

foo a1 a2

foo(a1,a2)
foo(a1(a2))
foo(a1).a2

foo a1 a2 a3

foo(a1,a2,a3)
foo(a1(a2(a3)))
foo(a1).a2(a3)
foo(a1,a2(a3))

This list is not intended to be complete.

Constraints

  • existing valid usages must be kept as much as possible (for obvious backwards compatibility reasons)

  • the evaluation must be easily explainable

  • the grammar should support it

Details

What I want to allow are expressions such as:

Expression Possible meanings Allowed in old syntax

foo {c}

foo({c})

[thumbs up] (same meaning)

foo a1

foo(a1)

[thumbs up] (same meaning)

foo a1()

foo(a1())

[thumbs up] (same meaning)

foo a1 {c}

foo(a1({c}))

[thumbs up] (same meaning)

foo a1 a2

[thumbs down]

[thumbs down]

foo a1() a2

[thumbs down]

[thumbs down]

foo a1 a2()

[thumbs down]

[thumbs down]

foo a1 a2 {c}

[thumbs down]

[thumbs down]

foo a1 {c} a2

[thumbs down]

[thumbs down]

foo a1 {c} a2 {c}

[thumbs down]

[thumbs down]

foo a1 a2 a3

foo(a1).a2(a3)

[thumbs down]

foo a1() a2 a3()

foo(a1()).a2(a3())

[thumbs down]

foo a1 a2() a3

[thumbs down]

[thumbs down]

foo a1 a2 a3 {c}

foo(a1).a2(a3({c}))

[thumbs down]

foo a1 a2 a3 a4

[thumbs down]

[thumbs down]

foo a1 a2 a3 a4 {c}

[thumbs down]

[thumbs down]

foo a1 a2 a3 a4 a5

foo(a1).a2(a3).a4(a5)

[thumbs down]

foo a1() a2 a3() a4 a5()

foo(a1()).a2(a3()).a4(a5())

[thumbs down]

foo a1 a2 a3 a4 a5 {c}

foo(a1).a2(a3).a4(a5({c})

[thumbs down]

The table shows enough to recognize the pattern. The attached block has a special role as it does not count as argument on its own directly. Instead the block is always bound to the identifier before and makes a method call. That itself is no command expression, but a normal method call expression. As can be seen too, this syntax nicely extends the existing Groovy syntax. Of course this also means, it will not be possible to omit commas if multiple arguments are used. A case that is not supported today anyway. For a DSL that is not really a problem though.

Summary of the pattern

  • A command-expression is composed of an even number of elements

  • The elements are alternating a method name, and its parameters (can be named and non-named parameters)

  • A parameter element can be any kind of expression (ie. a method call foo(), foo{}, or some expression like x+y)

  • All those pairs of method name and parameters are actually chained method calls (ie. send "hello" to "Guillaume" is two methods chained one after the other as send("hello").to("Guillaume"))

Interesting benefit of the enhanced command expressions

More and more do we see Java Fluent APIs that chain method calls, returning this, so as to "build" a new object. For instance, you can imagine a fluent API for building an Email message, that would look something like this in Java:

Email.from("foo@example.com").to("bar@example.com").subject("hello").body("how are you?")

In Groovy, with the extended command expressions, this could become:

Email.from "foo@example.com" to "bar@example.com" subject "hello" body "how are you?"

Notice the absence of parentheses and dots.

Example: A DSL for SQL

SELECT "column_name"
FROM "table_name"
WHERE "column_name" IN ('value1', 'value2', ...)

In current Groovy this could maybe expressed by

sql.select(
  "column_name",
  from:"table_name",
  where:"column_name",
  in:['value1','value2',...])

With this new command dsl you could also do

sql.
  select "column_name" \\
  from "table_name" \\
  where "column_name" \\
  in ['value1','value2',...]

It should be noticed, that both cases have quite different semantics. In the second case the writer saves a lot of commas, but of course not all of them. Also the lack of any kind of operator like the comma makes it diifivult to span the DSL across multiple lines. A more extended example would be

SELECT COUNT("column_name")
FROM "table_name"
sql.select count("column_name") from "table_name"

To express this in map style is a bit difficult, because of where to place count…​ a possible version is mabye

sql.select(sql.count("column_name"), from:"table_name"

More example ideas

Here are some additional examples which relate to various domains, which may make the idea more visual in our minds. These examples also mix named and non-named arguments, the use closures or not. In comments, alongside the example, you’ll see the equivalent non-command expression interpretation.

sell 100.shares of MSFT // sell(100.shares).of(MSFT)
every 10.minutes, execute {} // already possible with current command expressions
schedule executionOf { ... } every 10.minutes // scheduler(executionOf({})).every(10.minutes)
blend red, green of acrylic // blend(red, gree).of(acrylic)

// named parameters into the mix
select from: users where age > 32 and sex == 'male'
// equivalent to select(from: users).where(age > 32).and(sex == 'male')
// not that however for this example, it would be intersting
// to transparently convert the boolean conditions into closure expressions!

// a recipe DSL
take mediumBowl
combine soySauce, vinegar, chiliPowder, garlic
place chicken in sauce
turn once to coat
marinate 30.minutes at roomTemperature

Extension to command expressions in the case of assignments

Currently, command expressions are allowed as standalone top-leval statements or expressions, but you can’t assign such an expression to a variable with keeping that nice DSL syntax. For instance, while you can do:

move left

If you wanted to assign that command (which could return a Position instance), you would like to do

def newPosition = move left

But you still have to do

def newPosition = move(left)

So the GEP-3 proposal also suggests we extend command expressions to be allowed on the RHS of assignments.

Differences to Scala

For historic reasons

println foo

has to be supported. This seems to not to be a valid version in Scala, since that would be interpreted as

println.foo

and not as

this.println foo

On the other hand

foo bar a1

is interpreted as

foo.bar(a1)

in Scala and is invalid in current Groovy as well as after this proposal. So it could be stated, that this proposal is less object oriented then Scala, because the DSL usually starts with the method, not the object. On the other hand it is possible to write

foo.bar a1

So the Groovy notation would be a bit more verbose, but not much.

To be evaluated: Mixed case with explicit parentheses

A possible supported case is also when mixing method calls with explicit parentheses within that extended command expression. The benefit would be to allow the ability to also be able to call methods not taking parameters, as well as allowing an odd number of "elements" (ie. a method name or a parameter).

m1 a m2 b m3()
m1 a m2() m3 b
m1() m2 a m3 b

would be respectively equivalent to:

m1(a).m2(b).m3()
m1(a).m2().m3(b)
m1().m2(a).m3(b)

Note that the method calls with explicit parentheses could also take a number of arguments. For instance, this is also a valid mixed command expression:

m1 a m2(1, 2, 3) m3 b

JIRA issues:

  • Implement GEP-3: extended command expressions GROOVY-4384

  • Ability to use (extended) command expression on the RHS GROOVY-4401

  • Allow zero-args methods in the chain of calls GROOVY-4402

  • Disambiguate cases where minus something or [] or {} are used as the argument of extended command expressions GROOVY-4403

Update history

1 (2009-06-17)

Version as extracted from Codehaus wiki

2 (2018-10-11)

Numerous minor tweaks