PPL Source Code Examples (Quick Overview)

Christian Neumanns

2016-10-07


Table of Contents
Introduction
Null safety
Contract Programming (Design by Contract (TM))
Integrated Unit Testing
Intermezzo
Error Handling

Introduction

This document contains a few selected and simple examples of PPL source code that demonstrate some specific features.

The goal is to get a quick overview of what PPL code looks like, without digging into details, and without covering everything.

[Note]Note
Whenever you need information about basic PPL syntax rules please refer to the first chapters of the Quick reference manual.

Null safety

Null safety ensures that no null pointer error occurs at run-time. This eliminates the most common bug in non-null-safe languages.

Null safety in PPL works as follows:

  • All object references are non-nullable by default.

    variable s1 = "foo" // the type of s is inferred to be 'string'
                        // to make this explicit we could also write:
                        // variable s1 string = "foo"
    
    // not allowed (compile-time error)
    // s1 = null
    
    // ok
    s1 = "bar"
    
    // if 'null' is allowed then it must be explicitly specified
    variable s2 null or string = "foo" // the type of s2 is 'null or string'
    
    // ok
    s2 = null
    
    // ok
    s2 = "bar"
    
    [Note]Note
    When you look at a PPL API you will always know whether an object attribute or an input/output argument can be null or not. For example, if the type of an return value is string, it means it will never be null. On the other hand, if the type is null or string then we know we have to check for null (and the compiler will require this).

  • A nullable object reference must be checked for null before its features can be accessed.

    Suppose the return type of function get_string_or_null is null or string. Then the function can be used as follows:

    const s = get_string_or_null // the inferred type of s is 'null or string'
    
    // The following instruction is illegal and produces a compile-time error,
    // because 's' could be null
    // const size = s.size
    
    // We have to chek for null
    if s is not null then
       const size = s.size
       OS.out.write_line ( """Number of characters: {{size}}""")
    else
       OS.out.write_line ( "The value is null")
    .
    
    [Note]Note

    The need for null checks are also applied when arithmetic, comparison, or boolean infix operators are used. This means that expressions like the following ones are invalid if the left or right operand is nullable and has not yet been proved to be non-null at the moment of execution:

    a + b
    a < b
    a or b

  • Null checks are not required if the compiler can prove that a variable doesn't contain null at run-time. To achieve this, the compiler uses static code analysis and remembers states in nested ifs, loops, and case instructions. This reduces null checks to a minimum. Here is a simple example:

    variable s null or string = null
    
    // following instruction is invalid because s is null
    // const size = s.size
    
    s = "foo"
    
    // following instruction is valid because s is not null
    // (no need for a null check, although 's' is of type 'null or string')
    const size = s.size
    assert size =v 3
    
    // invalid because s is proved to be not null:
    // if s is null then
    //    ...
    // .
    
    

  • The .null? operator is handy in chained calls with possible nulls.

    Suppose that:

    • type customer has an attribute addresses that can be null

    • addresses has an attribute delivery that can be null

    Instead of nested ifs like this ...

    var street null or string = null
    const addresses = customer.addresses
    if addresses is not null then
        const delivery = addresses.delivery
        if delivery is not null then
            street = delivery.street
        .
    .
    
    if street is not null then
       // ...
    else
       // ...
    .
    

    ...you can simply write:

    const street = customer.addresses.null?.delivery.null?.street
    
    if street is not null then
       // ...
    else
       // ...
    .
    

  • The if_null: operator can be used to provide a default value.

    // if get_string_or_null returns null then "foo" will be assigned to s
    // the inferred type of s is 'string'
    const s = get_string_or_null if_null:"foo"
    
    assert s is not null
    
    // ok
    const size = s.size
    

    if_null: operators can be chained, and the last one can have the value error to denote that a program error should be thrown if the final result is null:

    const exchange_rate = get_rate_from_webservice \
        if_null: get_rate_from_database \
        if_null: get_rate_from_local_cache \
        if_null: error
    

    The .null? and if_null: operators can be combined:

    const street = customer.addresses.null?.delivery.null?.street if_null: "unknown"
    assert street is not null
    

Contract Programming (Design by Contract (TM))

Contract Programming allows you to easily add data validation checks in the source code. As soon as data doesn't pass a validation check at runtime, a program error is thrown.

This is an effective technique that helps to detect some bugs quickly.

