Functions

Functions and higher-order functions are the key building block of a functional programming language.

In Flix, top-level functions are defined with the def keyword. For example:

def add(x: Int32, y: Int32): Int32 = x + y + 1

A definition consists of the function name followed by an argument list, the return type, and the function body. Although Flix supports type inference, top-level function definitions must declare the type of their arguments and their return type.

In Flix, all function arguments and local variables must be used. If a function argument is not used it must be prefixed with an underscore to explicitly mark it as unused.

First-Class and Higher-Order Functions

A higher-order function is a function that takes a parameter which is itself a function. For example:

def twice(f: Int32 -> Int32, x: Int32): Int32 = f(f(x))

Here the twice function takes two arguments, a function f and an integer x, and applies f to x two times.

We can pass a lambda expression to the twice function:

twice(x -> x + 1, 42)

which evaluates to 44 since 42 is incremented twice.

We can also define a higher-order function that requires a function which takes two arguments:

def twice(f: (Int32, Int32) -> Int32, x: Int32): Int32 =
    f(f(x, x), f(x, x))

which can be called as follows:

twice((x, y) -> x + y, 42)

We can call a higher-order function with a top-level function as follows:

def inc(x: Int32): Int32 = x + 1

def twice(f: Int32 -> Int32, x: Int32): Int32 = f(f(x))

twice(inc, 42)

Function Type Syntax

Depending on the number of arguments to a function, the syntax for the function type differs:

Unit -> Int32                // For nullary functions
Int32 -> Int32               // For unary functions
(Int32, Int32, ...) -> Int32 // For the rest

Function Composition

Flix supports several operators for function composition and pipelining:

let f = x -> x + 1;
let g = x -> x * 2;
let h = f >> g;     // equivalent to x -> g(f(x))

Here >> is forward function composition.

We can also write function applications using the pipeline operator:

List.range(1, 100) |>
List.filter(x -> x mod 2 == 0) |>
List.map(x -> x * x) |>
println;

Here x |> f is equivalent to the function application f(x).

Curried by Default

Functions are curried by default. A curried function can be called with fewer arguments than it declares returning a new function that takes the remainder of the arguments. For example:

def sum(x: Int32, y: Int32): Int32 = x + y

def main(): Unit & Impure =
    let inc = sum(1);
    inc(42) |> println

Here the sum function takes two arguments, x and y, but it is only called with one argument inside main. This call returns a new function which is similar to sum, except that in this function x is always bound to 1. Hence when inc is called with 42 it returns 43.

Currying is useful in many programming patterns. For example, consider the List.map function. This function takes two arguments, a function of type a -> b and a list of type List[a], and returns a List[b] obtained by applying the function to every element of the list. Now, if we combine currying with the pipeline operator |> we are able to write:

def main(): Unit & Impure =
    List.range(1, 100) |>
    List.map(x -> x + 1) |>
    println

Here the call to List.map passes the function x -> x + 1 which returns a new function that expects a list argument. This list argument is then supplied by the pipeline operator |> which, in this case, expects a list and a function that takes a list.

Pipelines

Flix supports the pipeline operator |> which is simply a prefix version of function application (i.e. the argument appears before the function).

The pipeline operator can often be used to make functional code more readable. For example:

let l = 1 :: 2 :: 3 :: Nil;
l |>
List.map(x -> x * 2) |>
List.filter(x -> x < 4) |>
List.count(x -> x > 1)

Here is another example:

"Hello World" |> String.toUpperCase |> println

Pure, Impure, and Effect Polymorphic Functions

In Flix every function is pure, impure, or effect polymorphic.

The Flix type and effect system ensures that a pure function always returns the same result when given the same arguments and that it cannot have (observable) side effects.

In Flix every function definition is implicitly marked as Pure. For example, the function definition:

def add(x: Int32, y: Int32): Int32 = x + y

is actually equivalent to:

def add(x: Int32, y: Int32): Int32 & Pure = x + y

A function that prints to the console is Impure and must be marked as such:

def addAndPrint(x: Int32, y: Int32): Int32 & Impure =
    let r = x + y;
    println(r);
    r

since the type signature of the library function println specifies that it is Impure.

The purity (or impurity) of a higher-order function may depend on the purity of its argument(s). For example, whether List.map is pure or impure depends on whether function we map is pure or impure. Fortunately Flix can model such behavior using effect polymorphism. For example:

def map(f: a -> b & ef, l: List[a]): List[b] & ef = ???

Here the signature of map captures that if the function argument f has type a -> b with effect ef then the effect of map itself is ef. This means that if map is called with a pure (resp. impure) function argument then the overall expression is pure (resp. impure). For example:

List.map(x -> x + 123, l)    // pure
List.map(x -> println(x), l) // impure

Design Note

The Flix standard library enforces several program invariants using purity. For example, in Flix, the Eq and Order type classes require that their operations are pure. This ensures that collections, such as lists, sets, and maps, do not leak internal implementation details.