A tour of Fuyu
Fuyu is a statically typed, concurrent, functional programming language for building reliable concurrent software. Running on the BEAM, it combines immutable data, scalable concurrency, type inference, pattern matching, higher-order functions, and traits in a concise declarative syntax.
This tour covers all core language features and is designed to be accessible to programmers of any level. Readers having some experience writing functions in another language will be able to follow along. Some concepts involve deeper technical detail, but most of this guide remains straightforward.
Functions
Since Fuyu is a functional programming language, functions are a fundamental building block.
Main function
The main function is the entry point to the program.
It is evaluated when the program runs.
use std/io
def main: Nil -> Nil = io.print_line "Hello!"
Defining a function
A function is defined using the def keyword.
It must have at lease one argument and returns one type.
// Argument Return Type
// |------| |-|
def plus_one: (x: Int) -> Int = x + 1
// | | |---|
// Argument name | Function body
// |
// Argument type
Functions can have multiple arguments separated by ->.
def multiply: (a: Int) -> (b: Int) -> Int = a * b
Calling a function
A function is called by listing its arguments after its name.
plus_one 3 // Returns: 4
multiply 2 5 // Returns: 10
Recursion
Recursive functions call themselves. Fuyu does not have loops, so repetition must be achieved using recursion.
/// Computes the Fibonacci sequence: 0 1 1 2 3 5 8 13 21 34 55 ...
def fib: (n: Int) -> Int = match n with
0 -> 0
1 -> 1
n -> fib (n - 1) + fib (n - 2)
Currying
All functions are curried, which means that not all arguments need to be supplied up front. If not all arguments are given, then instead of evaluating the function, a new function is created that accepts the remaining arguments.
def multiply: (n: Int) -> (m: Int) -> Int = n * m
def example: Int =
let double: Int -> Int = multiply 2 // Partially apply `multiply`.
double 10 // 20
Anonymous functions
Functions can be created on the fly as needed using \arguments -> body.
// This function accepts another function!
// Doing this is a great way to compose behaviour.
//
// Function
// |--------|
def call_with_ten: (f: Int -> Int) -> Int = f 10
// This constant gets the value of `15`.
def example = call_with_ten (\x -> x + 5)
Multiple arguments can be used.
// The equation of a line: y = m * x + b.
\m x b = m * x + b
Polymorphic functions
Functions can be made polymorphic using type variables. This allows functions to be reused on different types.
// In the type of this function, the name `a` is a type variable.
// This function accepts three values that are all the same type (arguments x, y, and z)
// and returns a list of those three values.
def three: (x: a) -> (y: a) -> (z: a) -> List a = [x, y, z]
// `three` can be used with any type!
def ints: List Int = three 4 17 39 // [4, 17, 39]
def strings: List String = three "apple" "banana" "cherry" // ["apple", "banana", "cherry"]
Pipes
Pipes offer an easy way to write chained computations.
The pipe, |,
takes a value on the left side and inserts it as the argument to a function on the right side.
This works because functions are curried.
[1, 2, 3, 4, 5, 6]
| stream.filter int.odd
| stream.map \x -> x ** 2
| stream.collect
// [1, 9, 25]
Constants
Constants are evaluated at compile time. Functions, operators, and custom types can be used in constants.
def values: List Int = [1, 2, 3, 1 + 2 * 6 - 9]
Comments
All comments start with // and extend to the end of the line.
use std/io
// This function is called when the program is started.
def main: Nil -> Nil =
io.print_line "Hello" // Comments can appear after code.
Documentation comments begin with /// and are used to document the following declaration.
/// Compute the factorial of a given number.
def factorial: (n: Int) -> Int = match n with
0 -> 1
m -> m * factorial (m - 1)
Fundamental data types
Fundamental data types define the basic kinds of values the language can represent, manipulate, and combine in expressions.
Nil
Nil is a unary type that represents the absence of any other value.
The type is Nil and ut can only have the value Nil.
Booleans
Booleans are represented by the Bool type,
which can take on the values True and False.
Integers
Integers are represented by the Int type, which is an arbitrary precision signed integer
(commonly referred to as a bignum).
By default, integers are specified in decimal (base 10).
Case-sensitive prefixes of 0b for binary (base 2),
0o for octal (base 8), and 0x for hexadecimal (base 16) may be used.
Underscores (_) may appear anywhere in the literal, including before, after,
and in between in any digits or prefixes (e.g., 0b and 0_b are both valid prefixes).
Multiple underscores may appear consecutively.
0 50 1_000_000 // Decimal
0b10101010 0b1111_0110 // Binary
0o12345670 0o123_005_774 // Octal
0x1234567890abcdefABCDEF 0xfe_23_06 // Hexadecimal
// Excessive underscores.
_1__2___3____4_____
_0_b_1_0_1_0_
Floats
A Float is similar to an IEEE 754 floating-point number with at least double precision (i.e., 64 bits).
A Float literal must include either a . or e/E (or both) to distinguish it from an integer literal.
Like integers, underscores (_) can appear anywhere in the float literal.
0.0 23.45 1_057.1 3.141_593 // Regular notation.
1E9 2.5e-4 2_712.349_753e+10 // Scientific notation.
// Excessive underscores.
___6___.__7_8__9__e__-__4__5__
Fuyu runs on the BEAM (i.e., Erlang virtual machine).
The BEAM does not have IEEE 754 conformant floats,
as it cannot represent NaN or infinity.
Fuyu has a FloatIEEE type which can,
hoever,
it is not a primitive type.
Strings
A String represents textual data encoded in UTF-8.
String literals use double quotes ("...") and may contain Unicode characters.
Escapes are used to insert values that may not be easy or possible to type:
\n: Newline (U+000A).\r: Carriage return (U+000D).\t: Tab (U+0009).\\: Backslash (\).\": Double quote (").\u{X...}: Unicode escape where eachXis a hexadecimal digit. There must be at least 1 digit. The hexadecimal number is parsed and converted into the corresponding codepoint.
// These strings are equal.
let a = "🍉 is a watermelon"
let b = "\u{1F349} is a watermelon"
When a string spans multiple lines, all lines are joined by a space.
Leading whitespace on continuing lines, trailing whitespace on non-terminal lines,
and empty lines are ignored.
Escapes such as \n and \u{...} are preserved.
// These strings are equal.
let a = "apples
bananas cherries"
let b = "apples bananas cherries"
Block strings use three double quotes to delimit the start and end.
The following whitespaces are stripped:
leading whitespace before the first non-whitespace line of text,
leading whitespace common to the start of each line,
trailing whitespace on each line,
trailing whitespace after the last non-whitespace line of text,
and carriage returns (U+000D) are stripped.
Carriage returns included by an escape, such as \r, are respected.
// These strings are equal.
let a = """
text that
is spread
across
several lines
"""
let b = " text that\n is spread\nacross\n several lines"
// -.-. -.-.-.-. -.-.
Tuples
A tuple is an ordered collection of values of arbitrary types.
Tuples are created with (...) and may have zero or more elements.
The type of a tuple is simply the tuple of the types of each element.
let a: () = () // 0-tuple.
let b: (Int,) = (3.14,) // 1-tuple, trailing comma is required.
let c: (Bool, String, List Int) = (True, "text", [5, 6, 7]) // 3-tuple.
Elements of a tuple can be directly accessed with .,
such as c.0 to access the Boolean and c.1 to access the string.
Lists
A List is an ordered collection of values belonging to the same type,
implemented as a singly linked list.
Lists are created with [...] and may have any length (including zero).
let a: List Bool = [] // Empty.
let b: List Int = [1, 2, 3]
let c: List String = ["one", "two", "three"]
A list can also be created from a different list using a spread. There can be at most one spread, which must appear as the last entry in the list.
let xs = [3, 4]
let ys = [1, 2, ..xs] // [1, 2, 3, 4].
Records
A record is an unordered collection of named fields.
This describes a record with fields name of type String and age of type Int.
let person: { name: String, age: Int } = { name = "Sana", age = 33 }
Names in scope can be referenced with the shorthand.
let name = "Sana"
let age = 33
let person = { name, age } // Shorthand for { name = name, age = age }.
Use ... to copy the fields of an existing record and override selected fields.
def birthday (person: { name: String, age: Int }) -> { name: String, age: Int } =
{ age = age + 1, person... } // Keep all other fields from person (e.g., name).
Access a field with . infix syntax.
person.name // "Sana"
person.age // 33
Expressions
Expressions are the fundamental building blocks of the language, producing values through literals, operators, function calls, and other composable forms.
Let expressions
A let expression binds names to an irrefutable pattern.
let message = "Hello"
let (x, y, z) = (1, 2, 3)
If then else
If expressions evaluate condition and follow one arm when true and the other arm when false.
if x > 0 then "positive" else "non-negative"
Match
Match expressions compare a value against a series of patterns and produce the result of the first matching branch. The patterns must be exhaustive so that at least one pattern is guaranteed to match any scrutinee.
match status with
"backlog" -> "Not started"
"pending" -> "Still processing"
"complete" -> "Finished"
_ -> "Unknown status"
Guards can be added using if that add finer granularity.
The compiler loses the ability to check for exhaustion when guards are used,
so there must be a catch all pattern (even if it is never evaluated).
match number with
n if n < 0 -> "negative"
0 -> "zero"
n if n > 0 /\ n < 10 -> "small positive"
_ -> "large positive"
Patterns
A pattern simultaneously describes value and shape. Patterns can include:
- Names, such as
xorsecret_key, which always match. Int,Float, andStringvalues, which must match exactly.- Tuples, lists, and records.
- Value constructors (which includes
NilandBool).
The pattern _ matches anything, and it is idiomatic to mean that the value is not used.
Destructuring
Destructuring unpacks a composite value into its parts.
Tuples are destructured by position.
let (name, message) = ("Viviette", "Hello!")
Lists are destructured by position and the tail is captured with ..,
which can appear at most once in a list pattern and must be at the end of the list.
let [a, b, ..c] = [1, 2, 3, 4, 5] // (a, b, c) == (1, 2, [3, 4, 5])
let [x, y, ..z] = [1, 2] // (x, y, z) == (1, 2, [])
let [p, ..] = [1, 2, 3, 4, 5] // p == 1
Records are destructured by name,
and the remaining fields can be captured with ...
let { x, ... } = { x = 1, y = 2, z = 3 } // x == 1
match { name = "Cheyenne", age = 44 } with
{ name, age = 44 } -> name // (name, age) = ("Cheyenne", 44)
{ .. } -> "Person is not 44"
Value constructors are destructured by position.
type Groups a where
One a
Two a a
Three a a a
def first: (group: Group a) -> a = match group with
One x -> x
Two x _ -> x
Three x _ _ -> x
A pattern is said to be irrefutable if it always matches.
For example, matching on (), x, or (a, b) will always succeed,
so these are irrefutable patterns.
If the pattern can fail (e.g., matching on an Option), then it is refutable.
When matching with a match expression or function arguments, the pattern must be exhaustive,
meaning that every possible value of the match argument
must match with at least one of the patterns in the clauses.
Operators and precedence
The following table shows all operator and expression types sorted by decreasing precedence (i.e., expressions at the top of the table have higher precedence and are evaluated first).
| Operator/Expression | Description | Associativity |
|---|---|---|
()[]{} | Grouping, tuples Lists Records | — |
a.bf x | Access Function application | Left-to-right |
a ** b | Exponentiation | Right-to-left |
-a..a | Negation Spread | Right-to-left |
a * ba / ba % b | Multiplication Division Modulo | Left-to-right |
a + ba - b | Addition Subtraction | Left-to-right |
a == ba /= ba < ba <= ba >= ba > b | Equality Inequality Lesser Lesser or equal Greater or equal Greater | Left-to-right |
a /\ b | Logical and | Left-to-right |
a \/ b | Logical or | Left-to-right |
\x y -> x + y | Anonymous function | — |
a | f | Pipe | Left-to-right |
, | Sequence separator | Left-to-right |
Left-to-right associativity, such as +, means that a + b + c is equivalent to (a + b) + c.
Right-to-left associativity, such as **, means that a ** b ** c is equivalent to a ** (b ** c).
The - operator can be both binary (e.g., f - 10) and unary prefix (e.g., -10).
How is it interpreted?
How does the compiler know that f - 10 is "f minus 10" and not "the function f applied to -10"?
The rule is that if - can be interpreted as a binary operator,
then it is,
otherwise,
it is unary.
Alternatively,
the only place that - can be unary is at the start of an expression.
f - 10is binary.f (-10)is unary.
Types
Types define the shapes that a data can take.
Declaring a type
A type consists of a type name and one or more value constructors.
// Name
type Size where
Small // First value constructor.
Medium // Second value constructor.
Large // Third value constructor.
def default_size: Size = Medium
Value constructors
Value constructors may have positional data associated with them.
type Order where
Tea Size String
Coffee Size String
def black_coffee: (size: Size) -> Order = Coffee size "black"
Type variables
Type variables allow a type to be used in a generic way with different kinds of data. Generic type variables begin with a lowercase letter, while concrete types begin with an uppercase letter.
type Either a b where
This a
That b
def swap: (either: Either a b) -> Either b a = match either with
This x => That x
That x => This x
Recursive types
Types may store values that are of their own type.
For example, the Nodes of a Tree store the left subtree, the node value, and the right subtree,
while each Leaf simply stores a terminal value and ends recursion.
type Tree a where
Node (Tree a) a (Tree a)
Leaf a
// 4
// / \
// Create this tree: 2 5
// / \
// 1 3
def tree: Tree Int = Node (Node (Leaf 1) 2 (Leaf 3)) 4 (Leaf 5)
Types of functions
Consider the following function.
It has the type Int -> Int -> Int -> Int.
def three_sum: (x: Int) -> (y: Int) -> (z: Int) -> Int = x + y + z
Type annotations
Type annotations describe the type of a function or value.
// Type
// |-----------------------|
def f: (x: Int) (y: Int) -> Int = x + y
Local bindings can also be annotated.
The special type name _ means that any type can appear there.
let a: Int = 3
let (b, c): (Int, Int) = (100, 200)
let (d, e): (_, Int) = (300, 400) // Inference for d, specify type for e.
Type aliases
A type alias is just another name for a type. It does not create a new type, but offers a way to rename types, often for usability. The aliased type name can be used interchangeably with the underlying type as the alias is only a synonym.
alias Point = { x: Float, y: Float }
alias Pair a = (a, a)
// These two functions have the exact same type.
def point_to_pair1: (point: Point) -> Pair Float = (point.x, point.y)
def point_to_pair2: (point: { x: Float, y: Float }) -> (Float, Float) = (point.x, point.y)
Traits
Traits provide polymorphic behaviour by describing what values of a type can do. Implementations of traits are wired through a program using givens.
Implenting a trait
The standard library provides the Eq trait,
which allows one to use the == and != operators with a user defined type.
The Eq trait can be implemented for Rectangle,
which defines the eq function.
type Rectangle where
Rectangle Int Int // Width and height.
given Eq Rectangle where
def eq: (Rectangle w1 h1: Rectangle) -> (Rectangle w2 h2) -> Bool =
// If the rectangle is rotated it is still the same shape.
(w1, h1) == (w2, h2) \/ (h1, w1) == (w2, h2)
def example: Bool = Rectangle 3 7 == Rectangle 7 3 // True
Using a trait
A trait is simply a given, which means the compiler can automatically insert it in a function.
/// Test if a value is present in a list.
def contains: [Eq a] -> (value: a) -> (list: List a) -> Bool =
match list with
[x, ..rest] if x == value -> True
[_, ..rest] -> contains value rest
[] -> False
// The compiler automatically inserts a given for Eq Int.
def example: Bool = contains 2 [1, 2, 3] // True.
Declaring a trait
A trait is declared by listing functions to be defined by the trait. The trait must define at lease one function, and there is no limit on how many can be defined.
trait Draw a where
def draw: (drawable: a) -> Nil
trait MusicPlayer a where
def play: (mp: a) -> Nil
def pause: (mp: a) -> Nil
def set_volume: (volume: Float) -> (mp: a) -> Nil
Constraints on traits
The standard library defines the Eq trait for many types,
such as Int, Float, and String.
But what about composite types, such as a 2-tuple or a 3-tuple?
This is done using constraints.
The following implementation of Eq for a 3-tuple
is constrained by the the existence of Eq for the types contained in the tuple,
and can be used for any tuple for which the constraints are satisfied!
// Constraints
// |------------------------|
given [Eq a] -> [Eq b] -> [Eq c] -> Eq (a, b, c) where
def eq: (left: (a, b, c)) -> (right: (a, b, c)) -> Bool =
let (x1, y1, z1) = left
let (x2, y2, z2) = right
x1 == x2 /\ y1 == y2 /\ z1 == z2
Givens
Givens were already explored for traits, which are their most common use case. In general, a given is a value that the compiler uses to automatically feed through a program.
Creating a given
A given is created using the given keyword,
which can be used in both the module and function scopes.
There are two forms:
-
Expression givens take on the value resulting from the evaluation of an expression. This form is delimited by
=.type AngleUnit whereRadianDegree// Without a name.given AngleUnit = Radian// With a name.given angle_unit: AngleUnit = Radian -
Structural givens take on the value of a record in a value constructor of the outer given type. This form is delimited by
where.type Rectangle whereRectangle Int Int // Width and height.// Without a name.given Eq Rectangle wheredef eq: (Rectangle w1 h1: Rectangle) -> (Rectangle w2 h2) -> Bool =(w1, h1) == (w2, h2) \/ (h1, w1) == (w2, h2)// With a name.given eq_rectangle: Eq Rectangle wheredef eq: (Rectangle w1 h1: Rectangle) -> (Rectangle w2 h2) -> Bool =(w1, h1) == (w2, h2) \/ (h1, w1) == (w2, h2)
Accepting a given as an argument
A function that uses a given marks it as such in its argument list using square brackets.
use std/math
def cosine: [unit: AngleUnit] -> (theta: Float) -> Float = match theta with
Radian -> math.cos theta
Degree -> math.cos (math.radian_to_degree theta)
Calling a function with givens
When calling a function that uses givens, the compiler will automatically insert one based on the values in scope.
given AngleUnit = Degree
// This uses Degree in cosine.
// Takes the value 0.5.
def example_degree = cosine 60
// This uses Radian in cosine.
// Takes the value -0.5.
def example_radian =
given AngleUnit = Radian
cosine (2 * math.pi / 3)
Explicitly passing givens
A given can be explicitly passed by prefixing with &.
cosine &Degree 45
Given constraints
Constraints are a method of dependency injection. This is most practically used for constraints on traits, which gives an example for structural givens, however, it can be used on non-trait givens or expression givens. Constraints are always filled by the compiler at the last possible moment.
type A where A Int
type B where B Int
type C where C Int
type ABC where ABC Int
given A = 10
given B = 3
given C = 5
given [a: A] -> [b: B] -> [c: C] -> ABC = a + b + c // 18.
Resolution of givens
The compiler tries to find a given in the closest possible scope. If only one given with a compatible type is found in the closest given populated scope, then that value is used. It is an error if two or more givens are found in the closest given populated scope or if the search makes its way up to the module level scope and no givens are found.
There are several places that a given is searched for:
- Values in a scope.
- Fields of a record in a value constructor of a trait.
Modules and packages
Directory as a module
A module in Fuyu is defined by a directory.
All *.fuyu files that reside in the same directory belong to that module,
and the declarations made in any of those files are shared across the entire module.
- Module naming:
The base module takes the name of the package to which it belongs (e.g.,
my_package) and the submodules are named by concatenating the package name with the subdirectory name (e.g.,my_package/util/math_helpers). - Shared scope:
Every declaration in any
*.fuyufile within the directory is visible to all other files in the same directory. - Visibility: All declarations are private to their module by default. Private items are freely usable within that module, while only public symbols can be accessed by other modules in the same package.
- Test files:
Files matching
*.test.fuyuare treated as test sources, and are excluded from non-test builds.
The purpose of these rules is to:
- Eliminate the need to define the package structure.
- Make the package structure match the file system.
Packages
Packages are part of ongoing design work.
Use declarations
A use pulls in the module, giving it a namespace to access its contents.
Use declarations are only allowed at module level
and are scoped to the file, not the entire module.
// - Creates `vehicle` namespace.
use model/vehicle
Use a for clause to pull in specific items.
// - Creates `vehicle` namespace.
// - Uses trait `Vehicle`.
// - Uses type `Car`.
// - Uses constructors `Sedan`, `Truck`, and `Van`.
// - Uses function `drive`.
use model/vehicle for trait Vehicle, type Car, Sedan, Truck, Van, drive
Use as to rename.
// - Creates `automobile` namespace (`vehicle` namespace is not created).
// - Uses trait `Vehicle` as `Craft` (`Vehicle` is not defined).
// - Uses type `Car` as `Automobile` (`Car` is not defined).
// - Uses constructor `Van` as `PeopleCarrier` (`Van` is not defined).
// - Uses function `drive` as `go` (`drive` is not defined).
// - All public declarations in `model/vehicle` are available through the
// `automobile` namespace under their original names (e.g., `automobile.Van`
// but not `automobile.PeopleCarrier`).
use model/vehicle as automobile for
trait Vehicle as Craft,
type Car as Automobile,
Van as PeopleCarrier,
drive as go
The namesapce can be hidden if it is really not needed.
// Does not create any namespaces.
// - Uses type `Car`.
use model/vehicle as _ for type Car
Acyclic dependencies
When a module uses another module, it creates a dependency relationship. Connecting all of these relationships for a program yields a dependency graph. The dependency graph must be acyclic, meaning that no module (or package) may ultimately depend on itself, either directly or indirectly. In other words, there must be no cycles where two or more modules/packages depend on each other.
Because packages are collections of modules, the same rule applies at the package level. Both modules and packages must form acyclic dependency graphs.
Module search paths
Depending on the path given to use a module, different locations are searched. There are three flavors of uses:
-
Package path uses.
use a/b/c // Search for `a/b/c` in the package path.This is how the standard library and external modules are used.
-
Relative path uses.
use ./a/b/c // Search for `a/b/c` relative to the current module.use ../a/b/c // Search for `a/b/c` relative to the parent module.use ../../a/b/c // Search for `a/b/c` relative to the grandparent module.This is one way that modules within the same package are used.
-
Absolute path uses.
use /a/b/c // Search for `a/b/c` relative to the package root.This is one way that modules within the same package are used.
If a use path starts with an identifier (e.g., std),
then it is using a module outside of the current package.
If a use path starts with punctuation (e.g., /, ./, or ../),
then it is using a module inside of the current package.
Visibility
The pub keyword describes whether a declaration is visible when a module is used by another.
The bare keyword augments public types
by stating that the internal structure of the type is also public.
This is all or none:
either the type is bare and value constructors are visbible outside the module,
or the type is not bare and the value constructors are not visible outside the module.
pub type Ab where // Visible.
A // Not visible.
B // Not visible.
pub bare type Cd where // Visible.
C // Visible.
D // Visible.
Layout rule and indentation
Instead of braces or explicit keywords to mark scope, Fuyu follows a set of layout rules, which are:
- After a layout keyword (
=,where,with, or->), a new scope is created. The first non-whitespace token on the following line establishes that scope's indentation level, which must be strictly greater than the enclosing scope's level. - Statements at the same indentation level are treated as sequential siblings within a scope. Each new line at the scope's indentation level begins a new statement (and may trigger an implicit statement separator token).
- If a line is indented further than the current scope's indentation level, it continues the previous statement rather than starting a new one.
- A scope ends (i.e., dedentation) when the indentation level decreases to or below the scope's established indentation level.
/// Compute the nth number of the Fibonacci sequence
/// and print the intermediate values during recursion.
///
/// Note that `vfib` is short for "verbose Fibonacci",
/// not "ventricular fibrilliation" which may be expected
/// by a cardiologist.
let vfib: (n: Int) -> Int =
let next = match n with
0 -> 0
1 -> 1
n ->
let prev = vfib (n - 1)
let curr = vfib (n - 2)
prev + curr
io.print_line next
next
// │ │ │
// │ │ └─── Indentation of scope 3
// │ └─────── Indentation of scope 2
// └─────────── Indentation of scope 1
Naming rules
Names created in different ways have strict naming rules. These are not simply conventions, as the compiler will reject programs that do not follow these rules.
| Name of | Example | Regular Expression |
|---|---|---|
| Value, function, type parameter, module | x, some_value | _*[a-z][a-z0-9_]* |
| Type, trait, value constructor | List, SomeType | _*[A-Z][A-Za-z0-9_]* |
| Sink | _, ___ | _+ |