function name_starts_with_letter ( name string ) -> yes_no return name.first.is_letter .
Functions
Introduction
A function is a block of reusable code that can be called from other places in the source code.
A function is composed of:
-
the keyword
function
-
an identifier (unique name)
-
an optional list of modifiers
-
an optional list of type parameters
-
an optional list of input arguments
-
an optional list of output arguments
-
a body (the list of instructions)
Here is a simple example of a function definition that takes a name
as input and returns a boolean
indicating whether name
starts with a letter:
A function is called by specifying its identifier and providing input argument assignments within parenthesis. Each assignment takes the form input_id = expression
The above function could be called as follows:
const starts_with_letter yes_no = name_starts_with_letter ( name = "Bob" ) assert starts_with_letter
Note
|
For reasons of practicability, functions in PPL are not required to be pure, as in some functional languages like Haskell. PPL functions can have side effects and they can access other data besides the provided input arguments. For example, a function can use input/output, such as writing to the OS’s standard output device. However, it is always a good idea to always favor pure functions, because they are easier to reason about, less error-prone and easier to test. |
Input Arguments
A function can have zero, one or more input arguments:
-
no input argument:
function greet write_line ( "Hello" ) .
Usage:
greet
Note that there is no need to add
()
after the function name.Output:
Hello
-
one input argument:
function greet_with_name ( name string ) write_line ( """Hello {{name}}""" ) .
Usage:
greet_with_name ( "Bob ")
Output:
Hello Bob
-
more than one input argument:
function greet_with_full_name ( first_name string, middle_name string or null, last_name string ) write_line ( """Hello {{first_name}} {{?middle_name}} {{last_name}}""" ) .
NoteAll types in PPL are non-nullable by default. Hence, input arguments cannot be null
, unlessnull
is explicitly allowed ( e.g.string or null
)Usage:
greet_with_full_name ( first_name = "Bob" middle_name = null last_name = "Spiridigliotzky" )
Output:
Hello Bob Spiridigliotzky
Default value
Every input argument can have a default value:
function greet_with_name ( name string default:"guest" ) write_line ( """Hello {{name}}""" ) .
Usage:
greet_with_name ( "Bob ") greet_with_name
Output:
Hello Bob Hello guest
The default value can be any expression whose type is compatible to the input argument’s type.
Input checks
Every input argument can have a check
clause. The check
clause is a yes_no
(boolean) expression that must evaluate to yes
every time the function is called. If this condition is violated at run-time then a program error is thrown immediately.
For example, to specify that input argument name
must start with a letter, we can use the check
clause as follows:
function greet_with_name ( name string check:name.first.is_letter ) write_line ( """Hello {{name}}""" ) .
Note
|
The Thus, a |
A function can also have an in_check
clause defined after the function’s signature. This clause can be used to check the integrity of the set of input arguments, or to enforce any another condition not related to the input arguments.
function extract_lines_in_file ( file file check:file.exists, from_line pos_64, to_line pos_64 ) -> list<string> or null in_check: to_line >= from_line // fake implementation return null .
Syntax sugar
PPL provides the following syntax simplifications which are used frequently in practice:
-
If an input argument’s identifier is equal to the input argument’s type then the type can be omitted.
Hence, instead of:
function foo ( string string ) -> yes_no
... we can simply write:
function foo ( string ) -> yes_no
-
If a function has only one input argument, then the caller can omit the input argument’s identifier.
Instead of:
const c = name_starts_with_letter ( name = "Bob" )
... we can write:
const c = name_starts_with_letter ( "Bob" )
-
The caller can also omit the input argument’s identifier if the expression assigned to the input argument is equal to the input argument’s identifier.
For example, suppose that function
foo
has three input arguments:first_name
,last_name
andflag
.Then, instead of:
const first_name = "Bob" const last_name = "Nice" const c = foo ( first_name = first_name, last_name = last_name, flag = yes )
... we can simplify the function call:
const c = foo ( first_name, last_name, flag = yes )
Identifier prefix
The optional identifier prefix for input arguments is i_
. Hence, if a function has input argument name
then the function’s body instructions can refer to it with name
or with i_name
. Explicitly specifying the prefix is sometimes useful to increase readability.
Output Arguments
A function can have zero, one or more output arguments:
-
no output argument:
function greet write_line ( "Hello" ) .
Usage:
greet
Output:
Hello
Note
|
Functions without output argument are always impure. They exist for their side effect(s), such as writing to an output device. |
-
one output argument
function string_contains_digit ( string ) -> yes_no repeat for each char in string if char.is_digit then return yes . . return no .
Usage:
const digit_found = string_contains_digit ( "ab123cd" ) assert digit_found
-
more than one output argument
Example:
The following function
get_initials
returns the initials of a name. The name is a string composed of a first name, a space, and a last name. The function has two output arguments:first
: The first name’s initial characterlast
: The last name’s initial characterfunction get_initials ( name string check:name.matches_regex ( regex.create ( '''\w+ \w+''') ) ) -> ( first character, last character ) const space_index = name.find_first ( " " ) assert space_index is not null return first = name.first, last = name.get ( space_index + 1 ) .
Note how the correct format of input
name
is ensured by acheck
clause using the regular expression'''\w+ \w+'''
(one or more letters, followed by a space, followed by one or more letters).The above function signature in a single line is rather long. To avoid this, there is another syntax supported. This alternative syntax uses one line per input/output argument and is better suited for complex function signatures. To increase readability the above function can therefore be rewritten as follows:
function get_initials in name string check:name.matches_regex ( regex.create ( '''\w+ \w+''' ) ) out first character out last character const space_index = name.find_first ( " " ) assert space_index is not null return first = name.first, last = name.get ( space_index + 1 ) .
Usage:
get_initials ( "Bob Glad" ) ( const first = first const last = last ) assert first =v 'B' assert last =v 'G'
Note the syntax used to retrieve the function’s two outputs. We declare and and assign two constants. Constant
first
holds the value of output argumentfirst
, and constantlast
holds the value of output argumentlast
.
Output checks
Every output argument can have a check
clause. The check clause is a yes_no
(boolean) expression that must evaluate to yes
every time the function returns. If this condition is violated at run-time then a program error is thrown immediately.
A function can also have an out_check
clause defined after the function’s signature. This clause can be used to check the integrity of the set of output arguments, or to enforce any another condition not related to the output arguments.
Note
|
As for input arguments, the check and out_check clauses are features of 'Design By Contract'. They are part of the function’s API, and the same rules apply.
|
Example:
The following function get_positions
returns the start
and end
positions of a substring that is surrounded by curly brackets ({
and }
). For example, if the function’s input were "12{45}78"
, then it would return the integer 4 for output start
, and 5 for output end
. (Note: indexes start with 1 in PPL.)
function get_positions in string check:string.matches_regex ( regex.create ( '''.*\{.+\}.*''' ) ) out start pos_32 check:start <= string.size - 2 out end pos_32 check:end <= string.size out_check: end >= start message: "'end' must be greater or equal to 'start'" const open_index = string.find_first ( "{" ) assert open_index is not null const close_index = string.find_last ( "}" ) assert close_index is not null return start = open_index + 1, end = close_index - 1 .
The check
clause for input argument string
shields the function from illegal calls, such as a string with an open brace, but no closing brace. (Note: The regular expression could be improved to ensure there is exactly one pair of braces).
The check
clauses for output arguments start
and end
guarantee that the function will never return values greater than the size of the input string
. Note that the condition 'value must be greater or equal to 1' isn’t necessary, because we use type pos_32
(a positive 32 bits integer, not including zero).
Finally, the out_check
clause ensures that end
will always be greater or equal to start
. We also use an optional message
clause to specify a customized error message displayed in case of a violation of this condition.
Usage example:
get_positions ( string = "12{45}78" ) ( const start = start const end = end ) assert start =v 4 assert end =v 5
Type Parameters
A function can have type parameters. Type parameters make a function more generic, while at the same time preserving type safety.
Type parameters are prefixed with a $
. They are declared between <
and >
, after the function name (e.g. function foo <$tp1, $tp2, $tp3>
.
Example: The following function checks whether a collection contains a given reference. $element
is a type parameter that represents the type of elements stored in the collection.
function contains_reference <element> ( collection collection<element>, item element ) -> yes_no repeat for each element in collection if element =r item then return yes . . return no .
The compiler ensures type compatibility of the input arguments. For example, if the function is called with input argument collection
being of type list<customer>
, then input argument item
must be of type customer
.
Here is a usage example with a string
list:
const bob = "Bob" const list = ["Alice", bob, "Tim"] const contains_bob = contains_reference<string> ( collection = list, item = bob ) assert contains_bob
Access Rights
All functions are public by default. Visibility can be changed with the access
modifier. Possible values are
-
public
: the function can be called from anywhere -
private
: the function can only be called from within the software component in which it is declared.
Note: Finer grained access values will be supported in future versions of PPL.
Here is an example of a function made private with the access:private
clause:
function replace_spaces access:private \ ( string string, replace_by string ) -> string return string.replace_all ( to_replace = " " replace_by = i_replace_by ) .
functions
Block
A set of functions can be embedded within a functions
block. The functions
block can specify modifiers which are are applied to all functions contained in the block. The function
keyword can be omitted for any function contained in the block.
Here is an example of three private functions declared within a functions
block:
functions access:private foo ( string ) -> string // body . bar ( string ) -> yes_no // body . zar ( name string, age pos_32 ) -> yes_no // body . .
Name Space
Each function belongs to a name-space. The name-space is a combination of:
-
the relative file path of the file in which the function is declared. (The path is relative to the project’s source code root directory.)
-
the function’s identifier
Example: File foo/bar/se_zar.ppl
(located in the project’s root directory) contains a function called tar
.
Then the fully qualified path of the function is
foo.bar.se_zar.tar
.
If the function is called within file se_zar.ppl
then it can be called by simply specifying it’s identifier (i.e. tar
in our case)
If the function is called from another file then it must be referenced by file_name_without_extension.identifier
(i.e. se_zar.tar
in our case).
Note: There is no need for package
or import/using
statements as in C# or Java.