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 \ IO =
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 \ IO =
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
Operators
Flix has a number of built-in unary and infix operators. In addition Flix supports infix function application by enclosing the function name in backticks. For example:
123 `sum` 456
is equivalent to the normal function call:
sum(123, 456)
In addition, a function named with an operator name (some combination of +
, -
, *
, <
, >
, =
, !
, &
, |
, ^
, and $
) can also be used infix. For example:
def <*>(x: Int32, y: Int32): Int32 = ???
can be used as follows:
1 <*> 2
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 \ {} = x + y
Note the annotation for Pure
is \ {}
.
A function that prints to the console is Impure
and must be marked with \ IO
:
def addAndPrint(x: Int32, y: Int32): Int32 \ IO =
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
andOrder
type classes require that their operations are pure. This ensures that collections, such as lists, sets, and maps, do not leak internal implementation details.