Expressions
Expressions are things that can be evaluated to obtain a value. Combinations
of values, functions, and operations form expressions. Additionally, control
flow mechanisms such as when
and match
are expressions.
let x: Int = 2 * 4
let y: String = when {
x < 0 => "negative"
_ => "non-negative"
}
assert_eq(y, "non-negative")
Blocks
A block is a group of expressions. The value of the final expression in the block is the value of the block.
let message = {
let a = "have a byte"
let b = "of cake"
a + " " + b
}
assert_eq(message, "have a byte of cake")
Expression terminals
There are no explicit end of expression terminators, and expressions can easily span multiple lines. The compiler will automatically determine where an expression ends. An expression ends at the end of a line where the last token is any of the following:
- An identifier.
- A basic literal (such as an integer or a string).
- The
return
keyword. - One of the puntuation
)
,]
, or}
.
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 a[b] f(a)(using b) | Path Property Tuple element Index Function application | Left to right |
a ** b | Exponentiation | Right to left |
!a -a ..a | Logical not 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 | f | Pipe | 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 |
a..b a..=b | Exclusive range Inclusive range | Right to left |
when { ... } | When expression | |
match x { ... } | Match expression | |
fn(x) { ... } | Anonymous function | |
, | Separator | Left to right |
return | Return |
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 '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
assert_eq(n, m)
Names created in different ways have strict naming rules. These are not simply convention - the compiler will reject programs that do not follow these rules.
Name of | Created with | Example | Regular Expression |
---|---|---|---|
Value | let , using , match , function argument | x , some_value | _*[a-z][a-z0-9_]* |
Function | def | f , read_all | _*[a-z][a-z0-9_]* |
Type | type | List , SomeType | [A-Z][A-Za-z0-9]* |
Type parameter | type , let , def , provide | a , type_param | _*[a-z][a-z0-9_]* |
Module | Name of the module file | std , some_module | _*[a-z][a-z0-9_]* |
When an name with the same name as a reserved word is required, a raw name
starting with r#
can be used. For example, when
is a keyword, but r#when
is a name.
A namespace is created when a module is imported. Namespaces are not the same as names; thus, a namespace and a name can be identical, and there is no ambiguity.
// value.fuyu
export let value: Int = 100
// main.fuyu
import value // value namespace.
def main() {
let value = "hello" // Local value name.
assert_eq(value, "hello") // Use the local value name.
assert_eq(value::value, 100) // Use the value namespace.
}
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
or provide
) can only be
accessed within their scope. The following create new scopes:
- Bodies of
let
,def
,provide
, andfn
, - Within
(...)
,[...]
, and{...}
.
import std/fmt
import std/io
let x = 5
// x added to scope
when {
x < 0 => {
io::println(fmt::debug(x))
let y = 2 * x // y added to scope
// y goes out of scope
}
_ => {
io::println(fmt::debug(x))
let y = x + 1 // y added to scope
// y goes out of scope
}
}
// Uncommenting the following line is an error because y is not in scope.
// io::println(fmt::debug(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.
let x = 1
assert_eq(x, 1)
let x = "hello"
assert_eq(x, "hello")
A new scope can be created anywhere with a pair of {...}
. This can be useful
for locally scoping intermediate values.
{
let a = 1
}
// Uncommenting the following line is an error because a is not in scope.
// io::println(fmt::debug(a))