A function, or method, or action, is a named block of code that can be invoked.
A function declaration starts with the keyword def
. This declaration can only occur
in top-level features, such as agent
.
The standard function declaration follows the following syntax:
def NAME [([PARAMETER, PARAMETER, PARAMETER...])] [: RETURN TYPE] [BLOCK]
Note The parameters are implicitly declared with the keyword val
so are read-only.
The following code gives examples of function declarations:
// No parameter.
// Return type: void
def action1 {
}
// No parameter.
// Return type: int
def action2 : int {
return 0
}
// Parameter 1, named 'a', of type int.
// Return type: void
def action3(a : int) {
}
// Parameter 1, named 'a', of type int.
// Parameter 2, named 'b', of type String.
// Return type: void
def action4(a : int, b : String) {
}
// Parameter 1, named 'a', of type int.
// Return type: double
def action5(a : int) : double {
return 0
}
// Parameter 1, named 'a', of type int.
// Parameter 2, named 'b', of type String.
// Return type: String
def action6(a : int, b : String) : String {
}
The section “Exception Support” shows how to write an exception handler in the code. Sometimes, it is appropriate for code to catch exceptions that can occur within it. In other cases, however, it is better to let a method further down the call stack handle the exception.
If a function doesn’t catch the checked exceptions that can occur within it, the function may specify that it can throw these exceptions.
Note This specification is optional since the SARL compiler determines the exceptions that are not catched, and assumes that they are implicitly thrown outside the function.
The declaration of the thrown exceptions is done with the
throws
keyword, followed by a list of thrown exception types.
This declaration must be put between the list of formal parameters and the function’s code.
In the following example, the function myaction
is defined with no formal
parameters and no return type.
This function indicates to its caller that it could throw an exception of
type IllegalStateException
.
def myaction throws IllegalStateException {
}
Generic functions are methods that introduce their own type parameters. This is similar to declaring a generic type, but the type parameter’s scope is limited to the function where it is declared. Static and non-static generic functions are allowed.
You can write a single generic method declaration that can be called with arguments of different types. Based on the types of the arguments passed to the generic method, the compiler handles each method call appropriately. Following are the rules to define generic functions:
with
or the bracket syntax.A generic method’s body is declared like that of any other method.
Note Type parameters can represent only reference types, not primitive types (like int
, double
and char
).
Two syntaxes are allowed for defining the type parameters of the actions: the with
syntax, and the bracket syntax.
The with
syntax for a generic function includes a type parameter, after the with
keyword, between the function’s return type and the function’s body.
In the following example, the function specifies a type T
, which is used both
as type for the element parameter and the generic type of the Collection.
def addAndReturn(element : T, collection : Collection<T>) : T with T {
collection.add(element)
return element
}
The bracket syntax for a generic function includes a type parameter, inside angle brackets, and appears before the function’s name.
In the following example, the function specifies a type T
, which is used both
as type for the element parameter and the generic type of the Collection.
def <T> addAndReturn(element : T, collection : Collection<T>) : T {
collection.add(element)
return element
}
There may be times when you’ll want to restrict the kinds of types that are allowed to be passed to a type parameter. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for.
To declare a bounded type parameter, list the type parameter’s name, followed by the
extends
keyword, followed by a class name. This keyword indicates that T
must be a subtype of the following type.
def print(value : T) with T extends Number {
System.out.println("Type = " + value.getClass)
System.out.println("Value = " + value)
}
A variadic function is a function of indefinite arity: one which accepts a variable number of arguments.
SARL allows you to define the last parameter of a function as variadic with the operator *
.
This operator has an informal meaning similar to the cardinality in
UML: zero to many.
In the code of the variadic function, the variadic parameter is assumed to be an array of objects of the parameter type.
In other languages, such as Java and C++, the variadic operator is ...
.
In the following example, two variadic functions are defined:
// Function with indefinite number of integers as parameters
def action1(v : int*) {
for (value : v) {
info(value)
}
}
// Function which takes a boolean, a double and an indefinite
// number of integers as parameters
def action2(a : boolean, b : double, c : int*) {
info(a)
info(b)
for (value : c) {
info(value)
}
}
Examples of calls to the two previous variadic functions are:
action1()
action1(1)
action1(1, 3)
action2(true, 3.0)
action2(true, 3.0, 1)
action2(true, 3.0, 1, 5)
SARL allows you to specify a default value for a formal parameter.
When a default value is specified, it means that the caller of the action can skip passing a value for the corresponding argument. And, when the function is called, the default value is given to the skipped argument.
Important Note In SARL, if a formal parameter has a default value, the following formal parameters do not need to have default values as well. This is a major difference with the default values in other languages, such as C++.
// Function with one parameter with a default value.
def action1(v : int = 5) {
info("v == " + v)
}
// Function which takes a boolean, a double and an integer as parameters.
// The first and third parameters have default values.
def action2(a : boolean=true, b : double, c : int=7) {
info("a == " + a)
info("b == " + b)
info("c == " + c)
}
Examples of calls to the two previous functions are:
// v == 1
action1(1)
// v == 5
action1()
// a == true, b == 3.0, c == 1
action2(true, 3.0, 1)
// a == false, b == 4.0, c == 7
action2(false, 4.0)
// a == true, b == 7.0, c == 56
action2(7.0, 56)
// a == true, b == 9.0, c == 7
action2(9.0)
It is possible to mix the variadic parameter and the default values, except that the variadic parameter cannot have a default value.
def action(v : int = 5, a : float*) { }
Generally, method resolution and binding is done statically at compile time. Method calls are bound based on the static types of arguments.
Sometimes this is not what you want. Especially in the context of extension methods you would like to have polymorphic behavior.
The dispatch
modifier permits defining a dispatch method.
dispatch def getType(x : Integer) {
"it's an int"
}
dispatch def getType(x : String) {
"it's a string"
}
dispatch def getType(x : Number) {
"it's a number"
}
def clientCode {
getType(4.5).println
getType(4).println
getType("a string").println
}
For a set of visible dispatch methods in the current type hierarchy with the same name and the same number of arguments, the compiler infers a synthetic dispatcher method. From the example above, the SARL compiler infers the following function, named the synthesized dispatcher.
def printType(x : Object) {
if (x instanceof Integer) {
printType(x as Integer)
} else if (x instanceof Number) {
printType(x as Number)
} else if (x instanceof String) {
printType(x as String)
}
}
This dispatcher uses the common super type of all declared arguments. Client code always binds to the synthesized dispatcher method.
In the example, the calls to the getType
functions produces the output:
it's a number
it's an int
it's a string
A pure function is a function that has the following properties:
Most of the time, a pure function is a computational analogue of a mathematical function. Some authors, particularly from the imperative language community, use the term “pure” for all functions that just have the above property.
In SARL, a pure function is a function that has no side-effect on the state of the invoked object, the static state, and I/O targets. The following examples of SARL functions are pure:
def floor(value : double) : double {
val fvalue = value as int
return fvalue
}
def max(a : double, b : double) : double {
if (a >= b) a
else b
}
def f : void {
var x = 0
x++
}
The following examples of SARL functions are impure:
class MyClass {
var ifield : int
static var sfield : int
def incrementField : void {
this.ifield ++
}
def incrementGlobalField : void {
sfield ++
}
static def incrementGlobalField2 : void {
sfield ++
}
}
I/O is inherently impure: input operations undermine referential transparency, and output operations create side effects. Nevertheless, there is a sense in which function can perform input or output and still be pure, if the sequence of operations on the relevant I/O devices is modeled explicitly as both an argument and a result, and I/O operations are taken to fail when the input sequence does not describe the operations actually taken since the program began execution.
The second point ensures that the only sequence usable as an argument must change with each I/O action; the first allows different calls to an I/O-performing function to return different results on account of the sequence arguments having changed.
SARL compiler tries to figure out if the functions have side-effect. It does some clever analysis of the name of the method (e.g., getters) and also tries to check if the body has any method that is impure, i.e., that may have side-effects.
SARL compiler considers the following cases for determining if a function must be tagged as pure or not:
name starts with get
, has
or is
name equals to length
equals
, hashCode
, compare
or compareTo
.name equals to size
name starts with contains
, optionally followed by characters according to the
camel-case standard
iterator
clone
to
following the camel-case standardabs
, acos
, asin
, atan
, atan2
, cbrt
, ceil
, cos
, cosh
, exp
, floor
,
hypot
, log
, log10
, log1p
, max
, min
, pow
, random
, rint
, round
, scalb
, signum
, sin
,
sinh
, sqrt
, tan
, tanh
, ulp
.If none of the cases above is matching the current definition of a function, then the function is assumed to be impure.
In the case the SARL compiler is not able to automatically determine the purity of a function, but you are sure that the function is pure, it is possible to mark the function as pure manually.
@Pure
def myFunction() : int {
// Do something complex
return 0
}
This documentation is inspired by the documentations from the Xtext and Xtend projects.
Copyright © 2014-2024 SARL.io, the Original Authors and Main Authors.
Documentation text and medias are licensed under the Creative Common CC-BY-SA-4.0; you may not use this file except in compliance with CC-BY-SA-4.0. You may obtain a copy of CC-BY-4.0.
Examples of SARL code are licensed under the Apache License, Version 2.0; you may not use this file except in compliance with the Apache License. You may obtain a copy of the Apache License.
You are free to reproduce the content of this page on copyleft websites such as Wikipedia.
Generated with the translator docs.generator 0.14.0.