There are four kinds of checks supported in PPL:


  • Input argument checks (preconditions)

    Consider the following function that returns a list of digits found in a string range:

    function get_digits_in_string_range
        in string string
        in from_pos positive_32
        in to_pos positive_32
    
        out result null or list<character>
    
        return string.substring ( from = from_pos, to = to_pos )
            .to_stream
            .filter ( { return object.is_digit } )
            .to_list_or_null
    .
    

    Here is an example of how to use the function:

    const result = get_digits_in_string_range (
        string = "abc456def"
        from_pos = 4
        to_pos = 7 )
    
    assert result is not null
    assert result.to_long_string =v "[4, 5, 6]"
    
    [Note]Note
    • Indexes in PPL start with 1

    • The function declaration could also be written in one line as:

      function get_digits_in_string_range ( string string, from_pos positive_32, to_pos positive_32 ) -> null or list<character>
      
    • The body of the function could also be written in the classic style with a loop, like this:

      const r = mutable_list<character>.create
      repeat for each char in i_string.substring ( from = from_pos, to = to_pos )
          if char.is_digit then
              r.append ( char )
          .
      .
      result = r.make_immutable_or_null
      

    The above function is shielded against calling it with any input set to null, because (as we have seen previously) null is not allowed by default.

    However there is no protection against invalid values for input arguments from_pos and to_pos. Both values must be less than the size of the input string, and from_pos must be smaller or equal to to_pos. Suppose also that we want to limit the input string to a maximum of 100 characters.

    Contract Programming allows you to do this easily. You can change the function declaration as follows:

    function get_digits_in_string_range
        in string string check:string.size <= 100
        in from_pos positive_32
        in to_pos positive_32
        in_check
            check to_pos <= string.size
            check from_pos <= to_pos
        .
    
        out result null or list<character>
    
    [Note]Note
    A check from_pos <= string.size is not needed because the two existing checks imply this condition.

    Now a runtime error occurs immediately whenever an input condition is not fulfilled.


    For each check you can optionally specify a customized error message that will be displayed when the condition is violated. Example:

    check from_pos <= to_pos error_message: """The value of input argument from_pos ({{from_pos}}) must be less than or equal than to_pos ({{to_pos}})"""

    It is import to note that the input conditions are part of the function signature and its API. They are not part of the implementation, but part of the interface. Therefore they are also displayed in PPL's API browser.


  • Output argument checks (postconditions)

    Output argument checks are the counterpart to input argument checks. While input argument checks are conditions that must be fulfilled by the caller of a function, output argument checks must be fulfilled by the function itself (i.e. by its implementation code)

    For example, to state that function get_digits_in_string_range cannot return a list with more elements than in the string's range we can add the following output check:

    out result null or list<character>
        check
            if result is not null then
                check result.size <= ( to_pos - from_pos ) + 1
            .
        .
    .
    

  • Attribute checks (class invariants)

    Consider the following type:

    type product
        att id string
        att description string
    .
    
    [Note]Note
    Type attributes in PPL are non-nullable and immutable by default.

    Suppose it is required that:

    • the product's id attribute must start with an uppercase letter, followed by 7 digits

    • the description is limited to 200 characters

    You can specify these conditions as follows:

    type product_2
        att id          string check: id.matches_regex ( regex.create ( '''[A-Z]\d{7}''' ) )
        att description string check: description.size <= 200
    .
    

    Besides the benefit of these conditions being part of the type's API, there are two more advantages:

    • Contract Programming conditions are implicitly inherited in child types.

      [Note]Note
      As we will see later, inherited conditions can optionally be made stronger or weaker in child types, as long as type compatibility is still guaranteed.
    • Contract Programming conditions are implicitly enforced in all factories.


  • Script checks (asserts)

    Script checks use the assert keyword and work like assertions in some other languages. They are used to check that a given condition is true at runtime, and they can appear anywhere in any script.

    Here is an example:

    const s = "123".pad_left ( 10 )
    assert s.size =v 10
    

Integrated Unit Testing

Unit testing is an integrated feature of PPL (no setup or configuration required).

Suppose you want to write a test for function get_digits_in_string_range which we defined earlier. The whole code (implementation and unit tests) could look like this:

function get_digits_in_string_range
    in string string check:string.size <= 100
    in from_pos positive_32
    in to_pos positive_32
    in_check
        check to_pos <= string.size
        check from_pos <= to_pos
    .

    out result null or list<character>

    script
        return i_string.substring ( from = from_pos, to = to_pos )
            .to_stream
            .filter ( { return object.is_digit } )
            .to_list_or_null
    .
    tests
        // first test invocation
        test  ( string = "abc456def", from_pos = 4, to_pos = 7 )
        // verify result
        verify result is not null
        verify result.size =v 3
        verify result.to_long_string =v "[4, 5, 6]"

        // second test
        test  ( string = "123def", from_pos = 4, to_pos = 6 )
        verify result is null

        // third test (invalid input)
        test  ( string = "123def", from_pos = 4, to_pos = 7 )
        verify_error // a program error must occur because 'to_pos = 7' is invalid

        // more tests ...
   .

.

Intermezzo

So far, we have seen three important pillars of writing more reliable code in PPL. Let us recapitulate:

  • Null safety eliminates null pointer errors. They are detected at compile-time and cannot occur at run-time. In our example, the following conditions are implicitly ensured (without the need to write code):

    • function get_digits_in_string_range will never be called with any input argument set to null. The function's implementation code doesn't need to check for null

    • callers of the function cannot forget to check if the function returns null

    Nullability is part of the API and is displayed in PPL's API browser.

  • Contract programming detects invalid data at run-time and rejects them immediately. In our example, a run-time error occurs immediately if:

    • function get_digits_in_string_range is called with invalid data, such as from_pos = 7 and to_pos = 6

    • the function returns a list that is too large in the given context

    Input/output and attribute conditions are part of the API and are displayed in PPL's API browser.

  • Unit testing tests the correctness of the function's implementation (to some extent, depending on the quality and coverage of the test cases).

