Functions
Functions are declared with def
and have zero or more arguments in
parentheses (...)
. More formally these are called normal arguments (not to be
confused with implicit arguments), but in most cases, simply calling them
arguments is clear. Argument and return types of function declarations must be
specified; however, in a special case, omitting the return type is equivalent
to returning ()
.
def add_five(x: Int) -> Int {
return x + 5
}
Functions implicitly return the last evaluated expression.
def add_five(x: Int) -> Int {
x + 5
}
A function can be applied in a similar way to how it is written.
let eight = add_five(3)
assert_eq(eight, 8)
Recursive functions
Repetition can be achieved using a recursive function that calls itself,
the classic example is the factorial function, which accepts an integer
n
and computes the product of all positive integers up to and
including n
.
def factorial(n: Int) -> Int {
when {
n < 0 => panic("cannot compute factorial of a negative number")
n == 0 => 1
_ => n * factorial(n - 1)
}
}
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 optimized to use finite stack space. Tail
recursion usually requires adding another argument to track state.
def factorial_tail(n: Int, acc: Int) -> Int {
when {
n == 0 => acc
_ => factorial_tail(n - 1, n * acc)
}
}
def factorial(n: Int) -> Int {
when {
n < 0 => panic("cannot compute factorial of a negative number")
_ => factorial(n, 1)
}
}
Anonymous functions
Anonymous functions are created as needed and do not have names.
fn(x, y) { x + y } // With types.
fn(x: Int, y: Int) -> Int { x + y } // Without types.
Functions can be passed to other functions and used at a later time.
def apply(x: Int, f: fn(Int) -> Int) -> Int {
f(x)
}
let y = apply(5, fn(x) { 2 * x })
assert_eq(y, 10)
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, fn(x) { multiplier * x })
assert_eq(z, 50)
For simple functions of one argument, a terse form is available using $
. When
a $
appears with a unary operator, binary operator, or as an isolated argument
in a function call, that expression is turned into an anonymous function.
// This expression ... is translated into this.
-$ fn(x) { -x } // Unary operator.
$ + 3 fn(x) { x + 3 } // Binary operator.
2 ** $ fn(x) { 2 ** x } // Binary operator.
f(True, $, 5) fn(x) { f(True, x, 5) } // Function call.
// Be careful with arguments to function calls, as only the
// tightest expression is converted into an anonymous function.
//
// This expression ... is translated into this.
f(True, $ + 3, 5) f(True, fn(y) { y + 3 }, 5)
Passing a function as the last argument to another function is a common
pattern. In these cases the block syntax can be used, in which the anonymous
function is moved outside the function call, the fn
is removed, and the
argument list appears in |...|
inside of {...}
. The ||
is not used when
there are no arguments. The block syntax does not support argument or return
type annotations.
let y = apply(5) { |x| 2 * x }
assert_eq(y, 10)
// This is especially useful for longer function bodies.
let z = apply(5) { |x|
when {
x % 2 == 0 => 2 * x
_ => x + 1
}
}
assert_eq(z, 6)
fn call(f: fn()) {
f()
}
// No || when there are no arguments.
f() {
import std/io
io::println("Hello!")
}
Anonymous function blocks also accept implicits, although no using
list is
provided. The using
list will be inferred based on context.
import std/io
type Context(String)
fn show_context(using ctx: Context) {
io::println(ctx.0)
}
fn with_context(fn(Int)(using Context) -> String) -> String {
f(123)(using Context("Hello"))
}
with_context() { |x|
io::println(x) // Prints: 123
show_context() // Prints: Hello
}
In the above example, the block passed to with_context()
unifies to the type
of fn(Int)(using Context) -> String
, and the implicit argument of type
Context
is provided to the anonymous block.
For expressions
A for
expression is similar to the block syntax for anonymous functions,
however, it does not introduce a new pair of {
and }
. The for
expression
puts all of the expressions that follow it into an anonymous function block.
// This for expression
for f(x1, ..., xN)
body
// Becomes this function call.
f(x1, ..., xN) {
body
}
One or more arguments to the anonymous function block can be bound with patterns.
// This for expression
for a1, ..., aM <- f(x1, ..., xN)
body_that_uses_a1_to_am
// Becomes this function call.
f(x1, ..., xN) { |a1, ..., aM|
body_that_uses_a1_to_am
}
This is can eliminate visual nesting of anonymous function blocks when several are used.
import std/file
// Both of these functions open the file at `path1`, open the file
// at `path2`, then read the contents of the first file and write
// it to the second file, making sure to close the files after.
def copy_file_contents(path1: String, path2: String) {
file::with(path1) { |f1|
file::with_mode(path2, file::Write) { |f2|
let content = file::read(f1)
file::write(f2, content)
}
}
}
// The body of this function "desugars" to the function body above.
def copy_file_contents_with_for(path1: String, path2: String) {
for f1 <- file::with(path1)
for f2 <- file::with_mode(path2, file::Write)
let content = file::read(f1)
file::write(f2, content)
}
For expressions follow the same rules as anonymous blocks.
- The types of arguments are note specified.
- Implicit arguments are inferred but not listed.
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.
import std/iter
let squares = [1, 2, 3]
| iter::map($ ** 2)
| iter::collect()
assert_eq(squares, [1, 4, 9])
By default the |
passes the value from the left hand side into the first
argument of the function on the right. The &
name refers to the value on the
left, allowing the argument ordering to be changed.
let x = 5 | math::pow(2, &)
assert_eq(x, 32)
Using fn
has a special meaning when it appears after a |
. It is used to
pipe into a newly created function.
let x = 5 | fn(x) { 2 ** x }
assert_eq(x, 32)
The fn
can also accept an argument name and have types specified.
let x = 5 | fn(x: Int) -> Int { 2 ** x }
The rules for multiline anonymous functions apply to piped fn
s too.
let z = 5 | fn(x) {
when {
x < 0 => x - 3
_ => x + 3
}
}
assert_eq(z, 8)
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.
def id(x: a) -> a {
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.
x = id(123) // id unifies to fn(Int) -> Int
y = id("abc") // id unifies to fn(String) -> String
Multiple type arguments can be used at once.
import std/fmt
def apply(x: a, f: fn(a) -> b) -> b {
f(x)
}
assert_eq(apply(5, fmt::debug), "5")