Basics
Comments
Comments are used to annotate and document code. There are three kinds of comments.
- Line comments begin with
//
and extend to the end of the line. - Documentation comments begin with
///
and extend to the end of the line. Documentation comments are used to automatically create documentation from the source code. Documentation comments can use Markdown. - The shebang comment is a special comment that starts with
#!
and may only appear as the very first text in a source file.
#!/usr/bin/env fuyu
import std/io
/// This function is called when the program is started.
main: () -> ()
main () = io.println "Hello" // Comments can appear after code.
Booleans
Booleans are represented by the Bool
type, which can take on the values
True
and False
.
let is_fuyu_cool = True
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 except before
the first digit, after the last digit, and before, between, or after the
characters of the 0b
, 0o
, and 0x
prefixes. Multiple underscores may
appear consecutively.
let integers = [
0, 50, 1_000_000, // Decimal
0b10101010, 0b1111_0110, // Binary
0o12345670, 0o123_005_774, // Octal
0x1234567890abcdefABCDEF, 0xfe_23_06, // Hexadecimal
]
// Multiple underscores may appear consecutively.
let special_number = 1_2__3___4
Floats
A Float
is an IEEE 754 floating-point number with at least double precision
(i.e., 64 bits). A Float
literal must include either a .
or e
(or both)
to distinguish it from an integer literal. Underscores (_
) can be used to
separate digits and may appear anywhere in the literal except before the first
digit, after the last digit, immediately after the .
(radix point), and
before, between, or after the characters of the e
, e+
, and e-
.
let floats = [
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 for some reason.
let funky_float = 6___.7_8__9e4__5
Since Fuyu runs on the BEAM, it uses the same rules as the BEAM for floats. In
particular,
a Float
on the BEAM does not exactly match IEEE 754,
and an operation that produces a NaN
, +Inf
, or -Inf
will instead result
in a panic.
This means that NaN
, +Inf
, or -Inf
are not representable by a Float
and
do not appear in Fuyu.
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{XXXXXX}
: Unicode escape of up to 24 bits, where eachX
is a hex digit representing a Unicode code point. There can be 1 to 6 hex digits in the curly braces.
// These strings are equal.
let string_one = "🍉 is a watermelon"
let string_two = "\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 string_one = "apples
bananas cherries"
let string_two = "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 string_one = """
text that
is spread
across
several lines
"""
let string_two = " text that\n is spread\nacross\n several lines"
Raw strings are delimited with r"..."
and do not process any escapes. There
can be 0 or more #
characters between the r
and the opening "
, and the
same number of #
characters must appear after the closing "
. Carriage
returns (U+000D) are removed. The same whitespace processing rules as
String
s enclosed in "..."
are applied.
// These strings are equal.
let raw_string_one = r"no \n escapes"
let string_one = "no \\n escapes"
// These strings are equal.
let raw_string_two = r#"nested r"raw" string "#
let string_two = "nested r\"raw\" string"
Tuples
A tuple is an ordered collection of values of arbitrary types. Tuples are
created with (...)
and may have zero or more elements. Tuples are useful
for representing a single value (i.e., ()
) and for grouping multiple values
together. The type of a tuple is simply the tuple of the types of each
element.
let a: () = () // 0-tuple, called "unit".
let b: (Int,) = (3.14,) // 1-tuple, trailing comma is required.
let c: (String, String) = ("hello", "world") // 2-tuple.
let d: (Bool, String, List Int) = (True, "text", [5, 6, 7]) // 3-tuple.
Elements of a tuple can be accessed in two ways:
- Destructuring uses pattern matching to capture values from a tuple.
- Element access is more compact and generally more useful when only a
single element from a tuple is required. Elements are accessed with
#
, where the zero-indexed position of the element in the tuple comes after the#
.
let xyz = (7, 15, 3)
// Destructure.
let (_, y, _) = xyz
assert.eq y 15
// Element access.
assert.eq xyz#1 15
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). Lists are type parameterized, which means
that the type of a list depends on the type of the values it holds. For
example, a list of integers (List Int
) and a list of strings (List String
)
are two different types.
let a: List Bool = [] // Empty.
let b: List Int = [1, 2, 3]
let c: List String = ["one", "two", "three"]
List elements can be accessed with destructuring and make use of the ..
rest pattern to capture zero or more items at the end of a list.
let xs = [6, 7, 9]
xs match
[x, ..rs] ->
assert.eq x 6
assert.eq rs [7, 8]
_ -> unreachable
A list can also be created from a different list using a spread. The spread must appear as the very last entry in the list.
let xs = [3, 4]
let ys = [1, 2, ..xs]
assert.eq ys [1, 2, 3, 4]
Expressions
Fuyu is an expression-oriented language, which means almost everything is
an expression that evaluates to a value. For example,
control flow structures, such as if
, are expressions
and can be used to obtain a value.
let x = 2 * 4
let y = if x < 0 then "negative" else "non-negative"
assert.eq y "non-negative"
Anything that can appear to the right of an =
sign is an expression, which
turns out to include many things such as:
- Primitive types (integers, floats, and strings).
- Tuples and lists.
- Do expressions.
- If expressions, when expressions, and match expressions.
- Function calls.
- Anonymous functions.
- Value constructors.
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 | — |
a.b a#b a#0 f &[a] b | Namespace Record field Tuple field Function application | Left-to-right |
f @ g | Function composition | Right-to-left |
a ** b | Exponentiation | Right-to-left |
~a ..a | Negation Spread | Right-to-left |
a * b a / b a % b | Multiplication Division Modulo | Left-to-right |
a + b a - b | Addition Subtraction | Left-to-right |
a == b a /= b a < b a <= b a >= b a > 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 |
do | Do expression | — |
if | If expression | Right-to-left |
when | When expression | — |
x match | Match expression | — |
\&[x] y -> ... | Anonymous function | — |
a | f | Pipe | Left-to-right |
panic return todo try unimplemented unreachable | Panic Return Todo Try Unimplemented Unreachable | Right-to-left |
, | 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)
.
Constants
Constants may only appear at the top level of a module. A constant consists
of an interface declaration followed by its definition using an =
.
small_primes: List Int // Interface.
small_primes = [2, 3, 5, 7] // Definition.
Only the following are allowed in constants:
- Literals:
Int
,Float
,String
, tuples, and lists. - Anonymous functions declarations.
- Value constructors (e.g.,
True
,Some 123
). - Field accesses.
- Other constants.
A constant is a declaration, not a computation, so constructs such as let
,
if
, match
, function calls, and operators are not allowed.
Let bindings
A value is anything that can be bound to a name and is often referred to as a variable. In Fuyu, the use of the term 'variable' is avoided; instead, a name is said to be bound to a value in a particular context.
let message = "hello"
// ^^^^^^^ ^^^^^^^
// name value
Most types can be determined by inference, but types can be specified with :
.
let n = 123 // Int is inferred.
let m: Int = 123
Let bindings are really a pattern match; however, they must be irrefutable, meaning that the pattern can never fail.
let (x, y) = (3, "hello")
assert.eq x 3
assert.eq y "hello"
Scope
The scope of binding is the context in which a specific name is bound to a
specific value. Names (e.g., those created by let
) can only be accessed
within their scope. New scopes are created whenever indentation changes or
within {...}
(refer to the layout rules).
import std/io
main: () -> ()
main () = do
let x = do // `x` added to scope.
let y = 2 // `y` added to scope.
y + 1
io.println x // Works!
// Uncommenting the following line is an error because `y` is not in scope.
// io.println y
A binding can be overwritten with a different value and type. This binding is effective for the rest of the scope where the binding occurs.
import std/assert
main: () -> ()
main () = do
let x = 1
assert.eq x 1
let x = "hello"
assert.eq x "hello"
Naming rules
Names created in different ways have strict naming rules. These are not simply conventions — the compiler will reject programs that do not follow these rules.
Name of | Example | Regular Expression |
---|---|---|
Value | x , some_value | _*[a-z][a-z0-9_]* |
Function | f , read_all | _*[a-z][a-z0-9_]* |
Type | List , SomeType | _*[A-Z][A-Za-z0-9_]* |
Constructor | Some , None | _*[A-Z][A-Za-z0-9_]* |
Type parameter | a , type_param | _*[a-z][a-z0-9_]* |
Module | std , some_module | _*[a-z][a-z0-9_]* |
When a name with the same name as a reserved word is required, a raw name
starting with r#
can be used. For example, match
is a keyword, but
r#match
is a name.