Language Guide
- The REPL
- Command Line Scripts
- Statement Termination: Newlines and Semicolons
- Comments
- Variables
- Keywords
- Types
- Truthiness
- Variable Declarations
- Expressions and Operators
- Regular Expressions
- Eval Statement
- If/Else Statements
- If/Unless Statements
- Loop Statements
- Do Expression
- Print Statements
- Die Statements
- Functions
- Closures
- Collection Methods
- Checkpointing
- Classes
- Switch Expressions and Pattern Matching with Destructuring
- JSON Support
- Built-in Global Functions
- Built-in Methods
The REPL
The easiest way to start learning about the Jactl language is to use the REPL (read-evaluate-print-loop) that comes
with Jactl.
It gives you a >
prompt where you can enter Jactl code for immediate execution.
$ java -cp jactl-repl-2.0.0.jar jactl.Repl
> int x = 3 + 4
7
To exit the REPL use the :q
command or ctrl-D
.
Anything that does not start with :
is interpreted as Jactl code and is evaluated immediately and the result
of the evaluation is printed before prompting again (hence the term read-evaluate-print-loop). For each line that
is read, if the code does not look like it is complete the REPL will keep reading until it gets a complete
expression or statement that can be executed:
> 3 * 4
12
> int x = (17 * 13) % 6
5
> if (x < 4) {
println 'x is less than 4'
} else {
println 'x is not less than 4'
}
x is not less than 4
>
See Jactl REPL for more details on how to run the REPL.
Command Line Scripts
Jactl scripts can also be invoked from the command line:
$ java -jar jactl-2.0.0.jar
Usage: jactl [switches] [programFile] [inputFile]* [--] [arguments]*
-p : run in a print loop reading input from stdin or files
-n : run in a loop without printing each line
-e script : script string is interpreted as Jactl code (programFile not used)
-v : show verbose errors (give stack trace)
-V var=value : initialise Jactl variable before running script
-d : debug: output generated code
-h : print this help
Using the -e
option allows you to supply the script on the comand line itself:
$ java -jar jactl-2.0.0.jar -e '3 * 4'
12
If you have a Jactl script in a file called myscript.jactl
then you can the script like this:
$ java -jar jactl-2.0.0.jar myscript.jactl
See Command Line Scripts for more details about how to invoke scripts from the command line.
Statement Termination: Newlines and Semicolons
Jactl syntax borrows heavily from Java, Groovy, and Perl and so while Java and Perl both require a semicolon to terminate simple statements, Jactl adopts the Groovy approach where semicolons are optional. The only time a semicolon is required as a statement separator is if more than one statement appears on the same line:
> int x = 1; println 'x = ' + x; println 'x is 2' if x == 2
x = 1
The Jactl compiler, when it encounters a newline, will try to see if the current expression or statement continues on the next line. Sometimes it can be ambiguous whether the newline terminates the current statement or not and in these situations, Jactl will treat the newline is a terminator. For example, consider this:
def x = [1,2,3]
x
[0]
Since expressions on their own are valid statements, it is not clear whether the last two lines should be interpreted
as x[0]
or x
and another statement [0]
which is a list containing 0
.
In this case, Jactl will compile the code as two separate statements.
If such a situation occurs within parentheses or where it is clear that the current statement has not finished, Jactl will treat the newline as whitespace. For example:
def x = [1,2,3]
if (
x
[0]
) {
x = [4,5,6]
}
In this case, the newlines within the if
statement condition will be treated as whitespace and the expression
will be interpreted as x[0]
.
Comments
Comments in Jactl are the same as for Java and Groovy and are denoted using either line comment form, where //
is
used to begin a comment that extends to the end of the line, or with a pair of /*
and */
that delimit a comment that
can be either embeeded within a single line or span multiple lines:
> int x = 3 // define a new variable x
3
> int y = /* comment */ x * 4
12
> int /* this
is a multi-line comment
*/ z = x * y // another comment at the end of the line
36
Variables
A variable in Jactl is a symbol that is used to refer to a value. Variables are declared by specifying a type followed by their name and an optional expression to use as an initial value for the variable. If no initialization expression is given then the default value for that type is assigned.
After declaration, variables can have new values assigned to them as long as the new value is compatible with the type that was specified for the variable at declaration time. Variables can be used in expressions at which point their current value is retrieved and used within that expression:
> int x = 4
4
> x = 3 * 8
24
> int y // gets default value of 0
0
> y = x / 6
4
Valid Variable Names
Variable names must be a valid identifier. Identifiers start with a letter or with an underscore _
followed
by a combination of letters, digits and underscores. The only special case is that a single underscore _
is not a
valid identifier.
Variable names cannot clash with any built-in language keywords:
> int for = 3
Unexpected token 'for': Expecting identifier @ line 1, column 5
int for = 3
^
Note
Since class names in Jactl must start with a capital letter it is good practice not to start variable names with a capital letter in order to make your code easier to read.
Note
Unlike Java, the dollar sign$
is not a valid character for a variable name in Jactl.
While most of the examples presented here have single character variable names, in general, your code should use names that are more meaningful. It is recommended that when a variable name consists of more than one word that camel case is used to indicate the word boundaries by capitalizing the first letter of following words. For example:
- firstName
- accountNumber
- emailAddress
Keywords
The following table lists the reserved keywords used by Jactl:
Key Words | ||||
---|---|---|---|---|
BEGIN | Decimal | END | List | Map |
Object | String | _ | and | as |
boolean | break | byte | class | const |
continue | def | default | die | do |
double | else | eval | extends | false |
final | for | if | implements | import |
in | instanceof | int | interface | long |
new | not | null | or | package |
println | return | sealed | static | |
switch | true | unless | until | var |
void | while |
Types
Standard Types
Jactl supports a few built-in types as well as the ability to build your own types by creating Classes.
Types are used when declaring variables, fields of classes, function return types, and function parameter types.
The standard types are:
Name | Description | Default Value | Examples |
---|---|---|---|
boolean | True or false | false |
true , false |
int | Integers (32 bit) | 0 |
0 , 1 , -99 |
long | Large integers (64 bit) | 0L |
0L , 99999999999L |
double | Floating point numbers | 0D |
0.01D , 1.234D , -99.99D |
Decimal | Decimal numbers | 0 |
0.0 , 0.2345 , 1244.35 |
String | Strings | '' |
'abc' , 'some string value' , "y=${x * 4}" , /^x?[0-9]*$/ |
List | Used for lists of values | [] |
[1,2,3] , ['a',[1,2,3],'c'] |
Map | Map of key/value pairs | [:] |
[a:1] , [x:1, y:[1,2,3], z:[a:1,b:2]] , {d:2,e:5} |
def | Used for untyped variables | null |
Inferred Type
Variables can also be declared using the var
keyword if an initialiser expression is given.
Jactl will then create a variable of the same type as the initialisation expression:
> var x = 1 // x will have type int
1
> var y = 2L // y will have type long
2
> var z = x + y // z will have type long since adding int and long results in a long
3
> var label = 'some place' // label has type String
some place
Dynamic/Weak Typing
While Jactl supports the use of specific types when declaring variables and functions, Jactl can also be used as
a weakly or dynamically typed language (also known as duck typing). The keyword def
is used to define variables and
functions in situations where the type may not be known up front or where the variable will contain values of different
types at different points in time:
> def x = 123
123
> x = 'string value'
string value
Although we haven’t covered functions yet, here is an example of creating a function where the return type and
parameter type are specified as int
:
> int fib(int x) { x < 2 ? x : fib(x-1) + fib(x-2) }
Function@1534755892
> fib(20)
6765
Here is the same function where we use def
instead:
> def fib(def x) { x < 2 ? x : fib(x-1) + fib(x-2) }
Function@26757919
> fib(20)
6765
For parameters, the type is optional and if not present it is as though def
were specified:
> def fib(x) { x < 2 ? x : fib(x-1) + fib(x-2) }
Function@45822040
> fib(20)
6765
Numbers
Integers
Jactl supports two types of integers: int
and long
.
As with Java, 32-bit integers are represented by int
while long
is used for 64-bit integers.
The range of values are as follows:
Type | Minimum value | Maximum Value |
---|---|---|
int |
-2147483648 | 2147483647 |
long |
-9223372036854775808 | 9223372036854775807 |
To force a literal value to be a long
rather than an int
append L
to the number:
> 9223372036854775807
Error at line 1, column 1: Integer literal too large for int
9223372036854775807
^
> 9223372036854775807L
9223372036854775807
Floating Point
In Jactl, by default, floating point numbers are represented by the Decimal
type and numeric literals that have
a decimal point will be interpreted as Decimal numbers.
Decimal numbers are represented internally using the Java BigDecimal
class. This avoids the problems
of trying to store base 10 numbers inexactly using a binary floating point representation.
Jactl also offers the ability to use native floating point numbers by using the type double
(which corresponds to
the Java type double
) for situations where preformance is more important than having exact values.
When using doubles, constants of type double
should use the D
suffix to prevent them being interpreted
as Decimal constants and to avoid unnecessary overhead:
> double d = amount * 1.5D
To illustrate how Decimal values and double values behave differently, consider the following example:
> 12.12D + 12.11D // floating point double values give approximate value
24.229999999999997
> 12.12 + 12.11 // Decimal values give exact value
24.23
Strings
Strings in Jactl are usually delimited with single quotes:
> 'abc'
abc
Multi-line strings use triple quotes as delimiters:
> '''this is
a multi-line
string'''
this is
a multi-line
string
Special Characters
Special characters such as newlines can be embedded in strings by using the appropriate escape sequence. The following escape sequences are supported:
Character | Escape Sequence |
---|---|
Newline | \n |
Carriage return | \r |
Tab | \t |
Formfeed | \f |
Backspace | \b |
Backslash | \ |
Single quote | ' |
For example:
> 'a\\b\t\'c\'\td\ne'
a\b 'c' d
e
Subscripts and Characters
Subscript notation can be used to access individual characters:
> 'abc'[0]
a
Note that characters in Jactl are just strings whose length is 1. Unlike Java, there is no separate char
type to
represent an individual character:
> 'abc'[2] == 'c'
true
Subscripts can be negative which is interpreted as an offset from the end of the string.
So to get the last character from a string use an index of -1
:
> 'xyz'[-1]
z
If you need to get the Unicode number for a given character you can cast the single character string into an int:
> (int)'a'
97
To convert back from a Unicode number to a single character string use the asChar
method that exists for int values:
> 97.asChar()
a
> def x = (int)'X'
88
> x.asChar()
X
String Operators
Strings can be concatenated using +
:
> 'abc' + 'def'
abcdef
If two objects are added using +
and the left-hand side of the +
is a string then the other is converted to a
string before concatenation takes place:
> 'abc' + 1 + 2
abc12
The *
operator can be used to repeat multiple instances of a string:
> 'abc' * 3
abcabcabc
The in
and !in
operators can be used to check for substrings within a string:
> 'x' in 'xyz'
true
> 'bc' in 'abcd'
true
> 'xy' in 'abc'
false
> 'ab' !in 'xyz'
true
Expression Strings
Strings that are delimited with double quotes can have embedded expressions inside them where $
is used to denote the
start of the expression. If the expression is a simple identifier then it identifies a variable whose value should be
expanded in the string:
> def x = 5
5
> "x = $x"
x = 5
> def msg = 'some message'
some message
> "Received message '$msg' from sender"
Received message 'some message' from sender
If the expression is surrounded in {}
then any arbitrary Jactl expression can be included:
> def str = 'xyz'
xyz
> def i = 3
3
> "Here are $i copies of $str: ${str * i}"
Here are 3 copies of xyz: xyzxyzxyz
You can, of course, have further nested interpolated strings within the expression itself:
> "This is a ${((int)'a'+2).asChar() + 'on' + "tr${'i' * (3*6 % 17) + 118.asChar()}e" + 'd'} example"
This is a contrived example
As with standard single quoted strings, multi-line interpolated strings are supported with the use of triple double quotes:
> def x = 'pol'
pol
> def y = 'ate'
ate
> """This is a multi-line
inter${x + y + 'abcd'[3]} string"""
This is a multi-line
interpolated string
While it is good practice to use only simple expressions within a ${}
section of an expression string,
it is actually possible for the code block within the ${}
to contain multiple statements.
If the embeded expression contains multiple statements then the value of the last statement is used as the value
to be inserted into the string:
> "First 5 pyramid numbers: ${ def p(n){ n==1 ? 1 : n*n + p(n-1) }; [1,2,3,4,5].map{p(it)}.join(', ') }"
First 5 pyramid numbers: 1, 5, 14, 30, 55
You can use return
statements from anywhere within the block of statements to return the value to be used
for the embedded expression.
The return
just returns a value from the embedded expression; it does not cause a return
to occur in the surrounding function where the expression string resides.
For example:
> def x = 3; "x is ${return 'odd' if x % 2 == 1; return 'even' if x % 2 == 0}"
x is odd
Pattern Strings
In order to better support regular expressions, pattern strings can be delimited with /
and are multi-line strings
where standard backslash escaping for \n
, \r
, etc. is not performed. Backslashes can be used to escape /
, $
,
and any other regex specific characters such as [
, {
, (
etc. to treat those characters as normal and not have
them be interpreted as regex syntax:
> String x = 'abc.d[123]'
abc[123]
> String pattern = /c\.d\[12[0-9]]/
c\.d\[12[0-9]]
> x =~ pattern // check if x matches pattern using =~ regex operator
true
Pattern strings are also expression strings and thus support embedded expressions within ${}
sections of the regex
string:
> def x = 'abc.d[123]'
abc.d[123]
> x =~ /abc\.d\[${100+23}]/
true
Pattern strings also support multiple lines:
> def p = /this is
a multi-line regex string/
this is
a multi-line regex string
Note that an empty pattern string
//
is not supported since this is treated as the start of a line comment.
Lists
A Jactl List
represents a list of values of any type. Lists can have a mixture of types within them. Lists are
created using the []
syntax where the elements are a list of comma separated values:
> [] // empty list
[]
> [1,2,3]
[1, 2, 3]
> ['value1', 2, ['a','b']]
['value1', 2, ['a', 'b']]
The elements of a List
can themseles be a List
(as shown) or a Map
or any type supported by Jactl (including
instances of user defined classes).
The size()
function gives you the number of elements in a list. It can be invoked with the list as the argument or
can be used as a method call on the list by placing the call after the list:
> List x = ['value1', 2, ['a', 'b']]
['value1', 2, ['a', 'b']]
> size(x)
3
> x.size()
3
There are many other built-in functions provided that work with List
objects. These are described in more detail in the
section on Collections.
Lists can be added together:
> [1,2] + [2,3,4]
[1, 2, 2, 3, 4]
Note how 2
now appears twice: lists keep track of all elements, whether duplicated or not.
Single elements can be added to a list:
> ['a','b','c'] + 'd'
['a', 'b', 'c', 'd']
> def x = 'e'
e
> ['a','b','c'] + 'd' + x
['a', 'b', 'c', 'd', 'e']
> def y = ['a','b','c'] + 'd' + x // Can use a 'def' variable to hold a list
['a', 'b', 'c', 'd', 'e']
> y += 'f'
['a', 'b', 'c', 'd', 'e', 'f']
Consider this example:
> def x = [3]
[3]
> [1,2] + x
[1, 2, 3]
We are adding two lists so the result is the list of all the elements but what if we wanted to add x
itself to the
list and we didn’t know whether x
was itself a list or any other type?
We could do this:
> [1,2] + [x]
[1, 2, [3]]
Another way to force the value of x
to be added to the list is to use the <<
operator:
> [1,2] << x
[1, 2, [3]]
The <<
operator does not care whether the item being added is a list or not - it treats all items the same and adds
appends them to the list.
There are also corresponding +=
and <<=
operators for appending to an existing list:
> def y = [1,2]
[1, 2]
> y += 3
[1, 2, 3]
> y
[1, 2, 3]
> y <<= ['a']
[1, 2, 3, ['a']]
> y
[1, 2, 3, ['a']]
Note that both +=
and <<=
append to the existing list rather than creating a new list.
The in
operator allows you to check whether an element already exists in a list and there is also !in
which
checks for an element not being in a list:
> def x = ['a', 'b']
['a', 'b']
> def y = 'b'
b
> y in x
true
> 'a' !in x
false
Note
Thein
and!in
operators will search the list from the beginning of the list to try to find the element, so they are not very efficient once the list reaches any significant size. You might want to rethink your use of data structure if you are usingin
or!in
on lists with more than a few elements.
You can retrieve invidual elements of a list using subscript notation where the index of the element is enclosed
in []
immediately after the list and indexes start at 0
(so the first element is at position 0
):
> def x = ['value1', 2, ['a', 'b']]
['value1', 2, ['a', 'b']]
> x[0]
value1
> x[1]
2
> x[2]
['a', 'b']
> x[2][1]
b
Note how the last example retrieves an element from the list nested within x
.
As well as using []
to access individual elements, you can also use ?[]
as a null-safe way of retrieving elements.
The difference is that if the list is null, instead of getting a null error (when using []
) you will get null as the
value:
> def x = [1,2,3]
[1, 2, 3]
> x?[1]
2
> x = null
> x[1]
Null value for parent during field access @ line 1, column 3
x[1]
^
> x?[1] == null
true
You can also assign to elements of a list using the subscript notation. You can even assign to an element beyond the
current size of the list which will fill any gaps with null
:
> def x = ['value1', 2, ['a', 'b']]
['value1', 2, ['a', 'b']]
> x[1] = 3
3
> x
['value1', 3, ['a', 'b']]
> x[10] = 10
10
> x
['value1', 3, ['a', 'b'], null, null, null, null, null, null, null, 10]
Maps
A Map
in Jactl is used hold a set of key/value pairs and provides efficient lookup of a value based on a given key
value.
Unlike Java, Jactl
Map
objects only support keys which are strings. If you try to use an object that is not aString
as a key, Jactl will throw an error.
Maps can be constructed as a list of colon seperated key:value
pairs:
> Map x = ['a':1, 'b':2, 'key with spaces':[1,2,3]]
[a:1, b:2, 'key with spaces':[1, 2, 3]]
If the key is a valid identifier (or keyword) then the key does not need to be quoted:
> def x = [a:1, b:2]
[a:1, b:2]
> def y = [for:1, while:2, import:3] // keywords allowed as keys
[for:1, while:2, import:3]
> y.while
2
Variable Value as Key
If you have a variable whose value you wish to use as the map key then you should surround the key in parentheses ()
to tell the compiler to use the variable value and not the identifier as the key:
> def a = 'my key'
my key
> def x = [(a):1, b:2]
['my key':1, b:2]
You could also use an interpolated string as another way to achieve the same thing:
> def a = 'my key'
my key
> def x = ["$a":1, b:2]
['my key':1, b:2]
Map Addition
As with lists, you can add maps together:
> [a:1,b:2] + [c:3,d:4]
[a:1, b:2, c:3, d:4]
If the second map has a key that matches a key in the first list then its value is used in the resulting map:
> [a:1,b:2] + [b:4]
[a:1, b:4]
Keys in the left-hand map value that don’t exist in the right-hand map have their values taken from the left-hand map.
The +=
operator adds the values to an existing map rather than creating a new map:
> def x = [a:1,b:2]
[a:1, b:2]
> x += [c:3,a:2]
[a:2, b:2, c:3]
> x
[a:2, b:2, c:3]
Map Subtraction
You can subtract from a Map to remove specific keys from the Map (see also Map.remove()). To subtract one map from the other:
> [a:1,b:2,c:3] - [a:3,b:[1,2,3]]
[c:3]
This will produce a new Map where the entries in the first Map that match the keys in the second Map have been removed.
Keys in the second Map that don’t exist in the first Map will have no effect on the result:
> [a:1,b:2,c:3] - [x:'abc']
[a:1, b:2, c:3]
You can also subtract a List of values from a Map where the List is treated as a list of keys to be removed:
> [a:1,b:2,c:3] - ['a','c']
[b:2]
JSON Syntax
Jactl also supports JSON-like syntax for maps. This makes it handy if you want to cut-and-paste some JSON into your Jactl script:
> {"name":"Fred Smith", "employeeId":1234, "address":{"street":["Apartment 456", "123 High St"], "town":"Freetown"} }
[name:'Fred Smith', employeeId:1234, address:[street:['Apartment 456', '123 High St'], town:'Freetown']]
toString(indent)
Maps can be used to build up complex, nested data structures. The normal toString()
will convert the map to its
standard compact form but if you specify an indent amount it will provide a more readable form:
> def employee = [ name:'Fred Smith', employeeId:1234, dateOfBirth:'1-Jan-1970',
address:[street:'123 High St', suburb:'Freetown', postcode:'1234'],
phone:'555-555-555']
[name:'Fred Smith', employeeId:1234, dateOfBirth:'1-Jan-1970', address:[street:'123 High St', suburb:'Freetown', postcode:'1234'], phone:'555-555-555']
> employee.toString(2)
[
name: 'Fred Smith',
employeeId: 1234,
dateOfBirth: '1-Jan-1970',
address: [
street: '123 High St',
suburb: 'Freetown',
postcode: '1234'
],
phone: '555-555-555'
]
The
toString()
Jactl function outputs values in a form that is legal, executable Jactl code, which is useful for cut-and-pasting into scripts and when working with the REPL command line.
Map Field Access
Maps can be used as though they are objects with fields using .
:
> def x = [a:1, b:2]
[a:1, b:2]
> x.a
1
The value of the field could itself be another map so you can chain the access as needed:
> def x = [a:1, b:2, c:[d:4,e:5]]
[a:1, b:2, c:[d:4, e:5]]
> x.c.d
4
If you want the field name (the key) to itself be the value of another variable or expression then you can either use subscript notation (see below) or use an interpolated string expression:
> def x = [a:1, b:2, c:[d:4,e:5]]
[a:1, b:2, c:[d:4, e:5]]
> def y = 'c'
c
> x."$y".e
5
As well as .
you can use the ?.
operator for null-safe field access.
The difference being that if the map was null and you try to retrieve a field with .
you will get a null error
but when using ?.
the value returned will be null.
This makes it easy to retrieve nested fields without having to check at each level if the value is null:
> def x = [:]
[:]
> x.a.b
Null value for parent during field access @ line 1, column 5
x.a.b
^
> x.a?.b == null
true
> x?.a?.b?.c == null
true
As well as retrieving the value for an entry in a map, the field access notation can also be used to add a field or update the value of a field within the map:
> def x = [a:1, b:2, c:[d:4,e:5]]
[a:1, b:2, c:[d:4, e:5]]
> x.b = 4
4
> x
[a:1, b:4, c:[d:4, e:5]]
> x.c.e = [gg:2, hh:3]
[gg:2, hh:3]
> x
[a:1, b:4, c:[d:4, e:[gg:2, hh:3]]]
> x.c.f = [1,2,3]
[1, 2, 3]
> x
[a:1, b:4, c:[d:4, e:[gg:2, hh:3], f:[1, 2, 3]]]
Map Subscript Access
Maps can also be accessed using subscript notation:
> def x = [a:1, b:2, c:[d:4,e:5]]
[a:1, b:2, c:[d:4, e:5]]
> x['b']
2
> x['c']['d']
4
With subscript based access the value of the “index” within the []
is an expression that evaluates to the field (key)
name to be looked up. This makes accessing a field whose name comes from a variable more straightforward:
> def x = [a:1, b:2, c:[d:4,e:5]]
[a:1, b:2, c:[d:4, e:5]]
> def y = 'c'
c
> x[y]
[d:4, e:5]
There is also the ?[]
null-safe access as well if you don’t know whether the map is null and don’t want to
check beforehand.
As with the field notation access, new fields can be added and values for existing ones can be updated:
> def x = [a:1, b:2, c:[d:4,e:5]]
[a:1, b:2, c:[d:4, e:5]]
> x['b'] = 'abc'
abc
> x['zz'] = '123'
123
> x
[a:1, b:'abc', c:[d:4, e:5], zz:'123']
Auto-Creation
The field access mechanism can also be used to automatically create missing maps or lists, based on the context, when used on the left-hand side of an assignment.
Imagine that we need to execute something like this:
> x.a.b.c.d = 1
1
If we don’t actually know whether all the intermediate fields have been created then we would need to implement something like this:
> if (x == null) { x = [:] }
[:]
> if (x.a == null) { x.a = [:] }
[:]
> if (x.a.b == null) { x.a.b = [:] }
[:]
> if (x.a.b.c == null) { x.a.b.c = [:] }
[:]
> x.a.b.c.d = 1
1
> x
[a:[b:[c:[d:1]]]]
With Jactl these intermediate fields will be automatically created if they don’t exist so we only need write the last line of script:
> x.a.b.c.d = 1
1
> x
[a:[b:[c:[d:1]]]]
Note that Jactl will not automatically create a value for the top level variable, however, if it does not yet have a value. In this case, for example:
> def x
> x.a.b.c.d = 1
Null value for Map/List during field access @ line 1, column 2
x.a.b.c.d.e = 1
^
If part of the context of the field assignment looks like a List rather than a Map then a List will be created as required:
> def x = [:]
> x.a.b[0].c.d = 1
1
> x.a.b[1].c.d = 2
2
> x
[a:[b:[[c:[d:1]], [c:[d:2]]]]]
Note that in this case x.a.b
is an embedded List, not a Map.
Normally access to fields of a Map can also be done via subscript notation but if the field does not exist then Jactl will assume that access via subscript notation implies that an empty List should be created if the field is missing, rather than a Map:
> def x = [:]
[:]
> x.a['b'] = 1
Non-numeric value for index during List access @ line 1, column 4
x.a['b'] = 1
^
Truthiness
In Jactl we often want to know whether an expression is true
or not. The truthiness of an expression is used to
determine which branch of an if
statement to evaluate, or whether a for
loop or a while
loop should continue
or not, for example. In any situation where a boolean true
or false
is expected we need to evaluate the given
expression to determine whether it is true or not.
Obviously, if the expression is a simple boolean or boolean value then there is no issue with how to intepret the value:
true
is true, and false
is false.
Other types of expressions can also be evalauted in a boolean context and return true
or false
.
The rules are:
false
isfalse
0
isfalse
null
values arefalse
- Empty list or empty map is
false
- All other values are
true
For example:
> [] && true
false
> [[]] && true
true
> [:] && true
false
> [false] && true
true
> [a:1] && true
true
> '' && true
false
> 'abc' && true
true
> 1 && true
true
> 0 && true
false
> null && true
false
Variable Declarations
In Jactl, variables must be declared before they are used. A variable declaration has a type followed by the variable name and then optionally an initialiser that is used to initialise the variable:
> int i = 3
3
> int j // defaults to 0
0
You can use def
to define an untyped variable (equivalent to using Object
in Java):
> def x
> def y = 'abc'
abc
> def z = 1.234
1.234
Multiple variables can be declared at the same time if they are of the same type:
> int i, j // i and j will default to 0
0
> String s1 = 'abc', s2 = 'xyz'
xyz
> def x,y // x and y default to null
When using an initialiser, you can specify the variable type as var
and the type will be inferred from the
type of the initialiser:
> var i = 1 // i will be an int
1
> var s = 'abc' // s will be a String
abc
Another way to declare multiple variables in the same statement is to surround the variables with (
and )
and
then provide the optional initialisers in a separate list after a =
symbol:
> def (x,y) = [1,2]
2
The right-hand side can be any expression that evaluates to a list (or something that supports subscripting such as a String or an array):
> def stats = { x -> [x.sum(), x.size(), x.avg()] }
Function@511354923
> def values = [1, 4, 7, 4, 5, 13]
[1, 4, 7, 4, 5, 13]
> def (sum, count, avg) = stats(values)
5.6666666667
> sum
34
> count
6
> avg
5.6666666667
> def (first, second, third) = 'a string value' // grab first, second, and third letters from the string
s
This multi-declaration form supports the type being specified per variable:
> def (int i, String s) = [123, 'abc']
abc
Declaring Constants
The const
keyword can be used when declaring a variable to create a constant.
Constants cannot be assigned to or modified and are limited to these simple types:
- boolean
- byte
- int
- long
- double
- Decimal
- String
For example:
const int MAX_SIZE = 10000
const Decimal PI = 3.1415926536
The type is optional and will be inferred from the value of the initialiser:
const MAX_SIZE = 10000
const PI = 3.1415926536
A const
must have an initialiser expression to provide the value of the constant.
This expression can be a simple value (number, String, etc.) or can be a simple numerical
expression:
const PI = 3.1415926536
const PI_SQRD = PI * PI
Expressions and Operators
Operator Precedence
Jactl supports the following operators. Operators are shown in increasing precedence order and operators of the same precedence are shown with the same precedence value:
Precedence Level | Operator | Description |
---|---|---|
1 | or |
Logical or |
2 | and |
Logical and |
3 | not |
Logical not |
4 | = |
Assignment |
?= |
Conditional assignment | |
+= -= *= /= %= %%= |
Arithmetic assignment operators | |
<<= >>= >>>= |
Shift assignment operators | |
&= |= ^= |
Bitwise assignment operators | |
5 | ? : |
Ternary conditional opeartor |
?: |
Default value operator | |
6 | || |
Boolean or |
7 | && |
Boolean and |
8 | | |
Bitwise or |
9 | ^ |
Bitwise xor |
10 | & |
Bitwise and |
11 | == != === !== |
Equality and inequality operators |
<=> |
Compator operator | |
=~ !~ |
Regex compare and regex not compare | |
12 | < <= > >= |
Less than and greater than operators |
instanceof !instanceof |
Instance of and not instance of operators | |
in !in |
In and not in operators | |
as |
Conversion operator | |
13 | << >> >>> |
Shift operators |
14 | + - |
Addition and subtraction |
15 | * / |
Multiplication and division operators |
% %% |
Modulo and remainder operators | |
16 | ~ |
Bitwise negate |
! |
Boolean not | |
++ -- |
Prefix and postfix increment/decrement operators | |
+ - |
Prefix minus and plus operators | |
(type) |
Type cast | |
17 | . ?. |
Field access and null-safe field access |
[ ] ?[ ] |
Map/List/String element access and null-safe access | |
() |
Function/method invocation | |
{} |
Function/method invocation (closure passing syntax) | |
new |
New instance operator |
When evaluating expressions in Jactl operators with higher precedence are evaluated before operators with lower preedence. For example in the following expression the multiplicaton is evaluated before the addition or subtraction because it has higher precedence than addition or substraction:
> 3 + 2 * 4 - 1
10
Bracketing can be used to force the order of evaluation of sub-expressions where necessary:
> (3 + 2) * (4 - 1)
15
Assignment and Conditional Assignment
Variables can have values assigned to them using the =
operator:
> def x = 1
1
> x = 2
2
Since an assignment is an expression and has a value (the value being assigned) it is possible to use an assignment within another expression:
> def x = 1
1
> def y = x = 3
3
> x
3
> y
3
> y = (x = 4) + 2
6
> x
4
Conditional assignment uses the ?=
operator and means that the assignment only occurs if the expression on the right
hand side is non-null.
So x ?= y
means x
will be assigned the value of y
only if y
is not null:
> def x = 1
1
> def y // no initialiser so y will be null
> x ?= y
> x // x still has its original value since y was null
1
Basic Arithmetic Operators
The standard arithmetic operators +
, -
, *
, /
are supported for addition, subtraction, multiplication, and
division:
> 3 * 4 + 6 / 2 + 5 - 10
10
Remember that *
and /
have higher precedence and are evaluated before any addition or subtraction.
Prefix + and -
The +
and -
operators can also be used as prefix operators:
> -(3 - 4)
1
> +(3 - 4)
-1
The -
prefix operator negates the value following expression while the +
prefix operator does nothing but exists
to correspond to the -
case so that things like -3
and +3
can both be written.
Bitwise Operators
The bitwise operators are |
, &
, ^
, and ~
.
The |
operator performs a binary or at the bit level.
For each bit of the left-hand and right-hand side the corresponding bit in the result will be 1 if the bit in
either the left-hand side or right-hand side was 1.
For example, 5 | 3
is the same in binary as 0b101 | 0b011
which at the bit level results in 0b111
which is 7:
> 5 | 3
7
> 0b101 | 0b011 // result will be 0b111
7
The &
operator does an and of each bit and the resulting bit is 1 only if both left-hand side and right-hand side
bit values were 1:
> 5 & 3
1
> 0b101 & 0b011 // result is 0b001
1
The ^
operator is an xor and the result for each bit value is 0 if both left-hand side and right-hand side bit values
are the same and 1 if they are different:
> 5 ^ 3
6
> 0b101 ^ 0b011 // result is 0b110
6
The ~
is a prefix operator and does a bitwise not of the expression that follows so the result for each bit is the
opposite of the bit value in the expression:
> ~5
-6
> ~0b00000101 // result will be 0b11111111111111111111111111111001 which is -6
-6
Shift Operators
The shift operators <<
, >>
, >>>
work at the bit level and shift bits of a number by the given number of positions.
The <<
operator shifts the bits to the left meaning that the number gets bigger since we are multiplying by powers of 2.
For example, shifting left by 1 bit will multiply the number by 2, shifting left by 2 will multiply by 4, etc.
For example:
> 5 << 1 // 0b0101 << 1 --> 0b1010 which is 10
10
> 5 << 4 // same as multiplying by 16
80
The ‘»’ and ‘»>’ operators shift the bits to the right.
The difference between the two is how negative numbers are treated.
The >>
operator is a signed shift right and preserves the sign bit (the top most bit) so if it is 1 it remains 1 and
1 is shifted right each time from this top bit position.
The ‘»>’ operator treats the top bit like any other bit and shifts in 0s to replace the top bits when the bits are
shifted right.
For example:
> def x = 0b11111111000011110000111100001111 >> 16 // shift right 16 but shift in 1s at left since -ve
-241
> x.toBase(2)
11111111111111111111111100001111
> x = 0b11111111000011110000111100001111 >>> 16 // shift right 16 but shift in 0s at left
65295
> x.toBase(2) // note that leading 0s not shown
1111111100001111
Modulo % and Remainder %% operators
In addition to the basic four operators, Jactl also has %
(modulo) and %%
(remainder) operators.
Both operators perform similar functions in that they calculate the “remainder” after dividing by some number.
The difference comes from how they treat negative numbers.
The remainder operator %%
works exactly the same way as the Java remainder operator (which in Java is represented
by a single %
rather than the %%
in Jactl):
x %% y
is defined as beingx - (int)(x/y) * y
The problem is if x
is less than 0
then the result will also be less than 0:
> -5 %% 3
-2
When doing modulo arithmetic you usually want (in my opinion) values to only be between 0
and y - 1
when evaluating
something modulo y
(where y
is postive) and between 0
and -(y - 1)
if y
is negative.
Jactl, therefore, the definition of %
is:
x % y
is defined as((x %% y) + y) %% y
This means that in Jactl %
returns only postive values if the right-hand side is positive and only negative values
if the right-hand side is negative.
Jactl retains %%
for scenarios where you want compatibility with how Java does things or for when you know that
the left-hand side will always be positive and you care about performance (since %%
compiles to a single JVM
instruction while %
is several instructions).
Increment/Decrement Operators
Jactl offers increment ++
and decrement --
operators that increment or decrement a value by 1
.
Both prefix and postfix versions of these operators exist.
In prefix form the result is the result after applying the increment or decrement while in postfix form the value
is the value before the increment or decrement occurs.
For example:
> def x = 1
1
> x++ // increment x but return value of x before increment
1
> x
2
> ++x // increment x and return new value
3
> x
3
> --x // decrement x and return new value
2
> x-- // decrement x but return old value
2
> x
1
If the expression is not an actual variable or field that can be incremented then there is nothing to increment or decrement but in the prefix case the value returned is as though the value had been incremented or decremented:
> 3++
3
> ++3
4
> --(4 * 5)
19
> (4 * 5)--
20
Comparison Operators
The following table shows the operators that can be used to compare two values:
Operator | Description |
---|---|
== |
Equality: evaluates to true if the values are value-wise equal |
!= |
Inequality: evaluates to true if the values are not equal |
=== |
Identity: evaluates to true if the object on both sides is the same object |
!== |
Non-Identity: evaluates to true if the objects are not the same object |
< |
Less-than: true if left-hand side is less than right-hand side |
<= |
Less-than-or-equal-to: true if left-hand side is less than or equal to right-hand side |
> |
Greater-than: true if left-hand side is greater than right-hand side |
>= |
Greater-than-or-equal-to: true if left-hand side is greater than or equal to right-hand side |
The <
, <=
, >
, >=
operators can be used to compare any two numbers to check which is bigger than the other and
can also be used to compare strings.
For strings, the lexographic order of the strings determines which one is less than the other. String comparison is done character by character. If all characters are equal at the point at which one string ends then the shorter string is less than the other one. For example:
> 'a' < 'ab'
true
> 'abcde' < 'xyz'
true
The difference between ==
and ===
(and !=
and !==
) is that ==
compares values.
This makes a difference when comparing Lists and Maps (and class instances).
The ==
operator will compare the values of all elements and fields to see if the values are equivalent.
The ===
operator, on the other hand, is solely interested in knowing if the two Lists (or Maps, or objects) are the
same object or not.
For example:
> def x = [1, [2,3]]
[1, [2, 3]]
> def y = [1, [2,3]]
[1, [2, 3]]
> x == y // values are the same
true
> x === y // objects are not the same
false
> x !== y
true
> x = y
[1, [2, 3]]
> x === y
true
> x !== y
false
Comparator Operator
The <=>
comparator operator evaluates to -1 if the left-hand side is less than the right-hand side, 0 if the two
values are equal, and 1 if the left-hand side is greater than the right-hand side:
> 1 <=> 2
-1
> 2.3 <=> 2.3
0
> 4 <=> 3
1
> 'abc' <=> 'ab'
1
> 'a' <=> 'ab'
-1
The operator can be particularly useful when sorting lists as the sort
method takes as an argument a function
that needs to return -1, 0, or 1 based on the comparison of any two elements in the list:
> def employees = [[name:'Frank', salary:2000], [name:'Daisy', salary:3000], [name:'Joe', salary:1500]]
[[name:'Frank', salary:2000], [name:'Daisy', salary:3000], [name:'Joe', salary:1500]]
> employees.sort{ a,b -> a.salary <=> b.salary } // sort by salary increasing
[[name:'Joe', salary:1500], [name:'Frank', salary:2000], [name:'Daisy', salary:3000]]
> employees.sort{ a,b -> b.salary <=> a.salary } // sort by salary decreasing
[[name:'Daisy', salary:3000], [name:'Frank', salary:2000], [name:'Joe', salary:1500]]
Logical/Boolean Operators
There are two sets of logical or boolean operators:
&&
,||
,!
, andand
,or
,not
For both &&
and and
the result is true if both sides are true:
> 3.5 > 2 && 'abc' == 'ab' + 'c'
true
> 5 == 3 + 2 and 7 < 10
true
> 5 == 3 + 2 and 7 > 10
false
With the ||
and or
operators the result is true if either side evaluates to true:
> 5 == 3 + 2 || 7 > 10
true
> 5 < 4 or 7 < 10
true
The !
and not
operators negate the result of the following boolean expression:
> ! 5 == 3 + 2
false
> not 5 < 4
true
The difference between the two sets of operators is that the operators and
, or
, and not
have very low precedence;
even lower than the assignment operators.
This makes it possible to write things like this somewhat contrived example:
> def x = 7
7
> def y = 8
8
> x == 7 and y = x + (y % 13) // assign new value to y if x == 7
true
> y
15
In this case, there are obviously other ways to achieve the same thing (such as an if statement) but there are occasions where having these low-precedence versions of the boolean operators is useful. It comes down to personal preference whether and when to use them.
Conditional Operator
The conditional operator allows you to embed an if
statement inside an expression.
It takes the form:
condition
?
valueIfTrue:
valueIfFalse
The condition expression is evaluated and if it evaluates to true
then the result of the entire expression
is the value of the second expression (valueIfTrue).
If the condition is false then the result is the third expression (valueIfFalse).
For example:
> def x = 7
7
> def y = x % 2 == 1 ? 'odd' : 'even'
odd
> def z = y.size() > 3 ? x+y.size() : x - y.size()
4
Default Value Operator
The default value operator ?:
evaluates to the left-hand side if the left-hand side is not null and evaluates to
the right-hand side if the left-hand side is null.
It allows you to specify a default value in case an expression evaluates to null:
> def x
> def y = x ?: 7
7
> y ?: 8
7
Other Assignment Operators
Many operators also have assignment versions that perform the operation and then assign the result to the
left-hand side.
The corresponding assignment operator is the original operator followed immediately (no spaces) by the =
sign.
For example, the assignment operator for +
is +=
.
An expression such as x += 5
is just shorthand for x = x + 5
.
The full list of assignment operators is:
+=
-=
*=
/=
%=
%%=
<<=
>>=
>>>=
&=
|=
^=
For example:
> def x = 5
5
> x += 2 * x
15
> x
15
> x /= 3
5
> x %= 3
2
> x <<= 4
32
> x |= 15
47
Multi-Assignment
You assign to multiple variables at the same time by listing the variables within (
and )
and providing
a list of values on the right-hand side of the assignment operator:
> def x; def y
> (x,y) = [3,4]
4
> println "x=$x, y=$y"
x=3, y=4
Note that the value of a multi-assignment is the value of the last assignment in the list (which is why 4
is
printed by the REPL as the value of the (x,y) = [3,4]
expression).
The right-hand side can be another variable or expression that evaluates to a list:
> def x,y
> def str = 'abc'
abc
> (x,y) = str // extract the first and second characters of our string
b
> "x=$x, y=$y"
x=a, y=b
You can use any of the assignment operators such as +=
or -=
:
> def (x,y) = [1,2]
2
> (x,y) += [3,4]; println "x=$x, y=$y"
x=4, y=6
Any expression that can appear on the left-hand side of a normal assignment can appear in a multi-assignment (not just simple variable names):
> def x = [:]
[:]
> (x.('a' + '1').b, x.a1.c) = ['xab', 'xac']
xac
> x
[a1:[b:'xab', c:'xac']]
The conditional assignment operator ?=
is also supported.
In a multi-assignment, each of the individual assignments is evaluated to see if the value in the list on
the right-hand side is null or not so some values can be assigned while others aren’t:
> def (x,y) = [1,2]
2
> def z
> (x,y) ?= [3,z] // y unchanged since z has no value
> "x=$x, y=$y"
x=3, y=2
Multi-assignment can be used to swap the values of two variables:
> def (x,y) = [1,2]
2
> (x,y) = [y,x] // swap x and y
1
> "x=$x, y=$y"
x=2, y=1
Instance Of
The instanceof
and !instanceof
operators allow you to check if an object is an instance (or not an instance) of a
specific type.
The type can be a built-in type like int
, String
, List
, etc. or can be a user defined class:
> def x = 1
1
> x instanceof int
true
> x !instanceof String
true
> x = [a:1, b:[c:[1,2,3]]]
[a:1, b:[c:[1, 2, 3]]]
> x.b instanceof Map
true
> x.b.c instanceof List
true
> class X { int i = 1 }
> x = new X()
[i:1]
> x instanceof X
true
Type Casts
In Java, type casting is done for two reasons:
- You are passed an object of some generic type but you know it is actually a sepcific type and you want to treat it as that specific type (to invoke a method on it, for example), or
- You need to convert a primitive number type to another number type (for example, converting a long value to an int)
In Jactl there is less need to cast for the first reason since if the object supports the method you can always invoke
that method even if the reference to the object is a def
type.
The reason why you may still wish to cast to the specific type in this case is for readability to make it clear what type
is expected at that point in the code or for performance reasons since after the value has been cast to the desired
type the compiler then can use a more efficient means of invoking methods and other operations on the object.
A type cast is done by prefixing (type)
to an expression where type is the type to cast to. Type can be any builtin
type or any user defined class.
For example we could check the type of x
before invoking the List method sum()
on it:
> def x = [1,2,3]
[1, 2, 3]
> def sum = x instanceof List ? ((List)x).sum() : 0
6
Whereas in Java the cast would be required, since Jactl supports dynamic typing, the cast in this case is not necessary (but might make the execution slightly faster).
The other use for casts is to convert primitive number types to one another. For example, you can use casts to convert a double or decimal value to its corresponding integer representation (discarding anything after the decimal point):
> def hoursOwed = 175.15
175.15
> def hoursPerDay = 7.5
7.5
> def daysOwed = (int)(hoursOwed / hoursPerDay)
23
The other special case for cast is to cast a character to an integer value to get its Unicode value.
Remember that characters in Jactl are just single character strings so if you cast a single character string to
int
you will get is Unicode value:
> (int)'A'
65
As Operator
The as
operator is used to convert values from one type to another (where such a conversion is possible).
The operator is an infix operator where the left-hand side is the expression to be converted and the right-hand side
is the type to conver to.
It is similar to a type cast and can be used to do that same thing in some circumstances:
> (int)3.6
3
> 3.6 as int
3
You can use as
to convert a string representation of a number into the number:
> '123' as int
123
> '123.4' as Decimal
123.4
You can use it to convert anything to a string:
> 123 as String
123
> [1,2,3] as String
[1, 2, 3]
Of course, you can do the same thing with the built-in toString()
method as well:
> 123.toString()
123
> [1,2,3].toString()
[1, 2, 3]
The as
operator can convert between Lists and Maps (as long as such a conversion makes sense):
> [a:1,b:2] as List
[['a', 1], ['b', 2]]
> [['x',3],['y',4]] as Map
[x:3, y:4]
It is also possible to use as
to convert between objects of user defined classes and Maps (see section on Classes):
> class Point{ int x; int y }
> def x = [x:3, y:4] as Point
[x:3, y:4]
> x instanceof Point
true
> x as Map
[x:3, y:4]
In Operator
The in
and !in
operators are used to test if an object exists or not within a list of values.
For example:
> def country = 'France'
France
> country in ['Germany', 'Italy', 'France']
true
> def myCountries = ['Germany', 'Italy', 'France']
['Germany', 'Italy', 'France']
> country !in myCountries
false
This operator works by iterating through the list and comparing each value until it finds a match so if the list of values is particularly large then this will not be very efficient.
For Maps, the in
and !in
operators allow you to check if a key exists in the Map:
> def m = [abc:1, xyz:2]
[abc:1, xyz:2]
> 'abc' in m
true
> 'xyz' !in m
false
The in
and !in
operators also work on Strings to see if one string is contained within another:
> 'the' in 'some of the time'
true
Field Access Operators
The .
?.
[]
and ?[]
operators are used for accessing fields of Maps and class objects while the []
and ?[]
are also used to access elements of Lists.
The ?
form of the operators will return null instead of producing an error if the List/Map/object is null.
For example:
> Point p = new Point(x:1, y:2)
[x:1, y:2]
> p.x
1
> p['x']
1
> p = null
> p?.x == null
true
> p?['x'] == null
true
> [abc:1].('a'+'bc')
1
> [abc:1]?.('a'+'bc')
1
> [abc:1]?.x?.y?.z == null
true
Regular Expressions
Regex Matching
Jactl provides two operators for doing regular expression (regex) find and regex subsitutions.
The =~
operator is used for matching a string against a regex pattern.
It does the equivalent of Java’s Matcher.find()
to check if a substring matches the given pattern.
The regex pattern syntax is the same as that used by the Pattern class in Java so for detail information about how to
use regular expressions and what is supported see the Javadoc for the Pattern class.
Some examples:
> 'abc' =~ 'b'
true
> 'This should match' =~ '[A-Z][a-z]+ sho.l[a-f]'
true
Regular expression patterns are usually expressed in pattern strings to cut down on the number of backslashes needed.
For example /\d\d\s+[A-Z][a-z]*\s+\d{4}/
is easier to read and write than '\\d\\d\\s+[A-Z][a-z]*\\s+\\d{4}'
:
> '24 Mar 2014' =~ '\\d\\d\\s+[A-Z][a-z]*\\s+\\d{4}' // pattern written as standard string
true
> '24 Mar 2014' =~ /\d\d\s+[A-Z][a-z]*\s+\d{4}/ // same pattern written as pattern string
true
The !~
operator tests that the string does not match the pattern:
> '24 Mar 2014' !~ /\d\d\s+[A-Z][a-z]*\s+\d{4}/
false
Modifiers
When using a regex string (a string delimited with /
) you can append modifiers to the regex pattern to control the
behaviour of the pattern match.
For example by appending i
you can make the pattern match case-insensitive:
> 'This is a sentence.' =~ /this/
false
> 'This is a sentence.' =~ /this/i
true
More than on modifier can be appended and the order of the modifiers is unimportant. The supported modifiers are:
Modifier | Description |
---|---|
i | Pattern match will be case-insensitive. |
m | Multi-line mode: this makes ^ and $ match beginning and endings of lines rather than beginning and ending of entire string. |
s | Dot-all mode: makes . match line terminator (by default . won’t match end-of-line characters). |
g | Global find: remembers where previous pattern occurred and starts next find from previous location. This can be used to find all occurrences of a pattern within a string. |
n | If a capture group is numeric then interpret as a number. |
r | Regex match: this forces the pattern string to be interpreted as a regex match for implicit matching (see below). For substitutions this makes the substitution non-destructive. |
The g
modifier for global find can be used to iterate over a string, finding all instances of a given pattern within
the string:
> def str = 'This Example Text Is Not Complex'
This Example Text Is Not Complex
> int i = 0
0
> while (str =~ /Ex/ig) { i++ } // count how often pattern matches
> i
3
Note that the ‘g’ modifier on a regex match is only supported within the condition of a while or for statement. If you try to use it in any other context you will get an error:
> def x = 'abc'
abc
> x =~ /ab/g
Cannot use 'g' modifier outside of condition for while/for loop @ line 1, column 6
x =~ /ab/g
^
The other limitation is that only one occurrence of a pattern with a ‘g’ modifier can appear within the condition expression. Other regex matches can be present in the condition expression as long as they don’t use the ‘g’ global modifier:
> def x = 'abc'; def y = 'xyz'; def i = 0
0
> while (x =~ /[a-c]/g && y =~ /[x-z]/g) { i++ }
Regex match with global modifier can only occur once within while/for condition @ line 1, column 30
while (x =~ /[a-c]/g && y =~ /[x-z]/g) { i++ }
^
> while (x =~ /[a-c]/g && y =~ /[x-z]/) { i++ }
> i
3
These restrictions are due to what makes sense in terms of how capture variables work (see below) and due to the way in which Jactl saves the state of the match to remember where to continue from the next time.
The g
modifier is more useful when used with capture variables as described in the next section.
Capture Variables
With regex patterns you can provide capture groups using (
and )
.
These groupings then create capture variables $1
$2
etc. for each of the capture groups that correspond
to the characters in the source string that match that part of the pattern.
There is also a $0
variable that captures the portion of the source string that matches the entire pattern.
For example:
> def str = 'This Example Text Is Not Complex'
This Example Text Is Not Complex
> str =~ /p.*(.ex.).*(N[a-z].)/
true
> $1
Text
> $2
Not
> $0
ple Text Is Not
Note that if there are nested groups the capture variables numbering is based on the number of (
so far encountered
irrespective of the nesting.
You can use \
to escape the (
and )
characters if you want to match against those characters.
Using capture variables with the g
global modifier allows you to extract all parts of a string that match:
> def data = 'AAPL=$151.03, MSFT=$255.29, GOOG=$94.02'
AAPL=$151.03, MSFT=$255.29, GOOG=$94.02
> def stocks = [:]
[:]
> while (data =~ /(\w*)=\$(\d+.\d+)/g) { stocks[$1] = $2 as Decimal }
> stocks
[AAPL:151.03, MSFT:255.29, GOOG:94.02]
The n
modifier will make the capture variable a number if the string captured is numeric.
If the number has no decimal place then it will be converted to a long
value (as long as the value is not too large).
If it has a decimal point then it will become a Decimal
value.
For example:
> 'rate=-1234' =~ /(\w+)=([\d-]+)/n
true
> $1
rate
> $2
-1234
> $2 instanceof long
true
> 'rate=56.789' =~ /(\w+)=([\d-.]+)/n
true
> $2
56.789
> $2 instanceof Decimal
true
Regex Substitution
Jactl supports regex substitution where substrings in the source string that match the given pattern are replaced
with a supplied substitution string.
The same regex match operator =~
is used but the right-hand side now has the form s/pattern/subst/mods
where
pattern
is the regex pattern, subst
is the substitution string, and mods
are the optional modifiers:
> def x = 'This is the original string'
This is the original string
> x =~ s/the/not an/
This is not an original string
> x
This is not an original string
Notice that the value on the left-hand side is modified so this form cannot be applied to string literals since they cannot be modified:
> 'this is the string' =~ s/the/not a/
Invalid left-hand side for '=~' operator (invalid lvalue) @ line 1, column 2
'this is the string' =~ s/the/not a/
^
If you want to perform the substitution and get the new string value without altering the left-hand side then use
the r
modifier to perform a non-destructive replacement:
> 'this is the string' =~ s/the/not a/r
this is not a string
The other modifiers supported work the same way as for a regex match (see above): i
forces the match to be case-insensitve,
m
is for multi-line mode, s
allows .
to match line terminators, and g
changes all occurrences of the pattern
in the string.
For example, to change all upper case letters to xxx
:
> 'This SentenCe has Capital letTErs' =~ s/[A-Z]/xxx/rg
xxxhis xxxentenxxxe has xxxapital letxxxxxxrs
Both the pattern and the substitution strings are expression strings so expressions are allowed within the strings:
> def x = 'A-Z'; def y = 'y'
y
> 'This SentenCe has Capital letTers' =~ s/[$x]/${y * 3}/rg
yyyhis yyyentenyyye has yyyapital letyyyers
Furthermore, capture variables are supported in the substitution string to allow expressions using parts of the source string. For example to append the size of each word to each word in a sentence:
> 'This SentenCe has Capital letTers' =~ s/\b(\w+)\b/$1[${$1.size()}]/rg
This[4] SentenCe[8] has[3] Capital[7] letTers[7]
Implicit Matching and Substitution
For both regex matches and regex substitutions, if no left-hand side is given, Jactl will assume that the match or
the substitution should be done against the default it
variable.
This variable is the default variable passed in to closures (see later) when no variable name is specified.
The following example takes some input, splits it into lines, and then uses filter
with a regex to filter the lines
that match /Evac.*Pause/
and then uses map
to perform a regex substitute to transform these lines into a different
form:
> def data = '''[251.993s][info][gc] GC(281) Pause Young (Normal) (G1 Evacuation Pause) 2486M->35M(4096M) 6.630ms
[252.576s][info][gc] GC(282) Pause Young (Concurrent Start) (Metadata GC Threshold) 1584M->34M(4096M) 10.571ms
[252.576s][info][gc] GC(283) Concurrent Cycle
[252.632s][info][gc] GC(283) Pause Remark 48M->38M(4096M) 49.430ms
[252.636s][info][gc] GC(283) Pause Cleanup 45M->45M(4096M) 0.065ms
[252.638s][info][gc] GC(283) Concurrent Cycle 62.091ms
[253.537s][info][gc] GC(284) Pause Young (Normal) (G1 Evacuation Pause) 2476M->25M(4096M) 5.818ms
[254.453s][info][gc] GC(285) Pause Young (Normal) (G1 Evacuation Pause) 2475M->31M(4096M) 6.040ms
[255.358s][info][gc] GC(286) Pause Young (Normal) (G1 Evacuation Pause) 2475M->31M(4096M) 5.070ms
[256.272s][info][gc] GC(287) Pause Young (Normal) (G1 Evacuation Pause) 2477M->34M(4096M) 5.024ms'''
> data.lines().filter{ /Evac.*Pause/r }.map{ s/^\[([0-9.]+)s\].* ([0-9.]*)ms$/At $1s: Pause $2ms/ }.each{ println it }
At 251.993s: Pause 6.630ms
At 253.537s: Pause 5.818ms
At 254.453s: Pause 6.040ms
At 255.358s: Pause 5.070ms
At 256.272s: Pause 5.024ms
See later sections on closures, and collections, for a description of how the methods like filter()
and map()
and each()
work with closures and with the implicit it
variable.
You can, of course, define your own it
variable and have it automatically matched against:
> def it = 'my value for my xplictily defined it variable'
my value for my explictily defined it variable
> /MY/i // same as writing it =~ /MY/i
true
> s/MY/a random/ig
a random value for a random explictily defined it variable
> it
a random value for a random explictily defined it variable
Note, for matching, there needs to be a modifier since a regex pattern string delimited by /
with no modifiers is
treated as a string in Jactl.
To force the regex pattern to do a match against the it
variable add a modifier.
The r
modifier can be used to force a regex match if no other modifier makes sense:
> def it = 'abc'
abc
> /a/ // just a string
a
> /a/r // force a regex match
true
If there is no it
variable in the current scope you will get an error:
> s/MY/a random/ig
Reference to unknown variable 'it' @ line 1, column 1
s/MY/a random/ig
^
Split Method
The split()
method is another place where regex patterns are used.
Split operates on strings to split them into their constituent parts where the separator is based on a supplied
regex pattern:
> '1, 2,3, 4'.split(/\s*,\s*/) // split on comma ignoring whitespace
['1', '2', '3', '4']
The result of the split invocation is a list of the substrings formed by splitting the string whenever the pattern is encountered.
Unlike regex matches and substitutions, modifiers are optionally passed as the second argument to the method:
'1abc2Bad3Dead'.split(/[a-d]+/,'i')
['1', '2', '3', 'e', '']
Note that if the pattern occurs at the end of the string (as in the example) then an empty string will be created as the last element in the resulting list. Similarly, if the pattern occurs at the start of the string, an empty string will be the first element in the result list.
If more than one modifier is needed then they are passed as a string (e.g. 'ism'
or 'ms'
, etc.).
The order of the modifiers is unimportant.
The only modifiers supported for split
are i
(case-insensitive), s
(dot matches line terminators), and
m
(multi-line mode).)
Eval Statement
The eval()
statement can be used to evaluate arbitrary Jactl code.
It takes the string passed to it and compiles and runs it.
The return value is the value of the last expression in the script:
> eval('def f(n) { n == 1 ? 1 : n * f(n-1)}; f(5)')
120
You can return any type of value including a function or closure:
> def f = eval('def f(n) { n == 1 ? 1 : n * f(n-1)}')
Function@1894369629
> f(5)
120
You can pass an optional Map to the function if you want to prepopulate the values of some variables:
> eval('def f(n) { n == 1 ? 1 : n * f(n-1)}; f(x)', [x:6])
720
You can also use the Map as a way to capture additional output from the script. Top level variables in the script will become entries in the Map:
> def vars = [x:6]
[x:6]
> eval('def f(n) { n == 1 ? 1 : n * f(n-1)}; def first10 = 10.map{ f(it+1) }; f(x)', vars)
720
> vars
[x:6, f:Function@445918232, first10:[1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]]
Since it is compiled, there could be compile errors that prevent the script running. In this case the return value will be null:
> def vars = [x:6]
[x:6]
> def result = eval('def f(n) { : n == 1 ? 1 : n * f(n-1)}; def first10 = 10.map{ f(it+1) }; f(x)', vars)
> result == null
true
If you passed in a Map then the $error
entry in the Map will hold the compile error:
> vars.'$error'
io.jactl.CompileError: Unexpected token ':': Expected start of expression @ line 1, column 12
def f(n) { : n == 1 ? 1 : n * f(n-1)}; def first10 = 10.map{ f(it+1) }; f(x)
^
Note that this is not the most efficient way to run Jactl code since it has to compile the code before it can run it, so if it is to be used it should be used judiciously.
If/Else Statements
Jactl if
statements work in the same way as Java and Groovy if
statements.
The syntax is:
if (<cond>) <trueStmts> [ else <falseStmts> ]
The <cond>
condition will be evalauted as true
or false
based on the truthiness of the expression.
If it evaluates to true
then the <trueStmts>
will be executed.
If it evaulates to false
then the <falseStmts>
will be executed if the else
clause has been provided.
The <trueStmts>
and <falseStmts>
are either single statements or blocks of multiple statements wrapped in {}
.
For example:
> def x = 'Fred Smith'
'Fred Smith'
> if (x =~ /^[A-Z][a-z]*\s+[A-Z][a-z]*$/ and x.size() > 3) println "Valid name: $x"
Valid name: Fred Smith
Since the Jactl REPL will execute code as soon as a valid statement is parsed, it is difficult to show some multi-line
examples by capturing REPL interactions so here is an example Jactl script showing a multi-line if/else
:
def x = 'abc'
if (x.size() > 3) {
x = x.substring(0,3)
}
else {
x = x * 2
}
println x // should print: abcabc
It is possible to string multiple if/else
statements together:
def x = 'abc'
if (x.size() > 3) {
x = x.substring(0,3)
}
else
if (x.size() == 3) {
x = x * 2
}
else {
die "Unexpected size for $x: ${x.size()}"
}
If/Unless Statements
In addition to the if/else
statements, Jactl supports single statement if
and unless
statements where the
test for the condition comes at the end of the statement rather than the beginning.
The syntax is:
<statment> if <cond>
There is no need for ()
brackets around the <cond>
expression.
For example:
> def x = 'abc', y = 'xyz'
xyz
> x = x * 2 and y = "Y:$x" if x.size() < 4
true
> x
abcabc
> y
Y:abcabc
As well as supporting tests with if
, Jactl supports using unless
which just inverts the test.
The statement is executed unless the given condition is true:
> def x = 'abc'
xyz
> x = 'xxx' unless x.size() == 3
> x
abc
> die "x has invalid value: $x" unless x.size() == 3
Loop Statements
While Loops
Jactl while
loops work like Java and Groovy:
while (<cond>) <statement>
The <statement>
can be a single statement or a block of statements wrapped in {}
.
The statement or block of statements is repeatedly executed while the condition is true
.
For example:
int i = 0, sum = 0
while (i < 5) {
sum += i
i++
}
die if sum != 10
Break and Continue
Like Java, break
can be used to exit the while loop at any point and continue
can be used to goto the next
iteration of the loop before the current iteration has completed:
int sum = 0
int i = 0
while (i < 10) {
sum += i
break if sum >= 20 // exit loop once sum is >= 20
i++
}
die unless sum == 21 && i == 6
Another example using continue
:
int sum = 0
int i = 0
while (i < 100) {
i++
continue unless i % 3 == 0 // only interested in numbers that are multiples of 3
sum += i
}
die unless sum == 1683
For Loops
A for
loop is similar to a while
loop in that it executes a block of statements while a given condition is met.
It additionally has some initialisation that is run before the loop and some statements that are run for every loop
iteration.
The syntax is:
for (<init>; <cond>; <update>) <statement>
The <statement
can be a single statement or a block of statements surrounded by {}
.
The <init>
initialiser is a single declaration which can declare a list of variables of the same type with
(optionally) initial values supplied.
The <cond>
is a condition that is evaulated for truthiness each time to determine if the loop
should execute.
The <update>
is a comma separated list of (usually) assignment-like expressions that are evaluated each time
an iteration of the loop completes.
The canonical example of a for
loop is to loop a given number of times:
int result
for (int i = 0; i < 10; i++) {
result += i if i % 3 == 0
}
die unless result == 18
Here is an example with multiple initialisers and updates:
int result = 0
for (int i = 0, j = 0; i < 10 && j < 10; i++, j += 2) {
result += i + j
}
die unless result == 30
As for while
statements, a for
loop supports break
to exit the loop and continue
to continue with the next
iteration.
When continue
is used the <update>
statements are executed before the condition is re-evaluated.
Note that any or all of the <init>
<cond>
and <update>
sections of the for
loop can be empty:
int i = 0, sum = 0
for(; i++ < 10; ) { // empty initialiser and update section
sum += i
}
die unless sum == 55
If all sections are empty then it is the same as using while (true)
and will iterate until a break
statement
exits the loop:
int i = 0, sum = 0
for (;;) {
sum += ++i
break if i >= 10
}
die unless sum == 55
Do/Until Loops
Jactl does not support do/while
loops but does have a do/until
loop.
The do/until
loop works similarly to a while
loop.
It performs a statement or block of statements until a given condition evaluates to true
.
Note that it always runs the loop at least once since the condition is checked after each execution of the loop.
For example:
int count = 0;
do {
count++
} until (nextToken().isEof())
Labels for While, For, and Do/While Statements
With while
, for
, and do/while
loops, you can always break out of or continue the current loop using break
or continue
.
Jactl also allows you to break out of, or continue an outer loop by labelling the loops and using the label in the
break
or continue
.
Labels are a valid name followed by :
and can be attached to a while
, for
, or do/while
statement if they
occur just before the loop:
int sum = 0
OUTER: for (int i = 0; i < 10; i++) {
int j = 0
INNER:
while (true) {
sum += ++j
continue OUTER if j > i
break OUTER if sum > 30
}
}
die unless sum == 36
Label names are any valid identifier (any combination of letters, digits, and underscore as long as the first character is not a digit and identifier is not a single underscore). Label names can be the same as other variable names (although this is not recommended practise).
Do Expression
A do/while
loop without the until
expression can be used as a way to execute a set of statements (exactly one time)
in a context where an expression would normally be expected.
For example:
def commands = ['right 5', 'up 7', 'left 2', 'up 3', 'right 4']
int x, y, distance
commands.each{
/up (\d*)/r and do { y += $1 as int; distance += $1 as int }
/down (\d*)/r and do { y -= $1 as int; distance += $1 as int }
/right (\d*)/r and do { x += $1 as int; distance += $1 as int }
/left (\d*)/r and do { x -= $1 as int; distance += $1 as int }
}
die unless [x, y, distance] == [7, 10, 21]
A do
expression returns the value of the last statement/expression in the block.
When chaining with other boolean expressions using and
, be careful since if the value of the
last expression does not result in a truthiness value of true
then the subsequent
expressions won’t be evaluated.
For example, in the following, since x++
is the last value in the do
block the do
block will
evaluate to x
and if x
were 0
, then the break
would not be executed:
def found = false
while (true) {
f(x) == 1 and do { found = true; x++ } and break
}
You can combine do
with if
or unless
:
do { found = true; println "Found multiple of 17: $x" } if x % 17 == 0
do { found = true; println "Found multiple of 17: $x" } unless x % 17
Print Statements
Jactl has print
and println
statements for printing a string.
println
will print the string followed by a new-line.
For example:
> for (int i = 1; i <= 5; i++) { println "$i squared is ${i*i}" }
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25
The output will, by default, be printed to System.out
(standard out) but can be directed elsewhere (for example
to a diagnostics log file).
See Integration Guide for more details.
Die Statements
The die
statement allows you to instantly exit a Jactl script with an optional error message.
It should be used only in situations where something unexpected has occurred and it makes no sense to continue.
Functions
Jactl supports functions that take arguments and return a result.
Functions always return a result (even if it is just null
) - there is no concept of void
functions in Jactl.
Jactl supports the Java syntax for creating a function (although Java doesn’t really have functions as everything has to be a method). For example this code is valid Jactl and valid Java:
// Recursive implementation for Fibonacci numbers
int fib(int n) {
return n <= 2 ? 1 : fib(n - 1) + fib(n - 2);
}
die unless fib(20) == 6765
In Jactl the return value of a function is the result of the last statement in the function so return
is optional
if you are returning at the end of the function anyway.
Sometimes it makes it clearer what is going on and if you need to return a value from the middle of the function
it is obviously still useful.
Parameter types are optional (default type will be def
meaning any type if not specified) and def
can be used
as the return type for the function as well.
So with return
and paramter type not needed and with semi-colon statement terminators being optional, the function
above could be rewritten in Jactl as:
def fib(n) { n <= 2 ? 1 : fib(n - 1) + fib(n - 2) }
die unless fib(20) == 6765
Functions can take multiple parameters. Here is a function that joins pairs of strings in two lists with a given separator and returns a list of the joined strings. If one of the lists is shorter than the other then it uses an empty string for the missing elements:
List joinStrings(List left, List right, String separator) {
int count = left.size() > right.size() ? left.size() : right.size()
List result = []
for (int i = 0; i < count; i++) {
result <<= (left[i] ?: '') + separator + (right[i] ?: '')
}
return result
}
die unless joinStrings(['a','b','c'],['AA','BB'],':') == ['a:AA', 'b:BB', 'c:']
After reading the section on collections and functional programming you will see that this could have been written more succinctly as:
List joinStrings(List left, List right, String separator) {
[left.size(), right.size()].max().map{ (left[it] ?: '') + separator + (right[it] ?: '') }
}
die unless joinStrings(['a','b','c'],['AA','BB'],':') == ['a:AA', 'b:BB', 'c:']
Default Parameter Values
You can supply default values for parameters in situations where having a default value makes sense.
For example we could default the separator to ':'
in the function above:
List joinStrings(List left, List right, String separator = ':') {
[left.size(), right.size()].max().map{ (left[it] ?: '') + separator + (right[it] ?: '') }
}
die unless joinStrings(['a','b','c'],['AA','BB']) == ['a:AA', 'b:BB', 'c:']
Now we can leave out the third parameter because when not present it will be set to ':'
.
Default values can still be supplied even if the type is not given:
def joinStrings(left, right, separator = ':') {
[left.size(), right.size()].max().map{ (left[it] ?: '') + separator + (right[it] ?: '') }
}
die unless joinStrings(['a','b','c'],['AA','BB']) == ['a:AA', 'b:BB', 'c:']
If a default value is supplied then it is possible to define the parameter as a var
parameter in which case it will
get its type from the type of the initialiser (as for variable declarations).
For example:
def joinStrings(List left, var right = [], var separator = ':') {
[left.size(), right.size()].max().map{ (left[it] ?: '') + separator + (right[it] ?: '') }
}
die unless joinStrings(['a','b','c'],['AA','BB']) == ['a:AA', 'b:BB', 'c:']
die unless joinStrings(['a','b']) == ['a:', 'b:']
It is also possible for the parameter default values to use values from other parameter values as long as they refer only to earlier parameters:
def joinStrings(List left, var right = left, var separator = ':') {
[left.size(), right.size()].max().map{ (left[it] ?: '') + separator + (right[it] ?: '') }
}
die unless joinStrings(['a','b']) == ['a:a', 'b:b']
Named Argument Invocation
It is possible to invoke functions using named arguments where the names and the argument values are given as a list of colon separated pairs:
def joinStrings(List left, var right = left, var separator = ':') {
[left.size(), right.size()].max().map{ (left[it] ?: '') + separator + (right[it] ?: '') }
}
// Argument order is unimportant when using named args
die unless joinStrings(separator:'=', left:['a','b'], right:['A','B']) == ['a=A', 'b=B']
Declaration Scope
Functions can be declared wherever it makes sense based on where they need to be invoked from. Their visibility is based on the current scope in which they are declared, just like any variable.
For example, in the following script we define the function within the for
loop where it is needed.
The function cannot be invoked from outside this for
loop:
def getFactorials(n) {
def result = []
for (int i = 1; i <= n; i++) {
def fact(n) { n == 1 ? 1 : n * fact(n - 1) }
result <<= fact(i)
}
return result
}
die unless getFactorials(10) == [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
Again, this could be written more simply like this:
def factorials(n) {
def fact(n) { n == 1 ? 1 : n * fact(n - 1) }
n.map{ fact(it+1) }
}
die unless factorials(10) == [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
Functions as Values
A function declaration can be treated as a value and assigned to other variables, passed as an argument to another function, or returned as a value from a function. For example:
def fact(n) { n == 1 ? 1 : n * fact(n - 1) }
def g = fact // simple assignment
die unless g(5) == 120
Another example showing functions being passed to another function that returns yet another function that composes the two original functions:
def compose(f, g) { def composed(x){ f(g(x)) }; return composed }
def sqr(x) { x * x }
def twice(x) { x + x }
def func = compose(sqr, twice) // create new function that invokes sqr with result of twice
die unless func(3) == 36 // should be sqr(twice(3))
While you can assign functions to other variables, the function variable itself (the name of the function) cannot be assigned to.
Invocation With List of Arguments
Functions can be invoked with a single list supplying the argument values for the function (in the declared order).
For example:
def f(a,b,c) { a + b + c }
def x = [1, 2, 3]
def y = ['x','abc','123']
die unless f(x) == 6
die unless f(y) == 'xabc123' // since parameters are untyped we can pass strings and get string concatenation
Closures
Closures in Jactl are modelled after the closure syntax of Groovy. Closures are similar to functions in that they take one or more parameters and return a result. They are declared with slightly different syntax:
def sqr = { int x -> return x * x } // Assign closure to sqr
die unless sqr(4) == 16
The syntax of a closure is an open brace {
and then a list of parameters declarations followed by ->
and then
the actual code followed by a closing }
.
Multiple parameters can be declared:
def f = { String str, int x ->
String result
for (int i = 0; i < x; i++) {
result += str
}
return result
}
die unless f('ab', 3) == 'ababab'
die unless 'ab' * 3 == 'ababab' // much simpler way of achieving the same thing
As for functions, the type is optional and default values can be supplied:
def f = { str, x = 2 -> str * x }
die unless f('ab', 3) == 'ababab'
die unless f('ab') == 'abab'
Implicit it Parameter
It is common enough for a closure to have a single argument and Jactl follows the Groovy convention of creating an
implicit it
parameter if there is no ->
at the start of the closure:
def f = { it * it }
die unless f(3) == 9
No Parameter Closures
If you want a closure that has no arguments use the ->
and don’t list any parameters:
def f = { -> "The current timestamp is ${timestamp()}" }
die unless f() =~ /The current timestamp is/
Closure Passing Syntax
Closures can be passed as values like functions:
def ntimes(n, f) {
for (int i = 0; i < n; i++) {
f(i)
}
}
def clos = { x -> println x }
ntimes(5, clos) // this will result in 0, 1, 2, 3, 4 being printed
It is possible to pass the closure itself directly to the function without having to store it in an intermediate variable:
def ntimes(n, f) {
for (int i = 0; i < n; i++) {
f(i)
}
}
ntimes(5, { println it }) // use implicit it variable this time
If the last argument to a function is a closure then it can appear directly after the closing )
:
def ntimes(n, f) {
for (int i = 0; i < n; i++) {
f(i)
}
}
ntimes(5){ println it }
There is a built-in methods on Lists, Maps, Strings, and numbers called each
, map
, etc. that takes a single argument
which is a closure.
For numbers, the each
method iterates n
times and passes each value from 0
to n-1
to the closure.
For example:
> 5.each({ x -> println x })
0
1
2
3
4
If the only argument being passed is a closure then the brackets are optional so this can be written like so:
5.each{ x -> println x }
And since we can use the implicit it
variable it can also be written like this:
5.each{ println it }
Similarly, there is a map
method on numbers (and Lists, Maps, and Strings) which maps the parameter value passed in
to another value calculated by the closure and returns that:
> 5.map{
it++
it * it // last expression in closure/function is return value
}
[1, 4, 9, 16, 25]
Here is another example where we create a function for timing the execution of a passed in closure and get it to time how long it takes to calculate the first 40 Fibonacci numbers. (Note that the recursive Fibonacci function is a very inefficient way of calculating Fibonacci numbers - it is just used as an example of something that will take some time to run.)
def timeIt(f) {
def start = timestamp()
def result = f()
println "Duration: ${timestamp() - start}ms, result = $result"
}
// How long to calculate first 40 Fibonacci numbers
timeIt{
int fib(int n) { n <= 2 ? 1 : fib(n-1) + fib(n-2) }
40.map{ fib(it+1) }
}
// On my laptop this gives output of:
// Duration: 488ms, result = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610,
// 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811,
// 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169,
// 63245986, 102334155]
Functions vs Closures
Consider the following:
def f(x) { x + x }
def c = { x -> x + x }
The function f
and the closure assigned to c
both do the same thing so what are the differences between a function
and a closure?
In the example above c
is just a variable and can have other values assigned to it whereas f
cannot be assigned to
since it is a function.
In fact, c
is not a closure but happens to have a closure as its value until it is assigned a different value.
Closures have an implicit parameter it
decalared for them if they don’t define any expicit parameters.
This is not the case for functions.
Functions can be invoked before they are declared (as long as they are in the same scope):
int i = f(3) // forward reference
def f(x) { x + x }
Since closures are anonymous values that can be assigned to variables the variable cannot be referenced before it is declared and so it is not possible to invoke a closure via a forward reference.
Functions support recursion. A function can invoke itself while a closure cannot refer to the variable to which it is being assigned if the closure is the initialiser for the variable:
def f(x) { x == 1 ? 1 : x + f(x - 1) }
def c = { it == 1 ? 1 : it + c(it - 1) } // error: Variable initialisation cannot refer to itself
The closure could have been split into a declaration and an assignment to allow the recursion:
def c
c = { it == 1 ? 1 : it + c(it - 1) }
This is not recommended, however, since there is no guarantee that c
won’t be reassigned later in the code.
Since functions support forward references it is possible to have mutually recursive functions (where each function invokes the other one):
def f(x) { x == 1 ? 1 : x + g(x - 1) }
def g(x) { x == 1 ? 1 : x * f(x - 1) }
Closing Over Variables
Both functions and closures can “close over” variables in outer scopes, which means that they have access to
these variables and can read and/or modify them.
Closures are similar to lambda functions in Java but whereas lambda functions can only close over final
or effectively
final variables (and therefore cannot modify the values of these variables), closures and functions in Jactl can
access variables whether they are effectively final or not and are able to modify their values.
(In Java, lambda functions close over the value of the variable rather than the variable itself which is why the
variable needs to be effectively final and why the lambda function is not able to modify the variable’s value).
Here is an example of a Jactl closure that access a variable in an outer scope:
def countThings(x) {
int count = 0
if (x instanceof List) {
x.each{ count++ if /thing/i } // increment count in outer scope
}
return count
}
Note
This just an example. A better way to count items matching a regex would be something like:x.filter{ /thing/i }.size()
Now consider the following code where a function returns a closure that closes over a variable count
which is
local to the scope of the function:
def counter() {
int count = 0
return { -> ++count }
}
def x = counter()
def y = counter()
println x()
println x()
println y()
The output from this script will be:
1
2
1
The reason that the call to y()
returns a value of 1
and not 3
is because each time counter()
is invoked it
returns a new closure that is bound to a brand new counter
variable.
Even though the counter
variable is no longer in scope once counter()
returns, and would normally be discarded,
in this case the closure, having closed over it, retains a binding to it, and it lives on as long as the closure
remains in existence.
In the example, x
and y
are two different instances of the closure which each have their own counter
variable.
Note
In functional programming, side effects such as modifying variables outside the scope of the closure are generally frowned upon since pure functions don’t modify state, they just return values.
Collection Methods
Jactl has a number of built-in methods that operate on collections and things that can be iterated over. In Jactl this means Lists, Maps, Strings, and numbers.
each()
Consider this code that iterates over the elements of a List:
def list = [1, 2, 3, 4, 5]
for (int i = 0; i < list.size(); i++) {
println list[i]
}
Aside from the annoying boilerplate code that has to be written each time to do this iteration, it also makes it easier to introduce bugs if the iteration code is not done correctly. To achieve the same thing using a more Functional Programming style:
def list = [1, 2, 3, 4, 5]
list.each{ println it }
We take the code that was in the for
loop and pass it as a closure to the each()
List method.
This method iterates over the list and passes each element into the closure.
This makes the code more concise, easier to understand, and less error-prone to write.
In general, when explicitly iterating over a collection of some sort, consider whether there is a better way to achieve the same thing using the built-in collection methods.
map()
Another important collection method is map()
which, like each()
, iterates and passes each element to the given
closure. The difference is that the closure passed to map()
is expected to return a value so the result of using
map()
on a list is another list with these new values:
> [1,2,3,4].map{ it * it }
[1, 4, 9, 16]
The method is called map
because it maps one set of values to another set of values.
Since the output of map is another list we can chain them together:
> [1,2,3,4].map{ it * it }.map{ it + it }.each{ println it }
2
8
18
32
Map Iteration and collectEntries()
As well as applying to Lists, these methods can also be applied to Map objects. For Maps, each element passed into the closure is a two-element list consisting of the key and the value:
> [a:1, b:2, c:3].each{ println "Key=${it[0]}, value=${it[1]}" }
Key=a, value=1
Key=b, value=2
Key=c, value=3
For closures passed to methods that act on Maps you can choose to pass a closure with a single parameter as shown or provide a closure that takes two parameters. If you provide a closure that takes two parameters then the first parameter will be set to the key and the second parameter will be the value:
> [a:1, b:2, c:3].each{ k, v -> println "Key=$k, value=$v" }
Key=a, value=1
Key=b, value=2
Key=c, value=3
To convert a list of key/value pairs back into a Map you can use the collectEntries()
method:
> [a:1, b:2, c:3].map{ k, v -> [k, v + v] }.collectEntries()
[a:2, b:4, c:6]
The collectEntries()
method takes an optional closure to apply to each element before adding to the Map so the
above can also be written as this:
> [a:1, b:2, c:3].collectEntries{ k, v -> [k, v + v] }
[a:2, b:4, c:6]
The closure passed to collectEntries()
must return a two-element list with the first element being a String which
becomes the key in the Map and the second element being the value for that key.
If there are multiple entries with the same key then the value for the last element in the list with that key will become the value in the Map.
String Iteration and join()
Strings can also be iterated over in which case the elements are the characters of the string. (Remember that a character is just a single-character string.) For example:
> 'abc'.each{ println it }
a
b
c
> 'abc'.map{ it + it.toUpperCase() }
['aA', 'bB', 'cC']
To turn a list of characters (actually a list of strings) back into a string use the join()
method which takes an
optional argument which is the separator to use when joining the multiple strings together:
> 'abc'.map{ it + it.toUpperCase() }.join()
aAbBcC
> 'abc'.map{ it + it.toUpperCase() }.join(':')
aA:bB:cC
Number Iteration
Numbers can also be “iterated” over.
In this case the number acts as the number of times to iterate with the values of the iteration being the numbers
from 0
until one less than the number.
If the number is not an integer then the fractional part is ignored for the purpose of working out how many iterations
to do.
For example:
> 5.each{ println it }
0
1
2
3
4
> 10.5.map{ it + 1 }.map{ it * it }
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
filter()
The filter()
method can be used to retain only elements that match a given condition.
It is passed a closure that should evaluate to true
if the element should be retained or false
if the element
should be discarded.
The truthiness of the result is used to determine whether the result is true
or not.
For example, to find which of the first 40 Fibonacci numbers are odd multiples of 3:
> int fib(int x) { x <= 2 ? 1 : fib(x-1) + fib(x-2) }
Function@124888672
> 40.map{ fib(it+1) }.filter{ it & 1 }.filter{ it % 3 == 0 }
[3, 21, 987, 6765, 317811, 2178309, 102334155]
filter()
without any argument will test each element for truthiness, so it is equivalent to filter{ it }
:
> ['a', 0, 1, '', 2, null, [], false, 3].filter()
['a', 1, 2, 3]
> ['a', 0, 1, '', 2, null, [], false, 3].filter{ it }
['a', 1, 2, 3]
allMatch(), anyMatch(), and noneMatch()
These methods take a predicate closure (a function/closure that returns true or false) and apply the predicate to the elements and return true or false based on the test being performed:
Method | Description |
---|---|
allMatch() | Returns true if all elements satisfy the predicate (empty list will return true). |
anyMatch() | Returns true if any element satisfies the predicate (empty list will return false). Terminates iteration as soon as the first matching element found. |
noneMatch() | Returns true if none of the elements satisfy the predicate (empty list will return true). Terminates iteration as it encounters an element for which the predicate is true. |
For example to make sure that there are no words shorter than three letters:
> ['here', 'are', 'some', 'words'].allMatch{ it.size() > 2 }
true
> ['here', 'are', 'some', 'words'].noneMatch{ it.size() < 3 }
true
> !['here', 'are', 'some', 'words'].anyMatch{ it.size() < 3 }
true
Note that the predicate can return anything that will then use the [truthiness][#truthiness) check to determine if the element satisfies the predicate check or not. By default, if not predicate is supplied, the methods will use test the element itself for truthiness:
> [null, '', 0, false, [], [:]].noneMatch()
true
> [1, true, 'abc', [1], [a:123]].allMatch()
true
mapWithIndex()
The mapWithIndex()
method works like the map()
method except that as well as passing in the element to the
closure it will also pass in the index.
For a closure with a single argument you get a two-element list where the first entry is the list element and the
second is the index:
> ['a', 'b', 'c'].mapWithIndex{ "Element ${it[1]} is ${it[0]}" }
['Element 0 is a', 'Element 1 is b', 'Element 2 is c']
Note that indexes start at 0 and go up to one less than the list size.
If you give it a closure that takes two arguments then the first argument will be the list element and the second one will be the index:
> ['a', 'b', 'c'].mapWithIndex{ v, idx -> "Element $idx is $v" }
['Element 0 is a', 'Element 1 is b', 'Element 2 is c']
Without any closure passed to it mapWithIndex()
creates a two-element list for each element with the first value
being the list element and the second being the index:
> ['a','b','c'].mapWithIndex()
[['a', 0], ['b', 1], ['c', 2]]
flatMap()
The flatMap()
method is similar to the map()
method in that it is expected that each element is transformed into
a new value by the given closure:
> ['a', 'b', 'c'].flatMap{ it.toUpperCase() }
['A', 'B', 'C']
The difference with flatMap()
is that if the closure returns a List rather than an individual value, the elements
of that list will be added to the result:
> ['a', 'b', 'c'].flatMap{ [it, it.toUpperCase()] }
['a', 'A', 'b', 'B', 'c', 'C']
The other difference is that a null
value as the result of the closure will mean that nothing is added to the result
for that input element:
> ['a', 'b', 'c'].flatMap{ it == 'b' ? null : [it, it.toUpperCase()] }
['a', 'A', 'c', 'C']
If you have a list of lists, then flatMap()
without any closure will flatten the list of lists into a single list.
It is same as though the closure { it }
was passed in :
> [[1,2], [3,4], [5,6]].flatMap()
[1, 2, 3, 4, 5, 6]
> [[1,2], [3,4], [5,6]].flatMap{ it }
[1, 2, 3, 4, 5, 6]
Note that only one level of flattening will result:
> [[1,2], [3,4], [5,[6,7,8]]].flatMap()
[1, 2, 3, 4, 5, [6, 7, 8]]
skip() and limit()
If you know that you are not interested in the first n
elements or are only interested in the first n
elements
of the result you can use skip()
and limit()
to skip elements and limit the result.
For example:
> 26.map{ (int)'a' + it }.map{ it.asChar() }.skip(10).limit(5)
['k', 'l', 'm', 'n', 'o']
You can use negative numbers for offset relative to the end of the list.
So skip(-2)
means skip until there are only two elements left:
> [1,2,3,4,5].skip(-2)
[4, 5]
limit(-3)
means discard the last 3 elements:
> [1,2,3,4,5].limit(-3)
[1, 2]
transpose()
Jactl provides a transpose()
method that takes a list of lists and performs a matrix tranpose on them.
If you think of the list of lists as a table, then this has the effect of converting a list of rows into a list of columns.
(In actual fact, converting a list of rows into a list of columns is the same as converting a list of columns into
a list of rows.)
The list on which transpose()
operates should be the list of input lists.
For example:
def i = ['a','b','c']
def j = [1,2,3]
[i,j].transpose()
Result will be:
[['a', 1], ['b', 2], ['c', 3]]
In this case, the result is a list of pairs where each pair is a value from each of the two input lists.
The transpose()
method supports any number of input lists so the output list will consist of tuples that have as many
elements as the number of input lists.
If any of the input lists is shorter than the others null
will be used to supply the missing values for it.
For example:
def i = ['a','b','c']
def j = [1,2]
def k = [10,20,30,40]
[i,j,k].transpose(true)
Result will be:
[['a', 1, 10], ['b', 2, 20], ['c', null, 30], [null, null, 40]]
Note that if the input is a true matrix (all lists are of the same size), then transpose()
applied twice in
a row will result in a value which is equal to the original list since transpose()
is the inverse of itself:
def x = [['a','b','c'],[1,2,3]]
x.transpose().transpose() == x // Result is true
Lazy Evaluation, Side Effects, and collect()
In Jactl, if a collection method results in another list of values then most methods don’t actually generate the list unless they are the last method in the invocation chain. Consider this code:
long fib(long x) { x <= 2 ? 1 : fib(x-1) + fib(x-2) }
def values = 1000.map{ fib(it+1) }.filter{ it & 1 }.filter{ it % 3 == 0 }.limit(5)
The code tries to calculate the first 1000 Fibonacci numers and then filter them for odd multiples of 3 limiting the result to the first 5 such numbers found.
As you can see, the output of the first map()
method is another list of transformed values (in this case the actual
Fibonacci numbers) which then passes this list to filter()
which outputs another list of only the odd numbers.
This list is then filtered again by another filter()
method call to get a list of multiples of 3 and then finally
this is passed to limit()
to limit output to the first 5 elements.
None of these lists is actually created until the last result when it is needed in order to be stored into the values
variable.
In fact, we don’t calculate the first 1000 Fibonacci numbers (which would take a prohibitively long time using the
inefficient algorithm we have implemented here). We only calculate as many as are needed to get the first 5 odd
multiples of 3.
Jactl uses lazy evaluation to avoid creating unnecessary List objects for intermediate results. You can think of the chain of method calls acting as a conveyor belt where each element flows through each method before the next element is processed.
This works well, and is obviously much more efficient than creating the intermediate lists, but if you use side effects in your closures then you need to be aware of how this way of executing the methods works. Consider this code:
> int i = 0
0
> ['a','b','c'].map{ it + ++i }.map{ [it, i] }
[['a1', 1], ['b2', 2], ['c3', 3]]
As you can see, the value of i
is incremented as each element is completely processed by all methods in the
call chain and so the second element of each pair is 1
, 2
, and 3
.
This may not be what was intended.
It might be intended that the final value of i
(3
in this case) is the second value of each sub-list as
though the first map()
call had finished to completion before the second map()
call was invoked.
If you would like to force the creation of these intermediate lists in order then you can use the collect()
method
to force a list to be created wherever needed:
> int i = 0
0
> ['a','b','c'].map{ it + ++i }.collect().map{ [it, i] }.collect()
[['a1', 3], ['b2', 3], ['c3', 3]]
In Jactl, the collect()
also takes an optional closure like the Groovy form and so it is better to just write:
> int i = 0
0
> ['a','b','c'].collect{ it + ++i }.collect{ [it, i] }
[['a1', 3], ['b2', 3], ['c3', 3]]
Note that in Java, when using streams, it is necessary to invoke collect()
to convert the final stream of
values back into a list.
In Jactl, this is not necessary since this will automatically be done when the last method in the chain has
finished.
So in Jactl these two expressions are equivalent:
> [1,2,3].map{ it * it }.collect()
[1, 4, 9]
> [1,2,3].map{ it * it }
[1, 4, 9]
grouped()
The grouped()
method groups elements into sub-lists.
So grouped(2)
will create a list of pairs of elements from the source list, while grouped(3)
would split the list
into a list of three-element sub-lists.
For example:
> ['a','b','c','d','e'].grouped(2)
[['a', 'b'], ['c', 'd'], ['e']]
> ['a','b','c','d','e'].grouped(3)
[['a', 'b', 'c'], ['d', 'e']]
If there are not enough elements to complete the last sub-list then the last sub-list will just have whatever elements there are leftover.
Note that grouped(0)
returns the list of elements unchanged and grouped(1)
will create a list of single element
sub-lists:
> [1,2,3].grouped(0)
[1, 2, 3]
> [1,2,3].grouped(1)
[[1], [2], [3]]
windowSliding()
The windowSliding()
method groups the elements into a list of sliding windows of a given size.
So windowSliding(2)
will advance through the list producing sub-lists containing each element paired with the
subsequent element:
> ['a','b','c','d'].windowSliding(2)
[['a', 'b'], ['b', 'c'], ['c', 'd']]
> 'abc'.windowSliding(2)
[['a', 'b'], ['b', 'c']]
> [a:1, b:2, c:3].windowSliding(2) // Maps can also be treated as lists
[[['a', 1], ['b', 2]], [['b', 2], ['c', 3]]]
A size of 3
for the window will produce sub-lists with each element followed by the next 2 elements in the original
list:
> ['a','b','c','d','e'].windowSliding(3)
[['a', 'b', 'c'], ['b', 'c', 'd'], ['c', 'd', 'e']]
If there are not enough elements to complete the first sub-list then the result will be a single element list where the element is the original list:
> [1,2,3].windowSliding(4)
[[1, 2, 3]]
sort()
The sort()
method will sort a list of elements.
With no argument it will sort based on natural sort order if one exists:
> [3, 4, -1, 1, 10, 5].sort()
[-1, 1, 3, 4, 5, 10]
> ['this', 'is', 'a', 'list', 'of', 'words'].sort()
['a', 'is', 'list', 'of', 'this', 'words']
If elements have not natural ordering then you will get an error:
> [[1,2,3],[1,2]].sort()
Unexpected error: Cannot compare objects of type List and List @ line 1, column 17
[[1,2,3],[1,2]].sort()
^ (RuntimeError) @ line 1, column 17
[[1,2,3],[1,2]].sort()
^
> [1,'a'].sort()
Unexpected error: Cannot compare objects of type String and int @ line 1, column 9
[1,'a'].sort()
^ (RuntimeError) @ line 1, column 9
[1,'a'].sort()
^
You can pass a closure to the sort()
method that will be passed two elements and needs to return a negative number
(e.g. -1
) if the first element is smaller than the second one, 0
if they are the same, or a positive number
(e.g. 1
) if the first element is bigger than the second one:
> ['this', 'is', 'a', 'list', 'of', 'words'].sort{ it[0] < it[1] ? -1 : it[0] == it[1] ? 0 : 1 }
['a', 'is', 'list', 'of', 'this', 'words']
If the closure accepts two arguments then this can be written as:
> ['this', 'is', 'a', 'list', 'of', 'words'].sort{ a,b -> a < b ? -1 : a == b ? 0 : 1 }
['a', 'is', 'list', 'of', 'this', 'words']
This can be made more concise by using the comparator operator <=>
which will return -1
, 0
, or 1
if the
left-hand side is less than, equal, or greater than the right-hand side:
> ['this', 'is', 'a', 'list', 'of', 'words'].sort{ a,b -> a <=> b }
['a', 'is', 'list', 'of', 'this', 'words']
If you want to reverse the sort order you can swap the left-hand and right-hand sides:
> ['this', 'is', 'a', 'list', 'of', 'words'].sort{ a,b -> b <=> a }
['words', 'this', 'of', 'list', 'is', 'a']
Since you can sort based on arbitrary criteria, you can sort arbitrary objects:
> def employees = [[name:'Frank', salary:2000], [name:'Daisy', salary:3000], [name:'Joe', salary:1500]]
[[name:'Frank', salary:2000], [name:'Daisy', salary:3000], [name:'Joe', salary:1500]]
> employees.sort{ a,b -> a.salary <=> b.salary } // sort by salary increasing
[[name:'Joe', salary:1500], [name:'Frank', salary:2000], [name:'Daisy', salary:3000]]
unique()
The unique()
method allows you to eliminate duplicate elements in a list.
It works like the Unix uniq
command in that it only considers elements next to each other in the list to determine
what is unique.
For example:
> ['a','a','b','c','c','c','a'].unique()
['a', 'b', 'c', 'a']
Note that 'a'
still occurs twice since only runs of the same value were eliminated.
If you want to eliminate all duplicates regardless of where they are in the list then sort the list first:
> ['a','a','b','c','c','c','a'].sort().unique()
['a', 'b', 'c']
reverse()
The reverse()
method will reverse the order of the elements being iterated over:
> [1, 2, 3].reverse()
[3, 2, 1]
> 10.map{ it+1 }.map{ it*it }.reverse()
[100, 81, 64, 49, 36, 25, 16, 9, 4, 1]
reduce()
The reduce()
method will iterate over a list and invoke a given closure on each element.
In addition to passing in the element to the closure, reduce()
passes in the previous value that the
closure returned for the previous element so that the closure can calculate the new value based on the previous
value and the current element.
The final result is whatever the closure returns when passed in the final element and the previous calculated value.
Here is an example of how to use reduce()
to calculate the sum of the elements in a list (of course, using sum()
is simpler but this is just an example):
> [3, 4, -1, 1, 10, 5].reduce(0){ prev, it -> prev + it }
22
Note that reduce()
takes two arguments: the initial value to pass in, and the closure.
The closure can have one or two parameters.
If it has one parameter it is passed a list of two values with the first being the previous value calculated (or the
initial value) and the second being the current element of the list.
If it takes two parameters then the first one is the previous value and second one is the element.
Here is another example where we want to count the letter frequency in some text and print out the top 5 letters. We use reduce to build a Map keyed on the letter with the value being the number of occurrences:
> def text = 'this is some example text to use to count letter frequency'
this is some example text to use to count letter frequency
> text.filter{ it != ' ' }.reduce([:]){ m, letter -> m[letter]++; m }.sort{ a,b -> b[1] <=> a[1] }.limit(5)
[['e', 9], ['t', 8], ['s', 4], ['o', 4], ['u', 3]]
min() and max()
You can use min()
and max()
to find the minimum or maximum element from a list:
> [3, 4, -1, 1, 10, 5].min()
-1
> [3, 4, -1, 1, 10, 5].max()
10
> ['this', 'is', 'a', 'list', 'of', 'words'].min()
a
> ['this', 'is', 'a', 'list', 'of', 'words'].max()
words
If you want to be able to decide what comparison to use to decide which element is the one that is the smallest or the biggest, you can pass a closure that returns something that can be compared. For example, to find the employee with the smallest or biggest salary:
> def employees = [[name:'Frank', salary:2000], [name:'Daisy', salary:3000], [name:'Joe', salary:1500]]
[[name:'Frank', salary:2000], [name:'Daisy', salary:3000], [name:'Joe', salary:1500]]
> employees.min{ it.salary }
[name:'Joe', salary:1500]
> employees.max{ it.salary }
[name:'Daisy', salary:3000]
avg() and sum()
The avg()
and sum()
methods only work on lists of numbers and return the average or the sum of the values in the
list:
> [3, 4, -1, 1, 10, 5].sum()
22
> [3, 4, -1, 1, 10, 5].avg()
3.6666666667
groupBy()
The groupBy()
method takes a closure and applies the closure to each element of the List to get a key for that
element.
It creates a Map where the value for each key is a list of the elements of the List that returned the same key.
For example, to group words with the same lengths:
['list', 'of', 'words', 'with', 'different', 'sizes'].groupBy{ it.size().toString() }
// Result: ['4':['list', 'with'], '2':['of'], '5':['words', 'sizes'], '9':['different']]
Or, to group people based on their first name:
[ [first:'Fred',last:'Flinstone'], [first:'Barney',last:'Rubble'], [first:'Fred',last:'Fredson']].groupBy{ it.first }
// Result: [Fred:[[first:'Fred', last:'Flinstone'], [first:'Fred', last:'Fredson']],
// Barney:[[first:'Barney', last:'Rubble']]]
Note that the closure must return a String as Maps only support keys that are of type String.
Checkpointing
Jactl provides a checkpoint()
function that allows a Jactl script to checkpoint its current execution state and
provides a hook that allows these checkpoints to be persisted (to file system or a database) or to be distributed
over the network to another server.
The checkpoint state can later be recovered and the script will continue from where the checkpoint()
function had
been invoked.
The saving and recovering of checkpoints is done via the JactlEnv
implementation provided to the Jactl runtime.
The idea is to provide a custom implementation of this class and implement the saveCheckpoint()
and deleteCheckpoint()
methods as appropriate to the environment in which the scripts are running.
To recover a checkpoint and trigger the script to continue from where it had been checkpointed, the
JactlContext.recoverCheckpoint()
method should be used.
See Integration Guide for more details.
The checkpoint()
function optionally takes two parameters which are closures.
The first one (with parameter name commit
) is invoked when the checkpoint()
function is called, while the second
one (with parameter name recover
) is invoked if the script is recovered.
This allows the script to have different behaviour after a failure and subsequent recovery.
For example, if the script sends a message to a downstream system, it might need to set a “repeat” flag in the message
after a recovery to indicate that it is a possible repeat of an earlier message:
def response = checkpoint(commit: {
sendReceiveJson(destUrl, [request:req, possibleRepeat:false])
},
recover: {
sendReceiveJson(destUrl, [request:req, possibleRepeat:true])
})
The checkpoint()
function returns whatever the commit
or recover
closure returns (whichever one is the one
that is invoked).
Classes
Like Java, Jactl supports user defined classes but in a more simplified form. Jactl uses a syntax that (mostly) follows that of Java.
Classes provide a way to encapsulate state with fields, and behaviour with methods.
Here is an example of a simple class:
class Point {
int x
int y
}
The class declares two fields x
and y
of type int
.
If we enter the above class into the REPL we can then instantiate instances of the class:
> class Point {
int x
int y
}
> def point = new Point(1,2)
[x:1, y:2]
Note that in Jactl, you do not write explicit constructors for classes.
To instantiate a class you use the new
operator followed by the class name and then a list of arguments that become
the values for the mandatory fields of the class (in the order in which the fields are declared).
Notice that by default, the value of the object when printed by the REPL is shown as though the object were a Map of field values.
Field Access
To access the fields of the class object you use .
or ?.
just as for Maps:
> point.x
1
> point.y
2
You can use expressions that evaluate to the field name:
> point.('xyz'[0])
1
> point."${('x' + 'y').skip(1)[0]}"
2
You can also use ‘[]’ and ‘?[]’ to access the fields:
> point['x']
1
> point['y']
2
It is recommended to use ‘.’ and ‘?.’ with explicit names rather than expressions where possible as this is a more efficient access mechanism.
Class Name as Type
When declaring a variable to contain a class instance you can make the variable strongly typed if you want. If you then try to assign something else to the variable that is not a Point you will get an error:
> Point point = new Point(3,4)
[x:3, y:4]
> point = new Point(5,5)
[x:5, y:5]
> point = 'abc'
Cannot convert from type of right-hand side (String) to Instance<Point> @ line 1, column 7
point = 'abc'
^
You can use the class name wherever a type would normally be expected such as for parameter types and return types of functions:
> Point midPoint(Point p1, Point p2) {
new Point((p1.x + p2.x)/2, (p1.y + p2.y)/2)
}
Function@375457936
> midPoint(new Point(1,2), new Point(4,4))
[x:2, y:3]
Field Initialisers and Mandatory Fields
Like variables, fields can be declared with initialisers:
> class Point {
int x = 0
int y = 0
}
> def point = new Point()
[x:0, y:0]
Fields without intialisers are mandatory and a value must be supplied when instantiating instances with new
:
> class Point {
int x
int y = 0
}
> new Point()
Missing mandatory field: x @ line 1, column 10
new Point()
^
Only values for mandatory fields can be supplied this way:
> new Point(1,2)
Too many arguments for constructor (passed 2 but there is only 1 mandatory field) @ line 1, column 10
new Point(1,2)
^
> new Point(1)
[x:1, y:0]
Initialisers can use values from other fields:
> class Point {
int x
int y = x
}
> new Point(3)
[x:3, y:3]
Note that if a field ininitialiser refers to a prior field it will use the value that the field has been assigned but if it refers to a later field, since that field has not yet been initialised, it will just get whatever the default value is for the type of field:
> class Point {
int x = y
int y
}
> new Point(7)
[x:0, y:7]
Named Argument Passing for new
To set values for non-mandatory fields you can use the named argument way of invoking new
which allows you set
the values of any fields (as long as all mandatory fields are given a value):
> class Point {
int x
int y = 0
}
> new Point(x:3, y:4)
[x:3, y:4]
> new Point(x:4)
[x:4, y:0]
Converting between Maps and Class Instances
As shown, instances, when printed out look like Maps and passing named arguments to new
is like constructing a
Map literal.
Jactl also supports constructing instances by coercing a Map using the as
operator:
> Point p = [x:3, y:5] as Point
[x:3, y:5]
You can also convert the other way and take a class instance and coerce it to a Map:
> Map m = p as Map
[x:3, y:5]
When constructing an instance this way, you need to supply values for all mandatory fields but can leave out values for the non-mandatory fields:
> [y:5] as Point
Missing value for mandatory field 'x' @ line 1, column 7
[y:5] as Point
^
> [x:6] as Point
[x:6, y:0]
Field Types
We have shown, so far, only examples of fields with type int
but, as for variables and function parameters,
fields can have any type:
> class X {
int i = 0, j = i
String str = 'a string'
long longField = i * j
var decimalField = 3.5
double d = 1.23D
def closure = { it * it }
}
> new X()
[i:0, j:0, str:'a string', longField:0, decimalField:3.5, d:1.23, closure:Function@33533830]
Classes can even have fields of the same type as the class they are embedded in:
class Tree {
Tree left = null
Tree right = null
def data
}
Auto-Creation
Just as for Maps, Jactl supports “auto-creation” of fields when a field reference appears as a left-hand side of an
assignment-like operator such as =
, ?=
, +=
, -=
, etc., as long as the types of the fields are types that can
be created with no arguments.
In other words, the types must not have any mandatory fields.
For example:
> class X { Y y = null }; class Y { Z z = null }; class Z { int i = 3 }
> def x = new X()
[y:null]
> x.y.z.i = 4
4
> x
[y:[z:[i:4]]]
By assigning a value to x.y.z.i
we automatically created the missing values for x.y
and x.y.z
since they
were types that could be auto-created.
If a type has a mandatory field then the auto-create will fail:
> class X { Y y = null }; class Y { int j; Z z = null }; class Z { int i = 3 }
> def x = new X()
[y:null]
> x.y.z.i = 4
Cannot auto-create instance of type Class<Y> as there are mandatory fields @ line 1, column 3
x.y.z.i = 4
^
Instance Methods
We have seen classes with fields but classes are more than just a data structure for grouping related pieces of data. Classes can also have instance methods defined for them:
> class Point { int x,y }class Point { int x,y }
> class Rect {
Point p1, p2
int area() { (p1.x - p2.x).abs() * (p1.y - p2.y).abs() }
boolean contains(Point p) {
p.x >= [p1.x, p2.x].min() && p.x <= [p1.x, p2.x].max() &&
p.y >= [p1.y, p2.y].min() && p.y <= [p1.y, p2.y].max()
}
}
Instance methods are associated with an instance of the class and references to the fields within a method access the fields of that instance.
Methods are accessed the same way as fields and then invoked using ()
just as for functions:
> Rect rect = new Rect(new Point(3,4), new Point(7,8))
[p1:[x:3, y:4], p2:[x:7, y:8]]
> rect.area()
16
> rect.contains(new Point(0,2))
false
> rect.contains(new Point(5,6))
true
Since methods can be accessed like they are fields you can use ?.
and []
and ?[]
to access them:
> rect?."${'contains'}"(new Point(5,6))
true
> rect['are' + 'a']()
16
This
Within instance methods instance fields are accessed by referring directly to their names:
> class X {
int i = 3
int f() { i } // return the value of the i field
}
> new X().f()
3
There is an implicit variable this
for the instance that can also be used to refer to the fields.
This is handy if there are local variables or parameters that have the same name as on of the fields:
> class X {
int i = 3
int add(int i) { this.i + i } // Add this.i to the parameter i
}
> new X().add(4)
7
You need to use this
in situations where you want to pass a reference to the current instance to another function
or method:
> class X { int i = 3; def doSomething(closure) { closure(this) } }
> new X().doSomething{ println it }
[i:3]
Final Methods
Instance methods can be marked as final
which means that they cannot be overridden by a child class.
Where needed, this gives the parent class more control how the parent class can be used.
Final methods allow the Jactl compiler to perform some optimisations that cannot do for non-final methods and
there are also optimisations that the Java Virtual Machine itself can do for final
methods.
Here is an example of the use of final
:
> class X { final def func() { 'final function' } }
> class Y extends X { def func() { 'trying to override final function' } }
Method func() is final in base class X and cannot be overridden @ line 1, column 21
class Y extends X { def func() { 'trying to override final function' } }
^
Static Methods
The methods we saw previously were methods that are instance methods and apply to an instance of a class.
Jactl also supports methods that are defined as static
which are class methods rather than instance methods.
Instead of invoking these methods via a class instance, they are invoked using the classname itself and are
not associated with any specific instance of the class.
For example:
> class Point {
int x, y
static Point midPoint(Point p1, Point p2) {
new Point((p1.x + p2.x)/2, (p1.y + p2.y)/2)
}
}
> def mid = Point.midPoint(new Point(1,2), new Point(3,4))
[x:2, y:3]
If you try to access an instance field from within a static method you will get a compile error:
> class X {
int i = 0
static def staticMethod() { i++ }
}
Reference to field in static function @ line 3, column 33
static def staticMethod() { i++ }
^
As well as using the classname with the method invocation, you can invoke static methods through a class instance:
> Point p = new Point(15,16)
[x:15, y:16]
> p.midPoint(new Point(1,2), new Point(3,4))
[x:2, y:3]
Invoking a static method via an instance works even if the type of the variable is not known at compile time:
> def x = new Point(10,11)
[x:10, y:11]
> x.midPoint(new Point(1,2), new Point(3,4))
[x:2, y:3]
Class Constant Fields
Classes can declare constants.
These are similar to static final
fields in Java.
For example:
class Math {
const PI = 3.14159265356
}
def area(radius) { Math.PI * radius.sqr() }
Only simple types are supported (booleans, numbers, and Strings).
The type can be omitted (as in the example) and will be inferred from the value of the constant being assigned to it.
No Static Fields
In Jactl, there is no support for static fields. This differs from Java and Groovy which support static fields which are field that exist at the class level rather than the class instance level.
The reason that Jactl has this restriction is to do with the intended use of the language. The language is intended to be used in distributed event driven/reactive programming applications in order for users of these applications to be able to provide their own extensions and customisations.
Since Jactl scripts are intended to be run in a distributed multithreaded application where multiple threads in multiple application instances can be running the same script at the same time, it makes no sense to have class level fields since they would not be global fields but would exist per application instance. Rather than offering the illusion of global state, Jactl has taken the decision to not offer this feature at all. This also means not having to introduce the complexity, performance, and deadlock issues of dealing with locking/synchronisation between multiple threads.
If global state is really required then it is up to the application to provide its own functions for Jactl scripts to use that can update/read some global state, potentially in a database, or in a distributed in-memory key/value store of some sort.
Methods as Values
Like normal functions, methods can be assigned as values to variables and passed as values to other function/methods:
> class Point {
int x, y
static Point midPoint(Point p1, Point p2) {
new Point((p1.x + p2.x)/2, (p1.y + p2.y)/2)
}
}
> class Rect {
Point p1, p2
int area() { (p1.x - p2.x).abs() * (p1.y - p2.y).abs() }
}
We can get the value of the midPoint
static method and invoke it through a different variable:
> def mp = Point.midPoint
Function@438589491
> mp(new Point(1,2), new Point(3,4))
[x:2, y:3]
If we try to get the area
instance method of the Rect
class the same way we get this error:
> def a = Rect.area
Static access to non-static method 'area' for class Class<Rect> @ line 1, column 14
def a = Rect.area
^
We need to access it through an instance:
> def rect = new Rect(new Point(1,2), new Point(3,4))
[p1:[x:1, y:2], p2:[x:3, y:4]]
> def area = rect.area
Function@597307515
> area()
4
Note what happens if we now change the value of the rect
variable:
> rect = new Rect(new Point(2,3), new Point(10,12))
[p1:[x:2, y:3], p2:[x:10, y:12]]
> rect.area()
72
> area()
4
The area
variable points to the area()
method bound to the original instance so even if rect
gets a new value
area
is still bound to the old value.
Inner Classes
Inner classes can be defined within a class if desired:
> class Shapes {
class Point { def x,y }
class Rect {
Point p1, p2
def area() { (p1.x - p2.x).abs() * (p1.y - p2.y).abs() }
def centre() { new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2) }
}
class Circle {
Point centrePoint
def radius
def area() { radius * radius * 3.1415926536 }
def centre() { centrePoint }
}
List shapes = []
Rect createRect(x1,y1,x2,y2) {
def rect = new Rect(new Point(x1,y1),new Point(x2,y2))
shapes <<= rect
return rect
}
Circle createCircle(x,y,radius) {
def circ = new Circle(new Point(x,y),radius)
shapes <<= circ
return circ
}
def areas() { shapes.map{ it.area() } }
def centres() { shapes.map{ it.centre() } }
}
> def shapes = new Shapes()
[shapes:[]]
> shapes.createRect(1,2,5,6)
[p1:[x:1, y:2], p2:[x:5, y:6]]
> shapes.createCircle(3,4,5)
[centrePoint:[x:3, y:4], radius:5]
> shapes.areas()
[16, 78.5398163400]
> shapes.centres()
[[x:3, y:4], [x:3, y:4]]
You can access the inner classes from outside their outer class by fully qualifying them with the outer class name:
> new Shapes.Circle(new Shapes.Point(3,4), 12).area()
452.3893421184
You can, of course, also nest inner classes within inner classes:
> class X{ class Y { class Z { int i = 1 } } }
> new X.Y.Z().i
1
Note that Jactl inner classes are always “static inner classes” in Java terminology. This means that instances of inner classes are not implicitly bound to an instance of an outer class. There is no way in Jactl to declare a non-static inner class.
toString()
When converting a class instance to a string for the purposes of printing them or because they are being coerced
into a string use as
then the output is shown as though the instance were a Map:
> class X { int i,j }
> def x = new X(3,4)
[i:3, j:4]
> x as String
[i:3, j:4]
When printing a class instance, Jactl will first check to see if there is a toString()
method defined or not
before using the default string conversion.
If toString()
has been defined then this will be used instead so this is how you can customise the way in which
class instances for a given class should be converted into strings:
> class X { int i,j; def toString() { "i=$i, j=$j" } }
> new X(3,4)
i=3, j=4
The toString()
method must have no mandatory arguments and must have a return type of String
or def
.
Duck Typing
Jactl allows you to use strong typing, where fields, variables, and parameters are given an explicit type, or dynamic
typing where fields, variables, and parameters can be defined as def
which allows them to hold values of any type.
If you use strong typing then in most cases the Jactl compiler can determine at compile time what method is being invoked and can do compile time checking of argument count and argument types.
Dynamic typing (or weak typing) means that the method invocation can only be checked at runtime and a runtime error will be produced if the method doesn’t exist or the argument types don’t match. This is also known as “duck typing” which means that we don’t actually care if the object is a duck, we only want to know that it “talks like a duck” in that it has a method of the given name we are looking for.
For example the function func()
doesn’t care what type of object it is given as long as there is a method called
f()
it can invoke on it:
> class X { def f() { 'invoked X.f()' } }
> class Y { def f() { 'invoked Y.f()' } }
> def func(x) { x.f() }
Function@2130192211
> func(new X())
invoked X.f()
> func(new Y())
invoked Y.f()
Jactl does not take any position on whether strong typing or weak typing is more superior. It is up to you as the developer to decide what makes sense or what feels more natural.
Packages
Related classes can be grouped together into packages. When using the Jactl REPL all classes are put into the top level package and there is no way to specify any other package. Packages are useful when integrating Jactl scripts into a Java application.
A package can be thought of like a library. They allow a set of customisation classes to be grouped together into a library that can then be used by other Jactl scripts.
See the Integration Guide for more information about how packages work when compiling Jactl scripts and classes in your Java application.
When compiling Jactl classes and scripts the first line of the file can contain a package statement which specifies what package the script or class should be compiled into:
package a.b.c
class X {
int i,j
class Y {
static def someMethod() { 'some string' }
}
}
The package name that come after the package
keyword must be a dot separated list of lowercase names.
The package name can be a single name such as util
or can be list like xyz.util.messaging
.
The packages form a tree where a package a.b.c
is contained with the package a.b
which is inside a
.
Classes can exist at any level of the tree, not just at the most nested levels.
Fully Qualified Class Names and Import
When using packages, classes in the same package can be accessed directly via their name.
If a class exists in a different package then you can refer to the class using its fully qualified name which is
the package name followed by a .
and then the classname.
For example if there is a class X
in package a.b.c
then you access the class using a.b.c.X
:
new a.b.c.X(1,2)
If there is a nested class Y
in side the class X
then it can be accessed as a.b.c.X.Y
.
For example, to access a static method of class Y
you would use this:
a.b.c.X.Y.someMethod()
To save having to continually type the full package name each time you use a class from another package you can
use import
to import the class definition into the current Jactl script or class file:
package x.y.z
import a.b.c.X
def x = new X(1,2)
X.Y.someMethod() // Inner classes can now be accessed directly as X.xxx
You can also use import as
to import the class and give it an alias.
If you have a long class name you might want to use it with a different alias within the current file:
package x.y.z
import a.b.c.MyLongNamedClass as MLNC
def x = new MLNC()
Using import as
is also useful if you have classes of the same name in different packages that you need
access to.
Import Static
You can use import static
to selectively import static functions and constants from another class into the
current class/script.
So if a class a.b.c.X
has a static function f
you can import just that function like this:
import static a.b.c.X.f
Using import static
this way means that you can then refer to the static function directly, without having to
qualify it with the class name:
import static MathUtils.circleArea
def radius = 4
circleArea(radius)
Using as
you can import it and give it a different name:
import static MathUtils.circleArea as area
def radius = 4
area(radius)
The import static
directive also works for class constants defined in another class:
import static MathUtils.PI
import static MathUtils.PI_SQRD as pi_squared
Using *
you can import all static functions and constants from a class:
import static MathUtils.*
Scoping
Unlike functions which can be declared in any scope and then have visibility within that scope, classes can only be declared in two places:
- Top level of a script or class file, or
- As an inner class within another class declaration.
Switch Expressions and Pattern Matching with Destructuring
Switch Expression
The switch
expression in its simplest form works like a switch
expression in Java (first introduced in Java 17)
where you pass an expression to switch()
and then have a list of values to match against with a result for each match.
In Jactl there is no equivalent to a switch
statement like there is in Java.
The Jactl switch
expression can be used as either a statement (if you ignore the result) or an expression.
Note that there is no fall through between different cases and so break
will not break out of the switch, but will
break out of whatever enclosing for
/while
loop the switch
is contained within.
The main difference, syntactically, between Jactl switch
and Java switch
is that Jactl does not require the case
keyword (and use of ;
is optional if new lines are used).
Here is a simple example:
switch(x) {
1 -> println 'one'
2 -> println 'two'
default -> println 'unknown'
}
Note that, as in Java, an optional default
case can be used to specify the behaviour when none of the
cases match.
If you want to put more than one case on the same line then ;
can be used as a separator:
switch(x) { 1 -> println 'one'; 2 -> println 'two' }
If the result of the match involves multiple statements then they need to be wrapped in {
and }
:
switch(x) {
'Fred' -> {
fred++
println "Fred: $fred"
}
'Barney' -> {
barney++
println "Barney: $barney"
}
}
In Jactl, switch
is actually an expression so the code after each ->
is evaluated and the resulting
value is returned if that case succeeded:
def result = switch(x) {
1 -> 'one'
2 -> 'two'
default -> 'unknown'
}
If the code for the matching case does not result in an actual value (for example the last statement is a while
loop)
then null
is returned.
Note that invoking return
from within a switch
case will return from the enclosing function rather than cause
the switch
expression to return that value.
If multiple values should map to the same result you can separate the values with commas:
switch(x) {
1,3,5,7,9 -> 'odd'
2,4,6,8,10 -> 'even'
}
Matching on Literals
As we have seen in the examples so far, we can use switch
expressions to match on literals which are standard
primitive types such as int
, long
, double
, String
and so on.
We can also use literals which are List
or Map
literals, and we can support different literal types in the same
switch
(if the type of the value which is the subject of the switch
permits the comparison):
switch(x) {
1,2,3,4,'abc' -> 'primitive'
[1,2,3],[4,5] -> 'list'
[a:1,b:[4,5]] -> 'map'
}
If you try to match against a literal whose type is incompatible with the type of the subject of the switch
then
you will get a compile error.
For example:
int x = 2
switch(x) {
1,2 -> x
'abc' -> "x=$x"
}
io.jactl.CompileError: Type int can never match type String @ line 4, column 3
'abc' -> "x=$x"
^
Matching on Type
In addition to matching against literal values, Jactl also supports testing if an expression is of a given type:
switch(x) {
String -> 'string'
int,long -> 'integer'
double,Decimal -> 'floating point'
}
Types and literal values can be mixed in the same switch
expression:
switch(x) {
1,2,String -> '1, 2, or string'
int,long -> 'other integral value'
}
Note that if the order of the matches was the other way around you will get a compile error since int
already covers
the case of all int values:
switch(x) {
int,long -> 'other integral value'
1,2,String -> '1, 2, or string'
}
Switch pattern will never be evaluated (covered by previous pattern) @ line 3, column 3
1,2,String -> '1, 2, or string'
^
User classes can also be matched against:
class X {}
class Y extends X {}
def x = new Y()
switch(x) {
Y -> 'type is Y'
X -> 'type is X'
}
Again, if the match on type X
in the example was before the match on type Y
you will get a compile error since
the match on Y
can never succeed if it appears after the match on its parent class.
Matching on Regex
Switch expressions can also be used to do regular expression matches and support the use of capture variables like
$1
in the result for that match case:
switch(x) {
/(.*)\+(.*)/n -> $1 + $2
/(.*)-(.*)/n -> $1 - $2
default -> 0
}
Note that if the regex pattern has no modifiers after it then it will be treated as a standard expression string
and a literal match will occur instead.
To force it to be a regular expression match you must use a modifier (such as n
in the example, which forces the
capture variables to be treated as numbers where possible).
The r
modifier can be used to force the string to be treated as a regular expression if no other modifier is
appropriate.
Destructuring and Binding Variables
There were previous examples showing matches on literals of type List
and Map
.
When matching against a list or map we can use _
as a wildcard to match against arbitrary values within the list
or map:
switch(x) {
[_,_,_] -> 'a list of size 3'
[_,3] -> 'list of size 2 where last element is 3'
[k1:_,k2:_] -> 'a map of size 2 with keys k1 and k2'
}
We can use *
to match any number of elements:
switch(x) {
[_,_,*] -> 'a list with at least two elements'
[k:_,*] -> 'a map with at least one element with key k'
}
If you want to be able to specify the type of the wildcard you can use a type name instead of _
:
switch(x) {
[int,int,String] -> '2 ints and a string'
[int,String,*] -> 'list starting with int and string'
[k1:int,k2:String] -> 'map with k1 value being an int and k2 value being a String'
}
By specifying an identifier you can create a binding variable that binds to that part of the structure in the expression being switched on (hence the term destructuring):
For example, we can match a list or map and extract different elements of the list or map:
switch(x) {
[a,2] -> a + a // a will be bound to first element of list if second element is 2
[a,*,b] -> a + b // a bound to 1st element and b bound to last element
[k1:a,k2:b,*] -> a + b // a bound to value at k1 and b to value at k2
}
Note that for map literals, only the value can be bound to a binding variable. The keys themselves are literal values, not binding variables.
Binding variables can occur multiple times but will only match if the value for the variable is the same in all places where it is used:
switch(x) {
[a,a] -> "a=$a" // match all 2 element lists where both elements are the same
[a,_,a] -> "a=$a" // 3 element list where first and last are the same
}
Binding variables can also be typed if you want to match on the type:
switch(x) {
[int a,int b], [a] -> "a=$a" // match 1 or 2 element list if first element is an int
[int a, b, *] -> "a=$a, b=$b" // match if first element is int
}
Note that binding variables are shared across all patterns in the same comma separated list (see first pattern list in the example above). This means that their type can only be specified the first time they occur in the pattern list and that if the variable is used in a subsequent pattern (in the same comma separated list), it inherits the same type and will only match if the type matches.
Binding variables can occur anywhere within the structure being matched against, no matter how deep the nesting:
switch(x) {
[[a,b],*,[[a,*],[b,*]]] -> a + b
}
Binding variables can appear at the top level of a pattern as well and can also have a type:
switch(x) {
String s -> "string with value $s"
int i -> "int with value $i"
a -> "other type with value $a"
}
The wildcard variable _
can also appear at the top level and can be used as way to provide a default
case instead
of using default
:
switch(x) {
1,2,3 -> 'number'
_ -> 'other'
}
Note that if you use _
like this where it will match everything, it must occur last since otherwise the other cases
would never be evaluated.
This is different to the default
which can occur anywhere in the list of match cases.
As well as destructured matching based on type, you can also use a regular expression to match a string at a particular location within a nested structure:
switch (x) {
[/^abc/r, /xyz$/r] -> 'list of size 2 where first starts with abc and second ends in xyz'
[name:_, age:/^\d+$/r] -> "map has key called name and key called age whose value is a string of digits"
}
Note that if you use a capture group within the regular expressions then the capture variables will refer to the last
regular expression in the pattern.
For example, in the following code $1
will never correspond to the capture group in the first regular expression.
It will only have a value for the match on the second regex:
switch (x) {
[/^(a|b)+/r, /^(x|y|z)/r] -> "$1 will be x or y or z"
}
Implicit it
If you don’t specify a subject expression for the switch
to switch on then it will switch on the implicit it
variable.
For example, we can read lines from stdin and parse them using switch
like this since the closure passed to map
has an implicit it
parameter:
stream(nextLine).map{
switch {
/(.*)\+(.*)/n -> $1 + $2
/(.*)-(.*)/n -> $1 - $2
default -> die "Bad input: $it"
}
}
If you are in a context where there is no it
then you will get an error about a reference to an unknown it
variable.
Within the switch
expression itself, the it
variable is bound to the value of the expression passed to switch
.
For example:
switch(x.substring(3,6)) {
'ABC','XYZ' -> "${it.toLowerCase()}:123" // it has value of x.substring(3,6)
'XXY' -> 'xxy789'
}
Match with If
In addition to specifying a pattern or literal value to match against, each pattern/literal can also have an
optional if
expression that is evaluated if the pattern/literal matches which must then also evaluate to true
for that case to match:
switch(x.substring(3,6)) {
'ABC' if x.size() < 10, 'XYZ' if x.size() > 5 -> "${it}${it}"
'XXY' if x[0] == 'a' -> 'xxy'
}
Within the if
expression references to any binding variables and to it
are allowed:
switch(x) {
[int a,*] if a < 3 -> "list starts with $a < 3"
[String a,*] if x.size() < 5 -> "list with < 5 elems starts with string $a"
[a,*,b] if a.size() == b.size() -> "first and last elem of same size"
_ if x.size() == 0 -> 'empty list/string/map'
_ if x.size() == 1 -> 'single element list/map or single char string'
_ -> 'everything else'
}
Matching on Multiple Values
To match on multiple values at the same time just pass a list of the values to the switch
:
switch([x,y]) {
[a,a] -> 'x equals y'
[[a,*],[*,a]] -> 'first elem of x is same as last elem of y'
}
Destructured Matching on Class Instances
As mentioned, you can match on a type that is a user defined class. To match based on the field values use the constructor form of the class as the pattern.
For example:
class X { int i; int j; List list = [] }
X x = new X(i:2, j:3, list:[1,2,[3]])
switch(x) {
X(i:1) -> 'type is X: field i is 1'
X(list:[3,4]) -> 'X with list field being [3,4]'
}
If you use the constructor form with named fields (as in the example above) then you only need specify which field values you are interested in. All other fields can have any value to match that pattern.
The constructor form that does not use named field values requires you to supply values for all mandatory fields:
class X { int i; int j; List list = [] }
X x = new X(i:2, j:3, list:[1,2,[3]])
switch(x) {
X(1,3) -> 'type is X: i=1, j=3'
X(_,4) -> 'any X as long as j=4'
}
As for Maps and Lists you can “destructure” the fields to bind variables to field values or values within field values:
switch (x) {
X(i:i,j:j) -> "X with i=$i, j=$j"
X(list:[_,_,a]) -> "type is X: last elem of X.list is $a"
}
Variables and Expressions Inside Patterns
If you want to have a pattern that depends on the value of an existing variable then you can use $
to expand the
value of that variable inside the pattern.
For example, to match any list whose first element has the same value as the variable v
or to
match a two element list whose first element is twice the value of v
and whose last element is v
:
switch (x) {
[$v,*] -> 'matched'
[a,$v] if a == 2*v -> 'matched'
}
Just like in expression strings, if the value to be expanded is more than just a simple variable name you can wrap
the expression in { }
:
switch (x) {
[${2*v}, $v] -> 'matched'
}
You can refer to the value of binding variables (that are already bound) inside the expressions so these two switch
expressions are equivalent:
switch (x) {
[a,b,c] if b == 2*a && c == 3*a -> 'matched'
}
switch (x) {
[a, ${2*a}, ${3*a}] -> 'matched'
}
Matching Constants
Constants declared with the const
keyword can be used in a switch
expression pattern and work the same way as
though the literal value they correspond to was used:
const SPECIAL = 123
switch (x) {
SPECIAL -> 'is special'
}
Class constants can also be used:
class X { const A = 'a:', B= 'b:' }
switch (x) {
X.A -> 'matches A case'
X.B -> 'matches B case'
}
Switch Literal Comparisons
Note that switch
expressions will compare numeric values slightly differently than a standard ==
comparison.
In a switch
expression, numeric value comparisons will only match if the types are the same.
A long
value will not match an int
value.
This is to allow you to be able to match on exact types, especially in pattern matches.
For example, consider this:
switch (x) {
1 -> 'int 1'
1L -> 'long 1'
1D -> 'double 1'
(Decimal)1 -> 'Decimal 1'
1.0 -> 'Decimal 1.0'
}
When x
is 1
it will match the pattern that exactly matches its type and value so a long
will
only match 1L
, for example.
Note that (Decimal)1
is different to 1.0
.
Even though both are of type Decimal
, 1
is treated differently to 1.0
because the number of decimal
places is significant.
This means that this will return false
since the long
value will not match against an int
value
of 1
:
def x = 1L
switch (x) {
1 -> true
default -> false
}
This is different to standard comparisons using ==
:
def x = 1L
x == 1 // evaluates to true
If the compiler knows the type of the value being switched on and can tell that it can never
match one of the literals in the switch
expression you will get a compile error.
JSON Support
As well as supporting JSON syntax for creating Maps in the language itself, Jactl also offers functions for encoding objects into JSON and decoding JSON back into objects.
toJson()
Any Jactl object can be converted to a JSON string representation using the toJson()
method.
This method acts on any object type, including primitives:
> 123.toJson()
123
> 'abc'.toJson()
"abc"
> [1,2,3].toJson()
[1,2,3]
> [a:1,b:2,c:[1,2,[x:1,y:4]]].toJson()
{"a":1,"b":2,"c":[1,2,{"x":1,"y":4}]}
It can also be used on class instances:
> class Address { String street; String suburb; String state }
> class Employee { String name; int employeeId; Address homeAddress }
> def jim = new Employee('Jim Henderson', 1234, [street:'123 High St', suburb:'Downtown', state:'South Australia'])
[name:'Jim Henderson', employeeId:1234, homeAddress:[street:'123 High St', suburb:'Downtown', state:'South Australia']]
> jim.toJson()
{"name":"Jim Henderson","employeeId":1234,"homeAddress":{"street":"123 High St","suburb":"Downtown","state":"South Australia"}}
fromJson()
There is also a fromJson()
method that acts on strings to convert from a JSON string back into an object:
> '"abc"'.fromJson()
abc
> '"[1,2,3]"'.fromJson()
[1,2,3]
> '{"a":1,"b":2,"c":[1,2,{"x":1,"y":4}]}'.fromJson()
[a:1, b:2, c:[1, 2, [x:1, y:4]]]
By default, when converting from a JSON string, it will create Maps and Lists where appropriate:
> def json = '{"name":"Jim Henderson","employeeId":1234,"homeAddress":{"street":"123 High St","suburb":"Downtown","state":"South Australia"}}'
{"name":"Jim Henderson","employeeId":1234,"homeAddress":{"street":"123 High St","suburb":"Downtown","state":"South Australia"}}
> def jim = json.fromJson()
[name:'Jim Henderson', employeeId:1234, homeAddress:[street:'123 High St', suburb:'Downtown', state:'South Australia']]
> jim instanceof Map
true
It is possible to create a class instance instead by using the builtin fromJson()
class static method generated for
every class:
> class Address { String street; String suburb; String state }
> class Employee { String name; int employeeId; Address homeAddress }
> def json = '{"name":"Jim Henderson","employeeId":1234,"homeAddress":{"street":"123 High St","suburb":"Downtown","state":"South Australia"}}'
{"name":"Jim Henderson","employeeId":1234,"homeAddress":{"street":"123 High St","suburb":"Downtown","state":"South Australia"}}
> def jim = Employee.fromJson(json)
[name:'Jim Henderson', employeeId:1234, homeAddress:[street:'123 High St', suburb:'Downtown', state:'South Australia']]
> jim instanceof Employee
true
Another way to achieve the same goal is to read into a Map and then coercing the Map into the class type but this is more inefficient:
> json.fromJson() as Employee
[name:'Jim Henderson', employeeId:1234, homeAddress:[street:'123 High St', suburb:'Downtown', state:'South Australia']]
Built-in Global Functions
There are a handful of global functions.
timestamp() and nanoTime()
The timestamp()
function returns the current epoch time in milliseconds.
It is equivalent to Java’s System.currentTimeMillis()
:
> timestamp()
1678632694373
The nanoTime()
function returns the value of the system timer in nanoseconds.
It is equivalent in Java to System.nanoTime()
.
It is a number that can be used for timing but has no correlation with system or wall-clock time.
It has no value except within the currently running Java Virtual Machine instance, so it cannot be compared
to values from other processes, even ones running on the same machine.
For example:
> long fib(long x) { x <= 2 ? 1 : fib(x-1) + fib(x-2) }
Function@1000966072
> def time(closure) {
def start = nanoTime()
def result = closure()
println "Result: $result, duration: ${nanoTime() - start} nanoseconds"
}
Function@2050339061
> time{ fib(40) }
Result: 102334155, duration: 199072125 nanoseconds
sprintf()
You can use sprintf
to format strings.
It takes a format string as its first argument and then a list of arguments that are formatted according to the
format string.
The format string uses %s
for formatting strings, %f
for floating point numbers, %d
for integer amounts, and
so forth.
Between the %
and the letter indicating the type can be numbers controlling the width and whether the field is
left-aligned or right-aligned as well as number of decimal points for floating point numbers.
It uses Java’s String.format()
function so for a full description of how the format string works see
the Javadoc for String.format()
.
Here is an example:
> def employees = [[name:'Frank', salary:2000], [name:'Daisy', salary:3000], [name:'Joe', salary:1500]]
[[name:'Frank', salary:2000], [name:'Daisy', salary:3000], [name:'Joe', salary:1500]]
> println sprintf('%-10s %-10s %s', 'Name', 'Salary', 'Hourly Rate'); employees.each{
println sprintf('%-10s $%-10d $%.2f', it.name, it.salary, it.salary / 4.333333 / 37.5)
}
Name Salary Hourly Rate
Frank $2000 $12.31
Daisy $3000 $18.46
Joe $1500 $9.23
sleep()
The sleep()
function will pause execution of the script until the given delay time has expired.
The argument to sleep()
is the number of milliseconds to pause for.
For example:
> sleep(500)
Note that the script is suspended during this pause time and resumed once the time period has expired. The event loop thread on which the script is executing does not block. (When running in the REPL, the REPL waits for the entire script to complete, so in the REPL the next prompt won’t show until the sleep has completed).
There is a second optional argument which is the value returned by sleep()
once it has finished:
> sleep(500, 3) + sleep(500, 2)
5
> sleep(500, 'ab') + sleep(500, 'c')
abc
This is mainly just used for internal testing of Jactl when validating that the suspending and resuming works correctly.
eval()
The Eval Function section describes how this function works.
nextLine()
When running Jactl scripts from the command line this function reads the next line from the input.
When integrated into a Java application, this function will read the next line from the input that the application provides to the script. This is one way to pass information to a script.
If reading the next line of input would block, then the script is suspended and resumed when the next line becomes available.
The function will return null
when there are no more lines to read.
For example, here is a command line script that assumes each line is a number and adds them all together and prints out the result:
def n, sum = 0
while ((n = nextLine()) != null) {
sum += n as Decimal
}
println sum
stream()
The stream()
function takes a closure/function as argument and produces a stream of values by continually
invoking the closure/function until it returns null
.
These values can then be iterated over using any of the collection methods discussed previously.
For example, here is a complicated way to print the numbers 0 to 4:
> def i = 0
0
> def incrementer = { -> i < 5 ? i++ : null }
Function@1787189503
> stream(incrementer).each{ println it }
0
1
2
3
4
Since the nextLine()
function returns null
when it has reached the end of the input, you can use stream()
in conjunction with nextLine()
to iterate over the input lines:
def next = { -> nextLine() }
stream(next).map{ it as Decimal }.sum()
This is the same as:
stream{ nextLine() }.map{ it as Decimal }.sum()
Functions can be passed as values and are themselves callable, so you can pass the function directly as an argument:
stream(nextLine).map{ it as Decimal }.sum()
To read all lines into a list:
def lines = stream(nextLine)
Built-in Methods
Common Methods for Collections
In the section on Collection Methods we have covered all the methods that work on any type of collection such as Lists, Maps, and Strings (as well on numbers when they act as a stream of integers):
Method | Description |
---|---|
each() |
Apply closure to each element |
map() |
Map value of each element to a new value |
mapWithIndex() |
Map value and index of each element to a new value |
flatMap() |
Map element to a new value, flattening if new value is a list |
filter() |
Filter elements that match given criteria |
collect() |
Collect values into a new List |
collectEntries() |
Collect values into a new Map |
skip() |
Skip first n elements |
limit() |
Limit to first n elements |
unique() |
Remove sequences of duplicate elements |
join() |
Join elements into a string with given separator |
sort() |
Sort elements based on given sort order |
reverse() |
Reverse order of elements |
grouped() |
Group elements into sub-lists of given size |
reduce() |
Apply given function to reduce list of elements to single value |
min() |
Find minimum value |
max() |
Find maximum value |
sum() |
Calculate sum of values |
avg() |
Calculate average of values |
size() |
Number of elements in the list |
groupBy() |
Group elements by key returned from closure and create a Map |
Following sections will list the other methods for these types as well as methods for other types.
List Methods
List.size()
The size()
method returns the number of elements in a List, including any elements that are null
:
> [1, 'a', null, [1,2,3], null].size()
5
List.add()
The add()
method will add an element to the end of a list.
It works like the <<=
operator:
> def x = [1,2,3]
[1, 2, 3]
> x.add(4)
[1, 2, 3, 4]
> x
[1, 2, 3, 4]
> x <<= 5
[1, 2, 3, 4, 5]
> x
[1, 2, 3, 4, 5]
List.addAt()
To add an element at a given position in the list you can use the addAt()
method.
This will insert the given value into the position given by the first argument (with 0
being
the first position in the list):
> def x = ['a', 'b', 'c']
['a', 'b', 'c']
> x.addAt(0,'z')
['z', 'a', 'b', 'c']
> x.addAt(1, 'y')
['z', 'y', 'a', 'b', 'c']
> x
['z', 'y', 'a', 'b', 'c']
If you add an element to a position equal to the size of the list, then the list is expanded to include this additional value:
> def x = ['a', 'b', 'c']
['a', 'b', 'c']
> x.addAt(3, 'z')
['a', 'b', 'c', 'z']
If you add an element beyond the size of the list, then you will get an error:
> def x = ['a', 'b', 'c']
['a', 'b', 'c']
> x.addAt(10, 'z')
Index out of bounds: (10 is too large) @ line 1, column 3
x.addAt(10, 'z')
^
Note that x.addAt(3, 'z')
is different to x[3] = 'z'
:
> def x = ['a', 'b', 'c', 'd']
['a', 'b', 'c', 'd']
> x.addAt(3, 'z')
['a', 'b', 'c', 'z', 'd']
> def x = ['a', 'b', 'c', 'd']
['a', 'b', 'c', 'd']
> x[3] = 'z'
z
> x
['a', 'b', 'c', 'z']
The addAt()
method inserts into the list whereas []
replaces what was at the position with the new value.
List.remove()
The remove()
method removes the element at the given position from the list:
> def x = ['a', 'b', 'c', 'd']
['a', 'b', 'c', 'd']
> x.remove(3)
d
> x
['a', 'b', 'c']
The value returned from remove()
is the value of the element that was removed.
List.subList()
The subList()
method returns a sub-list of the list it is applied to.
It can have one or two arguments.
With one argument it returns the sub-list from the given position until the end of the list:
> [1, 2, 3, 4].subList(2)
[3, 4]
> ['a', 'b', 'c'].subList(1)
['b', 'c']
A value of 0
, therefore, returns a copy of the original list:
> ['a', 'b', 'c'].subList(0)
['a', 'b', 'c']
With two arguments, the first argument is the start index, and the second is one more than the end index.
This means that to extract a sub-list of length n
at index i
you would use subList(i, i + n)
:
> [1, 2, 3, 4].subList(1,3) // extract subList of length 2 at index 1
[2, 3]
If the index value is negative it will be treated as an offset from the end of the string:
> [1,2,3,4].subList(-1)
[4]
> [1,2,3,4].subList(-3,3)
[2, 3]
Since Maps, Strings, and numbers can be iterated over, subList()
can also be applied those types of objects as well:
> 'abcdef'.subList(2,4)
['c', 'd']
> [a:1,b:2,c:3].subList(2)
[['c', 3]]
> [a:1,b:2,c:3].subList(1,3)
[['b', 2], ['c', 3]]
> 10.subList(5,8)
[5, 6, 7]
Map Methods
Map.size()
The size()
method returns the number of entries in the Map:
> ['a':1, 'b':2, 'c':3].size()
3
Map.remove()
The remove()
method removes the entry with the given key from the Map and returns the value for that key:
> def x = ['a':1, 'b':2, 'c':3]
[a:1, b:2, c:3]
> x.remove('b')
2
> x
[a:1, c:3]
If there is no such entry that matches the key, remove()
will return null
:
> def x = ['a':1, 'b':2, 'c':3]
[a:1, b:2, c:3]
> x.remove('z') == null
true
> x
[a:1, b:2, c:3]
String Methods
String.size() and String.length()
Jactl supports the use of both size()
and length()
for getting the length of a string.
length()
is supported in order to make it easier for Java progammers who are used to using length()
while size()
is supported for consitency in naming across Lists, Maps, and Strings:
> 'abcde'.size()
5
> 'abcde'.length()
5
String.lines()
The lines()
method splits the string into a list of strings, one per line in the source string:
> def data = '''multi-line
string
on
four lines'''
multi-line
string
on
four lines
> data.lines()
['multi-line', 'string', 'on ', 'four lines']
> ''.lines()
['']
String.toUpperCase() and String.toLowerCase()
These methods turn a string into all upper case or all lower case:
> 'abc'.toUpperCase()
ABC
> 'A String With Capitals'.toLowerCase()
a string with capitals
String.substring()
The substring()
method allows you to extract a substring starting at a given index.
Like subList()
it has a single argument version that returns the remaining string from the given index, and a two
argument version that gives the two indexes that bound the substring.
The single argument version work like this:
> 'abcdef'.substring(3)
def
> 'abcdef'.substring(0)
abcdef
The two argument version extracts the substring starting at the first index until one less than the value of the second
index.
This means that to extract n
chars at index i
you use substring(i, i + n)
:
> 'abcde'.substring(2,4)
cd
Negative values for the indexes are treated as an offset from the end of the string:
> 'abcdef'.substring(-2)
ef
> 'abcdef'.substring(-4,-2)
cd
> 'abcdef'.substring(-4,5)
cde
String.split()
The split()
method splits a string based on a separator specified by a regex with optional modifiers.
See the previous section on the Split Method for more information.
String.asNum()
The asNum()
method parses a string of digits and returns their numeric value.
It takes an optional argument which is the base (or radix) for the number being parsed.
For example:
> '1234'.asNum()
1234
> 'ff14'.asNum(16)
65300
> '101011100110'.asNum(2)
2790
A base of up to 36 is supported:
> 'abzyAj13'.asNum(36)
809760160983
> 'abzyAj13'.asNum(37)
Base was 37 but must be no more than 36 @ line 1, column 12
'abzyAj13'.asNum(37)
^
Both lowercase and uppercase letters are supported for bases greater than 10:
> 'abcdef99'.asNum(16)
2882400153
> 'ABCDEF99'.asNum(16)
2882400153
Int Methods
int.asChar()
The asChar()
method converts a Unicode value back into its corresponding character (which is a single-character
string in Jactl):
> 0x41.asChar()
A
To convert from a character to its Unicode value, cast the single-character string to int
:
> (int)'Z'
90
> 90.asChar()
Z
Numeric Methods
Apart from toBase()
, these methods apply to all number types (int
, long
, double
, and Decimal
).
Number.toBase()
The toBase()
method converts an int
or long
to its character representation in the specified base:
> 1234.toBase(16)
4D2
> '4D2'.asNum(16)
1234
Bases between 2 and 36 are supported:
> 1234567879.toBase(37)
Base must be between 2 and 36 @ line 1, column 12
1234567879.toBase(37)
^
Number.abs()
The abs()
method returns the absolute value of the number:
> -15.abs()
15
> def distance(x1, x2) { (x1 - x2).abs() }
Function@1987169128
> distance(11, 121)
110
Number.sqr()
The sqr()
method returns the square of the given number:
> -1234.56.sqr()
1524138.3936
Number.sqrt()
The sqrt()
method returns the square root of the number:
> def x = 1234.5678.sqrt()
35.13641700572214
Number.pow()
The pow()
method allows you to calculate one number raised to the power of another.
For example, to calculate the cube of a number:
> def x = 123
123
> x.pow(3) // cube of x
1860867
The value of the exponent passed to pow()
can be fractional and can be negative:
> 16.pow(0.5) // another way to get square root
4
> 16.pow(-1.5)
0.015625
Object Methods
toString()
If passed no argument it prints the string representation of the object:
> 1234.toString()
1234
> def x = [a:1, b:[c:3,d:[1,2,3]]]
[a:1, b:[c:3, d:[1, 2, 3]]]
> x.toString()
[a:1, b:[c:3, d:[1, 2, 3]]]
If an optional indent amount is passed to it then it will do a pretty-print of complex objects such as Maps, Lists, and class instances:
> def x = [a:1, b:[c:3,d:[1,2,3]]]
[a:1, b:[c:3, d:[1, 2, 3]]]
> x.toString(2)
[
a:1,
b:[
c:3,
d:[1, 2, 3]
]
]
className()
The className()
method returns a String with the name of the class of the object it is invoked on:
> class X{}
> def x = new X()
[:]
> x.className()
X
It can be used on any type of object, including primitives and arrays:
> 123.className()
int
> > def x = ['a','b','c'] as String[]
['a', 'b', 'c']
> x.className()
String[]