This is the documentation for the Golo programming language.
Copyright (c) 2012-2018 Institut National des Sciences Appliquées de Lyon (INSA Lyon) and others This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0. SPDX-License-Identifier: EPL-2.0
Copyright (c) 2012-2018 Institut National des Sciences Appliquées de Lyon (INSA Lyon) and others All rights reserved. This Example Content is intended to demonstrate usage of Eclipse technology. It is provided to you under the terms and conditions of the Eclipse Distribution License v1.0 which is available at http://www.eclipse.org/org/documents/edl-v10.php
1. Basics
Let us start with the Golo basics.
1.1. Editor / IDE support
Editor and IDE support for Golo is available for:
1.2. Hello world
Golo source code need to be placed in modules. Module names are separated with dots, as in:
Foo
foo.Bar
foo.bar.Baz
(...)
It is suggested yet not enforced that the first elements in a module name are in lowercase, and that the last one have an uppercase first letter.
A Golo module can be executable if it has a function named main
and
that takes an argument for the JVM program arguments:
module hello.World
function main = |args| {
println("Hello world!")
}
println
is a predefined function that outputs a value to the standard
console. As you can easily guess, here we output Hello, world!
and
that is an awesome achievement.
Newlines are important in Golo, so make sure that your editor ends files with a newline. |
Golo identifiers can be non-ascii characters (e.g., Japanese, Emoji, Arabic, etc). |
1.3. Running "Hello world"
Of course, we need to run this incredibly complex application.
Golo comes with a golo
script found in the distribution bin/
folder. It provides several
commands, notably:
-
version
to query the Golo version, and -
compile
to compile some Golo code to JVM classes, and -
run
to execute some already compiled Golo code, and -
golo
to directly execute Golo code from source files, and -
diagnose
to print compiler internal diagnosis information, and -
check
to check code for compilation errors (e.g. as an editor hook), and -
doc
to generate module(s) documentation, and -
new
to generate new project(s).
The complete commands usage instructions can be listed by running golo --help
.
A command usage instructions can be listed by running golo --usage ${command}
.
The golo script comes with JVM tuning settings that may not be appropriate to your
environment. We also provide a vanilla-golo script with no tuning. You may use the $JAVA_OPTS
environment variable to provide custom JVM tuning to vanilla-golo .
|
Provided that golo
is available from your current $PATH
, you may run the program above as
follows:
$ golo golo --files samples/helloworld.golo
Hello world!
$ golo golo --files samples/ --module hello.World
Hello world!
$
golo golo
takes several Golo source files (*.golo and directories) as input.
It expects the last one to have a main
function to call (or use
--module
to define the golo module with the main
function).
The Golo code is compiled on the fly and executed straight into a JVM.
You may also pass arguments to the main
function by appending --args
on the command line invocation. Suppose that we have a module EchoArgs
as follows:
module EchoArgs
function main = |args| {
foreach arg in args {
println("-> " + arg)
}
}
We may invoke it as follows:
$ golo golo --files samples/echo-args.golo --args plop da plop
-> plop
-> da
-> plop
$
Note that args
is expected to be an array.
Finally, the --classpath
flag allows to specify a list of classpath elements, which can be either
directories or .jar
files. The system property golo.class.path
or the environment variable GOLOPATH
can also be used to specify these elements.
See the golo help
command for details on the various Golo commands.
1.4. Compiling Golo source code
Golo comes with a compiler that generates JVM bytecode in .class
files. We will give more details
in the chapter on interoperability with Java.
Compiling Golo files is straightforward:
$ golo compile --output classes samples/helloworld.golo
$
This compiles the code found in samples/helloworld.golo
and outputs
the generated classes to a classes
folder (it will be created if
needed):
$ tree classes/
classes/
└── hello
└── World.class
1 directory, 1 file
$
It is also possible to output to a Jar archive:
golo compile --output hello.jar samples/*.golo
This would take all .golo
files from the sample
folder, and assemble the resulting JVM class files in hello.jar
.
1.5. Running compiled Golo code
Golo provides a golo
command for running compiled Golo code:
$ cd classes
$ golo run --module hello.World
Hello world!
$
Simple, isn’t it?
1.6. Running Golo script
Golo provides a shebang
command for running a Golo file as a simple script.
module hello
function main = |args| {
require(args: length() > 1, "You should set at least one argument!")
println("Hello " + args: get(1) + " from '" + args: get(0) + "'!")
}
the script above can be executed with:
$ golo shebang hello.golo World
Hello World from 'hello.golo'!
$
Naturally the main goal is to use this command to make the script self-executable:
#!/path/to/golo shebang
module hello
function main = |args| {
require(args: length() > 1, "You should set at least one argument!")
println("Hello " + args: get(1) + " from '" + args: get(0) + "'!")
}
Now, we can run the script directly:
$ chmod +x hello.golo $ ./hello.golo World Hello World from 'hello.golo'! $
Golo also provides golosh script that is a shortcut for the golo shebang command, thus
a golo script can be hasbanged with env :
|
#!/usr/bin/env golosh
module hello
function main = |args| {
require(args: length() > 1, "You should set at least one argument!")
println("Hello " + args: get(1) + " from '" + args: get(0) + "'!")
}
Each golo and jar files present in the script file’s directory or the sub directories will be scanned.
This makes it easy to run scripts and have an automatic classpath for libraries, and automatically compile and load other Golo files.
|
$ tree ./
./
└── libs
└── libA.jar
└── libB.jar
└── commons
└── utils.golo
└── others.golo
└── vendors
└── otherlib.jar
└── hello.golo
└── library.golo
$
1.7. Passing JVM-specific flags
Both golo
and run
commands can be given JVM-specific flags using the JAVA_OPTS
environment
variable.
As an example, the following runs fibonacci.golo
and prints JIT compilation along the way:
# Exporting an environment variable
$ export JAVA_OPTS=-XX:+PrintCompilation
$ golo golo --files samples/fibonacci.golo
# ...or you may use this one-liner
$ JAVA_OPTS=-XX:+PrintCompilation golo golo --files samples/fibonacci.golo
1.8. Bash autocompletion
A bash script can be found in share/shell-completion/
called golo-bash-completion
that will provide autocomplete support for the golo
and vanilla-golo
CLI scripts. You may either source
the script, or drop the script into your bash_completion.d/
folder and restart your terminal.
Not sure where your bash_completion.d/ folder is? Try /etc/bash_completion.d/ on Linux or /usr/local/etc/bash_completion.d/ for Mac Homebrew users.
|
1.9. Zsh autocompletion
A zsh script can be found in share/shell-completion/
called golo-zsh-completion
that works using the golo-bash-completion
to provide autocomplete support using the bash autocomplete support provided by zsh. Place both files into the same directory and source golo-zsh-completion
from your terminal or .zshrc
to give it a try!
1.10. Comments
Golo comments start with a #
, just like in Bash, Python or Ruby:
# This is a comment
println("Plop") # it works here, too
1.11. Variable and constant references
Golo does not check for types at compile time, and they are not declared. Everything happens at runtime in Golo.
Variables are declared using the var
keyword, while constant references are declared with let
.
It is strongly advised that you favour let
over var
unless you are certain that you need
mutability.
Variables and constants need to be initialized when declared. Failing to do so results in a compilation error.
Here are a few examples:
# Ok
var i = 3
i = i + 1
# The assignment fails because truth is a constant
let truth = 42
truth = 666
# Invalid statement, variables / constants have to be initialized
var foo
Valid names contain upper and lower case letters within the [a..z]
range, underscores (_
),
dollar symbols ($
) and numbers. In any case, an identifier must not start with a number.
# Ok, but not necessarily great for humans...
let _$_f_o_$$666 = 666
# Wrong!
let 666_club = 666
1.12. Local definitions
It is possible to define a constant reference locally to the evaluation of an expression using the with
keyword.
For instance:
let r = [a + 1, a - 1] with { a = 42 }
is functionally equivalent to
let a = 42
let r = [a + 1, a - 1]
However, the a
variable exists only in the scope of the expression evaluation. Indeed, code like:
let r = [a + 1, a - 1] with { a = 42 }
println(a)
will not compile since a
is unknown in the outer scope.
Besides the locality, this construct is really interesting when constructing other expressions (e.g. match
constructs), and can help to keep a functional style form of writing functions.
1.13. Data literals
Golo supports a set of data literals. They directly map to their counterparts from the Java Standard API. We give them along with examples in the data literals table below.
Java type | Golo literals |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Speaking of strings, Golo also supports multi-line strings using the """
delimiters, as in:
let text = """This is
a multi-line string.
How
cool
is
that?"""
println(text)
This snippet would print the following to the standard console output:
This is a multi-line string. How cool is that?
1.14. Collection literals
Golo support special support for common collections. The syntax uses brackets prefixed by a collection name, as in:
let s = set[1, 2, "a", "b"]
let v = vector[1, 2, 3]
let m = map[[1, "a"], [2, "b"]]
# (...)
The syntax and type matchings are the following:
Collection | Java type | Syntax |
---|---|---|
Tuple |
|
|
Array |
|
|
List |
|
|
Vector |
|
|
Set |
|
|
Map |
|
|
Range |
|
|
1.14.1. A note on lists
Since in Golo, every value is actually an instance of Object
, there is no
overloading, and thus the remove
method on lists can’t be used to remove an
element at a given position. If you want to remove a list element given its
position, use the removeAt
method.
1.14.2. A note on tuples
Tuples essentially behave as comparable and immutable arrays.
The gololang.Tuple
class provides the following methods:
-
a constructor with a variable-arguments list of values,
-
a
get(index)
method to get the element at a specified index, -
a
head()
method to get the first element, -
a
tail()
method returning a copy without the first element, -
size()
andisEmpty()
methods that do what their names suggest, -
an
iterator()
method because tuples are iterable, -
toArray()
andTuple.fromArray()
for converting between tuples and arrays, -
subTuple(start, end)
to extract a new tuple, -
extend(…)
to create a new tuple with added values, and -
equals(other)
,hashCode()
andtoString()
do just what you would expect.
1.14.3. A note on maps
The map collection literal expects entries to be specified as tuples where the first entry is the key, and the second entry is the value. This allows nested structures to be specified as in:
map[
["foo", "bar"],
["plop", set[1, 2, 3, 4, 5]],
["mrbean", map[
["name", "Mr Bean"],
["email", "bean@outlook.com"]
]]
]
There are a few rules to observe:
-
not providing a series of tuples will yield class cast exceptions,
-
tuples must have at least 2 entries or will yield index bound exceptions,
-
tuples with more than 2 entries are ok, but only the first 2 entries matter.
Because of that, the following code compiles but raises exceptions at runtime:
let m1 = map[1, 2, 4, 5]
let m2 = map[
[1],
["a", "b"]
]
The rationale for map literals to be loose is that we let you put any valid Golo expression, like functions returning valid tuples:
let a = -> [1, 'a']
let b = -> [2, 'b']
let m = map[a(), b()]
1.15. Collection comprehension
In addition to literals, collections can be created using collection comprehension. This is a simple way to create a new collection based on another one (actually on any iterable object), by filtering and transforming its content. For instance:
let l1 = list[1, 2, 3, 4, 5, 6]
let l2 = list[x * 2 foreach x in l1 when (x % 2) == 0]
# l2 == list[4, 8, 12]
This is a more readable and more powerful version of filter
+map
. The
previous example could be rewritten as
let l2 = l2: filter(|x| -> (x % 2) == 0): map(|x| -> x * 2)
The general syntax is a collection literal containing an expression followed by one or more loop-like expression. If more than one loop is given, it is equivalent to nested loops. Thus
let l = list[ [x, y] foreach x in [1..4] foreach y in ["a", "b", "c"] ]
is equivalent to:
let l = list[]
foreach x in [1..4] {
foreach y in ["a", "b", "c"] {
l: add([x, y])
}
}
for
loop can be used, as in
let l = list[ 3 * i + 1 for (var i=0, i < 10, i = i + 2) ]
Contrary to the filter
+map
approach, where the kind on collection is kept,
comprehension can transform the source collection type, which can be any
iterable. For instance:
let dices = set[ x + y foreach x in [1..7] foreach y in [1..7]]
# dices == set[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
However, the result collection can only be of the type of one of the predefined collection literal types.
Destructuring can also be used in collection comprehension, as in
let couples = [ [1, 2], [2, 3], [3, 4] ]
let sums = [ a + b foreach a, b in couples ]
Maps can also be created, provided the given expression is either a pair tuple or
a instance of Map.Entry
(you can use the predefined mapEntry(key, value)
function to create such objects). For instance:
let myMap = map[ ["key" + i, 2 * i] foreach i in [0..4] ]
# myMap is {key0=0, key1=2, key2=4, key3=6}
A collection comprehension is a expression, and can thus be used as such. E.g.
foreach v in [[x,x] foreach x in [0..3]] {
println(v)
}
The analogy can be made between comprehension and SQL queries. As an illustration, compare:
select distinct
p.name, p.age, c.product
from
persons as p,
commands as c
where p.id == c.customer
and p.age > 18
with
let result = set[
[p: name(), p: age(), c: product()]
foreach p in persons
foreach c in commands
when p: id() == c: customer()
and p: age() > 18
]
1.15.1. Collection comprehension vs. map
and filter
Collection comprehension is actually quite similar to using map
and filter
higher-order function on a collection. Indeed, a comprehension such as:
list[f(x) foreach x in aList when pred(x)]
is equivalent to
aList: filter(^pred): map(^f)
Thus, should you use collection comprehension or higher-order functions? Despite some implementation differences, it’s above all a matter of taste. Some people consider comprehension more readable, since it is more similar to the mathematical set-builder notation. As an example, compare the two functionally equivalent expressions:
list[ 2 * x for x in aList when (x % 2) == 0 ]
aList: filter(x -> (x % 2) == 0): map(x -> 2 * x)
The more powerful expressiveness of comprehension shines when using nested iterators or destructuring. For instance, an expression such as
list[ k + ": " + (x * v)
foreach x in [1..10] when (x % 2) == 1
foreach k, v in aMap:entrySet() when k: startsWith("f") or v >= 42]
would be cumbersome to write using only map
and filter
.
The comprehension approach has also several advantages. First, while the code executed is almost identical when mapping a function and using a comprehension, that is something similar to
let tmp = list[]
foreach elt in aList {
tmp: add(f(elt))
}
return tmp
the comprehension code is generated at compile time, whereas the map
application is a function called at runtime.
As such, when using a filtering clause, the use of filter
creates an
intermediate list that will be fed to map
. This is not the case with
comprehension. Moreover, since the expression used to build the values of the
new collection is used at compile time, no closure is created, neither for the
filter.
An other advantage of comprehension is the fact that it can be used with any
iterable, to build a different kind of collection. The map
and filter
methods are
not (yet) available for any iterable, and for those that have them, the
result collection is of the same type as the initial one. This approach is more
polymorphic, but can be less readable if you need to change the collection type.
1.16. Destructuring
Golo supports simple destructuring, that is automatic extraction of values from an object and assignment to multiple variables in one instruction.
For instance, using destructuring on a tuple:
let a, b = [1, 2]
# a = 1, b = 2
If there are more variables than values, an exception is raised. If there are fewer, the remaining values are ignored. A special syntax is available to assign the rest of the values, similar to varargs notation. For instance:
let a, b, c = [1, 2] # raises an exception
let a, b = [1, 2, 3] # a = 1, b = 2, 3 ignored
let a, b, c... = [1, 2, 3, 4, 5] # a = 1, b = 2, c = [3, 4, 5]
Any object having a destruct()
method returning a tuple can be used in
destructuring assignments. Golo specific data structures and some Java native
ones (arrays, maps, collections) can be destructured. Augmentations can be used to make an existing class
destructurable.
For instance, golo structures are destructurable:
struct Point = {x, y}
#...
let p = Point(42, 1337)
let x, y = p # x = 42, y = 1337
as well as java lists:
let lst = list[1, 2, 3, 4, 5]
let head, tail... = lst # head = 1, tail = [2, 3, 4, 5]
Already defined variables can also be assigned with destructuring. For instance, one can easily swap two variables:
var a, b = [1, 2] # a = 1, b = 2
a, b = [b, a] # a = 2, b = 1
Destucturing can also be used in
foreach
loops:
foreach key, value in myMap: entrySet() {
# do something...
}
1.17. Operators
Golo supports the following set of operators.
Symbol(s) | Description | Examples |
---|---|---|
|
Addition on numbers and strings. |
|
|
Subtraction on numbers. |
|
|
Multiplication on numbers and strings. |
|
|
Division on numbers. |
|
'%' |
Modulo on numbers. |
|
|
Comparison between numbers and objects that implement |
|
|
Comparison of reference equality. |
|
|
Boolean operators. |
|
|
Checks the type of an object instance, equivalent to the |
|
|
Evaluates an expression and returns the value of another one if |
|
The algebraic operators can be used with any numeric type having a literal notation
(see the data literals table ), including java.math.BigInteger and java.math.BigDecimal .
|
The operator precedence rules are as follows:
Precedence | Operator |
---|---|
Strongest |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Lowest |
|
This means that:
not foo: bar() orIfNull "yo"
reads as:
(not (foo: bar())) orIfNull "yo"
1.18. Calling a method
Although we will discuss this in more details later on, you should already know that :
is used to
invoke instance methods.
You could for instance call the toString()
method that any Java object has, and print it out as
follows:
println(123: toString())
println(someObject: toString())
1.19. Java / JVM arrays
As you probably know, arrays on the JVM are special objects. Golo deals with such arrays as being
instances of Object[]
and does not provide a wrapper class like many languages do. A Java / JVM
array is just what it is supposed to be.
Golo adds some sugar to relieve the pain of working with arrays. Golo allows some special methods to be invoked on arrays:
-
get(index)
returns the value atindex
, -
set(index, value)
setsvalue
atindex
, -
length()
andsize()
return the array length, -
iterator()
returns ajava.util.Iterator
, -
toString()
delegates tojava.util.Arrays.toString(Object[])
, -
asList()
delegates tojava.util.Arrays.asList(Object[])
, -
equals(someArray)
delegates tojava.util.Arrays.equals(this, someArray)
, -
getClass()
returns the array class, -
head()
returns the first element of the array (ornull
if empty), -
tail()
returns a copy of the array without its first element (or an empty array if empty), -
isEmpty()
checks if the array is empty.
Given a reference a
on some array:
# Gets the element at index 0
a: get(0)
# Replaces the element at index 1 with "a"
a: set(1, "a")
# Nice print
println(a: toString())
# Convert to a real collection
let list = a: asList()
The methods above do not perform array bound checks. |
Finally, arrays can be created with the Array
function, as in:
let a = Array(1, 2, 3, 4)
let b = Array("a", "b")
You can of course take advantage of the array
collection literal, too:
let a = array[1, 2, 3, 4]
let b = array["a", "b"]
2. Creating new project(s)
The golo new
command can create new Golo project(s):
$ golo new Foo
The command creates a new Golo module named Foo
in a main.golo
file with a simple function
named main
that takes an argument for the JVM program arguments.
By default we create a new free-form project but you can specify the type of project with the
--type
command argument. Three types of projects are currently available:
-
Free-form project,
-
Maven-driven project,
-
Gradle-driven project.
The default value of the --type parameter can be changed by setting the golo.new.type property.
|
As an example if you want to create a Maven-driven project, just add --type maven
:
$ golo new Foo --type maven
By default we create the project directory where the golo
command is run. If you need to create
your project directory elsewhere you can use the --path
command argument:
$ golo new Bar --path /opt/golo
This creates the project directory named Bar
in /opt/golo
.
2.1. Free-form project
The structure of a free-form project is as follows:
$ tree Foo
Foo
├── imports
├── jars
└── main.golo
2.2. Maven-driven project
The structure of a Maven-driven project is as follows:
$ tree Foo/
Foo/
├── pom.xml
├── README.md
└── src
├── main
│ ├── golo
│ │ └── main.golo
│ └── resources
└── test
└── golo
The project can be built and packaged with Maven using the following command:
$ mvn package
You can now run the module Foo
with:
-
mvn
$ mvn exec:java
-
java
$ java -jar target/Foo-*-jar-with-dependencies.jar
-
golo
$ cd target/classes
$ golo run --module Foo
2.3. Gradle-driven project
The structure of a Gradle-driven project is as follows:
$ tree Foo/
Foo/
├── build.gradle
├── README.md
└── src
├── main
│ ├── golo
│ │ └── main.golo
│ └── resources
└── test
└── golo
The project can be built and packaged with Gradle using the following command:
$ gradle build
You can now run the module Foo
with:
-
gradle
$ gradle run
-
golo
$ cd build/classes/main
$ golo run --module Foo
2.4. Version control
With the --vcs
option, the command will create a ignore file and try to initialize the repository. Mercurial (hg
) and Git (git
) are currently supported. For instance,
$ golo new --vcs git --type gradle Foo
$ tree -a -L 1 Foo/
Foo/
├── build.gradle
├── .git
├── .gitignore
├── README.md
└── src
$ cat Foo/.gitignore
*.class
build/
.gradle/
or
$ golo new --vcs hg --type maven Foo
$ tree -a -L 1 Foo/
Foo/
├── .hg
├── .hgignore
├── pom.xml
├── README.md
└── src
$ cat Foo/.hgignore
syntax: glob
*.class
target/
If the option is not given, or if the value is none
, no repository is initialized.
The default value of the --vcs parameter can be changed by setting the golo.new.vcs property.
|
2.5. Profile
The --profile
option defines the kind of project you want to create, and will influence the files and hierarchy generated. Two profiles are currently supported:
-
app
will create a application project; -
lib
will create a library project.
3. Functions
Functions are first-class citizen in Golo. Here is how to define and call some.
3.1. Parameter-less functions
Golo modules can define functions as follows:
module sample
function hello = {
return "Hello!"
}
In turn, you may invoke a function with a familiar notation:
let str = hello()
A function needs to return a value using the return
keyword. Some
languages state that the last statement is the return value, but Golo
does not follow that trend. We believe that return
is more explicit,
and that a few keystrokes in favour of readability is still a good deal.
Still, you may omit return
statements if your function does not return
a value:
function printer = {
println("Hey!")
}
If you do so, the function will actually return null
, hence result
in the next statement is null
:
# result will be null
let result = printer()
3.2. Functions with parameters
Of course functions may take some parameters, as in:
function addition = |a, b| {
return a + b
}
Parameters are constant references, hence they cannot be reassigned. |
Invoking functions that take parameters is straightforward, too:
let three = addition(1, 2)
let hello_world = addition("hello ", "world!")
3.3. Variable-arity functions
Functions may take a varying number of parameters. To define one, just
add …
to the last parameter name:
function foo = |a, b, c...| {
# ...
}
Here, c
catches the variable arguments in an array, just like it would
be the case with Java. You can thus treat c
as being a Java object of
type Object[]
.
Calling variable-arity functions does not require wrapping the last
arguments in an array. While invoking the foo
function above, the
following examples are legit:
# a=1, b=2, c=[]
foo(1, 2)
# a=1, b=2, c=[3]
foo(1, 2, 3)
# a=1, b=2, c=[3,4]
foo(1, 2, 3, 4)
Because the parameter that catches the last arguments is an array, you may call array methods. Given:
function elementAt = |index, args...| {
return args: get(index)
}
then:
# prints "2"
println(elementAt(1, 1, 2, 3))
3.4. Named parameters
When you invoke Golo functions, you can use the name of the parameters explicitly in the call like so:
function post = |title, body, promoted, tags...| {
let data = map[
["title", title],
["body", body],
["promoted", promoted],
["tags", tags: asList()]
]
return gololang.JSON.stringify(data)
}
post(
tags = array["feature", "syntax"],
body = "it rocks!"
title = "Named parameters are alive",
promoted = true
)
Once you are using named parameters in your function call, the order doesn’t matter anymore. |
To name varargs argument, you have be box the values into an array[] (just has it’s done with the tags argument in the above snippet)
|
You must name either or none of the arguments. A compilation error will be raised, if you mix named an unamed arguments in a function invocation. |
3.5. Functions from other modules and imports
Suppose that we have a module foo.Bar
:
module foo.Bar
function f = {
return "f()"
}
We can invoke f
from another module by prefixing it with its module
name:
let r = foo.Bar.f()
Of course, we may also take advantage of an import
statement:
module Somewhere.Else
import foo.Bar
function plop = {
return f()
}
Imports in Golo do not work as in Java.
Golo is a dynamic language where symbols are being resolved at runtime. Module imports are
not checked at compilation time, and their sole purpose is to help in dynamic resolution. Back
to the previous example, f cannot be resolved from the current module, and the Golo runtime
subsequently tries to resolve f from each import statement. Also, note that the order of
import statements is important, as the resolution stops at the first module having the f
function.
|
You may prepend the last piece of the module name. The following invocations are equivalent:
module Somewhere.Else
import foo.Bar
function plop = {
let result = f()
let result_bis = Bar.f()
let result_full = foo.Bar.f()
return result
}
To help maintaining packages of several modules, and to avoid repeating the fully qualified name of the package when importing “local” modules,
import
can also be made relative to the package of the importing module. For instance, the following code:
module foo.bar.Spam
import .baz.Egg
is equivalent to
module foo.bar.Spam
import foo.bar.baz.Egg
Note that only modules in the same package or in a sub-package can be imported using relative name. In the previous example, to import the module foo.Plop
, its full name must be specified.
Moreover, it is possible to import several modules from the same package with a single import
statement, as in;
import java.util.{Collections, Objects, stream.Collectors}
Golo modules have a set of implicit imports:
-
gololang.Predefined
, -
gololang.StandardAugmentations
, -
gololang
, -
java.lang
.
These modules are imported after the module explicitly imported in the module, so that elements defined in these modules (e.g. predefined functions or augmentations) can be redefined.
3.6. Local functions
By default, functions are visible outside of their module. You may
restrict the visibility of a function by using the local
keyword:
module Foo
local function a = {
return 666
}
function b = {
return a()
}
Here, b
is visible while a
can only be invoked from within the Foo
module. Given another module called Bogus
, the following would fail at
runtime:
module Bogus
function i_will_crash = {
return Foo.a()
}
3.7. Recursive Tail Call Optimization
Golo feature recursive tail call optimization. If a function last action is to return the result of a recursive call, it is optimized to not stack a new call and is compiled in code equivalent to a loop. For instance, a function like:
function fact = |acc, v| -> match {
when v == 0 then acc
otherwise fact(acc * v, v - 1)
}
is compiled into something roughly equivalent to :
function fact = |acc, v| {
var acc_ = acc
var v_ = v
while v_ != 0 {
acc_ = acc_ * v_
v_ = v_ - 1
}
return acc_
}
This allows to create recursive functions that will not throw a StackOverflowError
even in the presence of a large number or recursive call.
The optimization can be disabled by setting the golo.optimize.tce
system property to false
(e.g. export GOLO_OPTS='-Dgolo.optimize.tce=false
).
A call is tail recursive when the function returns the result of calling itself directly. Indeed, since no evaluation nor flow analysis is done, many effectively tail recursive calls can’t be identified as such, and thus are not optimized. It is recommended to rewrite the code to make the tail call more direct. For instance, the following two functions are not optimized:
function foo = |a, v| {
let r = ^foo
if v == 0 {
return a
}
return r(a + v, v - 1)
}
function bar = |a, v| {
var r = null
if v == 0 {
r = a
} else {
r = bar(a + v, v - 1)
}
return r
}
while the following one is:
function ref = |a, v| {
if v == 0 {
return a
}
return ref(a + v, v - 1)
}
Note that returning a match
expression that may evaluate to a tail call will be optimized, such that the function
function baz = |a, v| -> match {
when v == 0 then a
otherwise ref(a + v, v - 1)
}
will be strictly equivalent to the previous one. A consequence of this behavior is that mutual recursions are not optimized.
3.7.1. On augmentations
Functions defined in augmentations can be optimized if written in a tail call style. For instance, in the following sample, the reduce
function is not optimized since it is viewed as a method call:
struct Cons = {head, tail}
augment Cons {
function reduce = |this, f, z| -> match {
when this: isEmpty() then z
otherwise this: tail(): reduce(f, f(this: head(), z))
}
}
On the other hand, the following one is optimized and works as expected:
augment Cons {
function reduce = |this, f, z| -> match {
when this: isEmpty() then z
otherwise reduce(this: tail(), f, f(this: head(), z))
}
}
3.7.2. Limitations on decorators
Since the optimization is done at compile time, most of dynamic features are not available. For instance, decorated functions can’t be optimized. Indeed, in that case, the decorator would not be applied on each call but only on the first one, which could lead to unexpected results.
For instance, the code:
function log = |f| -> |a, v| {
println("function called with " + a + ", " + v)
return f(a, v)
}
@log
function fact = |acc, v| -> match {
when v == 0 then acc
otherwise fact(acc * v, v - 1)
}
when optimised would print the message only for the first call. The tail call optimization is thus disabled for decorated functions. If the desired behavior is to optimize the function and apply the decorator only for the first call, one can create an undecorated version, and decorate explicitly direct calls or create a decorated wrapper, as in:
function log = |f| -> |a, v| {
println("function called with " + a + ", " + v)
return f(a, v)
}
function fact = |acc, v| -> match {
when v == 0 then acc
otherwise fact(acc * v, v - 1)
}
@log
function decoratedFact = |acc, v| -> fact(acc, v)
#(...)
let direclyDecorated = log(^fact)
directlyDecorated(1, 5)
log(^fact)(1, 5)
Note that this approach is not as cumbersome, since most of the time, tail recursive functions introduce an “artificial” accumulator that is hidden by a function calling the recursive one with the default accumulator value. For instance, in the factorial case, one would write:
local function fact = |acc, v| -> match {
when v == 0 then acc
otherwise fact(acc * v, v - 1)
}
function fact = |v| -> fact(1, v)
or equivalently:
function fact = |n| {
let _fact = |acc, v| -> match {
when v == 0 then acc
otherwise _fact(acc * v, v - 1)
}
return _fact(1, n)
}
In this case, the both the local fact
and the _fact
closure are optimized, while the public one can be decorated.
Similarly, variadic functions are not optimized, since we can’t know at compile-time if the last argument is already an array or a simple value that must be collected. The same kind of approach is recommended, by defining a (local) fixed arguments recursive version that is optimized and a variadic one that delegates on it.
3.8. Module-level state
You can declare let
and var
references at the module level, as in:
module Sample
let a = 1
var b = truth()
local function truth = {
return 42
}
These references get initialized when the module is being loaded by the Java virtual machine. In
fact, module-level state is implemented using private static
fields that get initialized in a
<clinit>
method.
Module-level references are only visible from their module, although a function may provide accessors to them.
It is important to note that such references get initialized in the order of declaration in the source file. Having initialization dependencies between such references would be silly anyway, but one should keep it in mind just in case.
Global state is a bad thing in general. We strongly advise you to think twice before you
introduce module-level state. Beware of potential memory leaks, just like static class fields in
the Java programming language.
|
4. Java interoperability
Golo aims at providing a seamless 2-way interoperability with the Java programming language.
4.1. Main function Java compliance
If the Golo compiler find a unary function named main
, it will be compiled to a void(String[])
static method.
This main
method can servers as a JVM entry point.
Suppose that we have the following Golo module:
module mainEntryPoint
function main = |args| {
println("-> " + args: get(0))
}
Once compiled, we may invoke it as follows:
$ golo compile mainEntryPoint.golo
$ java -cp ".:golo.jar" mainEntryPoint GoloRocks
-> GoloRocks
$
4.2. Calling static methods
Golo can invoke public Java static methods by treating them as functions:
module sample
import java.util.Arrays
function oneTwoThree = {
return asList(1, 2, 3)
}
In this example, asList
is resolved from the java.util.Arrays
import and called as a function.
Note that we could equivalently have written a qualified invocation as Arrays.asList(1, 2, 3)
.
4.3. Calling instance methods
When you have an object, you may invoke its methods using the :
operator.
The following would call the toString
method of any kind, then print it:
println(">>> " + someObject: toString())
Of course, you may chain calls as long as a method is not of a void
return type. Golo converts
Java void
methods by making them return null
. This is neither a bug or a feature: the
invokedynamic support on the JVM simply does so.
4.4. Named arguments
If you compile your Java 8 source code with the -parameters
option, then you will be able to invoke the functions
from Golo with named arguments.
package io;
public class Printer {
public static Object print(Object prefix, Object what) {
System.out.println(prefix + " " + what);
return null;
}
}
$ javac -parameters Printer.java
module bar
import io.Printer
let obj = DynamicObject()
function main = |args...| {
print(what = obj, prefix = ">")
}
Further documentation about Names Method Parameters with Java 8.
If the java code was not compiled with named parameters, the named arguments syntax is still valid, but the names are simply ignored and no reordering will be done. You can therefore use the argument names to improve readability and with the hope that the java library you use will later be compiled with the options, but keep them in the expected order, or you could have unexpected results. A warning is displayed when named arguments are used with functions without named parameters. It can be disabled by setting the |
4.5. Deprecated method
When calling a deprecated method or function, a warning is displayed. It can be disabled by setting the golo.warnings.deprecated
system property to false
.
4.6. null
-safe instance method invocations
Golo supports null
-safe methods invocations using the "Elvis" symbol: ?:
.
Suppose that we invoke the method bar()
on some reference foo
: foo: bar()
. If foo
is null
,
then invoking bar()
throws a java.lang.NullPointerException
, just like you would expect in Java.
By contrast:
-
foo?: bar()
simply returnsnull
, and -
null?: anything()
returnsnull
, too.
This is quite useful when querying data models where null
values could be returned. This can be
elegantly combined with the orIfNull
operator to return a default value, as illustrated by the
following example:
let person = dao: findByName("Mr Bean")
let city = person?: address()?: city() orIfNull "n/a"
This is more elegant than, say:
let person = dao: findByName("Mr Bean")
var city = "n/a"
if person isnt null {
let address = person: address()
if address isnt null {
city = address: city() ofIfNull "n/a"
}
}
The runtime implementation of null -safe method invocations is optimistic as it behaves
like a try block catching a NullPointerException . Performance is good unless most invocations
happen to be on null , in which case using ?: is probably not a great idea.
|
4.7. Creating objects
Golo doesn’t have an instantiation operator like new
in Java. Instead, creating an object and
calling its constructor is done as if it was just another function.
As an example, we may allocate a java.util.LinkedList
as follows:
module sample
import java.util
function aList = {
return LinkedList()
}
Another example would be using a java.lang.StringBuilder
.
function str_build = {
return java.lang.StringBuilder("h"):
append("e"):
append("l"):
append("l"):
append("o"):
toString()
}
As one would expect, the str_build
function above gives the "hello"
string.
4.8. Static fields
Golo treats public static fields as function, so one could get the maximum value for an Integer
as
follows:
module samples.MaxInt
local function max_int = {
return java.lang.Integer.MAX_VALUE()
}
function main = |args| {
println(max_int())
}
Given than most static fields are used as constants in Java, Golo does not provide support to change their values. This may change in the future if compelling general-interest use-cases emerge. |
4.9. Instance fields
Instance fields can be accessed as functions, both for reading and writing. Suppose that we have a Java class that looks as follows:
public class Foo {
public String bar;
}
We can access the bar
field as follows:
let foo = Foo()
# Write
foo: bar("baz")
# Read, prints "baz"
println(foo: bar())
An interesting behavior when writing fields is that the "methods" return the object, which means that you can chain invocations.
Suppose that we have a Java class as follows:
public class Foo {
public String bar;
public String baz;
}
We can set all fields by chaining invocations as in:
let foo = Foo(): bar(1): baz(2)
It should be noted that Golo won’t bypass the regular Java visibility access rules on fields.
What happens if there is both a field and a method with the same names?
Back to the previous example, suppose that we have both a field and a method with the same name, as in:
Golo resolves methods first, fields last. Hence, the following Golo code will resolve the
|
4.10. Properties support
Golo support property-style method calls.
Given a property name()
, Golo will translate the method call to a {get|is or set}Name
method call. Obviously, if a field
is not accessible (ie. private
) and doesn’t have a getter or setter, the resolution will fail.
Finally, write-only properties can be chained (ie: return the current this
instance), unless a return value is defined in the according setter method.
The property resolution does not check if an according field exists. Basically all the get|set|is| methods are candidates to
a property-style method invocation.
|
public class Person {
private String name;
private boolean goloComitter;
private String email;
private int score;
public String getName() {
...
}
public boolean isGoloComitter() {
...
}
public void setName(String name) {
...
}
public void setGoloCommitter(boolean goloCommitter) {
...
}
public int setScore(int score) {
this.score = score;
return score;
}
public boolean isRockStar() {
return goloCommitter;
}
}
let duke = Person()
try {
duke: email("duke@golo-lang.org")
} catch (e) {
require(e oftype java.lang.NoSuchMethodError.class, "the email field is private and no setter is defined")
}
require(duke: name("Duke"): goloCommiter(true) oftype Person.class, "the set mutators should be chained with fluent calls")
require(duke: name() is "Duke", "should find the getName() accessor.")
require(duke: goloCommitter() is true, "should invoke the isGoloCommitter accessor since the field it's a boolean")
require(print(duke: rockStar() is true, "even if the field rockstar doesn't exists, it should invoke the isRockStar accessor")
require(ducke: score(100) is 100, "setScore returns a value, the method isn't fluent")
4.11. Inner classes and enumerations
We will illustrate both how to deal with public static inner classes and enumerations at once.
The rules to deal with them in Golo are as follows.
-
Inner classes are identified by their real name in the JVM, with nested classes being separated by a
$
sign. Hence,Thread.State
in Java is writtenThread$State
in Golo. -
Enumerations are just normal objects. They expose each entry as a static field, and each entry is an instance of the enumeration class.
Let us consider the following example:
module sample.EnumsThreadState
import java.lang.Thread$State
function main = |args| {
# Call the enum entry like a function
let new = Thread$State.NEW()
println("name=" + new: name() + ", ordinal=" + new: ordinal())
# Walk through all enum entries
foreach element in Thread$State.values() {
println("name=" + element: name() + ", ordinal=" + element: ordinal())
}
}
Running it yields the following console output:
$ golo golo --files samples/enums-thread-state.golo
name=NEW, ordinal=0
name=NEW, ordinal=0
name=RUNNABLE, ordinal=1
name=BLOCKED, ordinal=2
name=WAITING, ordinal=3
name=TIMED_WAITING, ordinal=4
name=TERMINATED, ordinal=5
$
4.12. Clashes with Golo operators and escaping
Because Golo provides a few named operators such as is
, and
or not
, they are recognized as
operator tokens.
However, you may find yourself in a situation where you need to invoke a Java method whose name is a Golo operator, such as:
# Function call
is()
# Method call
someObject: foo(): is(): not(): bar()
This results in a parsing error, as is
and not
will be matched as operators instead of method
identifiers.
The solution is to use escaping, by prefixing identifiers with a backtick, as in:
# Function call
`is()
# Method call
someObject: foo(): `is(): `not(): bar()
4.13. Golo class loader
Golo provides a class loader for directly loading and compiling Golo modules. You may use it as follows:
import org.eclipse.golo.compiler.GoloClassLoader;
public class Foo {
public static void main(String... args) throws Throwable {
GoloClassLoader classLoader = new GoloClassLoader();
Class<?> moduleClass = classLoader.load("foo.golo", new FileInputStream("/path/to/foo.golo"));
Method bar = moduleClass.getMethod("bar", Object.class);
bar.invoke(null, "golo golo");
}
}
This would work with a Golo module defined as in:
module foo.Bar
function bar = |wat| -> println(wat)
Indeed, a Golo module is viewable as a Java class where each function is a static method.
GoloClassLoader is rather dumb at this stage, and you will get an exception if you try
to load two Golo source files with the same module name declaration. This is because it will
attempt to redefine an already defined class.
|
Later in the glorious and glamorous future, Golo will have objects and not just functions. Be patient, it’s coming in! |
4.14. Primitive Data Types
java.lang.Class
instances of primitive data types can be obtained as in Java way: byte.class
, short.class
, int.class
, long.class
, float.class
, double.class
, boolean.class
and char.class
.
This could be useful for example to deal with primitive arrays:
|
5. Control flow
Control flow in Golo is imperative and has the usual constructions found in upstream languages.
5.1. Conditional branching
Golo supports the traditional if
/ else
constructions, as in:
if goloIsGreat() {
println("Golo Golo")
}
if (someCondition) {
doThis()
} else if someOtherCondition {
doThat()
} else {
doThatThing()
}
The condition of an if
statement does not need parenthesis. You may add some to clarify a more
elaborated expression, though.
5.2. case
branching
Golo offers a versatile case
construction for conditional branching. It may be used in place of
multiple nested if
/ else
statements, as in:
function what = |obj| {
case {
when obj oftype String.class {
return "String"
}
when obj oftype Integer.class {
return "Integer"
}
otherwise {
return "alien"
}
}
}
A case
statement requires at least 1 when
clause and a mandatory otherwise
clause. Each clause
is being associated with a block. It is semantically equivalent to the corresponding if
/ else
chain:
function what = |obj| {
if obj oftype String.class {
return "String"
} else if obj oftype Integer.class {
return "Integer"
} else {
return "alien"
}
}
when clauses are being evaluated in the declaration order, and only the first satisfied
one is being executed.
|
5.3. match
expressions
The match
expression is a convenient shortcut for cases where a case
statement would be used to
match a value, and give back a result. While it may resemble pattern matching operators in some
other languages it is not fully equivalent, as Golo does not support full destructuring matching (but with
can help here).
match
is a great addition to the Golo programmer:
let item = "foo@bar.com"
let what_it_could_be = -> match {
when item: contains("@") then "an email?"
when item: startsWith("+33") then "a French phone number?"
when item: startsWith("http://") then "a website URL?"
otherwise "I have no clue, mate!"
}
# prints "an email?"
println(what_it_could_be(item))
The values to be returned are specified after a then
keyword that follows a boolean expression to
be evaluated.
Like case
statements, a match
construct needs at least one when
clause and one otherwise
clause.
5.4. while
loops
While loops in Golo are straightforward:
function times = |n| {
var times = 0
while (times < n) { times = times + 1 }
return times
}
The parenthesis in the while
condition may be omitted like it is the case for if
statements.
5.5. for
loops
This is the most versatile loop construction, as it features:
-
a variable declaration and initialization (a Golo variable is always initialized anyway), and
-
a loop progress condition, and
-
a loop progress statement.
The following function shows a for
loop:
function fact = |value, n| {
var result = 1
for (var i = 0, i < n, i = i + 1) {
result = result * value
}
return result
}
As you can see, it is very much like a for
loop in Java, except that:
-
the
for
loop elements are separated by','
instead of';'
, and -
there cannot be multiple variables in the loop, and
-
there cannot be multiple loop progress statements.
Again, this choice is dictated by the pursue of simplicity.
5.6. foreach
loops
Golo provides a "for each" style of iteration over iterable elements. Any object that is an instance
of java.lang.Iterable
can be used in foreach
loops, as in:
function concat_to_string = |iterable| {
var result = ""
foreach item in iterable {
result = result + item
}
return result
}
In this example, item
is a variable within the foreach
loop scope, and iterable
is an object
that is expected to be iterable.
You may use parenthesis around a foreach
expression, so foreach (foo in bar)
is equivalent to
foreach foo in bar
.
Although Java arrays (Object[] ) are not real objects, they can be used with foreach loops.
Golo provides a iterator() method for them.
|
5.7. foreach
loops with a guard
There is a variant of the foreach
loop with a when
guard.
The following code:
foreach item in collection {
if item < 10 {
println(item)
}
}
can be simplified as:
foreach item in collection when item < 10 {
println(item)
}
The when
guard can be any expression that evaluates to a boolean.
5.8. break
and continue
Although not strictly necessary, the break
and continue
statements can be useful to simplify
some loops in imperative languages.
Like in Java and many other languages:
-
break
exits the current inner-most loop, and -
continue
skips to the next iteration of the current inner-most loop.
Consider the following contrived example:
module test
function main = |args| {
var i = 0
while true {
i = i + 1
if i < 40 {
continue
} else {
print(i + " ")
}
if i == 50 {
break
}
}
println("bye")
}
It prints the following output:
40 41 42 43 44 45 46 47 48 49 50 bye
Golo does not support break
statements to labels like Java does. In fact, this is a goto
statement in disguise.
5.9. Why no value from most control flow constructions?
Some programming languages return values from selected control flow constructions, with the returned value being the evaluation of the last statement in a block. This can be handy in some situations such as the following code snippet in Scala:
println(if (4 % 2 == 0) "even" else "odd")
The Golo original author recognizes and appreciates the expressiveness of such construct. However, he often finds it harder to spot the returned values with such constructs, and he thought that trading a few keystrokes for explicitness was better than shorter construct based in implicitness.
Therefore, most Golo control flow constructions do not return values, and programmers are instead
required to extract a variable or provide an explicit return
statement.
6. Exceptions
Exception handling in Golo is simple. There is no distinction between checked and unchecked exceptions.
6.1. Raising exceptions
Golo provides 2 predefined functions for raising exceptions:
-
raise(message)
throws ajava.lang.RuntimeException
with a message given as a string, and -
raise(message, cause)
does the same and specifies a cause which must be an instance ofjava.lang.Throwable
.
Throwing an exception is thus as easy as:
if somethingIsWrong() {
raise("Woops!")
}
6.2. Raising specialized exceptions
Of course not every exception shall be an instance of java.lang.RuntimeException
. When a more
specialized type is required, you may simply instantiate a Java exception and throw it using the
throw
keyword as in the following example:
module golotest.execution.Exceptions
import java.lang.RuntimeException
function runtimeException = {
throw RuntimeException("w00t")
}
6.3. Exception handling
Exception handling uses the familiar try / catch
, try / catch / finally
and try / finally
constructions. Their semantics are the same as found in other languages such as Java, especially
regarding the handling of finally
blocks.
The following snippets show each exception handling form.
# Good old try / catch
try {
something()
} catch (e) {
e: printStackTrace()
}
# A try / finally
try {
doSomething()
} finally {
cleanup()
}
# Full try / catch / finally construct
try {
doSomething()
} catch (e) {
e: printStackTrace()
case {
when e oftype IOException.class {
println("Oh, an I/O exception that I was expecting!")
}
when e oftype SecurityException.class {
println("Damn, I didn't expect a security problem...")
throw e
}
otherwise {
throw e
}
}
} finally {
cleanup()
}
Because Golo is a weakly typed dynamic language, you need to check for the exception type
with the oftype operator. In a statically typed language like Java, you would instead have several
catch clauses with the exception reference given a specific type. We suggest that you take
advantage of the case branching statement.
|
7. Closures
Golo supports closures, which means that functions can be treated as first-class citizen.
7.1. Defining and using a closure
Defining a closure is straightforward as it derives from the way a function can be defined:
let adder = |a, b| {
return a + b
}
At runtime, a closure is an instance of gololang.FunctionReference
, which is essentially a boxing
of java.lang.invoke.MethodHandle
. This means that you can do all the operations that method
handles support, such as invoking them or inserting arguments as illustrated in the following
example:
let adder = |a, b| {
return a + b
}
println(adder: invoke(1, 2))
let addToTen = adder: bindTo(10)
println(addToTen: invoke(2))
As one would expect, this prints 3
and 12
.
In order to make closure’s invocations more fluent you can insert argument using the closure’s parameter names.
let mul = |a, b| {
return a * b
}
let tenTimes = mul: bindAt("a", 10)
# 20
println(tenTimes: invoke(2))
7.2. Compact closures
Golo supports a compact form of closures for the cases where their body consists of a single expression. The example above can be simplified as:
let adder = |a, b| -> a + b
You may also use this compact form when defining regular functions, as in:
module Foo
local function sayHello = |who| -> "Hello " + who + "!"
# Prints "Hello Julien!"
function main = |args| {
println(sayHello("Julien"))
}
7.3. Calling closures
While you may call function references using invoke
, there is a (much) better way.
When you have a reference to a closure, you may simply call it as a regular function. The previous
adder
example can be equivalently rewritten as:
let adder = |a, b| -> a + b
println(adder(1, 2))
let addToTen = adder: bindTo(10)
println(addToTen(2))
7.4. Limitations
Closures have access to the lexical scope of their defining environment. Consider this example:
function plus_3 = {
let foo = 3
return |x| -> x + foo
}
The plus_3
function returns a closure that has access to the foo
reference, just as you would
expect. The foo
reference is said to have been captured and made available in the closure.
It is important to note that captured references are constants within the closure. Consider the following example:
var a = 1
let f = {
a = 2 # Compilation error!
}
The compilation fails because although a
is declared using var
in its original scope, it is
actually passed as an argument to the f
closure. Because function parameters are implicitly
constant references, this results in a compilation error.
That being said, a closure has a reference on the same object as its defining environment, so a mutable object is a sensible way to pass data back from a closure as a side-effect, as in:
let list = java.util.LinkedList()
let pump_it = {
list: add("I heard you say")
list: add("Hey!")
list: add("Hey!")
}
pump_it()
println(list)
which prints [I heard you say, Hey!, Hey!]
.
7.5. Closures to single-method interfaces
The Java SE APIs have plenty of interfaces with a single method: java.util.concurrent.Callable
,
java.lang.Runnable
, javax.swing.ActionListener
, etc.
The predefined function asInterfaceInstance
can be used to convert a method handle or Golo closure
to an instance of a specific interface.
Here is how one could pass an action listener to a javax.swing.JButton
:
let button = JButton("Click me!")
let handler = |event| -> println("Clicked!")
button: addActionListener(asInterfaceInstance(ActionListener.class, handler))
Because the asInterfaceInstance
call consumes some readability budget, you may refactor it with a
local function as in:
local function listener = |handler| -> asInterfaceInstance(ActionListener.class, handler)
# (...)
let button = JButton("Click me!")
button: addActionListener(listener(|event| -> println("Clicked!")))
Here is another example that uses the java.util.concurrent
APIs to obtain an executor, pass it a
task, fetch the result with a Future
object then shut it down:
function give_me_hey = {
let executor = Executors.newSingleThreadExecutor()
let future = executor: submit(asInterfaceInstance(Callable.class, -> "hey!"))
let result = future: get()
executor: shutdown()
return result
}
7.6. Closures to Java 8 functional interfaces
Java 8 introduced support for the so-called lambdas.
The mechanism works through
functional interfaces,
that is, interfaces with a single abstract method that are annotated with @FunctionalInterface
.
Golo provides a asFunctionalInterface
pre-defined function to convert closures and that works like
the asInterfaceInstance
function:
let always_true = asFunctionalInterface(java.lang.function.Predicate.class, |obj| -> true)
7.7. Direct closure passing works
When a function or method parameter of a Java API expects a single method interface type or a functional interface, you can pass a closure directly, as in:
# Swing action listeners, a classic!
let button = JButton("Click me!")
button: addActionListener(|event| -> println("Clicked!"))
# Java 8 streams / lambdas interop
let result = list[1, 2, 3, 4, 5]:
stream():
map(|n| -> n * 10):
reduce(0, |acc, next| -> acc + next)
Note that this causes the creation of a method handle proxy object for each function or method
invocation. For performance-sensitive contexts, we suggest that you use either
asInterfaceInstance
, the to
conversion method described hereafter, or asFunctionalInterface
.
7.8. Conversion to single-method interfaces
Instead of using asInterfaceInstance
, you may use a class augmentation which is described later in this
documentation. In short, it allows you to call a to
method on instances of MethodHandle
, which
in turn calls asInterfaceInstance
. Back to the previous examples, the next 2 lines are equivalent:
# Calling asInterfaceInstance
future = executor: submit(asInterfaceInstance(Callable.class, -> "hey!"))
# Using a class augmentation
future = executor: submit((-> "hey!"): to(Callable.class))
7.9. Getting a reference to a closure / Golo function
You may also take advantage of the predefined fun
function to obtain a reference to a closure, as
in:
import golotest.Closures
local function local_fun = |x| -> x + 1
function call_local_fun = {
# local_fun, with a parameter
var f = fun("local_fun", golotest.Closures.module, 1)
# ...or just like this if there is only 1 local_fun definition
f = fun("local_fun", golotest.Closures.module)
return f(1)
}
Last but not least, we have an even shorter notation if function are not overridden:
import golotest.Closures
local function local_fun = |x| -> x + 1
function call_local_fun = {
# In the current module
var f = ^local_fun
# ...or with a full module name
f = ^golotest.Closures::local_fun
return f(1)
}
If the function is overridden, that is several functions exist with the same name but different arities,
you must specify the arity with the notation ^moduleName::functionName\arity
. For instance:
local function foo = |a, b| -> a * b
local function foo = |x| -> x + 1
function call_foo = {
return ^foo\2(^foo\1(20), 2)
}
In this case, one can even specify if a varargs function is needed or not (since arity alone can be ambiguous) using final …
:
local function foo = |a| -> "unary"
local function foo = |a...| -> "variable"
^foo\1(null) # unary
^foo\1...() # variable
Note that you can in the same way get a reference to a Java static method. For instance:
list["Foo", null, 42]: filter(^java.util.Objects::nonNull)
You can also get a reference to a non-static method. In this case, the function will accept an instance of the class as first argument, as in:
list["Foo", "Bar", "Hello", "Goodbye"]: map(^String::length)
7.10. Binding and composing
You can bind a function first argument using the bindTo(value)
method. If you need to bind an
argument at another position than 0, you may take advantage of bindAt(position, value)
:
let diff = |a, b| -> a - b
let minus10 = diff: bindAt(1, 10)
# 10
println(minus10(20))
You may compose functions using the andThen
method:
let f = (|x| -> x + 1): andThen(|x| -> x - 10): andThen(|x| -> x * 100)
# -500
println(f(4))
or:
function foo = |x| -> x + 1
function bar = |x| -> 2 * x
function main = |args| {
let newFunction = ^foo: andThen(^bar)
# 8
println(newFunction(3))
}
7.11. Calling functions that return functions
Given that functions are first-class objects in Golo, you may define functions (or closures) that return functions, as in:
let f = |x| -> |y| -> |z| -> x + y + z
You could use intermediate references to use the f
function above:
let f1 = f(1)
let f2 = f1(2)
let f3 = f2(3)
# Prints '6'
println(f3())
Golo supports a nicer syntax if you don’t need intermediate references:
# Prints '6'
println(f(1)(2)(3)())
8. Predefined functions
Every Golo module definition comes with gololang.Predefined
as a default import. It provides
useful functions.
8.1. Console output
print
and println
do just what you would expect.
print("Hey")
println()
println("Hey")
8.2. Console input
readln()
or readln(strMessage)
reads a single line of text from the console. It always returns a
string.
readPassword()
or readPassword(strPassword)
reads a password from the console with echoing
disabled. It always returns a string. There are also secureReadPassword()
and
secureReadPassword(strPassword)
variants that return a char[]
array.
let name = readln("what's your name? ")
let value = readln()
let pwd = readPassword("type your password:")
As of Golo 3.3, these functions are in the gololang.IO module, and those in gololang.Predefined are deprecated. They will be removed in Golo 4.0.
|
8.3. File I/O
Sometimes it is very desirable to read the content of a text file. The fileToText
function does just that:
let text = fileToText("/some/file.txt", "UTF-8")
The first parameter is either a java.lang.String
, a java.io.File
or a java.nio.file.Path
. The second parameter
represents the encoding charset, either as a java.lang.String
or a java.nio.charset.Charset
.
We can write some text to a file, too:
textToFile("Hello, world!", "/foo/bar.txt")
The textToFile
function overwrites existing files, and creates new ones if needed.
These functions are provided for convenience, so if you need more fine-grained control over reading and writing text
then we suggest that you look into the java.nio.file
package.
In addition, if you need to verify that a file exists, you can use the fileExists
function.
if fileExists("/foo/bar.txt") {
println("file found!")
}
As in the other File I/O methods, the parameter is either a java.lang.String
, a java.io.File
or a java.nio.file.Path
.
The fileExists
function will return true if the file exists, false if it doesn’t.
If you need the current path of execution, you can use the currentDir
function.
println(currentDir())
As of Golo 3.3, these functions are in the gololang.IO module, and those in gololang.Predefined are deprecated.
|
8.4. Number type conversions
The following functions convert any number of string value to another number type:
intValue(n)
, longValue(n)
, charValue(n)
, doubleValue(n)
and floatValue(n)
.
The usual Java type narrowing or widening conventions apply.
let i = intValue("666") # 666 (string to integer)
let j = intValue(1.234) # 1 (double to integer)
let k = intValue(666_L) # 666 (long to integer)
# etc
8.5. Exceptions
raise
can be used to throw a java.lang.RuntimeException
. It comes in two forms: one with a
message as a string, and one with a message and a cause.
try {
...
raise("Somehow something is wrong")
} catch (e) {
...
raise("Something was wrong, and here is the cause", e)
}
8.6. Preconditions
Preconditions are useful, especially in a dynamically-typed language.
require
can check for a boolean expression along with an error message. In case of error, it
throws an AssertionError
.
function foo = |a| {
require(a oftype String.class, "a must be a String")
...
}
You may also use requireNotNull
that… well… checks that its argument is not null
:
function foo = |a| {
requireNotNull(a)
...
}
8.7. Arrays
Golo arrays can be created using the array[…]
literal syntax. Such arrays are of JVM type Object[]
.
There are cases where one needs typed arrays rather than Object[]
arrays, especially when dealing with existing Java
libraries.
The newTypedArray
predefined function can help:
let data = newTypedArray(java.lang.String.class, 3)
data: set(0, "A")
data: set(1, "B")
data: set(2, "C")
8.8. Ranges
The range
function yields an iterable range over either Integer
, Long
or
Character
bounds:
# Prints 1 2 (...) 100
foreach i in range(1, 101) {
print(i + " ")
}
# Prints a b c d
foreach c in range('a', 'e') {
print(c + " ")
}
let r = range(0, 6): incrementBy(2)
println("Start: " + r: from())
println("End: " + r: to())
foreach i in r {
println(i)
}
println("Increment: " + r: increment())
The lower bound is inclusive, the upper bound is exclusive.
A range
with a lower bound greater than its upper bound will be empty, except
if the increment is explicitly negative:
# Prints nothing
foreach i in range(3, 0) {
print(i + " ")
}
# Prints 3 2 1
foreach i in range(3, 0): incrementBy(-1) {
print(i + " ")
}
# Prints 0 -2 -4
foreach i in range(0, -6):decrementBy(2) {
print(i + " ")
}
The reversedRange
function is an alias for range
with an increment of -1,
such that reversedRange(5, 1)
is the same as range(5, 1): decrementBy(1)
.
When range
is called with only one value, it is used as the upper bound, the
lower one being a default value (0 for numbers, 'A' for chars). For example,
range(5) == range(0, 5)
and range('Z') == range('A', 'Z')
. In the same way,
reversedRange(5) == reversedRange(5, 0)
.
Two ranges are equals if they have the same bounds and increment.
A range can also be defined with the literal notation [begin..end]
, which is
equivalent to range(begin, end)
.
8.9. Closures
Given a function reference, one can convert it to an instance of an interface with a single method declaration, as in:
local function listener = |handler| -> asInterfaceInstance(ActionListener.class, handler)
# (...)
let button = JButton("Click me!")
button: addActionListener(listener(|event| -> println("Clicked!")))
It is possible to test if an object is a closure or not with the isClosure
function. This is
useful to support values and delayed evaluation, as in:
if isClosure(value) {
map: put(key, value())
} else {
map: put(key, value)
}
You can get a reference to a closure using the predefined fun
function:
import golotest.Closures
local function local_fun = |x| -> x + 1
function call_local_fun = {
let f = fun("local_fun", golotest.Closures.module)
return f(1)
}
Because functions may be overloaded, there is a form that accepts an extra parameter for specifying the number of parameters:
import golotest.Closures
local function local_fun = |x| -> x + 1
function call_local_fun = {
let f = fun("local_fun", golotest.Closures.module, 1)
return f(1)
}
While asInterfaceInstance
works for single-method interfaces, Java 8 introduced default methods
and functional interfaces to support the so-called lambda expressions.
The asFunctionalInterface
function is similar to asInterfaceInstance
and supports these types of
adaptations:
let always_true = asFunctionalInterface(java.lang.function.Predicate.class, |obj| -> true)
8.10. Array types
Golo does not provide a literal syntax for array types, such as Object[].class
in Java.
Instead, we provide 3 helper functions.
-
isArray(object)
: returns a boolean ifobject
is an array. -
objectArrayType()
: returnsObject[].class
. -
arrayTypeOf(type)
: giventype
as ajava.lang.Class
, returnstype[].class
.
8.11. Misc.
mapEntry
gives instances of java.util.AbstractMap.SimpleEntry
, and is used as follows:
let e = mapEntry("foo", "bar")
# prints "foo => bar"
println(e: key() + " => " + e: value())
box
gives instances of java.util.concurrent.atomic.AtomicReference
, and can
be used to create a mutable reference where not possible otherwise, e.g. in
closures, as in:
function counter = |init, stepFun| {
let current = box(init)
return -> current: getAndUpdate(stepFun)
}
#...
let c2 = counter(3, |x| -> x * 2)
c() # -> 3
c() # -> 6
c() # -> 12
9. Class augmentations
Many dynamic languages support the ability to extend existing classes by adding new methods to them. You may think of categories in Objective-C and Groovy, or open classes in Ruby.
This is generally implemented by providing meta-classes. When some piece of code adds a method
foo
to, say, SomeClass
, then all instances of SomeClass
get that new foo
method. While very
convenient, such an open system may lead to well-known conflicts between the added methods.
Golo provides a more limited but explicit way to add methods to existing classes in the form of class augmentations.
9.1. Wrapping a string with a function
Let us motivate the value of augmentations by starting with the following example. Suppose that we would like a function to wrap a string with a left and right string. We could do that in Golo as follows:
function wrap = |left, str, right| -> left + str + right
# (...)
let str = wrap("(", "foo", ")")
println(str) # prints "(abc)"
Defining functions for such tasks makes perfect sense, but what if we could just add the wrap
method to all instances of java.lang.String
instead?
9.2. Augmenting classes
Defining an augmentation is a matter of adding a augment
block in a module:
module foo
augment java.lang.String {
function wrap = |this, left, right| -> left + this + right
}
function wrapped = -> "abc": wrap("(", ")")
More specifically:
-
a
augment
definition is made on a fully-qualified class name, and -
an augmentation function takes the receiver object as its first argument, followed by optional arguments, and
-
there can be as many augmentation functions as you want, and
-
there can be as many augmentations as you want.
It is a good convention to name the receiver this
, but you are free to call it differently.
Also, augmentation functions can take variable-arity arguments, as in:
augment java.lang.String {
function concatWith = |this, args...| {
var result = this
foreach(arg in args) {
result = result + arg
}
return result
}
}
# (...)
function varargs = -> "a": concatWith("b", "c", "d")
It should be noted that augmentations work with class hierarchies too. The following example adds an
augmentation to java.util.Collection
, which also adds it to concrete subclasses such as java.util.LinkedList
:
augment java.util.Collection {
function plop = |this| -> "plop!"
}
# (...)
function plop_in_a_list = -> java.util.LinkedList(): plop()
9.3. Augmentation scopes, reusable augmentations
By default, an augmentation is only visible from its defining module.
Augmentations are clear and explicit as they only affect the instances from which you have decided to make them visible.
It is advised to place reusable augmentations in separate module definitions. Then, a module that needs such augmentations can make them available through imports.
Suppose that you want to define augmentations for dealing with URLs from strings. You could define a
string-url-augmentations.golo
module source as follows:
module my.StringUrlAugmentations
import java.net
augment java.lang.String {
function toURL = |this| -> URL(this)
function httpGet = |this| {
# Open the URL, get a connection, grab the body as a string, etc
# (...)
}
# (...)
}
Then, a module willing to take advantage of those augmentations can simply import their defining module:
module my.App
import my.StringUrlAugmentations
function googPageBody = -> "http://www.google.com/": httpGet()
As a matter of style, we suggest that your module names end with Augmentations . Because importing a
module imports all of its augmentation definitions, we suggest that you modularize them with fine
taste (for what it means).
|
To allow building libraries of functions leveraging augmentations, the callstack is also looked up in order to find if a complying augmentation was applied to the function arguments. It is thus possible to create a function like:
module MyLib
function sayPlop = |o| {
println(o: plop())
}
that can be applied on any object having a plop
method, provided this is a native method or it is defined in an augmentation imported in the calling module.
9.4. Named augmentations
It is possible for augmentations to have a name. A named augmentation is a set of functions that can be applied to some classes or structures.
Named augmentations are defined with the augmentation
keyword.
As an example:
augmentation FooBar = {
function foo = |this| -> "foo"
function bar = |this, a| -> this: length() + a
}
augmentation Spamable = {
function spam = |this| -> "spam"
}
A named augmentation is applied using the augment … with
construct, as in
augment java.util.Collection with FooBar
augment MyStruct with Spamable
augment java.lang.String with FooBar, Spamable
When applying several named augmentations, they are used in the application
order. For instance, if AugmentA
and AugmentB
define both the method
meth
, and we augment augment java.lang.String with AugmentA, AugmentB
, then
calling "": meth()
will call AugmentA::meth
.
Augmentation rules about scopes and reusability apply. So, if we create a module
module MyAugmentations
augmentation Searchable = {
function search = |this, value| -> ...
}
augment java.util.Collection with Searchable
and import it, we can use the applied augmentation
import MyAugmentations
#...
list[1, 2, 3, 4]: search(2)
The augmentations defined in an other module can also be applied, provided they are fully qualified or the module is imported:
augment java.lang.String with MyAugmentations.Searchable
or
import MyAugmentations
augment java.lang.String with Searchable
If several imported modules define augmentations with the same name, the first imported one will be used. |
The validity of the application is not checked at compile time. Thus augmenting without importing the coresponding module, as in:
augment java.lang.String with Searchable
will not raise an error, but trying to call search
on a String
will throw a
java.lang.NoSuchMethodError: class java.lang.String::search
at runtime.
As for every augmentation, no checks are made that the augmentation
can be applied to the augmented class. For instance, augmenting java.lang.Number
with the previous FooBar augmentation will raise
java.lang.NoSuchMethodError: class java.lang.Integer::length
at runtime when trying to call 1:bar(1) . Calling 1:foo() will be OK however.
|
9.5. Augmentations Resolution Order
The augmentations resolution order is as follows:
-
native java method (i.e. an augmentation can’t override a native java method),
-
locally applied augmentations:
-
simple augmentations:
augment MyType { … }
, -
named augmentations:
augmentation Foo = { … }
andaugment MyType with Foo
in the current module. Multiple applications are searched in the application order, -
externally defined named augmentations with fully qualified name:
augmentation Foo = { … }
in moduleAugmentations
, andaugment MyType with Augmentations.Foo
in the current module, -
named augmentation defined in an imported module:
augmentation Foo = { … }
in moduleAugmentations
, andaugment MyType with Foo
in the current module thatimport Augmentations
(imported module are searched in the importation order),
-
-
augmentations applied in imported modules: using the same order than locally applied ones, in the importation order.
The first matching method found is used. It is thus possible to “override” an augmentation with a more higher priority one (in the sens of the previous order). Implicit modules are imported after explicit ones to allow to redefine standard augmentations.
Since importing a module imports all the applied augmentations, and given the somewhat complex resolution order when involving simple and named augmentations, being local, external or imported, and involving class hierarchies, knowing which method will be applied on a given type can be difficult. A good modularisation and a careful application are recommended. |
9.6. Defining a fallback behavior
Users can augment a class with a fallback
behavior to give a very last chance to a failed
method dispacth.
augment java.lang.String {
function fallback = |this, name, args...| {
return "Dispatch failed for method: " + name + " on instance " + this + ", with args: " + args: asList(): join(" ")
}
}
println("golo": notExistingMethod(1,2))
# Prints "Dispatch failed for method: notExistingMethod on instance golo, with args: [1, 2]"
9.7. Standard augmentations
Golo comes with a set of pre-defined augmentations over collections, strings, closures and more.
These augmentation do not require a special import, and they are defined in the
gololang.StandardAugmentations
module.
Here is an example:
let odd = [1, 2, 3, 4, 5]: filter(|n| -> (n % 2) == 0)
let m = map[]
println(m: getOrElse("foo", -> "bar"))
The full set of standard augmentations is documented in the generated golodoc (hint: look for
doc/golodoc
in the Golo distribution).
10. Structs
Golo allows the definition of simple structures using the struct
keyword. They resemble structures
in procedural languages such as C struct
or Pascal records. They are useful to store data when
the set of named entries is fixed.
10.1. Definition
Structures are defined at the module-level:
module sample
struct Person = { name, age, email }
function main = |args| {
let p1 = Person("Mr Bean", 54, "bean@gmail.com")
println(p1: name())
let p2 = Person(): name("John"): age(32): email("john@b-root.com")
println(p2: age())
}
When declaring a structure, it also defines two factory functions: one with no argument, and one
with all arguments in their order of declaration in the struct
statement. When not initialized,
member values are null
.
Each member yields a getter and a setter method: given a member a
, the getter is method a()
while the setter is method a(newValue)
. It should be noted that setter methods return the
structure instance which makes it possible to chain calls as illustrated in the previous example
while building p2
.
10.2. JVM existence
Each struct
is compiled to a self-contained JVM class.
Given:
module sample
struct Point = { x, y }
a class sample.types.Point
is being generated.
It is important to note that:
-
each
struct
class isfinal
, -
each
struct
class inherits fromgololang.GoloStruct
, -
proper definitions of
toString()
,hashCode()
andequals()
are being provided.
10.3. toString()
behavior
The toString()
method is being overridden to provide a meaningful description of a structure
content.
Given the following program:
module test
struct Point = { x, y }
function main = |args| {
println(Point(1, 2))
}
running it prints the following console output:
struct Point{x=1, y=2}
10.4. Immutable structs
Structure instances are mutable by default. Golo generates a factory function with the Immutable
prefix to directly build immutable instances:
module test
struct Point = { x, y }
function main = |args| {
let p = ImmutablePoint(1, 2)
println(p)
try {
# Fails! (p is immutable)
p: x(100)
} catch (expected) {
println(expected: getMessage())
}
}
10.5. Custom factories
Golo generates two factories for structures. One for the initial values of each member and a second one with no parameters:
module test
struct Point = { x, y }
function main = |args| {
println(Point(1, 2))
println(Point())
}
running it prints the following console output:
struct Point{x=1, y=2} struct Point{x=null, y=null}
By default the no-argument factory sets every member to null
|
The Factories generated by Golo can be overloaded by custom ones:
module test
struct Point = { x, y }
function Point = -> test.types.Point(0,0)
function main = |args| {
println(Point(1, 2))
println(Point())
}
running it prints the following console output:
struct Point{x=1, y=2} struct Point{x=0, y=0}
Immutable factories can be easily overloaded by returning a forzen copy. |
module test
struct Point = { x, y }
function ImmutablePoint = |a,b| -> test.types.Point(a,b): frozenCopy()
function main = |args| {
println(ImmutablePoint(1, 2): isFrozen()) # prints true
}
10.6. Copying
Instances of a structure provide copying methods:
-
copy()
returns a shallow copy of the structure instance, and -
frozenCopy()
returns a read-only shallow copy.
Trying to invoke any setter methods on an instance obtained through frozenCopy()
raises a
java.lang.IllegalStateException
.
The result of calling copy() on a frozen instance is a mutable copy, not a frozen
copy.
|
10.7. equals()
and hashCode()
semantics
Golo structures honor the contract of Java objects regarding equality and hash codes.
By default, equals()
and hashCode()
are the ones of java.lang.Object
. Indeed, structure
members can be changed, so they cannot be used to compute stable values.
Nevertheless, structure instances returned by frozenCopy()
have stable members, and members are
being used.
Consider the following program:
module test
struct Point = { x, y }
function main = |args| {
let p1 = Point(1, 2)
let p2 = Point(1, 2)
let p3 = p1: frozenCopy()
let p4 = p1: frozenCopy()
println("p1 == p2 " + (p1 == p2))
println("p1 == p3 " + (p1 == p3))
println("p3 == p4 " + (p3 == p4))
println("#p1 " + p1: hashCode())
println("#p2 " + p2: hashCode())
println("#p3 " + p3: hashCode())
println("#p4 " + p4: hashCode())
}
the console output is the following:
p1 == p2 false p1 == p3 false p3 == p4 true #p1 1555845260 #p2 104739310 #p3 994 #p4 994
It is recommended that you use Immutable<name of struct>(…) or frozenCopy() when you can,
especially when storing values into collections.
|
10.8. Comparison semantics
Golo structures are comparable with structures of the same type, provided that their members are comparable pairwise. Two structure are compared by comparing their members lexicographically, that is pairwise in the order they are defined.
For instance, the following listing:
module test
struct Triplet = {x, y, z}
struct Other = {x, y, z}
function main = |args| {
let t1 = Triplet(1, 2, 3)
let t2 = Triplet(1, 3, 2)
let o = Other(1, 2, 4)
println(t1 < t2)
println(t1: values() < o: values())
println(t1 < o)
}
will output:
true true Exception in thread "main" java.lang.IllegalArgumentException: struct Triplet{x=1, y=2, z=3} and struct Other{x=1, y=2, z=4} can't be compared
even if Triplet
and Other
have the same members.
10.9. Helper methods
A number of helper methods are being generated:
-
members()
returns a tuple of the member names, -
values()
returns a tuple with the current member values, -
isFrozen()
returns a boolean to check for frozen structure instances, -
iterator()
provides an iterator over a structure where each element is a tuple[member, value]
, -
get(name)
returns the value of a member by its name, -
set(name, value)
updates the value of a member by its name, and returns the same structure.
10.10. Private members
By default, all members in a struct can be accessed. It is possible to make some elements private by
prefixing them with _
, as in:
struct Foo = { a, _b, c }
# (...)
let foo = Foo(1, 2, 3)
In this case, _b
is a private struct member. This means that foo: _b()
and foo: _b(666)
are
valid calls only if made from:
-
a function from the declaring module, or
-
an augmentation defined in the declaring module.
Any call to, say, foo: _b()
from another module will yield a NoSuchMethodError
exception.
Private struct members also have the following impact:
-
they do not appear in
members()
andvalues()
calls, and -
they are not iterated through
iterator()
-provided iterators, and -
they are being used like other members in
equals()
andhashCode()
, and -
they do not appear in
toString()
representations.
10.11. Augmenting structs
Structs provide a simple data model, especially with private members for encapsulation.
Augmenting structs is encouraged, as in:
module Plop
struct Point = { _id, x, y }
augment Plop.types.Point {
function str = |this| -> "{id=" + this: _id() + ",x=" + this: x() + ",y=" + this: y() + "}"
}
When an augmentation on a struct is defined within the same module, then you can omit the full type name of the struct:
module Plop
struct Point = { _id, x, y }
augment Point {
function str = |this| -> "{id=" + this: _id() + ",x=" + this: x() + ",y=" + this: y() + "}"
}
Again, it is important to note that augmentations can only access private struct members when they originate from the same module.
Don’t do this at home
Of course doing the following is a bad idea, with the concise augmentation taking over the fully-qualified one:
|
11. Unions
Golo allows the definition of sum algebraic data types, also known as
tagged union,
as present in many functional languages: OCaml, Haskell, Rust, Scala to
name a few.
The dual algebraic data type, the product type is provided by struct
and tuple
.
11.1. Definition
Unions are defined at the module-level:
module sample
union Option = {
Some = { value }
None
}
function main = |args| {
let aString = Option.Some("Hello")
println(aString: value())
let noString = Option.None()
println(noString)
}
11.2. Usage example
Some well known usages of sum types are the following.
11.2.1. Enumerations
The plain old list of predefined values.
union Color = {
RED
GREEN
BLUE
}
This use is similar to Java enum
, with the same power since Golo union
can be extended through augmentation.
11.2.2. Option type
The monadic type as found for instance in OCaml (Option
), Haskell (Maybe
)
and many other languages (Rust, Scala, etc.)
union Option = {
Some = {value}
None
}
As illustrated here, and contrary to Java enum
, each alternative value can
have different fields. A union
alternative type is in this respect similar to immutable struct
.
11.2.3. Recursive data structures
The usual functional representation of linked lists:
union ConsList = {
List = { head, tail }
Empty
}
Binary trees:
union Tree = {
Empty
Leaf = { value }
Node = { left, right }
}
11.3. JVM existence
A union
type is compiled to an abstract JVM class. Each alternative value
type is itself compiled to a final immutable JVM class extending the abstract class.
The value classes are member classes of the abstract one.
Given:
module sample
union Option = {
Some = { value }
None
}
three classes are generated:
-
an abstract class
sample.types.Option
, -
a concrete final immutable inner class
sample.types.Option$Some
extending the first one, -
a similar class
sample.types.Option$None
.
For your convenience, the abstract class provides factories static methods for each of the possible values, and you can’t instantiate values directly, since values without fields are actually singletons.
Note that proper definitions of toString()
, hashCode()
and equals()
are
provided. These definitions are similar to the ones defined for frozen struct
.
union values with fields are similar to frozen struct , that is
are immutable, have getters for fields and are compared by values. However,
these types does not feature the same helper methods, and can’t have private
members.
|
11.4. Special testing methods
Unions feature special methods to test for the exact type of a value, as well
as members values if applicable. This allows to write readable tests, more
specially using the match
clause, to look like destructuring match in
langages like OCaml, Haskell or Scala.
For instance, given a union defining a binary tree:
union Tree = {
Node = {left, right}
Leaf = {value}
Empty
}
one can match a elt
value using:
match {
when elt: isEmpty() then // we have an empty value
when elt: isLeaf(0) then // we have a leaf containing 0
when elt: isLeaf() then // we have a leaf (whatever the value)
when elt: isNode(Empty(), Empty()) then // we have a node with empty children
when elt: isNode(Leaf(42), Leaf(42)) then // we have a node whose both children contain 42
when elt: isNode() then // we have a node, whatever the values
otherwise // default case...
}
More precisely, each possible union value provides parameterless methods
testing its exact type, named is<TypeName>
. In the tree example, three
methods are defined: isEmpty()
, isLeaf()
and isNode()
.
In addition to these methods, a method with parameters is defined for every
alternative with members, here isLeaf(value)
and isNode(left, right)
. The arguments
are compared for equality to the members of the union value.
For instance:
Leaf(0): isLeaf(0) # true
Leaf(42): isLeaf(0) # false
allowing readable test and match clauses.
A special singleton value is available to make these clauses even more
readable: the Unknown
value. This special singleton is considered equal to
any other object (except null
), and thus can be used in the parametrized test
methods to ignore some members. For instance, to match a Node
with only one
child, one can use:
let _ = Unknown.get()
function dealWithTree = |elt| -> match {
when elt: isNode(Empty(), _) or elt: isNode(_, Empty()) then ...
// one of the children is Empty, whatever the other one
otherwise ...
}
11.5. Augmenting unions
Since the union
itself is a abstract class, and each possible value is a
concrete class extending it, it is possible to augment the whole union
, as in:
augment Option {
function map = |this, func| -> match {
when this: isNone() then this
otherwise Option.Some(func(this: value()))
}
}
or just a value, as in:
augment ConsList$Empty {
function size = |this| -> 0
function head = |this| -> null
function tail = |this| -> this
}
augment ConsList$List {
function size = |this| -> 1 + this: tail(): size()
}
12. Dynamic objects
Dynamic objects can have values and methods being added and removed dynamically at runtime. You can think of it as an enhancement over using hash maps and putting closures in them.
12.1. Creating dynamic objects
Creating a dynamic object is as simple as calling the DynamicObject
function:
let foo = DynamicObject()
A dynamic object can also be tagged using an arbitrary value as its kind. To create a tagged dynamic object, simply use a value in the constructor:
let bar = DynamicObject("Bar")
Dynamic objects have the following reserved methods, that is, methods that you cannot override:
-
define(name, value)
allows to define an object property, which can be either a value or a closure, and -
get(name)
gives the value or closure for a property name, ornull
if there is none, and -
undefine(name)
removes a property from the object, and -
mixin(dynobj)
mixes in all the properties of the dynamic objectdynobj
, and -
copy()
gives a copy of a dynamic object, and -
freeze()
locks an object, and callingdefine
will raise anIllegalStateException
, and -
isFrozen()
checks whether a dynamic object is frozen or not, and -
properties()
gives the set of entries in the dynamic object, and -
hasMethod(name)
checks if a method is defined or not in the dynamic object, and -
invoker(name, type)
which is mostly used by the Golo runtime internals, and -
hasKind(value)
tests if the dynamic object is tagged with this value, and -
sameKind(other)
tests if two dynamic objects have the same kind, and -
fallback(handler)
defines a fallback behavior for property invocation.
Moreover, a suitable toString
method is provided, but you can override it by defining a toString
method.
12.2. Defining values
Defining values also defines getter and setter methods, as illustrated by the next example:
let person = DynamicObject():
define("name", "MrBean"):
define("email", "mrbean@gmail.com")
# prints "Mr Bean"
println(person: name())
# prints "Mr Beanz"
person: name("Mr Beanz")
println(person: name())
Calling a setter method for a non-existent property defines it, hence the previous example can be rewritten as:
let person = DynamicObject(): name("MrBean"): email("mrbean@gmail.com")
# prints "Mr Bean"
println(person: name())
# prints "Mr Beanz"
person: name("Mr Beanz")
println(person: name())
12.3. Defining methods
Dynamic object methods are simply defined as closures. They must take the dynamic object object as
their first argument, and we suggest that you call it this
. You can then define as many parameters
as you want.
Here is an example where we define a toString
-style of method:
local function mrbean = -> DynamicObject():
name("Mr Bean"):
email("mrbean@gmail.com"):
define("toString", |this| -> this: name() + " <" + this: email() + ">")
function main = |args| {
let bean = mrbean()
println(bean: toString())
bean: email("mrbean@outlook.com")
println(bean: toString())
}
You cannot overload methods, that is, providing methods with the same name but different signatures. |
It is strongly recommended that you use
Any call such as That being said, the following would fail:
Indeed, when the value of a dynamic object property is a function, it is understood to be a method,
hence calling
As a rule of thumb, prefer named setters for values and |
12.4. Querying the properties
The properties()
method returns a set of entries, as instances of java.util.Map.Entry
. You can
thus write code such as:
function dump = |obj| {
foreach prop in obj: properties() {
println(prop: key() + " -> " + prop: value())
}
}
Because dynamic object entries mix both values and function references, do not forget that the predefined
isClosure(obj)
function can be useful to distinguish them.
12.5. Defining a fallback behavior
The fallback(handler)
method let’s the user define a method that is invoked whenever the initial method dispatch fails.
Here is an example of how to define a fallback.
Calling a setter method for a non-existent property defines it, thus the fallback is not applicable for setters. |
let dynob = DynamicObject():
fallback(|this, method, args...| {
return "Dispatch failed for method: " + method + ", with args: " + args: asList(): join(" ")
})
println(dynob: casperGetter())
println(dynob: casperMethod("foo", "bar"))
Dispatch failed for method: casperGetter, with args:
Dispatch failed for method: casperMethod, with args: foo bar
A delegate
function is available to ease defining a fallback function that delegates on another dynamic object. For instance:
let t = DynamicObject("deleguee")
: name("Zaphod")
: define("sayHello", |this| -> "Hello, I'm " + this: name())
let s = DynamicObject("withFallback")
: fallback(DynamicObject.delegate(t))
require(s: sayHello() == "Hello, I'm Zaphod", "error")
12.6. Kind of dynamic objects
The kind of a dynamic object can be any value. The more typical values are strings and unions (enumerations). This value is used by the toString()
method to display a more specific representation of the object, and defaults to the string "DynamicObject"
. By using the two predefined methods hasKind(value)
and sameKind(other)
, it is possible to test for the kind of a dynamic object. Note that the kind of a created object can’t be changed.
function Person = |name, age| -> DynamicObject("Person")
: name(name)
: age(age)
: define("marry", |this, other| {
require(this: sameKind(other), "Can only marry a person")
this: spouse(other)
other: spouse(this)
})
function printAge = |obj| {
if (obj: hasKind("Person")) {
println(obj: age())
} else {
println("I have no age")
}
}
13. Adapters
There is already much you can do while in Golo land using functions, closures, structs, augmentations and dynamic objects.
Yet, the JVM is a wider ecosystem and you will soon be tempted to integrate existing Java libraries into your code. Calling Java libraries from Golo is quite easy, but what happens when you need to subclass classes or provide objects that implement specific interfaces?
As you can easily guess, this is all what adapters are about: they allow the definition of objects at runtime that can extend and inherit Java types.
13.1. A simple example
Let us get started with a simple example of a web application based on the nice Spark micro-framework [1].
Spark requires route handlers to extend an abstract base class called spark.Route
. The following
code snippet does just that:
module sparky
import spark
import spark.Spark
function main = |args| {
let conf = map[ (1)
["extends", "spark.Route"], (2)
["implements", map[ (3)
["handle", |this, request, response| { (4)
return "Golo, world!"
}]
]]
]
let fabric = AdapterFabric() (5)
let routeMaker = fabric: maker(conf) (6)
let route = routeMaker: newInstance("/hello") (7)
get(route) (8)
}
1 | An adapter configuration is provided by a map object. |
2 | The extends key allows specifying the name of the parent class (java.lang.Object by
default). |
3 | The implements provides a map of method implementations. |
4 | The implementation is given by a closure whose signature matches the parent class definition, and where the first argument is the receiver object that is going to be the adapter instance. |
5 | An adapter fabric provides context for creating adapters. It manages its own class loader. |
6 | An adapter maker creates instances based on a configuration. |
7 | The newInstance() method calls the right constructor based on the parent class constructors
and provided argument types. |
8 | The spark.Spark.get() static is method is happy as we feed it a subclass of spark.Route . |
Adapter objects implement the gololang.GoloAdapter marker interface, so you can do type
checks on them a in: (foo oftype gololang.GoloAdapter.class) .
|
13.2. Implementing interfaces
This is as easy as providing a java.lang.Iterable
as part of the configuration:
let result = array[1, 2, 3]
let conf = map[
["interfaces", ["java.io.Serializable", "java.lang.Runnable"]],
["implements", map[
["run", |this| {
for (var i = 0, i < result: length(), i = i + 1) {
result: set(i, result: get(i) + 10)
}
}]
]]
]
let runner = AdapterFabric(): maker(conf): newInstance()
runner: run() (1)
1 | As you may guess, this changes the result array values to [11, 12, 13] . |
13.3. Overrides
Implementations are great, but what happens if you need to call the parent class implementation of a
method? In Java, you would use a super
reference, but Golo does not provide that.
Instead, you can override methods, and have the parent class implementation given to you as a method handle parameter:
let conf = map[
["overrides", map[
["toString", |super, this| -> ">>> " + super(this)]
]]
]
println(AdapterFabric(): maker(conf): newInstance(): toString()) (1)
1 | This prints something like: >>> $Golo$Adapter$0@12fc7ceb . |
You can mix both implementations and overrides in an adapter configuration. |
13.4. Star implementations and overrides
You can pass *
as a name for implementations or overrides. In such cases, the provided closure
become the dispatch targets for all methods that do not have an implementation or override. Note
that providing both a star implementation and a star override is an error.
Let us see a concrete example:
let carbonCopy = list[] (1)
let conf = map[
["extends", "java.util.ArrayList"],
["overrides", map[
["*", |super, name, args| { (2)
if name == "add" {
if args: length() == 2 {
carbonCopy: add(args: get(1)) (3)
} else {
carbonCopy: add(args: get(1), args: get(2)) (4)
}
}
return super: spread(args) (5)
}
]]
]]
let list = AdapterFabric(): maker(conf): newInstance()
list: add("bar")
list: add(0, "foo")
list: add("baz") (6)
1 | We create an empty list, more on that later. |
2 | A star override takes 3 parameters: the parent class implementation, the method name and the arguments into an array (the element at index 0 is the receiver). |
3 | We copy into carbonCopy . |
4 | Same here, but we dispatch to a different method |
5 | We just call the parent class implementation of whatever method it is. Note that spread allows
to dispatch a closure call with an array of arguments. |
6 | At this point carbonCopy contains ["foo", "bar", "baz"] (and so does list , too). |
The case of star implementation is similar, except that the closure takes only 2 parameters:
|name, args|
.
13.5. Misc.
The AdapterFabric
constructor can also take a class loader as a parameter. When none is provided,
the current thread context class loader is being used as a parent for an AdapterFabric
-internal
classloader. There is also a static method withParentClassLoader(classloader)
to obtain a fabric
whose class loader is based on a provided parent.
As it is often the case for dynamic languages on the JVM, overloaded methods with the same name but
different methods are painful. In such cases, we suggest that you take advantage of
star-implementations or star-overrides as illustrated above on a ArrayList
subclass where the 2
add(obj)
and add(index, obj)
methods are being intercepted.
Finally we do not encourage you to use adapters as part of Golo code outside of providing bridges to third-party APIs.
13.6. Adapters helper
This is another way to use the adapters. You can see that as a kind of DSL for the Golo adapters. Let’s see how to re-write the examples in the previous paragraph
13.6.1. A simple example
Let us get started (again) with a simple example of a web application based on the Spark micro-framework.
module sparky
import gololang.Adapters
import spark
import spark.Spark
function main = |args| {
let sparkRouteAdapter = Adapter() (1)
: extends("spark.Route") (2)
: implements("handle", |this, request, response| { (3)
return "Golo, world!"
})
let route = sparkRouteAdapter: newInstance("/hello") (4)
get(route) (5)
}
1 | An adapter factory. |
2 | The extends method specifies the name of the parent class (java.lang.Object by
default). |
3 | The implements method specifies the method implementations.
The implementation is given by a closure whose signature matches the parent class definition,
and where the first argument is the receiver object that is going to be the adapter instance. |
4 | The newInstance() method calls the right constructor based on the parent class constructors
and provided argument types. |
5 | The spark.Spark.get() static is method is happy as we feed it a subclass of spark.Route . |
13.6.2. Implementing interfaces
let result = array[1, 2, 3]
let conf = Adapter(): interfaces(["java.io.Serializable", "java.lang.Runnable"])
: implements("run", |this| {
for (var i = 0, i < result: length(), i = i + 1) {
result: set(i, result: get(i) + 10)
}
})
let runner = conf: newInstance()
runner: run() (1)
1 | As you may guess, this changes the result array values to [11, 12, 13] . |
13.6.3. Overrides
let conf = Adapter(): overrides("toString", |super, this| -> ">>> " + super(this))
println(conf: newInstance(): toString()) (1)
1 | This prints something like: >>> $Golo$Adapter$0@12fc7ceb . |
13.6.4. Star implementations and overrides
let carbonCopy = list[] (1)
let conf = Adapter(): extends("java.util.ArrayList")
: overrides("*", |super, name, args| {
if name == "add" {
if args: length() == 2 {
carbonCopy: add(args: get(1))
} else {
carbonCopy: add(args: get(1), args: get(2))
}
}
return super: invoke(args)
})
let list = conf: newInstance()
list: add("bar")
list: add(0, "foo")
list: add("baz") (1)
1 | At this point carbonCopy contains ["foo", "bar", "baz"] (and so does list , too). |
14. Decorators
Golo features Python-like decorators.
14.1. Presentation
Decorators are similar in syntax and purpose to Java annotations. However, the concepts behind them are very different. Indeed, whereas Java annotations are compiler or VM directives, decorators are actually plain functions, more precisely higher order functions.
Higher order functions (HOF) are functions that process functions, i.e. that take a function as parameter, and may return a new function. |
A decorator is thus a function that take the function to decorate as parameter, and return a new function, generally a wrapper that do some stuffs before or after calling the original function.
The name can remind the well known
GoF Pattern, with good reason.
This pattern describe a design that allow an object to be augmented by wrapping
it in an other object with the same interface, delegating operations to the
wrapped object. This is exactly what a decorator does here, the interface
being "function" (more precisely a gololang.FunctionReference
).
14.2. Principles and syntax
As in Python, and similarly to Java annotations, a decorator is used with a
@
prefix before the function definition. As an example, the decorator
deco1
only prints its name before returning the result unchanged
function deco1 = |fun| {
return |args...| {
return "deco1 + " + fun: invoke(args)
}
}
It can be used as:
@deco1
function foo = |a| {
return "foo: " + a
}
Here, calling println(foo(1))
will print deco1 + foo: 1
.
To be the most generic, the function created by a decorator should be a
variable arity function, and thus call the decorated function with
invoke
, such that it can be applied to any function, regardless
of its arity, as in the previous example.
Indeed, suppose you what to a decorator dec
(that does nothing) used like:
@dec
function add = |a,b| -> a + b
Such a decorator can be implemented as:
function dec = |func| -> |a, b| -> func(a, b)
But in that case, it will be applicable to two parameters functions only. On the other hand, you cannot do:
function dec = |func| -> |args...| -> func(args)
Indeed, this will throw an exception because func
is not a variable arity
function (just a reference on add
function) and thus cannot take an array
as parameter. In this case, the decorator have to invoke the original function
like this:
function dec = |func| -> |args...| -> func(args: get(0), args: get(1))
which is equivalent to the first form, but is not generic. The more generic decorator is thus:
function dec = |func| -> |args...| -> func: invoke(args)
which can deal with any function.
As illustrated, the decorator is just a wrapper (closure) around the decorated
function. The @
syntax is just syntactic sugar. Indeed, it can also be used
as such:
function bar = |a| -> "bar: " + a
function main = |args| {
println(deco1(^bar)(1))
let decobar = deco1(^bar)
println(decobar(1))
println(deco1(|a| -> "bar: "+a)(1))
}
prints all deco1 + bar: 1
.
Decorators can also be stacked. For instance:
function deco2 = |fun| {
return |args...| {
return "deco2 + " + fun: invoke(args)
}
}
@deco2
@deco1
function baz = |a| -> "baz: " + a
println(baz(1))
will print deco2 + deco1 + baz: 1
This result can also be achieved by composing decorators, as in:
let deco3 = ^deco1: andThen(^deco2)
@deco3
function spam = |a| -> "spam: " + a
Again, println(spam(1))
will print deco2 + deco1 + spam: 1
Moreover, since decorator are just higher order functions, they can be closure on a first argument, i.e. parametrized decorators, as illustrated in the following listing:
module tests.LogDeco
function log = |msg| -> |fun| -> |args...| {
println(msg)
return fun: invoke(args)
}
@log("calling foo")
function foo = |a| {
println("foo got a " + a)
}
@log("I'am a bar")
function bar = |a| -> 2*a
function main = |args| {
foo("bar")
println(bar(21))
}
will print
calling foo foo got a bar I'am a bar 42
Here, log
create a closure on the message, and return the decorator function.
Thus, log("hello")
is a function that take a function as parameter, and
return a new function printing the message (hello
) before delegating to the
inner function.
Again, since all of this are just functions, you can create shortcuts:
let sayHello = log("Hello")
@sayHello
function baz = -> "Goodbye"
A call to println(baz())
will print
Hello Goodbye
The only requirement is that the effective decorator (the expression following
the @
) is eventually a HOF returning a closure on the decorated function. As
an example, it can be as elaborated as:
function log = |msgBefore| -> |msgAfter| -> |func| -> |args...| {
println(msgBefore)
let res = func: invoke(args)
println(msgAfter)
return res
}
@log("enter foo")("exit foo")
function foo = |a| {
println("foo: " + a)
}
where a call foo("bar")
will print
enter foo foo: bar exit foo
and with
function logEnterExit = |name| -> log("# enter " + name)("# exit " + name)
@logEnterExit("bar")
function bar = { println("doing something...") }
calling bar()
will print
# enter bar doing something... # exit bar
or even, without decorator syntax:
function main = |args| {
let strange_use = log("hello")("goodbye")({println(":p")})
strange_use()
log("another")("use")(|a|{println(a)})("strange")
}
A last thing (but not the least), the function returned by the decorator can have a different arity than the original one:
function curry = |f| -> |a| -> |b| -> f(a, b)
@curry
function add = |a,b| -> a + b
function main = |args| {
add(12)(30)
}
The @curry
decorator transform the add
function that takes two arguments,
into a function that takes only one argument and returns a function that takes
the second argument and finally return the result of the addition.
A decorator is applied only if the decorated function is called from Golo code. For example if you try to call a decorated Golo function from Java code it will not be the decorated function that will be called but the original one. |
It is possible to create decorator and decorated functions in pure Java: |
package decorators;
import gololang.annotations.DecoratedBy;
import gololang.FunctionReference;
public class Decorators {
public static Object decorator(Object original) {
FunctionReference reference = (FunctionReference) original;
// do some transformations
System.out.println("decorator!");
return reference;
}
@DecoratedBy("decorator")
public static int add(int a, int b) {
return a + b;
}
}
Let’s call this from Golo:
import decorators.Decorators
function main = |args| {
println(add(10,32))
}
will print:
decorator! 42
Let’s now illustrate with some use cases and examples, with a presentation of
some decorators of the standard module
gololang.Decorators
.
14.3. Use cases and examples
Use cases are at least the same as aspect oriented programming (AOP) and the Decorator design pattern, but your imagination is your limit. Some are presented here for illustration.
14.3.1. Logging
Logging is a classical example use case of AOP. See the Principles and syntax section for an example.
14.3.2. Pre/post conditions checking
Decorators can be used to check pre-conditions, that is conditions that must hold for arguments, and post-conditions, that is conditions that must hold for returned values, of a function.
Indeed, a decorated can execute code before delegating to the decorated function, of after the delegation.
The module gololang.Decorators
provide two
decorators and several utility functions to check pre and post conditions.
checkResult
is a parametrized decorator taking a checker as parameter. It
checks that the result of the decorated function is valid.
checkArguments
is a variable arity function, taking as much checkers as the
decorated function arguments. It checks that the arguments of the decorated
function are valid according to the corresponding checker (1st argument checked
by 1st checker, and so on).
A checker is a function that raises an exception if its argument is not valid
(e.g. using require
) or returns it unchanged, allowing checkers to be chained
using the andThen
method.
As an example, one can check that the arguments and result of a function are integers with:
let isInteger = |v| {
require(v oftype Integer.class, v + "is not an Integer")
return v
}
@checkResult(isInteger)
@checkArguments(isInteger, isInteger)
function add = |a, b| -> a + b
or that the argument is a positive integer:
let isPositive = |v| {
require(v > 0, v + "is not > 0")
return v
}
@checkArguments(isInteger: andThen(isPositive))
function inv = |v| -> 1.0 / v
Of course, again, you can take shortcuts:
let isPositiveInt = isInteger: andThen(isPositive)
@checkResult(isPositiveInt)
@checkArguments(isPositiveInt)
function double = |v| -> 2 * v
or even
let myCheck = checkArguments(isInteger: andThen(isPositive))
@myCheck
function inv = |v| -> 1.0 / v
@myCheck
function mul = |v| -> 10 * v
Several factory functions are available in
gololang.Decorators
to ease the creation
of checkers:
-
any
is a void checker that does nothing. It can used when you need to check only some arguments of a n-ary function. -
asChecker
is a factory that takes a boolean function and an error message and returns the corresponding checker. For instance:
let isPositive = asChecker(|v| -> v > 0, "is not positive")
-
isOfType
is a factory function that returns a function checking types, e.g.
let isInteger = isOfType(Integer.class)
The full set of standard checkers is documented in the generated golodoc
(hint: look for doc/golodoc
in the Golo distribution).
14.3.3. Locking
As seen, decorator can be used to wrap a function call between checking operation, but also between a lock/unlock in a concurrent context:
import java.util.concurrent.locks
function withLock = |lock| -> |fun| -> |args...| {
lock: lock()
try {
return fun: invoke(args)
} finally {
lock: unlock()
}
}
let myLock = ReentrantLock()
@withLock(myLock)
function foo = |a, b| {
return a + b
}
14.3.4. Memoization
Memoization is the optimization technique that stores the results of a expensive computation to return them directly on subsequent calls. It is quite easy, using decorators, to transform a function into a memoized one. The decorator creates a closure on a hashmap, and check the existence of the results before delegating to the decorated function, and storing the result in the hashmap if needed.
Such a decorator is provided in the
gololang.Decorators
module, presented
here as an example:
function memoizer = {
var cache = map[]
return |fun| {
return |args...| {
let key = [fun: hashCode(), Tuple(args)]
if (not cache: containsKey(key)) {
cache: add(key, fun: invoke(args))
}
return cache: get(key)
}
}
}
The cache key is the decorated function and its call arguments, thus the decorator can be used for every module functions. It must however be put in a module-level state, since in the current implementation, the decoration is invoked at each call. For instance:
let memo = memoizer()
@memo
function fib = |n| {
if n <= 1 {
return n
} else {
return fib(n - 1) + fib(n - 2)
}
}
@memo
function fact = |n| {
if n == 0 {
return 1
} else {
return n * fact(n - 1)
}
}
14.3.5. Generic context
Decorators can be used to define a generic wrapper around a function, that
extends the previous example (and can be used to implement most of them).
This functionality is provided by the
gololang.Decorators.withContext
standard decorator. This decorator take a context, such as the one returned by
gololang.Decorators.defaultContext
function.
A context is an object with 4 defined methods:
-
entry
, that takes and returns the function arguments. This method can be used to check arguments or apply transformation to them; -
exit
, that takes and returns the result of the function. This method can be used to check conditions or transform the result; -
catcher
, that deal with exceptions that occurs during function execution. It takes the exception as parameter; -
finallizer
, that is called in afinally
clause after function execution.
The context returned by gololang.Decorators.defaultContext
is a void one, that
is entry
and exit
return their parameters unchanged,
catcher
rethrow the exception and finallizer
does nothing.
The workflow of this decorator is as follow:
-
the context
entry
method is called on the function arguments; -
the decorated function is called with arguments returned by
entry
;-
if an exception is raised,
catcher
is called with it as parameter; -
else the result is passed to
exit
and the returned value is returned
-
-
the
finallizer
method is called.
Any of theses methods can modify the context internal state.
Here is an usage example:
module samples.ContextDecorator
import gololang.Decorators
let myContext = defaultContext():
count(0):
define("entry", |this, args| {
this: count(this: count() + 1)
println("hello:" + this: count())
return args
}):
define("exit", |this, result| {
require(result >= 3, "wrong value")
println("goobye")
return result
}):
define("catcher", |this, e| {
println("Caught " + e)
throw e
}):
define("finallizer", |this| {println("do some cleanup")})
@withContext(myContext)
function foo = |a, b| {
println("Hard computation")
return a + b
}
function main = |args| {
println(foo(1,2))
println("====")
println(withContext(myContext)(|a| -> 2*a)(3))
println("====")
try {
println(foo(1, 1))
} catch (e) { }
}
which prints
hello:1 Hard computation goobye do some cleanup 3 ==== hello:2 goobye do some cleanup 6 ==== hello:3 Hard computation Caught java.lang.AssertionError: wrong value do some cleanup
Since the context is here shared between decorations, the count
attribute is
incremented by each call to every decorated function, thus the output.
This generic decorator can be used to easily implement condition checking, logging, locking, and so on. It can be more interesting if you want to provide several functionalities, instead of stacking more specific decorators, since stacking, or decorator composition, adds indirection levels and deepen the call stack.
15. Banged function call
Golo uses invokedynamic [2] [3] to dynamically link at runtime an invocation instruction to the target code that will be effectively executed.
An invocation is done in three steps:
-
first, a computation is executed to find the target code,
-
then, the call site of the invocation is plugged to this target,
-
finally the target code is executed as if it had been linked at load time.
The first two phases are mostly executed once for a call site, according there’s no need to re-link the call site to his target code.
A function call marked with bang (!
) is directly linked to the result returned by the target execution.
A banged invocation is executed like this:
-
first, a computation is executed to find the target code,
-
then the target code is executed,
-
finally, the call site of the invocation is plugged to a constant MethodHandle [4] which returns the target computation result as a constant value.
15.1. Principles and syntax
A function call marked with bang (!
) will be called only once,
the result is stored as a constant and will be directly returned for every subsequent call.
A function call can be marked with a bang like in the following example:
module sample
function take_a_while = {
# ... complex computation
return 42
}
function main = |args| {
foreach i in range(0, 100) {
take_a_while!()
}
}
In this example take_a_while
is computed only once at the first call, and then this function returns directly the previously computed result as a constant for every subsequent call.
The ! notation can only be used on regular function calls. Indeed, since methods are context
dependant (the object itself), it is not allowed to “bang” them. As a consequence, a function
invocation using invoke of a function reference can’t use this feature.
|
Bang function call is a kind of memoization but regardless of the given parameters:
module sample
function hello = |name| {
return "Hello " + name + "!"
}
function main = |args| {
foreach name in ["Peter", "John", "James"] {
println( hello!(name) # will always print 'Hello Peter!'
}
}
In this example hello
is executed at the first call with the parameter
"Peter"
, then always returns "Hello Peter!"
, even when called with other
values.
Functions having side effects should not be marked, since the computation
is not done for subsequent calls, and thus the side effect can’t happen. In the
same way, function that depends on an outside context are risky. Indeed, a
change in the context won’t imply a change in the result any more. In other
words, only pure functions should be marked with a ! . No check is done by
the language, use it at your own risk.
|
The result of a banged function call is constant within the same call place, but different for each call instructions.
module sample
function hello = |name| {
return "Hello " + name + "!"
}
function main = |args| {
println( hello!("Foo") ) # will print 'Hello Foo!'
println( hello!("Bar") ) # will print 'Hello Bar!'
foreach name in ["Peter", "John", "James"] {
println( hello!(name) # will always print 'Hello Peter!'
}
foreach name in ["Peter", "John", "James"] {
println( hello(name) # will print 'Hello Peter!', 'Hello John!', 'Hello James!'
}
}
In the previous listing, the hello!(name)
in the loop is considered the same
call, and thus evaluated only on the first iteration. On the other hand, the
previous calls with "Foo"
and "Bar"
are distinct, and therefore prints
different results.
Anonymous function call and object constructor call can be banged too:
module sample
function closure = |x| {
return |y| {
return x * y
}
}
function singleton = -> java.lang.Object!()
function main = |args| {
foreach i in range(0, 100) {
println( closure(i)!(i) ) # will always print 0
}
require(
singleton(): hashCode() == singleton(): hashCode(),
"Houston, ..."
)
}
In this example closure(i)!(i)
always return 0
because:
-
closure(i)
returns a closure (|y| → x * y
) withx
as enclosed variable -
closure(i)
is computed for each value ofi
-
the closure returned by
closure(i)
is called at the first iteration with0
forx
andy
-
for every subsequent call
closure(i)
is still computed but ignored because the anonymous call is replaced by the return of a constant value
The singleton
function return a new java Object but the java.lang.Object
is created with a banged constructor call, then the returned reference is constant.
15.2. Banged decorators
As explained in the decorators part the following identity
function:
function decorator = |func| -> |x| -> func(x)
@decorator
function identity = |x| -> x
is expanded to:
function decorator = |func| -> |x| -> func(x)
function identity = |x| -> decorator(|x| -> x)(x)
A banged decorator declared with the @!
syntax:
function decorator = |func| -> |x| -> func(x)
@!decorator
function identity = |x| -> x
is expanded to:
function decorator = |func| -> |x| -> func(x)
function identity = |x| -> decorator!(|x| -> x)(x)
As seen previously, the decorator
function is called only the first time.
For every subsequent call, the function reference returned by the decorator is not re-computed but directly used as a constant.
Parametrized decorators can be banged too:
function decorator = |arg| -> |func| -> |x| -> func(x)
@!decorator(42)
function identity = |x| -> x
is expanded to:
function decorator = |arg| -> |func| -> |x| -> func(x)
function identity = |x| -> decorator(42)!(|x| -> x)(x)
Considering the return of a banged call is constant, a common pitfall is to think that differents calls share the same "context" regardless where the call is located into the code. |
As an example, consider two functions decorated with the same parametrized decorator:
@!deco("a")
function foo = |a| -> a
@!deco("b")
function bar = |b| -> b
These functions are expanded to
function foo = |a| -> deco("a")!(|a| -> a)(a)
function bar = |b| -> deco("b")!(|b| -> b)(b)
deco("a")!(|a| → a)
return a function that we can name for the example func_a
,
and deco("b")!(|b| → b)
return another function that we can name func_b
.
Then, for every subsequent call of foo
and bar
, the executed code is
somehow equivalent to:
function foo = |a| -> func_a(a)
function bar = |b| -> func_b(b)
func_a
and func_b
are now constant but different because they are not from the same "banged call instruction".
Performances can considerably increase with banged decorators, since the decorator function is no more called for each decorated function call. On the other hand, the decorator function has to be pure (without side-effects) and his parameters stable.
16. Dynamic code evaluation
Golo provides facilities for dynamically evaluating code from strings in the form of the
gololang.EvaluationEnvironment
class. It provides an API that is useful both when used from Golo
code, or when used from a polyglot JVM application that embeds Golo.
16.1. Loading a module
The code of a complete module can be evaluated by the asModule
method:
let env = gololang.EvaluationEnvironment()
let code =
"""
module foo
function a = -> "a!"
function b = -> "b!"
"""
let mod = env: asModule(code)
let a = fun("a", mod)
let b = fun("b", mod)
println(a())
println(b())
It is important to note that an EvaluationEnvironment
instance has a GoloClassloader
, and that
attempting to evaluate module code with the same module
declaration will cause an error. Indeed, a
class loader cannot load classes with the same name twice.
16.2. Anonymous modules
The anonymousModule
method is similar to asModule
, except that the code to evaluate is free of
module
declaration:
let env = gololang.EvaluationEnvironment()
let code =
"""
function a = -> "a!"
function b = -> "b!"
"""
let mod = env: anonymousModule(code)
let a = fun("a", mod)
let b = fun("b", mod)
println(a())
println(b())
The modules that get evaluated through anonymousModule
have unique names, hence this method is
suitable in cases where the same code is to be re-evaluated several times.
16.3. Functions
The asFunction
and def
methods evaluate function code. Here is how asFunction
can be used:
let env = gololang.EvaluationEnvironment()
let code = "return (a + b) * 2"
let f = env: asFunction(code, "a", "b")
println(f(10, 20))
It evaluates straight code as the body of a function. Note that imports
can be used to specify
import
statements to be available while evaluation the code:
env:
imports("java.util.LinkedList", "java.util.HashMap"):
asFunction("""let l = LinkedList()
let m = HashMap()""")
The def
method is similar, except that it has the parameters definition in the code to evaluate:
let env = gololang.EvaluationEnvironment()
let code = "|a, b| -> (a + b) * 2"
let f = env: def(code)
println(f(10, 20))
16.4. Running code
The first form of run
method works as follows:
let env = gololang.EvaluationEnvironment()
let code = """println(">>> run")
foreach i in range(0, 3) {
println("w00t")
}
return 666"""
println(env: run(code)) # => "w00t"x3 and "666"
The second form allows passing parameter values in a map:
let env = gololang.EvaluationEnvironment()
let code = """println(">>> run_map")
println(a)
println(b)
"""
let values = java.util.TreeMap(): add("a", 1): add("b", 2)
env: run(code, values)
It is important not to abuse run
, as each invocation triggers the generation of a one-shot
class. If the same code is to be run several times, we suggest that you take advantage of either
def
or asFunction
.
17. Concurrency with workers
Concurrency is hard. Fortunately for us the java.util.concurrent
packages bring useful
abstractions, data types and execution mechanisms to get concurrency "a little bit better".
Golo doesn’t provide a equivalent to the synchronized
keyword of Java. This is on-purpose: when
facing concurrency, we advise you to just use whatever is in java.util.concurrent
.
That being said we provide a simple abstraction for concurrent executions in the form of workers. They pretty much resemble JavaScript web workers or isolates in Dart, albeit they do not really isolate the workers data space.
17.1. The big picture
A worker is simply a Golo function that can be executed concurrently. You can pass messages to a worker, and they are eventually received and handled by their target worker. In other words, workers react to messages in an asynchronous fashion.
Communications between a worker and some client code happens through ports. A port is simply an object that is responsible for dispatching a message to its worker.
Ports are obtained by spawning a worker function from a worker environment. Internally, a worker
environment manages a java.util.concurrent
executor, which means that you do not have to deal with
thread management.
17.2. Worker environments
Worker environments are defined in the gololang.concurrent.workers.WorkerEnvironment
class /
module.
You can directly pass an instance of java.util.concurrent.ExecutorService
to its constructor, or
you may go through its builder object and call either of the following static methods:
-
withCachedThreadPool()
uses a cached thread pool, -
withFixedThreadPool(size)
uses a fixed number of threads in a pool, -
withFixedThreadPool()
uses a pool with 1 thread per processor core, -
withSingleThreadExecutor()
uses a single executor thread.
In most scenarios withCachedThreadPool()
is a safe choice, but as usual, your mileage varies. If
you have many concurrent tasks to perform and they are not IO-bound, then withFixedThreadPool()
is
probably a better option. You should always measure, and remember that you can always pass a
fine-tuned executor to the WorkerEnvironment()
constructor.
Worker environments also provide delegate methods to their internal executor. It is important to
call shutdown()
to close the workers environment and release the threads pool. You can also call
the awaitTermination
, isShutdown
and isTerminated
methods whose semantics are exactly those of
java.util.concurrent.ExecutorService
.
17.3. Spawning a worker and passing messages
Worker functions take a single parameter which is the message to be received. To obtain a port, you
need to call the spawn(target)
function of a worker environment, as in:
let env = WorkerEnvironment.builder(): withFixedThreadPool()
let port = env: spawn(|message| -> println(">>> " + message))
A port provides a send(message)
method:
port: send("hello"): send("world")
Messages are being put in a queue, and eventually dispatched to the function that we spawned.
17.4. A complete and useless example
To better understand how workers can be used, here is a (fairly useless) example:
module SampleWithWorkers
import java.lang.Thread
import java.util.concurrent
import gololang.concurrent.workers.WorkerEnvironment
local function pusher = |queue, message| -> queue: offer(message) (3)
local function generator = |port, message| { (1)
foreach i in range(0, 100) {
port: send(message) (2)
}
}
function main = |args| {
let env = WorkerEnvironment.builder(): withFixedThreadPool()
let queue = ConcurrentLinkedQueue()
let pusherPort = env: spawn(^pusher: bindTo(queue))
let generatorPort = env: spawn(^generator: bindTo(pusherPort))
let finishPort = env: spawn(|any| -> env: shutdown()) (5)
foreach i in range(0, 10) {
generatorPort: send("[" + i + "]")
}
Thread.sleep(2000_L)
finishPort: send("Die!") (4)
env: awaitTermination(2000)
println(queue: reduce("", |acc, next| -> acc + " " + next))
}
In this example, we spawn 3 workers:
1 | the first repeats a message 100 times, |
2 | …forwarding them to another one, |
3 | …that ultimately pushes them to a concurrent queue. |
4 | A message is sent to a final worker, |
5 | …that shuts the workers environment down. |
As an aside, the example illustrates that worker functions may take further dependencies as
arguments. The pusher
function takes a queue target and generator
needs a port.
You can satisfy dependencies by pre-binding function arguments, all you need is to make sure that
each function passed to spawn
only expects a single message as its argument, as in:
-
^pusher: bindTo(queue)
, and -
^generator: bindTo(pusherPort)
, and -
env: spawn(|any| → env: shutdown())
where the worker function is defined as a closure, and implicitly captures itsenv
dependency from the surrounding context.
18. Golo template engine
Golo comes with a built-in template engine that is reminiscent of Java Server Pages or Ruby ERB. It compiles template text into Golo functions.
18.1. Example
Consider the following example.
let template = """
<%@params posts %>
<!DOCTYPE html>
<html>
<head>
<title>Golo Chat</title>
</head>
<body>
<form action="/" method="post">
<input type="text" name="msg">
<input type="submit" value="Send">
</form>
<div>
<h3>Last posts</h3>
<% foreach post in posts { %>
<div>
<%= post %>
</div>
<% } %>
</div>
</body>
</html>
"""
This multi-line string has a Golo template. It can be compiled into a function as follows:
let tpl = gololang.TemplateEngine(): compile(template)
println(tpl(someDataModel: posts()))
18.2. Directives
As you may have guess from the previous example:
-
Golo code snippets are placed in
<% %>
blocks, and -
expressions can output values using
<%= %>
, and -
<%@import foo.bar.Baz %>
causesfoo.bar.Baz
to be imported, and -
<%@params foo, bar, baz %>
causes the template function to have 3 parameters, i.e., it is a|foo, bar, baz| { … }
function.
When no <%@params … %>
exists, the function is assumed to have a single params
parameter.
The template engine is a simple one and makes no verification either on the templates
or the resulting Golo source code. The compile method may throw a GoloCompilation exception
though, and you can query the exception getSourceCode() and getProblems() methods to obtain
more details.
|
19. Documenting Golo code
Of course you can document your code using comments (#
), but who reads source code?
19.1. Documentation blocks
Golo provides a support for documentation blocks on modules, functions, augmentations, structs and unions, as well as fields.
Blocks are delimited by ----
and contain free-form Markdown
[5]
text.
Here is a quick example:
----
This is a *nice* module that does a bunch of useless things.
See more at [our website](http://www.typeunsafe.org).
----
module Hello
----
Adds 2 elements, which is quite surprising given the name.
* `x` is the first argument,
* `y` is the second argument.
The following snipped prints `3`:
let result = adder(1, 2)
println(result)
Impressive!
----
function adder = |x, y| -> x + y
Sections can be added in the documentation using the normal markdown syntax. The level of the titles is adapted to the generated documentation. For instance:
----
Encode a linked list as cons cells.
# Examples
A list is constructed from cons
let myList = Cons(1, Cons(2, Cons(3, Empty())))
----
union List = {
----
The empty list
----
Empty
----
A cell in the list
----
Cons = {
----
The head of the list
----
head,
----
The tail of the list
----
tail
}
}
The first sentence of a module documentation is displayed in module indexes.
19.2. Package documentation
Packages does not exists per se in Golo, but are represented in the form of module qualified names. However, it is common practice to group the modules of the same namespace into a common directory. There exists several ways to document such packages. The first is to create an empty golo module named after the package, containing the documentation. This file can have any name and be in any directory. For instance, given several modules pkg.MyModule
and pkg.OtherModule
, one can document the pkg
package by creating a module containing:
----
Here goes the package documentation
----
module pkg
No class will be compiled from this module, only a documentation page.
The other way to document the pkg
package, assuming that both modules are in src/pkg/MyModule.golo
and src/pkg/OtherModule.golo
respectively, is to create pure markdown file, containing the documentation for the package. This file must be named either src/pkg/README.md
, src/pkg/package.md
or src/pkg.md
. If a golo file corresponding to the package exists, these files are ignored.
If several candidates are found, only the first one is used, and a warning is printed. It can be disabled by setting the golo.warnings.doc.multiple-package-desc
system property to false
.
19.3. Rendering documentation
The golo doc
command can render documentation in html
(the default) or markdown
format:
$ golo doc --output target/documentation src/**/*.golo
In addition, golo doc
can also produce ctags tags
file, to be used by
editors such as Vim or emacs. In this mode, the special output target -
can
be used to print the tags on standard output, which is needed by some editors
or extensions.
Please consult golo --usage doc
for more details.
19.4. Alignment
It is sometimes necessary to indent documentation blocks to match the surrounding code format. Documentation blocks erase indentation based on the indentation level of the opening block:
----
The most useful augmentation *ever*.
----
augment java.lang.String {
----
Creates a URL from a string, as in: `let url = "http://foo.bar/plop": toURL()`.
----
function toURL = |this| -> java.net.URL(this)
}
When generating documentation from the code above, the documentation block of the toURL
function
is unindented of 2 spaces.
20. Misc. modules
Not everything fits into the main documentation. We encourage you to also look at the javadocs and golodocs.
The next subsections provide summaries of misc. modules found as part of Golo.
20.1. Standard augmentations (gololang.StandardAugmentations
)
This Golo module provides standard augmentations for various classes of the Java standard classes and Golo types. It does not have to be imported explicitely.
Here are a few examples.
Java collections can be have functional methods:
println(list[1, 2, 3, 4]: filter(|n| -> (n % 2) == 0))
println(list[1, 2, 3]: map(|n| -> n * 10))
Insert a map entry only if the key is not present, and get a default value if an entry is missing:
map: putIfAbsent(key, -> expensiveOperation())
map: getOrElse(key, "n/a")
Repeat an operation many times:
3: times(-> println("Hey!")
3: times(|i| -> println(i))
20.2. JSON support (gololang.JSON
)
Golo includes the JSON Simple library to provide JSON support.
While json-simple
only supports encoding from lists and maps, this API brings support for sets,
arrays, Golo tuples, dynamic objects and structs.
Given a simple data structure, we can obtain a JSON representation:
let data = map[
["name", "Somebody"],
["age", 69],
["friends", list[
"Mr Bean", "John B", "Larry"
]]
]
let asText = JSON.stringify(data)
Given some JSON as text, we can get back a data structure:
let data = JSON.parse(text)
println(data: get("name"))
The gololang.JSON
module also provides helpers for JSON serialization and deserialization
with both dynamic objects and structs.
20.3. Scala-like dynamic variable (gololang.DynamicVariable
)
Golo has a DynamicVariable
type that mimics the eponymous class from the Scala standard library.
A dynamic variable has inheritable thread-local semantics: updates to its value are confined to the current thread and its future child threads.
Given the following code:
let dyn = DynamicVariable("Foo")
println(dyn: value())
let t1 = Thread({
dyn: withValue(666, {
println(dyn: value())
})
})
let t2 = Thread({
dyn: withValue(69, {
println(dyn: value())
})
})
t1: start()
t2: start()
t1: join()
t2: join()
println(dyn: value())
one gets an output similar to:
Foo 69 666 Foo
with the 69
and 666
swapping order over runs.
20.4. Observable references (gololang.Observable
)
An observable value notifies observers of updates in a thread-safe manner. An observable can also be
constructed from another observable using the map
and filter
combinators:
let foo = Observable("Foo")
foo: onChange(|v| -> println("foo = " + v))
let mapped = foo: map(|v| -> v + "!")
mapped: onChange(|v| -> println("mapped = " + v))
foo: set("69")
This yields the following output:
foo = 69 mapped = 69!
20.5. Asynchronous programming helpers (gololang.Async
)
This module offers asynchronous programming helpers, especially execution context agnostic promises and futures. The provided APIs are orthogonal to the execution strategy: it is up to you to execute code from the same thread, from a separate thread, or by pushing new tasks to a service executor.
Here is an example:
module samples.Concurrency
import java.util.concurrent
import gololang.Async
local function fib = |n| {
if n <= 1 {
return n
} else {
return fib(n - 1) + fib(n - 2)
}
}
function main = |args| {
let executor = Executors.newFixedThreadPool(2)
let results = [30, 34, 35, 38, 39, 40, 41, 42]:
map(|n| -> executor: enqueue(-> fib(n)):
map(|res| -> [n, res]))
reduce(results, "", |acc, next| -> acc + next: get(0) + " -> " + next: get(1) + "\n"):
onSet(|s| -> println("Results:\n" + s)):
onFail(|e| -> e: printStackTrace())
executor: shutdown()
executor: awaitTermination(120_L, TimeUnit.SECONDS())
}
This example takes advantages of an executor augmentation and composable promises and futures to compute Fibonacci numbers.
20.6. Lazy lists (gololang.lazylist
)
This module defines a lazy list structure, as well as some utilities to work with it.
A lazy list behaves like an immutable linked list whose elements are evaluated only when needed, as can be found in Haskell for example.
The next element in the list (the tail) is represented by a closure. The generated value is cached so that the closure representing the next element is evaluated only once.
This is very useful when using higher order function such as map
. Mapping
a long lazy list with a function and using only the 3 first elements will only
apply the function to these elements, as opposed to regular lists.
Since the tail closure will be called at most once, and we can’t guarantee when, or even if, it will be called, this closure must be a pure, side-effect free, function.
Lazy lists can also be used to create infinite lists, also known as generators (or anamorphisms).
Lastly, they allow for elegant recursive implementations of several classical algorithms.
For instance, one can create a infinite lazy list containing integers as:
function count = |start| -> cons(start, -> count(start + 1_L))
function count = -> count(0_L)
To construct a infinite list of all even multiples of 3, one can then use:
let l = count(): map(|x| -> 3 * x): filter(|x| -> (x % 2) == 0)
The cons
function returns a new lazy list whose head is its first argument,
and the tail its second.
A lazy list implement the Collection
, interface, while remaining lazy. It is
thus possible to use it in imperative style with a foreach
construct, or
recursively using head
and tail
.
On the other hand, functions or methods like equals
, size
or contains
are
not very efficients, since they must evaluate the whole list, and thus negate
the laziness. They are here for completeness and compatibility with the regular
lists interface, but you should avoid such methods.
20.7. Console ANSI codes (gololang.AnsiCodes
)
The gololang.AnsiCodes
modules offers a set of functions to work with
ANSI codes.
The following would print Hello world!
in blinking yellow text:
import gololang.AnsiCodes
# (...)
function hello = {
blink()
fg_yellow()
println("Hello world!")
reset()
}
ANSI codes support varies from terminal and operating system capacities. Microsoft Windows
does not support these codes without a 3rd-party driver. You may use the likelySupported() function
to test if the host operating system is likely to support ANSI codes.
|
20.8. Functional errors (gololang.Errors
)
The gololang.Errors
module offers types, augmentations and decorators to deal with errors in a more functional way. This is similar for instance to Haskell
Maybe
and
Either
or Rust
Option
and
Result
.
20.8.1. Option
This is just an augmentation of the java java.util.Optional
class together with factory functions and decorators. The added methods allows to better chain optional results and improve matching.
This is particularly useful to chain and compose functions that can return no result. Instead of returning null
, a None()
value can be returned. Such functions can then be chained using the andThen
method. The option
decorator can be used to transform a regular function into a function returning an Option
.
For instance, given:
function foo = |arg| {
# can return an integer, null or even throw an exception
}
function bar = -> 42
function plus2 = |x| -> x + 2
function mult2 = |x| -> x * 2
code such as:
var r
try {
r = foo(x)
if r is null {
r = bar()
} else {
r = mult2(plus2(r))
}
} catch (e) {
r = bar()
}
can be rewritten:
let optionalFoo = option(foo)
r = optionalFoo(x): andThen(^plus2): either(^mult2, ^bar)
Of course, Optional
methods such as flatMap
and orElseGet
can also be used:
@option
function safeGetter = |aMap, aKey| -> aMap: get(aKey)
let m = map[["a", 2], ["answer", 12]]
# (19 + 2) * 2 = 42
safeGetter(m, "answer"): map(^plus2): map(^mult2): orElseGet(^bar)
# get null, option returns None, bar gives 42
safeGetter(m, "plop"): map(^plus2): map(^mult2): orElseGet(^bar)
# null pointer exception, option returns None, bar gives 42
safeGetter(null, "answer"): map(^plus2): map(^mult2): orElseGet(^bar)
See the gololang.Errors
module documentation for a full description of the added methods.
20.8.2. Result
Result
is a similar object, but it keep the errors, meaning that a result can be empty (similar to None
), containing an error (a Throwable
) or a result. It can be used in a way very similar to Optional
, but gives more control on the behavior.
The result
decorator is the equivalent to option
. The trying
higher order function takes an anonymous block, executes it and returns a corresponding Result
.
See the gololang.Errors
module documentation and the javadoc for the gololang.error.Result
class for more details.
As a bonus, Result
objects can be destructured into the error and the value. For instance:
@result
function foo = {
# can throw an exception, return null or a plain value
}
let e, v = foo()
let err, val = trying({
# statements sequence than can throw an exception
...
return somevalue
})
The error is the left value, the correct one the right (mnemonic: “right” also means “correct”). This allows to deal with error in the same way as Go does for instance.
The decorators nullify
and raising
are the duals of option
or result
21. Common pitfalls
Discovering a new programming language is fun. Yet, we all make mistakes in the beginning, as we idiomatically repeat habits from other languages.
Because Golo works closely with the Java programming language, it is likely that Java programmers will make some of the following mistakes early on.
21.1. new
Golo does not have a new
operator for allocating objects. Instead, one should just call a
constructor as a function:
# Good
let foo = java.util.LinkedList()
# Compilation fails
let foo = new java.util.LinkedList()
21.2. Imports
Golo does not have star imports like in Java. Imports are only used at runtime as Golo tries to resolve names of types, functions, and so on.
You must think of import
statements as a notational shortcut, nothing else. Golo tries to resolve
a name as-is, then tries to complete with every import until a match is found.
import java.util
import java.util.concurrent.AtomicInteger
# (...)
# Direct resolution at runtime
let foo = java.util.LinkedList()
# Resolution with the 1st import
let foo = LinkedList()
# Resolution with the 2nd import
let foo = AtomicInteger(666)
21.3. Method invocations
Keep in mind that instance methods are invoked using the :
operator, not with dots (.
) like in
many languages.
This is a common mistake!
# Calls toString() on foo
foo: toString()
# Looks for a function toString() in module foo
foo.toString()
21.4. match
is not a closure
One thing to keep in mind is that match
returns a value, and that it is not a closure unless you
want it to.
let foo = match {
case plop then 1
case ploped then 2
otherwise -1
}
# Ok
println(foo)
# Bad! foo is an integer!
println(foo("abc"))
21.5. Shebang scans sub-folders
When running shebang scripts, the sub-folders are being scanned for .jar
and .golo
files.
This is useful for automatically:
-
putting all third-party libraries into the classpath, and
-
dynamically compiling and loading other Golo source files.
Be aware that this may lead to surprising errors if you run code from, say, a clone of the Golo source code repository since it contains duplicated type and module definitions, as well as files from the test suite that have errors on purpose.