This is translation of https://zenn.dev/mizchi/articles/introduce-moonbit from japanese
Recently, I came across a language called MoonBit, and the more I looked into it, the more I liked it, so I'd like to introduce it.
Grammatically, it's like Rust with garbage collection, and it compiles to WebAssembly. It seems particularly designed for running on CDN Edge Workers, which I love.
Note: Since it's a young language, the language specifications are expected to change significantly. This information is accurate as of now.
- Pros
- Can mostly be seen as Rust with garbage collection
- Easy to catch up on the syntax
- No need to worry about the complexity of lifetimes
- Already has an ecosystem with a VS Code extension and package manager
- Can mostly be seen as Rust with garbage collection
- Cons
- Still unstable / source code might not be published for a while
- Currently lacks learning resources and packages, requiring significant effort from the writer
I've been searching for an alternative language to TypeScript for front-end use.
The reason being, I believe there are inherent limitations to extending TypeScript, which is based on JavaScript. These are some issues I've always had:
- No distinction between integers and floating-point numbers; the Number type is the standard
- This makes it difficult to compile for wasm or integrate with vanilla JS
- Performance optimization requires understanding the quirks of JIT
- Despite having a type system, it's discarded at transpilation time, not utilized by the compiler, which is a waste
- Due to backward compatibility, old syntax cannot be discarded, leading to some rigid specifications
- TC39 is doing a great job, but still...
- No pattern matching
- Using objects as substitutes for records, resulting in forced practices like adding a
type: "datatype"
property to everything
Rust would be a better foundation for extensions. However, it can be pretty tough to use for application layers like GUIs.
To understand the difficulty of Rust Wasm, let's look at an example using requestAnimationFrame
.
#[wasm_bindgen(start)]
fn run() -> Result<(), JsValue> {
let f = Rc::new(RefCell::new(None));
let g = f.clone();
let mut i = 0;
*g.borrow_mut() = Some(Closure::new(move || {
if i > 300 {
body().set_text_content(Some("All done!"));
let _ = f.borrow_mut().take();
return;
}
i += 1;
let text = format!("requestAnimationFrame has been called {} times.", i);
body().set_text_content(Some(&text));
request_animation_frame(f.borrow().as_ref().unwrap());
}));
request_animation_frame(g.borrow().as_ref().unwrap());
Ok(())
}
This isn't to say Rust is a bad language, but in front-end GUIs with frequent event handlers, the complexity and nuisance of lifetime management are glaringly apparent.
I've previously discussed this in a stream with koba789 on YouTube.
https://www.youtube.com/watch?v=Cij3CUJmLXI
Languages like Zig or Grain aren't bad for wasm, but they're not primarily focused on wasm, so there were some impracticalities.
So, let's try using MoonBit
You can try it on VS Code in the browser.
For local installation:
https://www.moonbitlang.com/download/
Installing the VS Code extension allows for smooth local development.
https://marketplace.visualstudio.com/items?itemName=moonbit.moonbit-lang
The official completion extension is also useful, incorporating concepts like local and global sampling at the language level for Copilot-like completions.
https://marketplace.visualstudio.com/items?itemName=moonbit.moonbit-ai
Let's create and run a project.
$ moon new hello
$ cd hello
$ moon run main
hello, world
Now, let's write in main/main.mbt
.
It's mostly like Rust. There are no lifetime traits since it's a GC language.
// function
fn add(a: Int, b: Int) -> Int {
return a + b
}
// generics
fn self[T](v: T) -> T {
return v
}
// generics trait
fn _lge[X : Compare](a: X, b: X) -> Bool {
return a >= b
}
// data structure
enum T {
A
B
}
struct Point {
x: Int
y: Int
} derive(Debug)
fn distance(self: Point, other: Point) -> Double {
sqrt((self.x * other.x + self.y * other.y).to_double())
}
// Generics and derived trait
struct Point3d[N] {
x: N
y: N
z: N
} derive(Debug)
// newtype
// type Point3dInt Point3d[Int]
// trait
trait Animal {
speak(Self) -> Unit
}
struct Duck {
name: String
}
fn speak(self: Duck) -> Unit {
let name = self.name
println("\(name): Quak!")
}
fn main {
println("Hello, World!")
// variable
let _x = "hello"
let mut y: Int = 2
let _multiline_text =
#| hello
#| world
#| multiline
y = 3
let p = Point::{x: 1, y: 2}
println(p.distance(Point::{x: 3, y: 4}))
debug(p)
// function and call
let _ = add(1, 2)
// pipeline
let _ = 1 |> add(2) |> add(4)
// call ./foo.mbt: pub fn foo() -> Int { 1 }
let _ = foo()
// list and iterator
let list = [1, p.x, p.y]
let mapped = list.map(fn (x) -> Int { return x + 1 })
println(mapped)
// inference
let _: Int = self(1) // as Int
// trait
let duck = Duck::{name: "Donald"} as Animal
duck.speak()
// if else
if y == 2 {
println("y is 1")
} else {
println("y is not 1")
}
// match
let mut m = T::A
m = T::B
let _ = match m {
T::A => 1
T::B => 2
}
// for
for i = 1; i < 5; i = i + 1 {
print(i)
}
println("")
// while
let mut i = 0
while i > 0 {
i = i - 1
}
// scope
{
let x = 1
println(x)
}
}
// inline test
test {
let a = 1
let b = 2
let c = add(a, b)
@assertion.assert_eq(c, 3)?
}
Apart from differences in macros, derive, and generics, it's quite similar to Rust. Syntax highlighting also borrows from Rust.
The structure, pattern matching, and pipeline operator feel right. Unlike TypeScript, if it compiles, it runs, giving a sense of security. The completions also feel great once you write in it.
Of course, MoonBit's language features are not limited to this, but if you're familiar with other programming languages, this should give you a basic understanding.
Since MoonBit compiles to wasm, it can be executed in the browser.
$ moon build
<html lang="en">
<body>
<canvas id="canvas" width="150" height="150"></canvas>
</body>
<script>
let memory;
const [log, flush] = (() => {
let buffer = [];
function flush() {
if (buffer.length > 0) {
console.log(new TextDecoder("utf-16").decode(new Uint16Array(buffer).valueOf()));
buffer = [];
}
}
function log(ch) {
if (ch == '\n'.charCodeAt(0)) { flush(); }
else if (ch == '\r'.charCodeAt(0)) { /* noop */ }
else { buffer.push(ch); }
}
return [log, flush]
})();
const importObject = {
spectest: {
print_char: log
},
js_string: {
new: (offset, length) => {
const bytes = new Uint16Array(memory.buffer, offset, length);
const string = new TextDecoder("utf-16").decode(bytes);
return string
},
empty: () => { return "" },
log: (string) => { console.log(string) },
append: (s1, s2) => { return (s1 + s2) },
}
};
WebAssembly.instantiateStreaming(fetch("/target/wasm/release/build/main/main.wasm"), importObject).then(
(obj) => {
memory = obj.instance.exports["moonbit.memory"];
obj.instance.exports._start();
flush();
}
)
</script>
</html>
When in doubt, the official examples usually cover most scenarios.
https://github.com/moonbitlang/moonbit-docs/tree/main/examples
Discussions often provide a wealth of information.
https://discuss.moonbitlang.com
awesome-moonbit doesn't have a lot yet, but it's worth checking out.
https://github.com/moonbitlang/awesome-moonbit
MoonBit is strongly focused on Rust, AI, and Wasm for Edge Workers. I'm already a fan of this direction.
https://www.moonbitlang.com/blog/moonbit-ai
AI-integrated completion is an interesting feature, and the toolchain is already well-established, making it pleasant to write in.
Firstly, there are simply not enough packages yet, which is understandable given its newness.
The explicit scoping in moon.pkg.json
or moon.mod.json
means there's no direct equivalent to JS's import
or Rust's use
, which can make file scopes a bit challenging to grasp.
Despite the hype around AI-powered completions, they often lean too heavily on Rust, leading to irrelevant suggestions. Consequently, I've turned off Copilot.
{
"github.copilot.editor.enableCodeActions": false,
"github.copilot.editor.enableAutoCompletions": false
}
MoonBit is still too new, and I haven't used it for large projects outside of game-like scenarios with few assumptions. However, the potential is exciting, especially for technologies with minimal prerequisites like CDN edge workers, making them ideal for new tech.
Official examples include CF Worker demos.
https://github.com/moonbitlang/moonbit-docs/tree/main/examples/cf_worker
pub fn fib(n : Int) -> Int64 {
loop 0L, 1L, n {
a, _, 0 => a
a, b, n => continue b, a + b, n - 1
}
}
import wasm from '../target/wasm-gc/release/build/hello.wasm';
const module = await WebAssembly.instantiate(wasm);
module.exports._start();
export interface Env {}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return new Response(`Hello World! ${module.exports.fib(10)}`);
}
};
Currently, it seems limited to numerical computations, but as the ecosystem grows, broader applications should become feasible. Asynchronous runtime support seems necessary as well.
Despite potential significant changes on the horizon, MoonBit has the potential to dominate its niche, a future I'm eagerly anticipating.