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:

function name_starts_with_letter ( name string ) -> yes_no
    return name.first.is_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}}""" )
    .
    Note
    All types in PPL are non-nullable by default. Hence, input arguments cannot be null, unless null 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 check clause is part of the function’s API. It is displayed in PPL’s API explorer.

Thus, a check clause is very different from an assert instruction in the function’s body. The check clause is part of 'Design By Contract' which is fully supported in PPL and helps to quickly find bugs at run-time. check clauses are implicitly inherited in child types and they are implicitly enforced by all implementations of a type. More detailed explanations and examples are provided in subsequent chapters.

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 and flag.

    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 character

    last: The last name’s initial character

    function 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 a check 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 argument first, and constant last holds the value of output argument last.

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.

results matching ""

    No results matching ""