A Tour of MoonBit for Beginners
This guide is intended for newcomers, and it's not meant to be a 5-minute quick tour. This article tries to be a succinct yet easy to understand guide for those who haven't programmed in a way that MoonBit enables them to, that is, in a more modern, functional way.
See the General Introduction if you want to straight delve into the language.
Installation
the Extension
Currently, MoonBit development support are through VS Code extension. Navigate to VS Code Marketplace to download MoonBit language support.
the toolchain
(Recommended) If you've installed the extension above, the runtime can be directly installed by running 'Install moonbit toolchain' in the action menu and you may skip this part:
We also provide an installation script: Linux & macOS users can install via
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
For Windows users, powershell is used:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser; irm https://cli.moonbitlang.com/install/powershell.ps1 | iex
This automatically installs MoonBit in $HOME/.moon
and adds it to your PATH
.
If you encounter moon
not found after installation, try restarting your terminal or vscode to let the environment variable take effect.
Do notice that MoonBit is not production-ready at the moment, it's under active development. To update MoonBit, just run the commands above again.
Running moon help
gives us a bunch of subcommands. But right now the only commands we need are build
run
and new
.
To create a project (or module, more formally), run moon new
. You will be greeted with a creation wizard, filling up all the info and we get
my-project
├── LICENSE
├── moon.mod.json
├── README.md
└── src
├── lib
│ ├── hello.mbt
│ ├── hello_test.mbt
│ └── moon.pkg.json
└── main
├── main.mbt
└── moon.pkg.json
This resembles a typical MoonBit module structure. Try running moon run src/main
.
Now, we can get started.
Start Writing
In our tour, we will write all of the codes below in main.mbt
. As you may have guessed, the main
function within the main
package is the main entrance of a program.
For a thorough introduction, please take a look at our build system tutorial.
Variables
Variables are defined with let
:
let e = 2.718281828459045 // double
let int_min = -2147483648 // int
let int_max : Int = 2147483647 // explicit type annotation
let tuple = (1, 2) // 2-tuple
fn init {
let array = [1, 2, 3, 4, 5]
// array = [4, 5, 6, 7, 8] // WRONG: let creates immutable bindings
let mut mut_array = [1, 2, 3, 4, 5]
mut_array = [4, 5, 6, 7, 8]
println(mut_array)
}
MoonBit is a strictly typed language with type inference. In the example above, let
binds (we prefer the word bind to assign) a symbol to a value. The symbol is inferred
to have the same type as the value. Hover over any of the symbols to check its type.
By default, the let
- binding creates an immutable reference to a value. That is, you cannot change the symbol to reference something else without rebinding it (using let
). Otherwise one should use let mut
.
Function
Function is just a piece of code that takes some inputs and produce a result. We may define a function using the keyword fn
(function name in MoonBit should not begin with uppercase letters A-Z):
fn identity[T](x : T) -> T {
// `Identity` won't work as it violates naming convention
x
}
In this example, we provide types explicitly. Notice how it differs from traditional C-like languages
which uses prefix type notation T x
, here we use postfix type notation x: T
(Formally, we call it type annotation).
We write a arrow ->
before the return type to show the nature of a function: a map from some types to some other types. Formally, we call this syntax trailing return type (languages such as C++, Rust, Swift, etc have this syntax as well).
The word expression is loosely used. Intuitively, An expression is something with a value we care about.
Consequently, a function type is denoted (S) -> T
where S
(within parenthesis) is the parameter type and T
is the return type.
Functions in MoonBit are first-class, meaning it's always possible to pass functions around if you
get the type right:
fn compose[S, T, U](f : (T) -> U, g : (S) -> T) -> (S) -> U {
let composition = fn(x : S) { f(g(x)) } // returns a composition of `f` and `g`
// moonbit also provides the pipe `|>` operator,
// similar to a lot of functional languages.
fn(x : S) { g(x) |> f } // equivalent
}
Languages nowadays have something called lambda expression. Most languages implement it as a mere syntactic sugar. A lambda expression is really just a anonymous closure, this, is resembled in our MoonBit's syntax:
a closure only captures variables in its surroundings, together with its bound variable, that is, having the same indentation level (suppose we've formatted the code already).
fn foo() -> Int {
fn inc(x) { x + 1 } // named as `inc`
(fn (x) { x + inc(2) })(6) // anonymous, a so-called 'lambda expression'
// function automatically captures the result of the last expression
}
foo() // => 9
Now we've learned the very basic, let's learn the rest by coding.
Implementing List
enum type
A linked list is a series of node whose right cell is a reference to its successor node. Sounds recursive? Because it is. Let's define it that way using MoonBit:
enum List[T] {
Nil // base case: empty list
Cons(T, List[T]) // an recursive definition
}
The enum
type works like any enum
from traditional OO languages. However, let's refrain from using the OO-term case
, we'll use constructor from now on. We may read the above code as
the type
List[T]
can be constructed from the constructorNil
orCons
, the former represents an empty list; the latter carries some data of typeT
and the rest of the list.
The square bracket used here denotes a polymorphic (generic) definition, meaning a list of something of type T
. Should we instantiate T
with a concrete type like Int
, we define a list containing integers.
Another datatype frequently used in MoonBit is our good old Struct
, which works like you would expect. Let's create a list of User
using the definition above and Struct
:
struct User {
id : Int
name : String
// by default the properties/fields of a struct is immutable.
// the `mut` keyword works exactly the way we've mentioned before.
mut email : String
} derive(Show)
// a method of User is defined by passing a object of type User as self first.
// just like what you would do in Python.
// Note that methods may only be defined within the same package the type is in.
// We may not define methods for foreign types directly
fn greetUser(self : User) -> String { // a method of struct/type/class `User`
let id = self.id
let name = self.name
"Greetings, \{name} of id \{id}" // string interpolation
}
// construct a User object.
let evan : User = { id: 0, name: "Evan", email: "someone@example.com" }
// we use a shorthand by duplicating evan's information
// and replacing w/ someone elses' email.
let listOfUser : List[User] = Cons(evan, Cons({ ..evan, email: "someoneelse@example.com" }, Nil))
Another datatype is type
, a specific case of enum
type. type
can be thought as a wrapper
around an existing type, keeping the methods of String
but allows additional methods to be defined.
Through this we extends the method definition of a foreign type without actually
modifying it. Consider the type of name
in User
,
we may define it as
type UserName String // a newtype `UserName` based on `String`
// defining a method for UserName is allowed but not String.
fn is_blank(self : UserName) -> Bool {
// use `.0` to access its basetype String
// iter() creates a *internal iterator*
// which provides a functional way to iterate on sequences.
// find_first short circuits on the first `true` i.e. non-blank character
let res = self.0.iter().find_first(
fn(c) { if c == ' ' { false } else { true } },
)
match res {
Some(_) => false
// found NO non-blank character, thus it's a blank string.
None => true
}
}
enum
, struct
and newtype
are the 3 ways to define a datatype.
There isn't class
in MoonBit, nor does it need that.
the derive
keyword is like Java's implements
. Here Show
is a trait which
indicates a type is printable. So what is a trait?
Trait
A trait (or type trait) is what we would call an interface
in traditional OO-languages.
println(evan)
would print {id: 0, name: "Evan", email: "someone@example.com"}
. As User
consists
of builtin types Int
String
, which already implements Show
.
Therefore we do not need to implement it explicitly. Let's implement our own
trait Printable
by implementing to_string()
:
trait Printable {
to_string(Self) -> String
}
fn to_string(self : User) -> String {
(self.id, self.name, self.email).to_string()
} // now `Printable` is implemented
fn to_string[T : Printable](self : List[T]) -> String {
let string_aux = to_string_aux(self)
// function arguments can have label
"[" + string_aux.substring(end=string_aux.length() - 1) + "]"
}
// polymorphic functions have to be toplevel.
fn to_string_aux[T : Printable](self : List[T]) -> String {
match self {
Nil => ""
Cons(x, xs) => "\{x} " + to_string_aux(xs)
}
}
listOfUser.to_string()
// => [(0, Evan, someone@example.com) (0, Evan, someoneelse@example.com)]
We use <T extends Printable>
in Java to constrain the type of list element to make sure objects of type
T
can be printed, similarly, in MoonBit we would write [T: Printable]
.
Pattern Matching
In the example above we use the match
expression, a core feature of MoonBit
(and many other functional programming languages.) In short, we use pattern matching
to destructure (to strip the encapsulation of) a structure.
We may express the above match
code as
if
self
is constructed withNil
(an empty list), we return""
; otherwise ifself
is constructed withCons(x,xs)
(a non-empty list) we printx
and rest of the list. Wherex
is the head of theself
andxs
being the rest.
Intuitively, we extract x
and xs
(they are bound in situ) from self
using pattern matching.
Let's implement typical list operations such as map
reduce
zip
:
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))
}
}
fn zip[T](self : List[T], other : List[T]) -> List[T] {
match (self, other) {
(Nil, _) => Nil // we use underscore to ignore the value we don't care
(_, Nil) => Nil
(Cons(x, xs), Cons(y, ys)) => Cons(x, Cons(y, zip(xs, ys)))
}
}
Now we have a somewhat usable List
type. Realistically, we always prefer the builtin Array
which is much more efficient.
Pattern matching can be used in let
as well. In greetUser()
, instead of writing
2 let
's, we may write
fn greetUserAlt(self : User) -> String {
// extract `id` `name` from `self` of type User. ignores email.
let { id: id, name: name, email: _ } = self
// equivalent, but ignores the rest.
let { id, name, .. } = self
"Greetings, \{name} of id \{id}"
}
Iteration
Finally, let's talk about the major point of every OO-language: looping.
Although we've been using recursion most of the times,
MoonBit is designed to be multi-paradigm,
thus it retains C-style imperative for
while
loop.
Additionally, MoonBit provides a more interesting loop construct, the functional loop. For example the Fibonacci number can be calculated by
fn fib(n : Int) -> Int {
loop n, 0, 1 { // introduces 3 loop variables: `n` `a = 0` `b = 1`
// pattern matching is available in `loop`
0, a, b => a // what can be constructed from 0 -- Only 0 it self!
// assign `b` to `a`, `(a + b)` to `b`, decrease counter `n`
n, a, b => continue n - 1, b, a + b
}
}
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(fib) // => [1,1,2,3,5,8,13,21,34,55]
Semantic-wise, the loop
construct focuses more on the transition of each state, providing
better readability, preserving recursive flavor and same performance without writing tail-recursion explicitly.
Closing
At this point, we've learned about the very basic and most not-so-trivial features of MoonBit, yet MoonBit is a feature-rich, multi-paradigm programming language. After making sure that you are comfortable with the basics of MoonBit, we suggest that you look into some interesting examples to get a better hold of MoonBit.