Language Features

This page describes a few of the language features that Jactl offers. Jactl syntax is largely based on Java with parts also liberally borrowed from Groovy and Perl.

Semi-Colon Optional

Semi-colon statement terminator is optional and is only needed if multiple statements are on the same line:

int  x = 1
long y = 2

or

int x = 1; long y = 2

Functions Return Value of Last Statement

Functions can explicitly return a value using the return statement but if the last statement is not a return then its value will be returned:

int fib(int x) { return x <= 2 ? 1 : fib(x-1) + fib(x-2) }

is the same as:

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

Dynamic Typing with Optional Static Typing

Sppecifying types is optional in Jactl when declaring variables, parameters, and fields. If a type is specified then Jactl can perform compile-time checking and some optimisations.

Static typing:

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

Dynamic typing:

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

Map and List Literals

Map and list literals:

// Lists
[1,2,3,4]
['abc', 'def', 'xyz']

// Maps (keys only need to be quoted if they are not simple identifiers)
[a:1, b:2, c:3]
['a key':'a value', b:'another value']

// Can easily return maps or lists from functions
def addAndMultiply(x,y) { [sum:x + y, product:x * y] }  

Maps can use the canonical [ ] form or can also be provided in JSON syntax:

Map m1 = [x:1, y:2]
Map m2 = {"x":1, "y":2}

Interpolated Strings

Strings can be delimited with single quotes for simple strings, or double quotes for interpolated strings where $ is used to insert the value of a variable:

def x = 'this is a simple string'
def y = "Value of x = $x"

More complex expressions can be inserted into interpolated strings using ${ }:

def x = 3
def y = "x * x is ${x * x}"

// Expressions can contain other interpolated strings
def z = "Interpolated with ${"embedded interpolated (x*x=${x*x})"} string"

Multi-Line Strings

Multi-line simple strings and interpolated strings can be specified using triple quotes:

def simple = '''
multi-line
simple string'''

def interpolated = """
This is not 
a ${$simple}"""

Triple quotes are useful when you want to embed quotes and don’t want to have to escape them:

def x = 3
def y = """Value "${x*x}" for input of $x"""
def z = '''Triple quoted simple 'string' with embedded quotes'''

Functions as Values

Functions are just objects and can be passed around as values:

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

def applyArg(f, x) { f(x) }

def g = fib        // assign to another variable
g(40)

applyArg(fib, 40)  // pass as arg to a function

Methods passed as value are bound to the owning object:

class Multiplier {
  int multiple = 1
  def multiply(int x) { x * multiple }
}

def m = new Multplier(multiple:4)

g = m.multiply
g(3)                      // result is 12

applyArg(m.multiply, 5)   // result is 20

Closures

Closures in Jactl follow the Groovy syntax where the parameters are declared (with optional type) after the initial {:

def add = { x,y -> x + y }

add(2,3)           // result is 5

def f = add
f('abc', 'xyz')    // result is 'abcxyz'

Closures can close over variables visible in the existing scope:

def x = 2
def sumx = { y -> x + y }

sumx(3)   // result is 5
x = 7
sumx(3)   // result is 10

Variables can be modified within closures:

def x = 5
def incx = { x++ }
incx()    // x will now be 6    

Closure Passing Syntax

If the last parameter to a function is a closure then the closure can be written after the closing ) for the other arguments:

def logResult(prefix, clos) { "$prefix: result is ${ clos() }"}

logResult("Addition", { 2 + 3 })   // Pass closure as 2nd argument

logResult("Addition"){ 2 + 3 }     // Closure arg can be provided after the ')'

If there are no other arguments then the parentheses are optional:

def fib(x) { x <= 2 ? 1 : fib(x-1) + fib(x-2) }
def measure(clos) { def start = nanoTime(); clos(); nanoTime() - start }

def totalTime

totalTime = measure{
  for (int i = 0; i < 40; i++) {
    fib(i + 1)
  }
}

// Or using built-in "each" method
totalTime = measure{ 40.each{ i -> fib(i + 1) } } 

Higher-Order Functions

Since functions and closures can be passed by value it is possible to write other functions that operate on a function. For example, we can create a higher-order function compose that returns a new function that is the composition of two other functions:

def compose(f,g) { return { x -> f(g(x)) } }
def twice(x) { x * 2 }
def plus3(x) { x + 3 }

def plus3Twice = compose(twice, plus3)

plus3Twice(7)        // returns 20

Efficient Built-in Higher-Order Functions

There are also many built-in higher-order functions for operating on collection types (List, Map, array) including map, flatMap, filter, and each:

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

// Find first fibonacci number under 1000 that is a multiple of 57
def x = 1000.map{ [it, fib(it)] }.filter{ n,fib -> fib % 57 == 0 }.limit(1)[0]

// Returns [36, 14930352] so 36th fibonacci number (14930352) is a multiple of 57  

