.. _expression_functions:

# Expression language functions

RiskScape allows you to define custom functions using the built in :ref:`expressions`.
This can be helpful if you want to reuse or highlight a particular expression within your 
project.

Take the following bit of a pipeline as an example:

```
select({
    *, 
    if(
        loss < 0, 
        then: '-$' + abs(round(loss, 2))
        else: '$' + round(loss, 2)
    ) as formatted_loss
})
```

This bit of pipeline formats the numeric loss as a formatted string value showing
the dollar amount so that it looks nice in a report or in a spreadsheet.
This intent of this expression might not be obvious to a casual reader, and while commenting
can make that clearer, turning it in to an expression function can also be a good option.  It will
also allow the function to be reused across models within your project.

We can convert this to an expression function by adding this to your project:

```ini
[function format_dollars]
framework = expression
description = Convert a floating point value in to a formatted dollar amount string
argument-types = [floating]
return-type = text
source = """
(val) ->
  if(val < 0,
    then: '-$' + str(abs(round(val, 2))),
    else: '$' + str(round(val, 2)))
"""
```

With this in your project, we can test it on the command like so:

```
$ riskscape expression eval "format_dollars(-2.5555)"
-$2.56
```

Using this new function, our pipeline code now becomes a lot simpler, like this:
```
select({*, format_dollars(loss) as formatted_loss})
```

### `argument-types`

Expression functions can be defined with or without an `argument-types` parameter.
When given, these types are displayed in the function's reference and are used to validate
the use of the function within another expression.

If we tried to give our `format_dollars` from the earlier example some text instead of
a floating point number, it would fail with a type mismatch error.

It is also valid to leave out the `argument-types` parameter entirely and let the function's 
body accept whatever arguments it's given and decide whether it works or not.

### `return-type`

The `return-type` parameter is also optional for expression functions, but can be useful when 
you want to force a particular type to be returned from your function.

Be aware that if the value your expression returns can not be converted in to the 
desired `return-type`, then your model will fail at that point.

### Chaining complex expressions

In addition to using `if()` and `switch()` functions for control logic, you can also
use the `map()` function for cases where you might want to 'chain' multiple expressions
together.

For example, the following code is a customized spatial sampling function that always
returns _both_ the max and the average hazard intensity value. It does this by using `map()`
to take the result of one expression (i.e. the `sample_values()` result), and then apply _another_ expression
to that (taking the `mean()` and `max()`).

```ini
[function custom_sample]
framework = expression
source = '''
(exposure, coverage) ->
  map({
        # first sample all intersecting values
        values: sample_values(exposure, coverage)
      },
      # then take the list of sampled values and return *both* the mean and max
      sampled -> {
          max: max(sampled.values),
          mean: mean(sampled.values),
      })
'''
```

.. tip::
    For more examples of advanced operations you can perform with structs,
    refer to :ref:`manipulating_pipeline_data`.

### Nullable arguments

If `argument-types` is not defined, expression functions implicitly define each argument to
be of type `nullable(anything)`.  This means the function will be called even if null values
are given.

.. tip::
    For more information about how RiskScape handles nulls when calling functions, see
    :ref:`functions_and_null`

As an example, comment out or remove the `argument-types` line from the `format_dollars` example from
earlier.  If you run `riskscape function info format_dollars` you will see that the `val` argument is
now defined as `nullable(anything)`, instead of the previous `floating` type.

Evaluate the function with a `null` argument and see what happens:

```
$ riskscape expression eval "format_dollars(null_of('floating'))"
$
```

The function is called, even though the `val` argument is `null`, because RiskScape has
respected the `nullable(anything)` type of the argument.  The function's expression is
evaluated normally, allowing each function within the expression to handle the null
individually.  The `if` and the `str` function both handle nullable types and so are
called, whereas `abs` and `round` are not.  The `str` function then returns an empty
string and our `format_dollars` function happily appends that to the dollar sign,
resulting in the `$` text that was printed out.

### Expression functions and constants

Some RiskScape functions require arguments to be constant - that is, their values
can not change between calls to the function (as different rows of data are fed to it
through a pipeline).

```
# names must be constant as it affects the type that is returned
list_to_columns(losses_by_return_period, columns: {names: ['10y', '100y', '1000y']})
```

Expression functions may need to call these functions and allow
those required-constant arguments to be supplied as arguments themselves, but this
can fail as the expression function call can not determine whether the argument is
or is not constant.

```
[function format_columns]
framework = expression
source = '''
  (losses_by_return_period, return_periods) ->
    # return periods is no longer constant - format_columns could be constant or
    # could change with each row of data
    list_to_columns(losses_by_return_period, columns: {names: return_periods})
'''
```

The solution to this is that users of the function must wrap those arguments with a
call to the `const` function.  This remembers the constant value as part of realizing
the expression and works around this limitation.

```
# fails - even though the return_periods argument is constant, the expression
# function can not see it.
format_columns(losses, ['10y', '100y', '1000y'])

# succeeds - the const function "bakes" the constant-ness in to the expression
# and works around the limitation with expression functions.
format_columns(losses, const(['10y', '100y', '1000y']))

```
