Skip to main content

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:

NameDescriptionDefault ValueExamples
booleanTrue or falsefalsetrue, false
intIntegers (32 bit)00, 1, -99
longLarge integers (64 bit)0L0L, 99999999999L
doubleFloating point numbers0D0.01D, 1.234D, -99.99D
DecimalDecimal numbers00.0, 0.2345, 1244.35
StringStrings'''abc', 'some string value', "y=${x * 4}", /^x?[0-9]*$/
ListUsed for lists of values[][1,2,3], ['a',[1,2,3],'c']
MapMap of key/value pairs[:][a:1], [x:1, y:[1,2,3], z:[a:1,b:2]], {d:2,e:5}
defUsed for untyped variablesnull

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

var y = 2L // y will have type long

var z = x + y // z will have type long since adding int and long results in a long

var label = 'some place' // label has type String

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
x = '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) }

Here is the same function where we use def instead:

def fib(def x) { x < 2 ? x : fib(x-1) + fib(x-2) }

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) }

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:

TypeMinimum valueMaximum Value
int-21474836482147483647
long-92233720368547758089223372036854775807

To force a literal value to be a long rather than an int append L to the number:

9223372036854775807L

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 numb ers.

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 of:
// 24.229999999999997
12.12 + 12.11 // Decimal values give exact value:
// 24.23

Strings

Strings in Jactl are usually delimited with single quotes:

'abc'

Multi-line strings use triple quotes as delimiters and will include embedded newlines as newline characters inside the 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:

CharacterEscape Sequence
Newline\n
Carriage return\r
Tab\t
Formfeed\f
Backspace\b
Backslash\
Single quote'

For example:

println 'a\\b\t\'c\'\td\ne'

The resulting output is:

a\b	'c'	d
e

Subscripts and Characters

Subscript notation can be used to access individual characters:

'abc'[0]  // value is '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'

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]   // value is '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'   // result is 97

To convert back from a Unicode number to a single character string use the asChar method that exists for int values:

97.asChar()        // result is 'a'
def x = (int)'X' // result is 88
x.asChar() // result is 'X'

String Operators

Strings can be concatenated using +:

'abc' + 'def'      // result is '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      // result is 'abc12'0

The * operator can be used to repeat multiple instances of a string:

'abc' * 3          // result is '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
println "x = $x" // Output: x = 5
def msg = 'some message'
println "Received message '$msg' from sender"
// Output: Received message 'some message' from sender

If the expression is surrounded in {} then any arbitrary Jactl expression can be included:

def str = 'xyz'
def i = 3
println "Here are $i copies of $str: ${str * i}"
// Output: Here are 3 copies of xyz: xyzxyzxyz

You can, of course, have further nested interpolated strings within the expression itself:

println "This is a ${((int)'a'+2).asChar() + 'on' + "tr${\'i\' * (3*6 % 17) + 118.asChar()}e" + 'd'} example"
// Output: 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'
def y = 'ate'
println """This is a multi-line
inter${x + y + 'abcd'[3]} string"""
// Output:
// 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:

println "First 5 pyramid numbers: ${ def p(n){ n==1 ? 1 : n*n + p(n-1) }; [1,2,3,4,5].map{p(it)}.join(', ') }"
// Output: 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}"
// Output: 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]'
String pattern = /c\.d\[12[0-9]]/

// check if x matches pattern using =~ regex operator
x =~ pattern // result is true

Pattern strings are also expression strings and thus support embedded expressions within ${} sections of the regex string:

def x = 'abc.d[123]'
x =~ /abc\.d\[${100+23}]/ // result is true

Pattern strings also support multiple lines:

def p = /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]
['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']]
size(x) // result is 3
x.size() // result is 3

There are many other built-in functions provided that work with List objects. These are described in more detail in the section on Collection Methods.

Lists can be added together:

[1,2] + [2,3,4]    // result is [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'
def x = 'e'
['a','b','c'] + 'd' + x
def y = ['a','b','c'] + 'd' + x // Can use a 'def' variable to hold a list

y += 'f' // result is ['a', 'b', 'c', 'd', 'e', 'f']

Consider this example:

def x = [3]
[1,2] + x // result is [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]       // result is [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        // result is [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]
y += 3 // result is [1, 2, 3]
y <<= ['a'] // result is [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']
def y = 'b'
y in x // true
'a' !in x // false
warning

The in 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 using in 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']]
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]
x?[1] // 2
x = null
x[1] // error
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']]
x[1] = 3
x // ['value1', 3, ['a', 'b']]
x[5] = 10
x // ['value1', 3, ['a', 'b'], 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 a String 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]]

If the key is a valid identifier (or keyword) then the key does not need to be quoted:

def x = [a:1, b:2]
def y = [for:1, while:2, import:3] // keywords allowed as keys
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'
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'
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]
x += [c:3,a:2] // [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]

Map Remove

You can remove individual entries from a Map using the remove(key) method:

def m = [a:1, b:2]
m.remove('a') // returns old value of key: 1
m // [b:2]

The return value of remove() is the value of that key in the map or null if the key did not exist.

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"} }

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']
println employee.toString(2)
// Output:
// [
// 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]
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]]
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]]
def y = '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 // error: Null value for parent during field access
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]]
x.b = 4
x // [a:1, b:4, c:[d:4, e:5]]
x.c.e = [gg:2, hh:3]
x // [a:1, b:4, c:[d:4, e:[gg:2, hh:3]]]
x.c.f = [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]]
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]]
def y = '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]]
x['b'] = 'abc'
x['zz'] = '123'
x // [a:1, b:'abc', c:[d:4, e:5], zz:'123']

Auto-Creation of Map and List Entries

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

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
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
x // [a:[b:[c:[d: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
x.a.b[1].c.d = 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 // error: Non-numeric value for index during List access