iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🐇

Recent Changes in Moonbit (April 2024 – August 2024)

に公開

The Moonbit language, which I've been following as a WebAssembly-first language, has undergone several major changes since I last wrote about it. So, I'll be introducing some of the new language features and syntax.

https://zenn.dev/mizchi/articles/introduce-moonbit

I am looking at the weekly updates from 4/15 to 8/22 at https://www.moonbitlang.com/weekly-updates/.

Since there probably aren't many people actually using it in Japan yet, it's better to read this with the intent of grasping the direction of its evolution by looking at the changes, rather than looking for changes for practical use.

Array literals are now mutable by default

This is the biggest change since I last used it. (This broke almost all existing code.)

The mutable @vec.Vec[T] has been renamed to Array[T], and array literals like let x = [] (where the type is omitted) have changed from the immutable List[T] to Array[T].

fn main {
  let x = [1, 2, 3] // Array[Int]
  x.push(4)
}

This can be described as a change that shifts the language from a functional language toward a general-purpose programming language.
Beyond this, there are many changes overall intended for general-purpose programming.

Keyword Arguments

It's now possible to create keyword arguments by adding a tilde ~x in the argument declaration. Default arguments are also supported.

pub fn f(v : Int, ~x : Int, ~y : Int, ~z : Int = 0) -> Unit {
  println(v + x + y + z)
}

fn run_f() -> Unit {
  let _ = f(1, x=2, y=3)
  let _ = f(1, x=2, y=3, z=4)
}

When first introduced, the caller had to write f(1, ~x=1, ~y=2), but later, the ~ on the caller side became optional.

Exception Handling

https://www.moonbitlang.com/weekly-updates/2024/08/05/weekly-08-05

raise, try, catch, etc., have been introduced. This is another change that impacts the core of the language.

// Use type! when declaring an error type
type! E1 Int
fn f1() -> Unit!E1 { .. }
fn f2() -> Unit!Error { .. }
fn main {
  try f1!() { E1(errno) => println(errno) }  // this error handling is complete
  try f2!() {
    E1(errno) => println(errno)
    _ => println("unknown error")
  }
}

try looks more like a specialized pattern match against raise rather than a global exit exception handling mechanism.

fn test_try() -> Unit {
  fn f() -> Int!String {
    raise "err"
  }
  try { 
    println("this is try body")
    f!()
  } except {
    err => println(err)
  } else {
    val => println(val)
  }
}

It seems to make the declaration and handling of side effects mandatory, similar to checked exceptions in Java.

Currently, Result<T, E> still exists, so I'm curious which one they intend to make the mainstream. Looking at the standard library implementation, they seem to be generally leaning towards the Unit!Error type. Preferences might vary.

Update:

Simplified Error Handling Syntax: The syntax for capturing potential errors in function calls has been updated from f(x)!! to f?(x), which returns a Result type.

JSON Literal

First-class support for JSON types and syntax has been introduced.

https://www.moonbitlang.com/weekly-updates/2024/07/29/weekly-07-29

// You can pattern match on the json type
fn json_process(x : @json.JsonValue) -> Double {
  match x {
    {
      "outer": {
        "middle": { "inner": [{ "x": Number(x) }, { "y": Number(y) }] },
      },
    } => x + y
    _ => 0
  }
}

fn main {
  // Cast as json.
  let x : @json.JsonValue = {
    "outer": { "middle": { "inner": [{ "x": 42 }, { "y": 24 }] } },
    "another_field": "string value",
  }
  json_process(x) |> println
}

Since it collapses into the JsonValue type, it can't be handled as JSON with assumed types like in TypeScript, but it seems convenient for processing JSON passed from the outside.

For type mapping like serde, one would want serializers/deserializers.

WebAssembly.Memory Initialization Method

This only concerns those using the wasm-gc backend, but whereas previously the shared memory was fixed as instance.exports['moonbit.memory'], it is now possible to import memory or change the name of the exported memory via configuration.

main/moon.pkg.json

{
  "import": [],
  "link": {
    "wasm-gc": {
      "export-memory-name": "memory",
      "exports": ["run"]
    }
  }
}
let memory: WebAssembly.Memory;
// ...
const obj = await WebAssembly.instantiate(wasm, imports);
const exports = obj.instance.exports as any;
memory = exports.memory as WebAssembly.Memory;

Alternatively, you can bind the import path using "import-memory".

{
  "link": {
    "wasm-gc": {
      "import-memory": {
        "module": "env",
        "name": "memory"
      },
      "exports": ["run"]
    }
  }
}

(TODO: Try it myself)

Future Expectations

These are not the only changes, but they are the most prominent ones.

I have been consistently looking forward to the introduction of asynchronous processing like async/await. I believe this would allow it to become a standalone language that doesn't depend on the host, acting as a true general-purpose programming language...

Below is an example of an attempt to use it with Cloudflare Workers:

https://github.com/mizchi/mbt-cfw-example/blob/main/slide.md

Since it is a young language, the specifications change rapidly, and things break quickly. I don't mean this in a negative way; I think it's the kind of trial and error that's only possible at this stage, and I hope it builds a solid foundation for the future.

Discussion