Error Handling

Unlike many popular programming languages, PPL doesn't use an exception mechanisms for error handling.

Instead PPL uses union types (also called disjoint union types or sum types).

[Note]Note
The reason for this deliberate design choice is out of the scope of this article.

Handling a single error

Suppose we we want to create function get_URL_text_content that takes a URL string as input and returns a string which is the text content of the URL.

If this function never failed, its signature would be:

function get_URL_text_content ( URL string ) -> string

However, all IO operations can fail. Therefore the function might also return an error object. It will return a string in case of success, and an error in case of failure. This can easily be expressed with a union type.

The function signature changes from ...

function get_URL_text_content ( URL string ) -> string

... to:

function try_get_URL_text_content ( URL string ) -> string or error
[Note]Note
By convention, PPL functions that might fail are prefixed with try_.

The function's return type string or error is a union type. It simply states that the result returned by the function will be either an object of type string or an object of type error.

The code to call the above function looks like this:

case type of try_get_URL_text_content ( "http://www.foo.org" )
    when string s
        OS.out.write_line ( """The content of the URL is: {{s}}""" )
    when error e
        OS.out.write_line ( """The following error occurred: {{e.description}}""" )
.

As you can see, the actual type returned by the function is checked with a case type of instruction, and the appropriate action is executed in one of the two when branches.

The compiler always checks that all member types in a union type (in our case: member type string and member type error) are covered in the when branches of a case type of instruction. It is therefore not possible to accidentally ignore errors returned by functions. This is a fundamental and important principle in PPL.

[Note]Note

If there is a good reason to explicitly ignore an error, you can do it like this:

case type of try_get_URL_text_content ( "http://www.foo.org" )
    when string s
        OS.out.write_line ( s )
    when error
        // do nothing
.

What if the URL content can be empty and we consider that this is not an error? An immutable string in PPL can't be empty (for reasons out of the scope of this article, but explained here). Instead, emptiness in idiomatic PPL code is represented with null. Hence the function signature becomes:

function try_get_URL_text_content ( URL string ) -> null or string or error

And the calling code becomes:

case type of try_get_URL_text_content ( "http://www.foo.org" )
    when string s
        OS.out.write_line ( """The content of the URL is: {{s}}""" )
    when null
        OS.out.write_line ( """The content of the URL is empty""" )
    when error e
        OS.out.write_line ( """The following error occurred: {{e.description}}""" )
.

Handling multiple errors

Let us now look at the implementation of try_get_URL_text_content.

If the function could never fail the code would be:

function get_URL_text_content ( URL string ) -> null or string
    return URL.create ( i_URL )
        .open_connection
        .get_content_as_string
.
[Note]Note
There is no need to close the URL connection, because this is done already in get_content_as_string.

In practice, each one of the three operations used in the above code can fail:

  • URL.create: the input string might be an invalid (malformed) URL

  • open_connection: the URL might not be available

  • get_content_as_string: there might be an error retrieving the content

As said already, functions that might fail in PPL:

  • are prefixed wit try_

  • return a union type with one member being an error type (or a child type or error).

Therefore the three functions called in the above code are defined as follows in the standard PPL library:

  • service URL_creator:

    function try_create ( URL string ) -> URL or invalid_URL_error
  • type URL:

    function get_connection -> URL_connection or URL_connection_error
  • type URL_connection:

    function get_content_as_string -> string or URL_connection_error

Hence, we could implement our function as follows:

function try_get_URL_text_content ( URL_string string ) -> null or string or error

    case type of URL_creator.try_create ( URL_string )
        when URL URL
            case type of URL.try_open_connection
                when URL_connection connection
                    return connection.try_get_content_as_string
                when URL_error e
                    return e
            .
        when invalid_URL_error e
            return e
    .
.

Needless to say, multi-level-indented-boiler-plate code like this one is not the code we want to write. Therefore, PPL provides an .error? operator (which is the counterpart to the .null? operator we saw already here). The implementation becomes a one-liner:

function try_get_URL_text_content ( URL_string string ) -> null or string or error

    return URL_creator.try_create ( URL_string )
        .error?
        .try_open_connection
        .error?
        .try_get_content_as_string
.

As we can see, the .error? operator must be inserted after each function call that might result in an error. Hence the code shows where an error can appear, which improves readability. As soon as a function in the chain returns an error, code execution will be cancelled and the error object is the result of the expression.


The above code uses function chaining (f1.f2.f3). However, function chaining can only be used if the components we use in the code support function chaining.

Therefore, PPL provides another, more general way to write compact error handling code.

To be continued ...