MoonBit
MoonBit is an end-to-end programming language toolchain for cloud and edge computing using WebAssembly. The IDE environment is available at https://try.moonbitlang.com without any installation; it does not rely on any server either.
Status and aimed timeline
MoonBit is currently in beta-preview. We expect to reach beta in 2024/11/22, and 1.0 in 2025.
When MoonBit reaches beta, it means any backwards-incompatible changes will be seriously evaluated and MoonBit can be used in production(very rare compiler bugs). MoonBit is developed by a talented full time team who had extensive experience in building language toolchains, so we will grow much faster than the typical language ecosystem, you won't wait long to use MoonBit in your production.
Main advantages
- Generate significantly smaller WASM output than any existing solutions.
- Much faster runtime performance.
- State of the art compile-time performance.
- Simple but practical, data-oriented language design.
Overview
A MoonBit program consists of type definitions, function definitions, and variable bindings.
Program entrance
There is a specialized function called init
function. The init
function is special in two aspects:
- There can be multiple
init
functions in the same package. - An
init
function can't be explicitly called or referred to by other functions. Instead, allinit
functions will be implicitly called when initializing a package. Therefore,init
functions should only consist of statements.
fn main {
let x = 1
// x // fail
println(x) // success
}
For WebAssembly backend, it means that it will be executed before the instance is available, meaning that the FFIs that relies on the instance's exportations can not be used at this stage; for JavaScript backend, it means that it will be executed during the importation stage.
There is another specialized function called main
function. The main
function is the main entrance of the program, and it will be executed after the initialization stage.
Only packages that are main
packages can define such main
function. Check out build system tutorial for detail.
The two functions above need to drop the parameter list and the return type.
Expressions and Statements
MoonBit distinguishes between statements and expressions. In a function body, only the last clause should be an expression, which serves as a return value. For example:
fn foo() -> Int {
let x = 1
x + 1 // OK
}
fn bar() -> Int {
let x = 1
x + 1 // fail
x + 2
}
Expressions include:
- Value literals (e.g. Boolean values, numbers, characters, strings, arrays, tuples, structs)
- Arithmetical, logical, or comparison operations
- Accesses to array elements (e.g.
a[0]
) or struct fields (e.gr.x
) or tuple components (e.g.t.0
) - Variables and (capitalized) enum constructors
- Anonymous local function definitions
match
andif
expressions
Statements include:
- Named local function definitions
- Local variable bindings
- Assignments
return
statements- Any expression whose return type is
Unit
Functions
Functions take arguments and produce a result. In MoonBit, functions are first-class, which means that functions can be arguments or return values of other functions. MoonBit's naming convention requires that function names should not begin with uppercase letters (A-Z). Compare for constructors in the enum
section below.
Top-Level Functions
Functions can be defined as top-level or local. We can use the fn
keyword to define a top-level function that sums three integers and returns the result, as follows:
fn add3(x: Int, y: Int, z: Int)-> Int {
x + y + z
}
Note that the arguments and return value of top-level functions require explicit type annotations.
Local Functions
Local functions can be named or anonymous. Type annotations can be omitted for local function definitions: they can be automatically inferred in most cases. For example:
fn foo() -> Int {
fn inc(x) { x + 1 } // named as `inc`
fn (x) { x + inc(2) } (6) // anonymous, instantly applied to integer literal 6
}
fn main {
println(foo())
}
Functions, whether named or anonymous, are lexical closures: any identifiers without a local binding must refer to bindings from a surrounding lexical scope. For example:
let y = 3
fn foo(x: Int) -> Unit {
fn inc() { x + 1 } // OK, will return x + 1
fn four() { y + 1 } // Ok, will return 4
println(inc())
println(four())
}
fn main {
foo(2)
}
Function Applications
A function can be applied to a list of arguments in parentheses:
add3(1, 2, 7)
This works whether add3
is a function defined with a name (as in the previous example), or a variable bound to a function value, as shown below:
fn main {
let add3 = fn(x, y, z) { x + y + z }
println(add3(1, 2, 7))
}
The expression add3(1, 2, 7)
returns 10
. Any expression that evaluates to a function value is applicable:
fn main {
let f = fn (x) { x + 1 }
let g = fn (x) { x + 2 }
println((if true { f } else { g })(3)) // OK
}
Labelled arguments
Functions can declare labelled argument with the syntax label~ : Type
. label
will also serve as parameter name inside function body:
fn labelled(arg1~ : Int, arg2~ : Int) -> Int {
arg1 + arg2
}
Labelled arguments can be supplied via the syntax label=arg
. label=label
can be abbreviated as label~
:
fn init {
let arg1 = 1
println(labelled(arg2=2, arg1~)) // 3
}
Labelled function can be supplied in any order. The evaluation order of arguments is the same as the order of parameters in function declaration.
Optional arguments
A labelled argument can be made optional by supplying a default expression with the syntax label~ : Type = default_expr
. If this argument is not supplied at call site, the default expression will be used:
fn optional(opt~ : Int = 42) -> Int {
opt
}
fn main {
println(optional()) // 42
println(optional(opt=0)) // 0
}
The default expression will be evaluated every time it is used. And the side effect in the default expression, if any, will also be triggered. For example:
fn incr(counter~ : Ref[Int] = { val: 0 }) -> Ref[Int] {
counter.val = counter.val + 1
counter
}
fn main {
println(incr()) // 1
println(incr()) // still 1, since a new reference is created every time default expression is used
let counter : Ref[Int] = { val: 0 }
println(incr(counter~)) // 1
println(incr(counter~)) // 2, since the same counter is used
}
If you want to share the result of default expression between different function calls, you can lift the default expression to a toplevel let
declaration:
let default_counter : Ref[Int] = { val: 0 }
fn incr(counter~ : Ref[Int] = default_counter) -> Int {
counter.val = counter.val + 1
counter.val
}
fn main {
println(incr()) // 1
println(incr()) // 2
}
Default expression can depend on the value of previous arguments. For example:
fn sub_array[X](xs : Array[X], offset~ : Int, len~ : Int = xs.length() - offset) -> Array[X] {
... // take a sub array of [xs], starting from [offset] with length [len]
}
fn init {
println(sub_array([1, 2, 3], offset=1)) // [2, 3]
println(sub_array([1, 2, 3], offset=1, len=1)) // [2]
}
Automatically insert Some
when supplying optional arguments
It is quite often optional arguments have type T?
with None
as default value.
In this case, passing the argument explicitly requires wrapping a Some
:
fn image(width~ : Int? = None, height~ : Int? = None) -> Image { ... }
fn main {
let img = image(width=Some(1920), height=Some(1080)) // ugly!
...
}
Fortunately, MoonBit provides a special kind of optional arguments to solve this problem.
Optional arguments declared with label? : T
has type T?
and None
as default value.
When supplying this kind of optional argument directly, MoonBit will automatically insert a Some
:
fn image(width? : Int, height? : Int) -> Image { ... }
fn main {
let img = image(width=1920, height=1080) // much better!
...
}
Sometimes, it is also useful to pass a value of type T?
directly,
for example when forwarding optional argument.
MoonBit provides a syntax label?=value
for this, with label?
being an abbreviation of label?=label
:
fn image(width? : Int, height? : Int) -> Image { ... }
fn fixed_width_image(height? : Int) -> Image {
image(width=1920, height?)
}
Autofill arguments
MoonBit supports filling specific types of arguments automatically at different call site, such as the source location of a function call.
To declare an autofill argument, simply declare an optional argument with _
as default value.
Now if the argument is not explicitly supplied, MoonBit will automatically fill it at the call site.
Currently MoonBit supports two types of autofill arguments, SourceLoc
, which is the source location of the whole function call,
and ArgsLoc
, which is a array containing the source location of each argument, if any:
fn f(_x : Int, _y : Int, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit {
println("loc of whole function call: \{loc}")
println("loc of arguments: \{args_loc}")
}
fn main {
f(1, 2)
// loc of whole function call: <filename>:7:3-7:10
// loc of arguments: [Some(<filename>:7:5-7:6), Some(<filename>:7:8-7:9), None, None]
}
Autofill arguments are very useful for writing debugging and testing utilities.
Control Structures
Conditional Expressions
A conditional expression consists of a condition, a consequent, and an optional else clause.
if x == y {
expr1
} else {
expr2
}
if x == y {
expr1
}
The else clause can also contain another if-else expression:
if x == y {
expr1
} else if z == k {
expr2
}
Curly brackets are used to group multiple expressions in the consequent or the else clause.
Note that a conditional expression always returns a value in MoonBit, and the return values of the consequent and the else clause must be of the same type. Here is an example:
let initial = if size < 1 { 1 } else { size }
While loop
In MoonBit, while
loop can be used to execute a block of code repeatedly as long as a condition is true. The condition is evaluated before executing the block of code. The while
loop is defined using the while
keyword, followed by a condition and the loop body. The loop body is a sequence of statements. The loop body is executed as long as the condition is true.
let mut i = 5
while i > 0 {
println(i)
i = i - 1
}
The loop body supports break
and continue
. Using break
allows you to exit the current loop, while using continue
skips the remaining part of the current iteration and proceeds to the next iteration.
fn main {
let mut i = 5
while i > 0 {
i = i - 1
if i == 4 { continue }
if i == 1 { break }
println(i)
}
}
The while
loop also supports an optional else
clause. When the loop condition becomes false, the else
clause will be executed, and then the loop will end.
fn main {
let mut i = 2
while i > 0 {
println(i)
i = i - 1
} else {
println(i)
}
}
When there is an else
clause, the while
loop can also return a value. The return value is the evaluation result of the else
clause. In this case, if you use break
to exit the loop, you need to provide a return value after break
, which should be of the same type as the return value of the else
clause.
let mut i = 10
let r1 =
while i > 0 {
i = i - 1
if i % 2 == 0 { break 5 } // break with 5
} else {
7
}
println(r1) //output: 5
let mut i = 10
let r2 =
while i > 0 {
i = i - 1
} else {
7
}
println(r2) //output: 7
For Loop
MoonBit also supports C-style For loops. The keyword for
is followed by variable initialization clauses, loop conditions, and update clauses separated by semicolons. They do not need to be enclosed in parentheses.
For example, the code below creates a new variable binding i
, which has a scope throughout the entire loop and is immutable. This makes it easier to write clear code and reason about it:
for i = 0; i < 5; i = i + 1 {
println(i)
}
// output:
// 0
// 1
// 2
The variable initialization clause can create multiple bindings:
for i = 0, j = 0; i + j < 100; i = i + 1, j = j + 1 {
println(i)
}
It should be noted that in the update clause, when there are multiple binding variables, the semantics are to update them simultaneously. In other words, in the example above, the update clause does not execute i = i + 1
, j = j + 1
sequentially, but rather increments i
and j
at the same time. Therefore, when reading the values of the binding variables in the update clause, you will always get the values updated in the previous iteration.
Variable initialization clauses, loop conditions, and update clauses are all optional. For example, the following two are infinite loops:
for i=1;; i=i+1 {
println(i) // loop forever!
}
for {
println("loop forever!")
}
The for
loop also supports continue
, break
, and else
clauses. Like the while
loop, the for
loop can also return a value using the break
and else
clauses.
The continue
statement skips the remaining part of the current iteration of the for
loop (including the update clause) and proceeds to the next iteration. The continue
statement can also update the binding variables of the for
loop, as long as it is followed by expressions that match the number of binding variables, separated by commas.
For example, the following program calculates the sum of even numbers from 1 to 6:
fn main {
let sum =
for i = 1, acc = 0; i <= 6; i = i + 1 {
if i % 2 == 0 {
println("even: \{i}")
continue i + 1, acc + i
}
} else {
acc
}
println(sum)
}
for .. in
loop
MoonBit supports traversing elements of different data structures and sequences via the for .. in
loop syntax:
for x in [ 1, 2, 3 ] {
println(x)
}
for .. in
loop is translated to the use of Iter
in MoonBit's standard library. Any type with a method .iter() : Iter[T]
can be traversed using for .. in
.
For more information of the Iter
type, see Iterator below.
In addition to sequences of a single value, MoonBit also supports traversing sequences of two values, such as Map
, via the Iter2
type in MoonBit's standard library.
Any type with method .iter2() : Iter2[A, B]
can be traversed using for .. in
with two loop variables:
for k, v in { "x": 1, "y": 2, "z": 3 } {
println("\{k} => \{v}")
}
Another example of for .. in
with two loop variables is traversing an array while keeping track of array index:
for index, elem in [ 4, 5, 6 ] {
let i = index + 1
println("The \{i}-th element of the array is \{elem}")
}
Control flow operations such as return
, break
and error handling are supported in the body of for .. in
loop:
test "map test" {
let map = { "x": 1, "y": 2, "z": 3 }
for k, v in map {
assert_eq!(map[k], Some(v))
}
}
If a loop variable is unused, it can be ignored with _
.
Functional loop
Functional loop is a powerful feature in MoonBit that enables you to write loops in a functional style.
A functional loop consumes arguments and returns a value. It is defined using the loop
keyword, followed by its arguments and the loop body. The loop body is a sequence of clauses, each of which consists of a pattern and an expression. The clause whose pattern matches the input will be executed, and the loop will return the value of the expression. If no pattern matches, the loop will panic. Use the continue
keyword with arguments to start the next iteration of the loop. Use the break
keyword with arguments to return a value from the loop. The break
keyword can be omitted if the value is the last expression in the loop body.
fn sum(xs: @immut/list.T[Int]) -> Int {
loop xs, 0 {
Nil, acc => break acc // break can be omitted
Cons(x, rest), acc => continue rest, x + acc
}
}
fn main {
println(sum(Cons(1, Cons(2, Cons(3, Nil)))))
}
Guard Statement
The guard
statement is used to check a specified invariant.
If the condition of the invariant is satisfied, the program continues executing
the subsequent statements and returns. If the condition is not satisfied (i.e., false),
the code in the else
block is executed and its evaluation result is returned (the subsequent statements are skipped).
guard index >= 0 && index < len else {
abort("Index out of range")
}
The guard
statement also supports pattern matching: in the following example,
getProcessedText
assumes that the input path
points to resources that are all plain text,
and it uses the guard
statement to ensure this invariant. Compared to using
a match
statement, the subsequent processing of text
can have one less level of indentation.
enum Resource {
Folder(Array[String])
PlainText(String)
JsonConfig(Json)
}
fn getProcessedText(resources : Map[String, Resource], path : String) -> String!Error {
guard let Some(PlainText(text)) = resources[path] else {
None => fail!("\{path} not found")
Some(Folder(_)) => fail!("\{path} is a folder")
Some(JsonConfig(_)) => fail!("\{path} is a json config")
}
...
process(text)
}
When the else
part is omitted, the program terminates if the condition specified
in the guard
statement is not true or cannot be matched.
guard condition // equivalent to `guard condition else { panic() }`
guard let Some(x) = expr // equivalent to `guard let Some(x) = expr else { _ => panic() }`
Iterator
An iterator is an object that traverse through a sequence while providing access
to its elements. Traditional OO languages like Java's Iterator<T>
use next()
hasNext()
to step through the iteration process, whereas functional languages
(JavaScript's forEach
, Lisp's mapcar
) provides a high-order function which
takes an operation and a sequence then consumes the sequence with that operation
being applied to the sequence. The former is called external iterator (visible
to user) and the latter is called internal iterator (invisible to user).
The built-in type Iter[T]
is MoonBit's internal iterator implementation.
Almost all built-in sequential data structures have implemented Iter
:
fn filter_even(l : Array[Int]) -> Array[Int] {
let l_iter : Iter[Int] = l.iter()
l_iter.filter(fn { x => (x & 1) == 1 }).collect()
}
fn fact(n : Int) -> Int {
let start = 1
start.until(n).fold(Int::op_mul, init=start)
}
Commonly used methods include:
-
each
: Iterates over each element in the iterator, applying some function to each element. -
fold
: Folds the elements of the iterator using the given function, starting with the given initial value. -
collect
: Collects the elements of the iterator into an array. -
filter
: lazy Filters the elements of the iterator based on a predicate function. -
map
: lazy Transforms the elements of the iterator using a mapping function. -
concat
: lazy Combines two iterators into one by appending the elements of the second iterator to the first.
Methods like filter
map
are very common on a sequence object e.g. Array.
But what makes Iter
special is that any method that constructs a new Iter
is
lazy (i.e. iteration doesn't start on call because it's wrapped inside a
function), as a result of no allocation for intermediate value. That's what
makes Iter
superior for traversing through sequence: no extra cost. MoonBit
encourages user to pass an Iter
across functions instead of the sequence
object itself.
Pre-defined sequence structures like Array
and its iterators should be
enough to use. But to take advantages of these methods when used with a custom
sequence with elements of type S
, we will need to implement Iter
, namely, a function that returns
an Iter[S]
. Take Bytes
as an example:
fn iter(data : Bytes) -> Iter[Byte] {
Iter::new(
fn(yield) {
// The code that actually does the iteration
/////////////////////////////////////////////
for i = 0, len = data.length(); i < len; i = i + 1 {
if yield(data[i]) == IterEnd {
break IterEnd
}
/////////////////////////////////////////////
} else {
IterContinue
}
},
)
}
Almost all Iter
implementations are identical to that of Bytes
, the only
main difference being the code block that actually does the iteration.
Implementation details
The type Iter[T]
is basically a type alias for ((T) -> IterResult) -> IterResult
,
a higher-order function that takes an operation and IterResult
is an enum
object that tracks the state of current iteration which consists any of the 2
states:
IterEnd
: marking the end of an iterationIterContinue
: marking the end of an iteration is yet to be reached, implying the iteration will still continue at this state.
To put it simply, Iter[T]
takes a function (T) -> IterResult
and use it to
transform Iter[T]
itself to a new state of type IterResult
. Whether that
state being IterEnd
IterContinue
depends on the function.
Iterator provides a unified way to iterate through data structures, and they
can be constructed at basically no cost: as long as fn(yield)
doesn't
execute, the iteration process doesn't start.
Internally a Iter::run()
is used to trigger the iteration. Chaining all sorts
of Iter
methods might be visually pleasing, but do notice the heavy work
underneath the abstraction.
Thus, unlike an external iterator, once the iteration starts
there's no way to stop unless the end is reached. Methods such as count()
which counts the number of elements in a iterator looks like an O(1)
operation
but actually has linear time complexity. Carefully use iterators or
performance issue might occur.
Built-in Data Structures
Boolean
MoonBit has a built-in boolean type, which has two values: true
and false
. The boolean type is used in conditional expressions and control structures.
let a = true
let b = false
let c = a && b
let d = a || b
let e = not(a)
Number
MoonBit have integer type and floating point type:
type | description | example |
---|---|---|
Int | 32-bit signed integer | 42 |
Int64 | 64-bit signed integer | 1000L |
UInt | 32-bit unsigned integer | 14U |
UInt64 | 64-bit unsigned integer | 14UL |
Double | 64-bit floating point, defined by IEEE754 | 3.14 |
Float | 32-bit floating point | (3.14 : Float) |
BigInt | represents numeric values larger than other types | 10000000000000000000000N |
MoonBit also supports numeric literals, including decimal, binary, octal, and hexadecimal numbers.
To improve readability, you may place underscores in the middle of numeric literals such as 1_000_000
. Note that underscores can be placed anywhere within a number, not just every three digits.
-
There is nothing surprising about decimal numbers.
let a = 1234 let b = 1_000_000 + a let large_num = 9_223_372_036_854_775_807L // Integers of the Int64 type must have an 'L' as a suffix let unsigned_num = 4_294_967_295U // Integers of the UInt type must have an 'U' suffix
-
A binary number has a leading zero followed by a letter "B", i.e.
0b
/0B
. Note that the digits after0b
/0B
must be0
or1
.let bin = 0b110010 let another_bin = 0B110010
-
An octal number has a leading zero followed by a letter "O", i.e.
0o
/0O
. Note that the digits after0o
/0O
must be in the range from0
through7
:let octal = 0o1234 let another_octal = 0O1234
-
A hexadecimal number has a leading zero followed by a letter "X", i.e.
0x
/0X
. Note that the digits after the0x
/0X
must be in the range0123456789ABCDEF
.let hex = 0XA let another_hex = 0xA
Overloaded int literal
When the expected type is known, MoonBit can automatically overload integer literal, and there is no need to specify the type of number via letter postfix:
let int : Int = 42
let uint : UInt = 42
let int64 : Int64 = 42
let double : Double = 42
let float : Float = 42
let bigint : BigInt = 42
String
String
holds a sequence of UTF-16 code units. You can use double quotes to create a string, or use #|
to write a multi-line string.
let a = "兔rabbit"
println(a[0]) // output: 兔
println(a[1]) // output: r
let b =
#| Hello
#| MoonBit
#|
In double quotes string, a backslash followed by certain special characters forms an escape sequence:
escape sequences | description |
---|---|
\n ,\r ,\t ,\b | New line, Carriage return, Horizontal tab, Backspace |
\\ | Backslash |
\x41 | Hexadecimal escape sequence |
\o102 | Octal escape sequence |
\u5154 ,\u{1F600} | Unicode escape sequence |
MoonBit supports string interpolation. It enables you to substitute variables within interpolated strings. This feature simplifies the process of constructing dynamic strings by directly embedding variable values into the text. Variables used for string interpolation must support the to_string
method.
let x = 42
println("The answer is \{x}")
Multi-line strings do not support interpolation by default, but you can enable interpolation for a specific line by changing the leading #|
to $|
:
let lang = "MoonBit"
let str =
#| Hello
#| ---
$| \{lang}\n
#| ---
println(str)
Output:
Hello
---
MoonBit
---
Char
Char
is an integer representing a Unicode code point.
let a : Char = 'A'
let b = '\x41'
let c = '🐰'
let zero = '\u{30}'
let zero = '\u0030'
Byte(s)
A byte literal in MoonBit is either a single ASCII character or a single escape enclosed in single quotes '
, and preceded by the character b
. Byte literals are of type Byte
. For example:
fn main {
let b1 : Byte = b'a'
println(b1.to_int())
let b2 = b'\xff'
println(b2.to_int())
}
A Bytes
is a sequence of bytes. Similar to byte, bytes literals have the form of b"..."
. For example:
fn main {
let b1 : Bytes = b"abcd"
let b2 = b"\x61\x62\x63\x64"
println(b1 == b2) // true
}
Tuple
A tuple is a collection of finite values constructed using round brackets ()
with the elements separated by commas ,
. The order of elements matters; for example, (1,true)
and (true,1)
have different types. Here's an example:
fn pack(a: Bool, b: Int, c: String, d: Double) -> (Bool, Int, String, Double) {
(a, b, c, d)
}
fn init {
let quad = pack(false, 100, "text", 3.14)
let (bool_val, int_val, str, float_val) = quad
println("\{bool_val} \{int_val} \{str} \{float_val}")
}
Tuples can be accessed via pattern matching or index:
fn f(t : (Int, Int)) -> Unit {
let (x1, y1) = t // access via pattern matching
// access via index
let x2 = t.0
let y2 = t.1
if (x1 == x2 && y1 == y2) {
println("yes")
} else {
println("no")
}
}
fn main {
f((1, 2))
}
Array
An array is a finite sequence of values constructed using square brackets []
, with elements separated by commas ,
. For example:
let numbers = [1, 2, 3, 4]
You can use numbers[x]
to refer to the xth element. The index starts from zero.
fn main {
let numbers = [1, 2, 3, 4]
let a = numbers[2]
numbers[3] = 5
let b = a + numbers[3]
println(b) // prints 8
}
Map
MoonBit provides a hash map data structure that preserves insertion orde called Map
in its standard library.
Map
s can be created via a convenient literal syntax:
let map : Map[String, Int] = { "x": 1, "y": 2, "z": 3 }
Currently keys in map literal syntax must be constant. Map
s can also be destructed elegantly with pattern matching, see Map Pattern.
Json literal
MoonBit supports convenient json handling by overloading literals.
When the expected type of an expression is Json
, number, string, array and map literals can be directly used to create json data:
let moon_pkg_json_example : Json = {
"import": [ "moonbitlang/core/builtin", "moonbitlang/core/coverage" ],
"test-import": [ "moonbitlang/core/random" ]
}
Json values can be pattern matched too, see Json Pattern.
Variable Binding
A variable can be declared as mutable or immutable using let mut
or let
, respectively. A mutable variable can be reassigned to a new value, while an immutable one cannot.
let zero = 0
fn main {
let mut i = 10
i = 20
println(i + zero)
}
Data Types
There are two ways to create new data types: struct
and enum
.
Struct
In MoonBit, structs are similar to tuples, but their fields are indexed by field names. A struct can be constructed using a struct literal, which is composed of a set of labeled values and delimited with curly brackets. The type of a struct literal can be automatically inferred if its fields exactly match the type definition. A field can be accessed using the dot syntax s.f
. If a field is marked as mutable using the keyword mut
, it can be assigned a new value.
struct User {
id: Int
name: String
mut email: String
}
fn main {
let u = { id: 0, name: "John Doe", email: "john@doe.com" }
u.email = "john@doe.name"
println(u.id)
println(u.name)
println(u.email)
}
Constructing Struct with Shorthand
If you already have some variable like name
and email
, it's redundant to repeat those names when constructing a struct:
fn main {
let name = "john"
let email = "john@doe.com"
let u = { id: 0, name: name, email: email }
}
You can use shorthand instead, it behaves exactly the same.
fn main {
let name = "john"
let email = "john@doe.com"
let u = { id: 0, name, email }
}
Struct Update Syntax
It's useful to create a new struct based on an existing one, but with some fields updated.
struct User {
id: Int
name: String
email: String
} derive(Show)
fn main {
let user = { id: 0, name: "John Doe", email: "john@doe.com" }
let updated_user = { ..user, email: "john@doe.name" }
println(user) // output: { id: 0, name: "John Doe", email: "john@doe.com" }
println(updated_user) // output: { id: 0, name: "John Doe", email: "john@doe.name" }
}
Enum
Enum types are similar to algebraic data types in functional languages. Users familiar with C/C++ may prefer calling it tagged union.
An enum can have a set of cases (constructors). Constructor names must start with capitalized letter. You can use these names to construct corresponding cases of an enum, or checking which branch an enum value belongs to in pattern matching:
// An enum type that represents the ordering relation between two values,
// with three cases "Smaller", "Greater" and "Equal"
enum Relation {
Smaller
Greater
Equal
}
// compare the ordering relation between two integers
fn compare_int(x: Int, y: Int) -> Relation {
if x < y {
// when creating an enum, if the target type is known, you can write the constructor name directly
Smaller
} else if x > y {
// but when the target type is not known,
// you can always use `TypeName::Constructor` to create an enum unambiguously
Relation::Greater
} else {
Equal
}
}
// output a value of type `Relation`
fn print_relation(r: Relation) -> Unit {
// use pattern matching to decide which case `r` belongs to
match r {
// during pattern matching, if the type is known, writing the name of constructor is sufficient
Smaller => println("smaller!")
// but you can use the `TypeName::Constructor` syntax for pattern matching as well
Relation::Greater => println("greater!")
Equal => println("equal!")
}
}
fn main {
print_relation(compare_int(0, 1)) // smaller!
print_relation(compare_int(1, 1)) // equal!
print_relation(compare_int(2, 1)) // greater!
}
Enum cases can also carry payload data. Here's an example of defining an integer list type using enum:
enum List {
Nil
// constructor `Cons` carries additional payload: the first element of the list,
// and the remaining parts of the list
Cons (Int, List)
}
fn main {
// when creating values using `Cons`, the payload of by `Cons` must be provided
let l: List = Cons(1, Cons(2, Nil))
println(is_singleton(l))
print_list(l)
}
fn print_list(l: List) -> Unit {
// when pattern-matching an enum with payload,
// in additional to deciding which case a value belongs to
// you can extract the payload data inside that case
match l {
Nil => println("nil")
// Here `x` and `xs` are defining new variables instead of referring to existing variables,
// if `l` is a `Cons`, then the payload of `Cons` (the first element and the rest of the list)
// will be bind to `x` and `xs
Cons(x, xs) => {
println(x)
println(",")
print_list(xs)
}
}
}
// In addition to binding payload to variables,
// you can also continue matching payload data inside constructors.
// Here's a function that decides if a list contains only one element
fn is_singleton(l: List) -> Bool {
match l {
// This branch only matches values of shape `Cons(_, Nil)`, i.e. lists of length 1
Cons(_, Nil) => true
// Use `_` to match everything else
_ => false
}
}
Constructor with labelled arguments
Enum constructors can have labelled argument:
enum E {
// `x` and `y` are labelled argument
C(x~ : Int, y~ : Int)
}
// pattern matching constructor with labelled arguments
fn f(e : E) -> Unit {
match e {
// `label=pattern`
C(x=0, y=0) => println("0!")
// `x~` is an abbreviation for `x=x`
// Unmatched labelled arguments can be omitted via `..`
C(x~, ..) => println(x)
}
}
// creating constructor with labelled arguments
fn main {
f(C(x=0, y=0)) // `label=value`
let x = 0
f(C(x~, y=1)) // `~x` is an abbreviation for `x=x`
}
It is also possible to access labelled arguments of constructors like accessing struct fields in pattern matching:
enum Object {
Point(x~ : Double, y~ : Double)
Circle(x~ : Double, y~ : Double, radius~ : Double)
}
type! NotImplementedError derive(Show)
fn distance_with(self : Object, other : Object) -> Double!NotImplementedError {
match (self, other) {
// For variables defined via `Point(..) as p`,
// the compiler knows it must be of constructor `Point`,
// so you can access fields of `Point` directly via `p.x`, `p.y` etc.
(Point(_) as p1, Point(_) as p2) => {
let dx = p2.x - p1.x
let dy = p2.y - p1.y
(dx * dx + dy * dy).sqrt()
}
(Point(_), Circle(_)) | (Circle(_), Point(_)) | (Circle(_), Circle(_)) =>
raise NotImplementedError
}
}
fn main {
let p1 : Object = Point(x=0, y=0)
let p2 : Object = Point(x=3, y=4)
let c1 : Object = Circle(x=0, y=0, radius=2)
try {
println(p1.distance_with!(p2)) // 5.0
println(p1.distance_with!(c1))
} catch {
e => println(e)
}
}
Constructor with mutable fields
It is also possible to define mutable fields for constructor. This is especially useful for defining imperative data structures:
// A mutable binary search tree with parent pointer
enum Tree[X] {
Nil
// only labelled arguments can be mutable
Node(mut value~ : X, mut left~ : Tree[X], mut right~ : Tree[X], mut parent~ : Tree[X])
}
// A set implemented using mutable binary search tree.
struct Set[X] {
mut root : Tree[X]
}
fn Set::insert[X : Compare](self : Set[X], x : X) -> Unit {
self.root = self.root.insert(x, parent=Nil)
}
// In-place insert a new element to a binary search tree.
// Return the new tree root
fn Tree::insert[X : Compare](self : Tree[X], x : X, parent~ : Tree[X]) -> Tree[X] {
match self {
Nil => Node(value=x, left=Nil, right=Nil, parent~)
Node(_) as node => {
let order = x.compare(node.value)
if order == 0 {
// mutate the field of a constructor
node.value = x
} else if order < 0 {
// cycle between `node` and `node.left` created here
node.left = node.left.insert(x, parent=node)
} else {
node.right = node.right.insert(x, parent=node)
}
// The tree is non-empty, so the new root is just the original tree
node
}
}
}
Newtype
MoonBit supports a special kind of enum called newtype:
// `UserId` is a fresh new type different from `Int`, and you can define new methods for `UserId`, etc.
// But at the same time, the internal representation of `UserId` is exactly the same as `Int`
type UserId Int
type UserName String
Newtypes are similar to enums with only one constructor (with the same name as the newtype itself). So, you can use the constructor to create values of newtype, or use pattern matching to extract the underlying representation of a newtype:
fn init {
let id: UserId = UserId(1)
let name: UserName = UserName("John Doe")
let UserId(uid) = id // the type of `uid` is `Int`
let UserName(uname) = name // the type of `uname` is `String`
println(uid)
println(uname)
}
Besides pattern matching, you can also use ._
to extract the internal representation of newtypes:
fn init {
let id: UserId = UserId(1)
let uid: Int = id._
println(uid)
}
Type alias
MoonBit supports type alias via the syntax typealias Name = TargetType
:
pub typealias Index = Int
// type alias are private by default
typealias MapString[X] = Map[String, X]
unlike all other kinds of type declaration above, type alias does not define a new type, it is merely a type macro that behaves exactly the same as its definition. So for example one cannot define new methods or implement traits for a type alias.
Type alias can be used to perform incremental code refactor.
For example, if you want to move a type T
from @pkgA
to @pkgB
,
you can leave a type alias typealias T = @pkgB.T
in @pkgA
, and incrementally port uses of @pkgA.T
to @pkgB.T
.
The type alias can be removed after all uses of @pkgA.T
is migrated to @pkgB.T
.
Pattern Matching
We have shown a use case of pattern matching for enums, but pattern matching is not restricted to enums. For example, we can also match expressions against Boolean values, numbers, characters, strings, tuples, arrays, and struct literals. Since there is only one case for those types other than enums, we can pattern match them using let
binding instead of match
expressions. Note that the scope of bound variables in match
is limited to the case where the variable is introduced, while let
binding will introduce every variable to the current scope. Furthermore, we can use underscores _
as wildcards for the values we don't care about, use ..
to ignore remaining fields of struct or elements of array.
let id = match u {
{ id: id, name: _, email: _ } => id
}
// is equivalent to
let { id: id, name: _, email: _ } = u
// or
let { id: id, ..} = u
let ary = [1,2,3,4]
let [a, b, ..] = ary // a = 1, b = 2
let [.., a, b] = ary // a = 3, b = 4
There are some other useful constructs in pattern matching. For example, we can use as
to give a name to some pattern, and we can use |
to match several cases at once. A variable name can only be bound once in a single pattern, and the same set of variables should be bound on both sides of |
patterns.
match expr {
Lit(n) as a => ...
Add(e1, e2) | Mul(e1, e2) => ...
_ => ...
}
Range Pattern
For builtin integer types and Char
, MoonBit allows matching whether the value falls in a specific range.
Range patterns have the form a..<b
or a..=b
, where ..<
means the upper bound is exclusive, and ..=
means inclusive upper bound.
a
and b
can be one of:
- literal
- named constant declared with
const
_
, meaning the pattern has no restriction on this side
Here are some examples:
const Zero = 0
fn sign(x : Int) -> Int {
match x {
_..<Zero => -1
Zero => 0
1..<_ => 1
}
}
fn classify_char(c : Char) -> String {
match c {
'a'..='z' => "lowercase"
'A'..='Z' => "uppercase"
'0'..='9' => "digit"
_ => "other"
}
}
Map Pattern
MoonBit allows convenient matching on map-like data structures.
Inside a map pattern, the key : value
syntax will match if key
exists in the map, and match the value of key
with pattern value
.
The key? : value
syntax will match no matter key
exists or not, and value
will be matched against map[key]
(an optional).
match map {
// matches if any only if "b" exists in `map`
{ "b": _ } => ..
// matches if and only if "b" does not exist in `map` and "a" exists in `map`.
// When matches, bind the value of "a" in `map` to `x`
{ "b"? : None, "a": x } => ..
// compiler reports missing case: { "b"? : None, "a"? : None }
}
- To match a data type
T
using map pattern,T
must have a methodop_get(Self, K) -> Option[V]
for some typeK
andV
. - Currently, the key part of map pattern must be a constant
- Map patterns are always open: unmatched keys are silently ignored
- Map pattern will be compiled to efficient code: every key will be fetched at most once
Json Pattern
When the matched value has type Json
, literal patterns can be used directly:
match json {
{ "version": "1.0.0", "import": [..] as imports } => ...
_ => ...
}
Operators
Operator Overloading
MoonBit supports operator overloading of builtin operators via methods. The method name corresponding to a operator <op>
is op_<op>
. For example:
struct T {
x:Int
} derive(Show)
fn op_add(self: T, other: T) -> T {
{ x: self.x + other.x }
}
fn main {
let a = { x: 0 }
let b = { x: 2 }
println(a + b)
}
Another example about op_get
and op_set
:
struct Coord {
mut x: Int
mut y: Int
} derive(Show)
fn op_get(self: Coord, key: String) -> Int {
match key {
"x" => self.x
"y" => self.y
}
}
fn op_set(self: Coord, key: String, val: Int) -> Unit {
match key {
"x" => self.x = val
"y" => self.y = val
}
}
fn main {
let c = { x: 1, y: 2 }
println(c)
println(c["y"])
c["x"] = 23
println(c)
println(c["x"])
}
Currently, the following operators can be overloaded:
Operator Name | Method Name |
---|---|
+ | op_add |
- | op_sub |
* | op_mul |
/ | op_div |
% | op_mod |
= | op_equal |
<< | op_shl |
>> | op_shr |
- (unary) | op_neg |
_[_] (get item) | op_get |
_[_] = _ (set item) | op_set |
_[_:_] (view) | op_as_view |
Pipe operator
MoonBit provides a convenient pipe operator |>
, which can be used to chain regular function calls:
fn init {
x |> f // equivalent to f(x)
x |> f(y) // equivalent to f(x, y)
// Chain calls at multiple lines
arg_val
|> f1 // equivalent to f1(arg_val)
|> f2(other_args) // equivalent to f2(f1(arg_val), other_args)
}
Cascade Operator
The cascade operator ..
is used to perform a series of mutable operations on
the same value consecutively. The syntax is as follows:
x..f()
x..f()..g()
is equivalent to {x.f(); x.g(); x}
.
Consider the following scenario: for a MyStringBuilder
type that has methods
like add_string
, add_char
, add_int
, etc., we often need to perform
a series of operations on the same MyStringBuilder
value:
let builder = MyStringBuilder::new()
builder.add_char('a')
builder.add_char('a')
builder.add_int(1001)
builder.add_string("abcdef")
let result = builder.to_string()
To avoid repetitive typing of builder
, its methods are often designed to
return self
itself, allowing operations to be chained using the .
operator.
To distinguish between immutable and mutable operations, in MoonBit,
for all methods that return Unit
, cascade operator can be used for
consecutive operations without the need to modify the return type of the methods.
let result =
MyStringBuilder::new()
..add_char('a')
..add_char('a')
..add_int(1001)
..add_string("abcdef")
.to_string()
Bitwise Operator
MoonBit supports C-Style bitwise operators.
Operator | Perform |
---|---|
& | land |
| | lor |
^ | lxor |
<< | op_shl |
>> | op_shr |
Error Handling
Error types
The error values used in MoonBit must have an error type. An error type can be defined in the following forms:
type! E1 Int // error type E1 has one constructor E1 with an Int payload
type! E2 // error type E2 has one constructor E2 with no payload
type! E3 { // error type E3 has three constructors like a normal enum type
A
B(Int, x~ : String)
C(mut x~ : String, Char, y~ : Bool)
}
The return type of a function can include an error type to indicate that the
function might return an error. For example, the following function div
might
return an error of type DivError
:
type! DivError String
fn div(x: Int, y: Int) -> Int!DivError {
if y == 0 {
raise DivError("division by zero")
}
x / y
}
Here, the keyword raise
is used to interrupt the function execution and return
an error.
The Default Error Type
MoonBit provides a default error type Error
that can be used when the concrete
error type is not important. For convenience, you can annotate the function name
or the return type with the suffix !
to indicate that the Error
type is
used. For example, the following function signatures are equivalent:
fn f() -> Unit! { .. }
fn f!() -> Unit { .. }
fn f() -> Unit!Error { .. }
For anonymous function and matrix function, you can annotate the keyword fn
with the !
suffix to achieve that. For example,
type! IntError Int
fn h(f: (x: Int) -> Int!, x: Int) -> Unit { .. }
fn main {
let _ = h(fn! { x => raise(IntError(x)) }, 0) // matrix function
let _ = h(fn! (x) { x => raise(IntError(x)) }, 0) // anonymous function
}
As shown in the above example, the error types defined by type!
can be used as
value of the type Error
when the error is raised.
Note that only error types or the type Error
can be used as errors. For
functions that are generic in the error type, you can use the Error
bound to
do that. For example,
pub fn unwrap_or_error[T, E : Error](self : Result[T, E]) -> T!E {
match self {
Ok(x) => x
Err(e) => raise e
}
}
Since the type Error
can include multiple error types, pattern matching on the
Error
type must use the wildcard _
to match all error types. For example,
type! E1
type! E2
fn f(e: Error) -> Unit {
match e {
E1 => println("E1")
E2 => println("E2")
_ => println("unknown error")
}
}
Handling Errors
There are three ways to handle errors:
- Append
!
after the function name in a function application to rethrow the error directly in case of an error, for example:
fn div_reraise(x: Int, y: Int) -> Int!DivError {
div!(x, y) // Rethrow the error if `div` raised an error
}
- Append
?
after the function name to convert the result into a first-class value of theResult
type, for example:
test {
let res = div?(6, 3)
inspect!(res, content="Ok(2)")
let res = div?(6, 0)
inspect!(res, content="Err(division by zero)")
}
- Use
try
andcatch
to catch and handle errors, for example:
fn main {
try {
div!(42, 0)
} catch {
DivError(s) => println(s)
} else {
v => println(v)
}
}
Here, try
is used to call a function that might throw an error, and catch
is
used to match and handle the caught error. If no error is caught, the catch
block will not be executed and the else
block will be executed instead.
The else
block can be omitted if no action is needed when no error is caught.
For example:
fn main {
try {
println(div!(42, 0))
} catch {
_ => println("Error")
}
}
The catch
keyword is optional, and when the body of try
is a simple
expression, the curly braces can be omitted. For example:
fn main {
let a = try div!(42, 0) { _ => 0 }
println(a)
}
The !
and ?
attributes can also be used on method invocation and pipe
operator. For example:
type T Int
type! E Int derive(Show)
fn f(self: T) -> Unit!E { raise E(self._) }
fn main {
let x = T(42)
try f!(x) { e => println(e) }
try x.f!() { e => println(e) }
try x |> f!() { e => println(e) }
}
However for infix operators such as +
*
that may raise an error,
the original form has to be used, e.g. x.op_add!(y)
, x.op_mul!(y)
.
Additionally, if the return type of a function includes an error type, the
function call must use !
or ?
for error handling, otherwise the compiler
will report an error.
Error Inference
Within a try
block, several different kinds of errors can be raised. When that
happens, the compiler will use the type Error
as the common error type.
Accordingly, the handler must use the wildcard _
to make sure all errors are
caught. For example,
type! E1
type! E2
fn f1() -> Unit!E1 { raise E1 }
fn f2() -> Unit!E2 { raise E2 }
fn main {
try {
f1!()
f2!()
} catch {
E1 => println("E1")
E2 => println("E2")
_ => println("unknown error")
}
}
You can also use catch!
to rethrow the uncaught errors for convenience. This
is useful when you only want to handle a specific error and rethrow others. For
example,
type! E1
type! E2
fn f1() -> Unit!E1 { raise E1 }
fn f2() -> Unit!E2 { raise E2 }
fn f() -> Unit! {
try {
f1!()
f2!()
} catch! {
E1 => println("E1")
}
}
Generics
Generics are supported in top-level function and data type definitions. Type parameters can be introduced within square brackets. We can rewrite the aforementioned data type List
to add a type parameter T
to obtain a generic version of lists. We can then define generic functions over lists like map
and reduce
.
enum List[T] {
Nil
Cons(T, List[T])
}
fn map[S, T](self: List[S], f: (S) -> T) -> List[T] {
match self {
Nil => Nil
Cons(x, xs) => Cons(f(x), map(xs, f))
}
}
fn reduce[S, T](self: List[S], op: (T, S) -> T, init: T) -> T {
match self {
Nil => init
Cons(x, xs) => reduce(xs, op, op(init, x))
}
}
Access Control
By default, all function definitions and variable bindings are invisible to other packages.
You can use the pub
modifier before toplevel let
/fn
to make them public.
There are four different kinds of visibility for types in MoonBit:
- private type, declared with
priv
, completely invisible to the outside world - abstract type, which is the default visibility for types. Only the name of an abstract type is visible outside, the internal representation of the type is hidden
- readonly types, declared with
pub(readonly)
. The internal representation of readonly types are visible outside, but users can only read the values of these types from outside, construction and mutation are not allowed - fully public types, declared with
pub(all)
. The outside world can freely construct, modify and read values of these types
Currently, the semantic of pub
is pub(all)
. But in the future, the meaning of pub
will be ported to pub(readonly)
.
In addition to the visibility of the type itself, the fields of a public struct
can be annotated with priv
,
which will hide the field from the outside world completely.
Note that struct
s with private fields cannot be constructed directly outside,
but you can update the public fields using the functional struct update syntax.
Readonly types is a very useful feature, inspired by private types in OCaml. In short, values of pub(readonly)
types can be destructed by pattern matching and the dot syntax, but cannot be constructed or mutated in other packages. Note that there is no restriction within the same package where pub(readonly)
types are defined.
// Package A
pub(readonly) struct RO {
field: Int
}
fn init {
let r = { field: 4 } // OK
let r = { ..r, field: 8 } // OK
}
// Package B
fn println(r : RO) -> Unit {
println("{ field: ")
println(r.field) // OK
println(" }")
}
fn init {
let r : RO = { field: 4 } // ERROR: Cannot create values of the public read-only type RO!
let r = { ..r, field: 8 } // ERROR: Cannot mutate a public read-only field!
}
Access control in MoonBit adheres to the principle that a pub
type, function, or variable cannot be defined in terms of a private type. This is because the private type may not be accessible everywhere that the pub
entity is used. MoonBit incorporates sanity checks to prevent the occurrence of use cases that violate this principle.
pub struct S {
x: T1 // OK
y: T2 // OK
z: T3 // ERROR: public field has private type `T3`!
}
// ERROR: public function has private parameter type `T3`!
pub fn f1(_x: T3) -> T1 { T1::A(0) }
// ERROR: public function has private return type `T3`!
pub fn f2(_x: T1) -> T3 { T3::A(0) }
// OK
pub fn f3(_x: T1) -> T1 { T1::A(0) }
pub let a: T3 // ERROR: public variable has private type `T3`!
Method system
MoonBit supports methods in a different way from traditional object-oriented languages. A method in MoonBit is just a toplevel function associated with a type constructor. Methods can be defined using the syntax fn TypeName::method_name(...) -> ...
:
enum MyList[X] {
Nil
Cons(X, MyList[X])
}
fn MyList::map[X, Y](xs: MyList[X], f: (X) -> Y) -> MyList[Y] { ... }
fn MyList::concat[X](xs: MyList[MyList[X]]) -> MyList[X] { ... }
As a convenient shorthand, when the first parameter of a function is named self
, MoonBit automatically defines the function as a method of the type of self
:
fn map[X, Y](self: MyList[X], f: (X) -> Y) -> List[Y] { ... }
// equivalent to
fn MyList::map[X, Y](xs: MyList[X], f: (X) -> Y) -> List[Y] { ... }
Methods are just regular functions owned by a type constructor. So when there is no ambiguity, methods can be called using regular function call syntax directly:
fn init {
let xs: MyList[MyList[_]] = ...
let ys = concat(xs)
}
Unlike regular functions, methods support overloading: different types can define methods of the same name. If there are multiple methods of the same name (but for different types) in scope, one can still call them by explicitly adding a TypeName::
prefix:
struct T1 { x1: Int }
fn T1::default() -> { { x1: 0 } }
struct T2 { x2: Int }
fn T2::default() -> { { x2: 0 } }
fn init {
// default() is ambiguous!
let t1 = T1::default() // ok
let t2 = T2::default() // ok
}
When the first parameter of a method is also the type it belongs to, methods can be called using dot syntax x.method(...)
. MoonBit automatically finds the correct method based on the type of x
, there is no need to write the type name and even the package name of the method:
// a package named @list
enum List[X] { ... }
fn List::length[X](xs: List[X]) -> Int { ... }
// another package that uses @list
fn init {
let xs: @list.List[_] = ...
println(xs.length()) // always work
println(@list.List::length(xs)) // always work, but verbose
println(@list.length(xs)) // simpler, but only possible when there is no ambiguity in @list
}
View
Analogous to slice
in other languages, the view is a reference to a
specific segment of collections. You can use data[start:end]
to create a
view of array data
, referencing elements from start
to end
(exclusive).
Both start
and end
indices can be omitted.
fn init {
let xs = [0,1,2,3,4,5]
let s1 : ArrayView[Int] = xs[2:]
print_array_view(s1) //output: 2345
xs[:4] |> print_array_view() //output: 0123
xs[2:5] |> print_array_view() //output: 234
xs[:] |> print_array_view() //output: 012345
// create a view of another view
xs[2:5][1:] |> print_array_view() //output: 34
}
fn print_array_view[T : Show](view : ArrayView[T]) -> Unit {
for i=0; i<view.length(); i = i + 1 {
println(view[i])
}
println("\n")
}
By implementing length
and op_as_view
method, you can also create a view for a user-defined type. Here is an example:
struct MyList[A] {
elems : Array[A]
}
struct MyListView[A] {
ls : MyList[A]
start : Int
end : Int
}
pub fn length[A](self : MyList[A]) -> Int {
self.elems.length()
}
pub fn op_as_view[A](self : MyList[A], start~ : Int, end~ : Int) -> MyListView[A] {
println("op_as_view: [\{start},\{end})")
if start < 0 || end > self.length() { abort("index out of bounds") }
{ ls: self, start, end }
}
fn init {
let ls = { elems: [1,2,3,4,5] }
ls[:] |> ignore()
ls[1:] |> ignore()
ls[:2] |> ignore()
ls[1:2] |> ignore()
}
Output:
op_as_view: [0,5)
op_as_view: [1,5)
op_as_view: [0,2)
op_as_view: [1,2)
Trait system
MoonBit features a structural trait system for overloading/ad-hoc polymorphism. Traits declare a list of operations, which must be supplied when a type wants to implement the trait. Traits can be declared as follows:
trait I {
method(...) -> ...
}
In the body of a trait definition, a special type Self
is used to refer to the type that implements the trait.
To implement a trait, a type must provide all the methods required by the trait.
Implementation for trait methods can be provided via the syntax impl Trait for Type with method_name(...) { ... }
, for example:
trait Show {
to_string(Self) -> String
}
struct MyType { ... }
impl Show for MyType with to_string(self) { ... }
// trait implementation with type parameters.
// `[X : Show]` means the type parameter `X` must implement `Show`,
// this will be covered later.
impl[X : Show] Show for Array[X] with to_string(self) { ... }
Type annotation can be omitted for trait impl
: MoonBit will automatically infer the type based on the signature of Trait::method
and the self type.
The author of the trait can also define default implementations for some methods in the trait, for example:
trait I {
f(Self) -> Unit
f_twice(Self) -> Unit
}
impl I with f_twice(self) {
self.f()
self.f()
}
Implementers of trait I
don't have to provide an implementation for f_twice
: to implement I
, only f
is necessary.
They can always override the default implementation with an explicit impl I for Type with f_twice
, if desired, though.
If an explicit impl
or default implementation is not found, trait method resolution falls back to regular methods.
This allows types to implement a trait implicitly, hence allowing different packages to work together without seeing or depending on each other.
For example, the following trait is automatically implemented for builtin number types such as Int
and Double
:
trait Number {
op_add(Self, Self) -> Self
op_mul(Self, Self) -> Self
}
When declaring a generic function, the type parameters can be annotated with the traits they should implement, allowing the definition of constrained generic functions. For example:
trait Number {
op_add(Self, Self) -> Self
op_mul(Self, Self) -> Self
}
fn square[N: Number](x: N) -> N {
x * x // same as `x.op_mul(x)`
}
Without the Number
requirement, the expression x * x
in square
will result in a method/operator not found error. Now, the function square
can be called with any type that implements Number
, for example:
fn main {
println(square(2)) // 4
println(square(1.5)) // 2.25
println(square({ x: 2, y: 3 })) // {x: 4, y: 9}
}
trait Number {
op_add(Self, Self) -> Self
op_mul(Self, Self) -> Self
}
fn square[N: Number](x: N) -> N {
x * x // same as `x.op_mul(x)`
}
struct Point {
x: Int
y: Int
} derive(Show)
impl Number for Point with op_add(p1, p2) {
{ x: p1.x + p2.x, y: p1.y + p2.y }
}
impl Number for Point with op_mul(p1, p2) {
{ x: p1.x * p2.x, y: p1.y * p2.y }
}
MoonBit provides the following useful builtin traits:
trait Eq {
op_equal(Self, Self) -> Bool
}
trait Compare {
// `0` for equal, `-1` for smaller, `1` for greater
op_equal(Self, Self) -> Int
}
trait Hash {
hash(Self) -> Int
}
trait Show {
// writes a string representation of `Self` into a `Logger`
output(Self, Logger) -> Unit
to_string(Self) -> String
}
trait Default {
default() -> Self
}
Involke trait methods directly
Methods of a trait can be called directly via Trait::method
. MoonBit will infer the type of Self
and check if Self
indeed implements Trait
, for example:
fn main {
println(Show::to_string(42))
println(Compare::compare(1.0, 2.5))
}
Trait implementations can also be involked via dot syntax, with the following restrictions:
- if a regular method is present, the regular method is always favored when using dot syntax
- only trait implementations that are located in the package of the self type can be involked via dot syntax
- if there are multiple trait methods (from different traits) with the same name available, an ambiguity error is reported
- if neither of the above two rules apply, trait
impl
s in current package will also be searched for dot syntax. This allows extending a foreign type locally.- these
impl
s can only be called via dot syntax locally, even if they are public.
- these
The above rules ensures that MoonBit's dot syntax enjoys good property while being flexible. For example, adding a new dependency never break existing code with dot syntax due to ambiguity. These rules also make name resolution of MoonBit extremely simple: the method called via dot syntax must always come from current package or the package of the type!
Here's an example of calling trait impl
with dot syntax:
struct MyType { ... }
impl Show for MyType with ...
fn main {
let x : MyType = ...
println(x.to_string()) // ok
}
Access control of methods and trait implementations
To make the trait system coherent (i.e. there is a globally unique implementation for every Type: Trait
pair),
and prevent third-party packages from modifying behavior of existing programs by accident,
MoonBit employs the following restrictions on who can define methods/implement traits for types:
- only the package that defines a type can define methods for it. So one cannot define new methods or override old methods for builtin and foreign types.
- only the package of the type or the package of the trait can define an implementation.
For example, only
@pkg1
and@pkg2
are allowed to writeimpl @pkg1.Trait for @pkg2.Type
.
The second rule above allows one to add new functionality to a foreign type by defining a new trait and implementing it. This makes MoonBit's trait & method system flexible while enjoying good coherence property.
Visibility of traits and sealed traits
There are four visibility for traits, just like struct
and enum
: private, abstract, readonly and fully public.
Private traits are declared with priv trait
, and they are completely invisible from outside.
Abstract trait is the default visibility. Only the name of the trait is visible from outside, and the methods in the trait are not exposed.
Readonly traits are declared with pub(readonly) trait
, their methods can be involked from outside, but only the current package can add new implementation for readonly traits.
Finally, fully public traits are declared with pub(open) trait
, they are open to new implementations outside current package, and their methods can be freely used.
Currently, pub trait
defaults to pub(open) trait
. But in the future, the semantic of pub trait
will be ported to pub(readonly)
.
Abstract and readonly traits are sealed, because only the package defining the trait can implement them.
Implementing a sealed (abstract or readonly) trait outside its package result in compiler error.
If you are the owner of a sealed trait, and you want to make some implementation available to users of your package,
make sure there is at least one declaration of the form impl Trait for Type with ...
in your package.
Implementations with only regular method and default implementations will not be available outside.
Here's an example of abstract trait:
trait Number {
op_add(Self, Self) -> Self
op_sub(Self, Self) -> Self
}
fn add[N : Number](x : X, y: X) -> X {
Number::op_add(x, y)
}
fn sub[N : Number](x : X, y: X) -> X {
Number::op_sub(x, y)
}
impl Number for Int with op_add(x, y) { x + y }
impl Number for Int with op_sub(x, y) { x - y }
impl Number for Double with op_add(x, y) { x + y }
impl Number for Double with op_sub(x, y) { x - y }
From outside this package, users can only see the following:
trait Number
fn op_add[N : Number](x : N, y : N) -> N
fn op_sub[N : Number](x : N, y : N) -> N
impl Number for Int
impl Number for Double
The author of Number
can make use of the fact that only Int
and Double
can ever implement Number
,
because new implementations are not allowed outside.
Automatically derive builtin traits
MoonBit can automatically derive implementations for some builtin traits:
struct T {
x: Int
y: Int
} derive(Eq, Compare, Show, Default)
fn main {
let t1 = T::default()
let t2 = { x: 1, y: 1 }
println(t1) // {x: 0, y: 0}
println(t2) // {x: 1, y: 1}
println(t1 == t2) // false
println(t1 < t2) // true
}
Trait objects
MoonBit supports runtime polymorphism via trait objects.
If t
is of type T
, which implements trait I
,
one can pack the methods of T
that implements I
, together with t
,
into a runtime object via t as I
.
Trait object erases the concrete type of a value,
so objects created from different concrete types can be put in the same data structure and handled uniformly:
trait Animal {
speak(Self) -> Unit
}
type Duck String
fn Duck::make(name: String) -> Duck { Duck(name) }
fn speak(self: Duck) -> Unit {
println(self._ + ": quack!")
}
type Fox String
fn Fox::make(name: String) -> Fox { Fox(name) }
fn Fox::speak(_self: Fox) -> Unit {
println("What does the fox say?")
}
fn main {
let duck1 = Duck::make("duck1")
let duck2 = Duck::make("duck2")
let fox1 = Fox::make("fox1")
let animals = [ duck1 as Animal, duck2 as Animal, fox1 as Animal ]
let mut i = 0
while i < animals.length() {
animals[i].speak()
i = i + 1
}
}
Not all traits can be used to create objects. "object-safe" traits' methods must satisfy the following conditions:
Self
must be the first parameter of a method- There must be only one occurrence of
Self
in the type of the method (i.e. the first parameter)
Users can define new methods for trait objects, just like defining new methods for structs and enums:
trait Logger {
write_string(Self, String) -> Unit
}
trait CanLog {
log(Self, Logger) -> Unit
}
fn Logger::write_object[Obj : CanLog](self : Logger, obj : Obj) -> Unit {
obj.log(self)
}
// use the new method to simplify code
impl[A : CanLog, B : CanLog] CanLog for (A, B) with log(self, logger) {
let (a, b) = self
logger
..write_string("(")
..write_object(a)
..write_string(", ")
..write_object(b)
.write_string(")")
}
Test Blocks
MoonBit provides the test code block for writing test cases. For example:
test "test_name" {
assert_eq!(1 + 1, 2)
assert_eq!(2 + 2, 4)
}
A test code block is essentially a function that returns a Unit
but may throws a String
on error, or Unit!String
as one would see in its signature at the position of return type. It is called during the execution of moon test
and outputs a test report through the build system. The assert_eq
function is from the standard library; if the assertion fails, it prints an error message and terminates the test. The string "test_name"
is used to identify the test case and is optional. If it starts with "panic"
, it indicates that the expected behavior of the test is to trigger a panic, and the test will only pass if the panic is triggered. For example:
test "panic_test" {
let _ : Int = Option::None.unwrap()
}
Doc Comments
Doc comments are comments prefix with ///
in each line in the leading of toplevel structure like fn
,let
,enum
,struct
,type
. The doc comments contains a markdown text and several pragmas.
/// Return a new array with reversed elements.
///
/// # Example
///
/// ```
/// reverse([1,2,3,4]) |> println()
/// ```
fn reverse[T](xs : Array[T]) -> Array[T] {
...
}
Pragmas
Pragmas are annotations inside doc comments. They all take the form /// @word ...
. The word indicates the type of pragma and is followed optionally by several word or string literals. Pragmas do not normally affect the meaning of programs. Unrecognized pragmas will be reported as warnings.
-
Alert Pragmas
Alert pragmas in doc comments of functions will be reported when those functions are referenced. This mechanism is a generalized way to mark functions as
deprecated
orunsafe
.It takes the form
@alert category "alert message..."
.The category can be an arbitrary identifier. It allows configuration to decide which alerts are enabled or turned into errors.
/// @alert deprecated "Use foo2 instead" pub fn foo() -> Unit { ... } /// @alert unsafe "Div will cause an error when y is zero" pub fn div(x: Int, y: Int) -> Int { ... } fn main { foo() // warning: Use foo2 instead div(x, y) |> ignore // warning: Div will cause an error when y is zero }
Special Syntax
TODO syntax
The todo
syntax (...
) is a special construct used to mark sections of code that are not yet implemented or are placeholders for future functionality. For example:
fn todo_in_func() -> Int {
...
}
MoonBit's build system
The introduction to the build system is available at MoonBit's Build System Tutorial.