## Let expressions

Functions in pez are designed to be simple. Typically there aren’t side effects, nor should there be a lot of branching logic. Indeed, function bodies can only contain a single expression! So how can more complex functions be written? Enter the let expression, which supports creating temporary variables. The basic syntax is

let *statements*
in *expression*. For example, we can generate two random Poisson variables like this:

1 2 3 4 5 |
f = fn n: let x = rpois n 4 y = rpois n 4 in {x, y} |

We can also define a temporary function within the let expression instead.

1 2 3 4 |
f = fn n: let rp = fn: rpois n 4 in {rp(), rp()} |

Philosophically, the function syntax is designed in such a way that all code within a function can collapse into a single chain of function composition. This approach has a lot of benefits in terms of readability and inference.

## The arrow operator

In the CSV load example, we saw the -> operator used to cast data to a specific type. This approach can be used to create a random matrix: rnorm 200 -> matrix [20,10]. This expression generates a random variable and then passes this value to the matrix constructor, which takes an additional shape argument. This is equivalent to matrix(rnorm 200, [20,10]).

The arrow operator is not limited to type conversions. More generally, it is used as a form of function composition that goes right-to-left in application. This is opposite of the . operator. Also note that -> operates on the result of function application, whereas . operates on function. Consider two functions, pred and succ.

1 2 |
pred = fn x: x - 1 succ = fn x: x + 1 |

Using function composition, the identity can be defined id = succ . pred and called id 3. In contrast, the arrow operates on function calls, such as succ 3 -> pred.

## Partial application

Let’s define a function that calculates the area of a triangle based on its length and width:

1 |
!pez area = fn w h: w * h / 2 |

This function behaves as expected. Now suppose we want another function that fixes one of the arguments. We can partially apply the function, which returns another function that binds one of the arguments to the function.

1 2 3 |
!pez area_5w = area 5 !pez area_5w 4 10 |

Previously we discussed how pez does not support default arguments. Partial application is one of the reasons why default arguments are unnecessary.

### Partial application with named arguments

Named arguments can be used with partial application. To do this, you need to call the function using parentheses.

1 2 3 |
!pez area_10h = area(h=10) !pez area_10h 4 20 |

## Functions as Values

As we’ve seen variables can hold any value, including functions. This means that we can work with functions just like any other value.

### Function composition

As with mathematics functions can be easily composed using the . operator. This is convenient to quickly combine functions.

1 2 |
!pez h = eigen . cov !pez h (rnorm 200 -> matrix [20,10]) |

## Higher-Order Functions

Part of what makes functional programming powerful are higher-order functions. These are functions that either operate on functions or return functions. In mathematics, the derivative and integral are higher-order functions. Many higher-order functions in programming languages provide the machinery for iterating over objects. In languages like pez, where vectorization is built-in the need for this may seem slight, but many functions that work on complex data structures are not vectorized. Functions like map and fold enable vectorization of these types of functions.

### Map

1 2 3 4 |
!pez groups = sample 1..4 50 true get_age = fn n: abs(10 * (rnorm n)) -> round x = { group=groups, age=get_age(50) } map (fn g: mean x[x$group==g,'age']) (unique groups) |