Functions
Declaring functions
As Fuyu is a functional language, functions are extremely common. So common, in fact, that there is not a keyword to declare them. A function declaration is simply a function interface, followed by the definition clauses.
add_many: Int -> Int -> Int -> Int // Interface.
add_many x y z = x + y + z // Definition clause.
Argument and return types of function declarations must be specified; however,
in a special case, omitting the return type is equivalent to returning ()
.
Functions can either explicitly return with the return
keyword or implicitly
return the last evaluated expression.
import std/assert
// Explicit return.
add_five: Int -> Int
add_five x = return x + 5
// Implicit return.
multiply_ten: Int -> Int
multiply_ten x = 10 * x
main: () -> ()
main () = do
let eight = add_five 3
assert.eq eight 8
Clauses support patterns and guards, and must be exhaustive over those patterns.
surround: Bool -> String -> String
surround True s = "(((" + s + ")))"
surround False s = s
When multiple clauses are specified for a function, they are processed top to bottom, and the first match is evaluated. This is identical to the behavior of match expressions.
Named function declarations are only allowed at the module level. Anonymous functions are allowed in expression position.
Currying
All functions are curried, meaning that a function that accepts more than one argument is actually a function that accepts a single argument and returns a new function that accepts the rest, which in turn returns a new function, and so on. This also means that functions can be partially applied, where not all arguments are given.
add: Int -> Int -> Int
add x y = x + y
A new function can be defined in terms of the add
function using currying,
where not all arguments are given.
add_five: Int -> Int
add_five = add 5
Recursion
Since everything is immutable and Fuyu has no loops, repetition is achieved
using a recursive function that calls itself. Consider the factorial function,
which accepts an integer n
and computes the product of all positive integers
up to and including n
.
factorial: Int -> Int
factorial n if n == 0 = 1
factorial n = n * factorial (n - 1)
factorial n if n < 0 = panic "Cannot compute factorial of a negative number."
This form is not tail recursive, which means that the factorial
function will
require an amount of memory proportional to n
. Tail recursive functions are
preferred as they will use finite stack space. Tail recursion usually requires
adding another argument to track state, often called an accumulator.
#[public]
factorial: Int -> Int
factorial n if n < 0 = panic "Cannot compute factorial of a negative number."
factorial n = factorial_tail n 1
// This is usually not public. Instead, the `factorial` function
// with the nicer interface is exposed.
factorial_tail: Int -> Int -> Int
factorial_tail n acc if n == 0 = acc
factorial_tail n acc = factorial_tail (n - 1) (n * acc)
Higher-order functions
It is often useful to pass a function another function. For example, the map
function in std/stream
accepts a stream and a function that is applied to
every item of the stream to produce a new stream.
import std/assert
import std/stream
square: Int -> Int
square x = x ** 2
main: () -> ()
main () = do
let it = stream.map square [1, 2, 3]
let squares = stream.collect it // Collect the stream into a list.
assert.eq squares [1, 4, 9]
This is not idiomatic Fuyu. A better version would be:
let squares = [1, 2, 3]
| stream.map \x -> x ** 2
| stream.collect
This uses anonymous functions and pipes.
Anonymous functions
Anonymous functions are created as needed and do not have names. Use \
to
create an anonymous function. Types are not specified.
// Body
// -----
let f = \x y -> x + y
// ---
// Arguments
Functions can be passed to other functions and used at a later time.
import std/assert
apply: Int -> (Int -> Int) -> Int
apply x f = f x
main: () -> ()
main () = do
let y = apply 5 \x -> 2 * x
assert.eq y 10
Implicit arguments can be accepted by wrapping the argument
with &[...]
.
\&[z] x -> f x z
Closures
Anonymous functions are closures and can use any values in scope at the point where they are defined.
let multiplier = 10
let z = apply 5 \x -> multiplier * x
assert.eq z 50
Pipes
Pipes are a special syntax that enables an easy way to write computation chains. Pipes make the flow of data match the order in which it is read by the programmer.
let squares = [1, 2, 3]
| stream.map \x -> x ** 2
| stream.collect
assert.eq squares [1, 4, 9]
The pipe, |
, is a binary operator corresponding to a function of type
a -> (a -> b) -> b
, where the left side of |
is the first argument and the
right side of |
is the second. This means that anything with a type that
unifies to a -> b
can be used on the right side!
The function on the right-hand side of a pipe can be qualified with try
to
treat it as a try expression.
import std/math
import std/stream
/// Squares the first odd number in a list.
square_first_odd: List Int -> Option Int
square_first_odd numbers = numbers
| try stream.find \x -> x % 2 == 1
| math.square
| Some
Parameterized functions
Functions may be parameterized to accept values of more than a single
type. For example, the id
function accepts a value and returns it. The
id
function is provided by the language, but it can also be simply
implemented in a generic way.
id: a -> a
id x = x
Concrete types, such as Int
and String
, begin with uppercase letters,
while type parameters, such as a
and b
, begin with lowercase letters.
Type parameters indicate that any type can be used in that spot. When the
id
function is used, its type will be unified to the specific type that
the context requires.
let x = id 123 // `id` unifies to `Int -> Int`.
let y = id "abc" // `id` unifies to `String -> String`.
Multiple type arguments can be used at once.
import std/assert
apply: a -> (a -> b) -> b
apply x f = f x
main: () -> ()
main () = do
// `apply` unifies to `Int -> (Int -> String) -> String`.
let z = apply 5 \x -> when
x == 5 -> "five"
else -> "a different number"
assert.eq z "five"
Composition
Functions can be composed with the @
operator, which is syntactic sugar for
std/ops.compose
. If there are two functions, f
and g
, the f (g x)
is
equivalent to (f @ g) x
.
import std/assert
plus_one: Int -> Int
plus_one x = x + 1
double: Int -> Int
double x = 2 * x
main: () -> ()
main () = do
let f = plus_one @ double
let g = double @ plus_one
let x = 10
assert.eq (f x) ((2 * x) + 1)
assert.eq (g x) ((x + 1) * 2)
Since @
is right associative, expressions like f (g (h x))
can be written
as (f @ g @ h) x
.
Implicit arguments
Since implicits are a large topic, they are covered in their own section; refer to the implicits section for more details.