Skip to main content

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.

info

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]
tip

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!

tip

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)
tip

Since @ is right associative, expressions like f (g (h x)) can be written as (f @ g @ h) x.

Implicit arguments

info

Since implicits are a large topic, they are covered in their own section; refer to the implicits section for more details.