These higher-order functions that operate over collections work by iterating over the collections rather than creating a new collection each time. So in the example above where we iterate over the first 1000 Fibonacci numbers looking for the first one that is a multiple of 57, we don’t actually calculate the first 1000 Fibonacci numbers. The limit(1) means that we stop iterating as soon as we find the first one that matches. Each number flows through each of the higher-order functions one at a time before we iterate over the subsequent number.

Regex Support with Capture Variables $1, $2, …

Regular expression matching is part of the language rather than being delegated to library calls. The =~ operator searches a string for a substring that matches the given pattern:

'abcxdef' =~ /x/     // Result is true

Capture expressions in a regex pattern cause variables $1, $2, and so on to get populated with the corresponding values from the string being matched:

if ('Total: 14ms' =~ /: (\d+)(.*)$/) {
  println "Amount is $1, unit is $2"   // Prints: Amount is 14, unit is ms     
}

def x = 'a=1,bcd=234,e=3'
def result = []
// Extract all key=value values from x into list of [key,value] pairs:
while (x =~ /([^,=]+)=(\d+)/g) {
  result <<= [$1, $2]
}
println result
// Prints: [['a', '1'], ['bcd', '234'], ['e', '3']]

Regex substitutions can be done using s/.../.../ syntax:

def x = 'abcdef'
x =~ s/[ace]/x/   // Substitute only first match
println x         // Prints: xbcdef

x =~ s/[ace]/x/g  // Replace all matching substrings
println x         // Prints: xbxdxf

Embedded expressions can be used in the replacement string and can refer to capture variables:

def x = 'abcdef'
x =~ s/([ace])(.)/${ $2 + $1 }/g
println x                            // Prints: badcfe 

Implicit it Parameter

Closures that don’t declare a parameter have an automatic it parameter created for them:

def twice = { it + it }
twice(3)          // 6

// All numbers from 1 to 100 that are multiples of 7 and whose digits sum to be a multiple of 5:
def nums = 100.map{ it + 1 }
              .filter{ it % 7 == 0 }
              .map{ [it, it.toString().map{ it as int }.sum()] }
              .filter{ it[1] % 5 == 0 }
              .map{ it[0] }
// Result: [14, 28, 91]

Regular expressions operate on the implicit it variable if no string value is provided:

// Sanitise text to make suitable for a link
def linkify = { s/ /-/g;  s/[^\w-]//g }

// Find all top level headings in input and generate markdown for table of contents:
stream(nextLine).filter{ /^# /r }
                .map{ s/# // }
                .map{ "* [$it](#${ linkify(it.toLowerCase()) })" }
                .each{ println it }

Pattern Matching with Destructuring

Jactl provides switch expressions that can match on literal values but can also be used to match on the structure of the data and bind variables to different parts of the structure:

switch (x) {
  /X=(\d+),Y=(\d+)/n ->  $1 + $2   // regex match with capture vars
  [1,*]              -> 'matched'  // list whose first element is 1
  [_,_]              -> 'matched'  // list with 2 elements
  [int,String,_]     -> 'matched'  // 3 element list. 1st is an int, 2nd is a String
  [a,_,a]            -> a * 2      // 1st and last elements the same in 3 element list
  [a,*,a] if a < 10  -> a * 3      // list with at least 2 elements. 1st and last the same and < 10
  [a,${2*a},${3*a}]  -> a          // match if list is of form [2,4,6] or [3,6,9] etc
  [a,b,c,d]          -> a+b+c+d    // 4 element list
}

Statement if/unless Condition

As well as standard if statements, it is possible to have the if and the condition come after the statement to be executed so this standard form of if:

def fib(x) {
  if (x <= 2) {
    return 1
  }
  return fib(x-1) + fib(x-2)
}

can be written:

def fib(x) {
  return 1 if x <= 2
  return fib(x-2) + fib(x-1)
}

It is also possible to us unless instead of if when that is more natural:

def fib(x) {
  return 1 unless x > 2
  return fib(x-2) + fib(x-1)
}

Additional and, or, and not Operators

As well as the standard &&, ||, and ! boolean operators, there are and, or, and not operators that have much lower precedence (lower than assignment expressions, for example). This allows for this style of programming where it makes sense:

stream(nextLine).each{
  /^\$ *cd +\/$/r   and do { cwd = root;             return }
  /^\$ *cd +\.\.$/r and do { cwd = cwd.parent;       return }
  /^\$ *cd +(.*)$/r and do { cwd = cwd.children[$1]; return }
  /^\$ *ls/r        and return
  /^dir +(.*)$/r    and cwd.children[$1] = new Dir($1,-1,cwd)
  /^(\d+) +(.*)$/n  and cwd.children[$2] = new File($2,$1)
}

Default Parameter Values

Functions and closures can have parameters with default values:

def format(num, int base = 10) { sprintf("%9s", num.toBase(base) as String) }

format(300)      // Defaults to decimal: '      300'
format(300, 16)  // Hex:                 '      12C' 
format(300, 2)   // Binary:              '100101100'

Named Arguments

When invoking fuctions/closures, the parameter names can be supplied:

def format(num, int base = 10) { sprintf("%9s", num.toBase(base) as String) }

format(num:400, base:16)
format(base:8,  num:400)       // any order supported for named args 

eval() Statement

Jactl has a built-in eval statement which will compile and execute a string of Jactl code at run-time. A map of values for variables that the script references can be passed in as an optional argument:

eval('3 + 4')                                        // returns 7
eval('x + y', [x:3, y:4])                            // returns 7
eval('def twice = {it+it}; twice(x+y)', [x:3, y:4])  // returns 14

Built-in JSON Support

Jactl can convert objects into JSON using the toJson() method:

def x = [a:1,b:[x:3,y:4],c:['a','b','c']]
x.toJson()         // {"a":1,"b":{"x":3,"y":4},"c":["a","b","c"]}

The corresponding fromJson() method on strings will convert back from JSON to simple objects:

def json = '{"a":1,"b":{"x":3,"y":4},"c":["a","b","c"]}'
json.fromJson()   // [a:1, b:[x:3, y:4], c:['a', 'b', 'c']] 

For user defined classes, Jactl will generate a toJson() method and a class static method fromJson():

class X { int i; String s }
X x = new X(i:3, s:'abc')
x.toJson()      // {"i":3,"s":"abc"}
x = X.fromJson('{"i":3,"s":"abc"}')

Decimal Number Support

Jactl uses BigDecimal for “floating point” numbers by default (known as Decimal numbers in Jactl). This makes it suitable for manipulating currency amounts where arbitrary precision is required. Jactl also offers double numbers for situations where speed is more important than accuracy. For example:

1211.12 / 100            // Result is: 12.1112
((double)1211.12) / 100  // Result is: 12.111199999999998

Default Value Operator ?:

Jactl offers the ?: operator that will return the value on the right-hand side if the left-hand side is null:

def x
x ?: 123    // value will be 123 since x is null

Spaceship Operator <=>

The <=> operator is a comparator operator that returns -1, 0, or 1 if the left-hand side is less than, equal, or greater than the right-hand side respectively. This makes it easy to create a comparator function for sorting things:

def employees = [[name:'Mary', salary:10000], [name:'Susan', salary:5000], [name:'Fred', salary:7000]]

// Sort according to salary
employees.sort{ a,b -> a.salary <=> b.salary }

// Result: [[name:'Susan', salary:5000], [name:'Fred', salary:7000], [name:'Mary', salary:10000]]

Field Access ?. and ?[ Operators

When accessing fields of a map or a list/array the ?. operator (for maps) and the ?[ operator (for lists) allows you to safely dereference a null value. If the map/list is null then the resulting field reference will also be null if you use these operators:

def x
x.a            // Generates a NullError since x is null
x?.a           // Value will be null
x?[0]          // Value will be null
x?.a?.b?[0]    // Value will be null

Auto-Creation of Fields

When assigning a value to a field expression like a.b.c the values for intermediate fields will be automatically constructed where possible. For example:

def x = [:]       // empty map
x.a.b.c = '123'   // automatically create fields for x.a and x.a.b
x                 // x now has a value of: [a:[b:[c:'123']]]
x.d.e[2] = 'abc'  // x now has a value of: [a:[b:[c:'123']], d:[e:[null, null, 'abc']]]

Multi-Assignments

Multiple variables can be declared and initialised in one statement and multiple assignments can also be done in one statement:

def (x,y,z) = [4,5,6]                    // initialisation
(x,y,z) = [0,1,2]                        // assignment
(x,y) += [2,3]                           // supports all assignment operators +=, -=, *=, etc

def a = [:]
(a.b.c[x], a.b.c[y]) = [x + y, y + z]    // multi-assign with auto-creation of subfields

You can use a multi-assignment to swap the values of two variables:

(x,y) = [y,x]      // swaps x and y

Classes

Users can define their own classes:

class X {
  int i
  String field
  def anotherField
  def method() { field + ':' + anotherField + ':' + i }
}

class Y extends X {
  // Override method in base class 
  def method() { "Y: " + super.method() }
}

def y = new Y(i:4, field:'value1', anotherField:'value2')
y.method()      // returns 'Y: value1:value2:4'

Packages and Import Statements

Classes can be grouped into packages (libraries) just like in Java/Groovy using a package statement:

package org.customer.utils

class Useful {
  static def func() {
    ...
  }
}

Import statements allow you to use a class without needing to fully qualify it with its package name each time:

import org.customer.utils.Useful
def x = Useful.func()

Import allows you to give a class an alias using as:

import org.customer.utils.Useful as U
def x = U.func()

You can also import static functions and class constants using import static:

import static org.customer.utils.Useful.func as f
def x = f()   // invokes Useful.func()