はじめに
CNCFのCloud Native Landscapeを見ていたところ、WASM関連技術のページにいくつか新しいプログラミング言語が載っていました。
今回は、中でも個人的に気になったMoonBitについて紹介したいと思います。
触ってみる
MoonBit(moonbitlang)は執筆時点(2023/9)ではOSS化されておらず、オンライン上のPlayground(VSCode環境)でのみ実行できます。issueによると、安定版ができる2024年秋ごろにOSS化するそうです1。
Playgroundにはサンプルコードがいくつかあるため、見るだけで文法の雰囲気がつかめます。当記事でも、文法の紹介でいくつか引用します。
文法
※2023/9時点の文法です。今後大きく変わる可能性があります!
個人的な印象ですが、GoとRustを足して2で割ったような見た目です。
型
関数や変数では型を明示する必要があります。
func twice(x: Int) -> Int {
let n: Int = 2
x * n
}
組み込み型の他、それを組み合わせたstruct
, enum
, tuple
を定義できます。
struct
// pubを付けたときのみパッケージ外から参照可能
pub struct T {
// フィールドには型を明示
x:Int
}
構造体は以下のように初期化します。型は推論できるので指定不要です。
let a = { x:0 }
tuple
let x = (1, 2) // (Int, Int) に推論される
println(x.0) // 1
println(x.1) // 2
enum
単なる整数ではなく代数的データ型のため、要素ごとに別々の型の値を持たせることができます。
// Optional型の実装(Tはジェネリクスの型パラメータ)
enum Opt[T] {
None // 中身が無い
Some (T) // 中身がある
}
パターンマッチを使うことで、分岐や内部の値を安全に扱うことができます。
// 値の取り出し
// 値を持たない場合
let e: Opt[Unit] = Opt::None
match e {
Some(_) => println("error")
None => println("ok")
}
// 値を持つ場合中身を取り出す
let e2 = Opt::Some(2)
match e2 {
Some(x) => println(x)
_ => println("error")
}
interface
Goのようにメソッドを実装するだけで暗黙的にinterfaceの実装となります。
interface Show {
to_string(Self) -> String
}
struct MyStruct {}
func to_string(self: MyStruct) -> String {
"MyStruct"
}
func init() {
let ms: MyStruct = {}
// printlnの引数はShow
println(ms) // MyStruct
}
関数
以下の特徴を持ちます。
- 第一級関数
- 最後の行は式である必要がある(暗黙的に
return
となる) - 再帰はコンパイル時にループになる
- トップレベルの処理は
init
に実装(所謂main関数)
match
や if
が式であることと相まって、式指向なプログラミングができるのが嬉しいです。
func fib2(num : Int) -> Int {
// ローカル関数の引数の型は推論される
fn aux(n, acc1, acc2) {
match n {
0 => acc1
1 => acc2
_ => aux(n - 1, acc2, acc1 + acc2)
}
}
aux(num, 0, 1)
}
メソッド
MoonBitではメソッドは単なる関数(統一関数呼び出し構文)として実装されています。
A method is defined as a top-level function with self as the name of its first parameter. The self parameter will be the subject of a method call. For example, l.map(f) is equivalent to map(l, f). Such syntax enables method chaining rather than heavily nested function calls.
メソッドは、第一引数の名前が
self
であるトップレベル関数として定義されます。self引数はメソッド呼び出しの主語(訳注: レシーバのこと?)になります。例えば、l.map(f)
はmap(l, f)
と等価です。この構文によって、とても深い入れ子の関数呼び出しの代わりにメソッドチェーンを使うことができます。
struct MyStruct {}
func to_string(self: MyStruct) -> String {
"MyStruct"
}
この仕様によって、以下のようなことが可能です。
組み込み型の拡張
RubyやJSのように組み込み型に自前のメソッドを新たに生やすことが可能です2。
func times(self: Int, f: (Int) -> Unit) -> Unit {
// 推論できないのでIntのみ指定
fn _times(cnt: Int, f) {
if cnt <= self {
f(cnt)
_times(cnt+1, f)
}
}
_times(1, f)
}
func init() {
10.times(println)
}
1
2
3
4
5
6
7
8
9
10
型に紐づく関数
コンストラクタ等、所謂クラスメソッドも定義可能です。self
を含まない関数は、定義も呼び出しも {型名}::{メソッド名}
で指定します。
func Vector::new_with_default[X](len : Int, default : X) -> Vector[X] {
{ data: Array::make(len, default), len }
}
実装してみる:複素数
というわけで、ここまでの文法を使って実際にコードを書いてみます。お題は複素数型の実装です。
// 型の定義
struct Complex {
re:Float64 // 実部
im:Float64 // 虚部
}
// 2項演算子 + で暗黙的に呼ばれるメソッド
func op_add(self: Complex, other: Complex) -> Complex {
{re:self.re + other.re, im:self.im + other.im}
}
// 2項演算子 -
func op_sub(self: Complex, other: Complex) -> Complex {
{re:self.re - other.re, im:self.im - other.im}
}
// 2項演算子 *
func op_mul(self: Complex, other: Complex) -> Complex {
{re:self.re * other.re - self.im * other.im, im:self.re * other.im + self.im * other.re}
}
// 2項演算子 /
func op_div(self: Complex, other: Complex) -> Complex {
let denominator = other.re * other.re + other.im * other.im
{re:(self.re * other.re + self.im * other.im) / denominator, im:(self.im * other.re - self.re * other.im) / denominator}
}
func to_string(self: Complex) -> String {
// NOTE: 現時点では Float64#to_string() が実装されていないので、intに変換してからto_string
let re = self.re.to_int().to_string()
let im = self.im.to_int().to_string()
"\(re) + \(im)i"
}
func init() {
// Complexに推論される(フィールド名から推論している?)
let c1 = {re: 6.0, im: 3.0}
let c2 = {re: 2.0, im: 1.0}
println(c1 + c2) // 8 + 4i
println(c1 - c2) // 4 + 2i
println(c1 * c2) // 9 + 12i
println(c1 / c2) // 3 + 0i
// リテラルをそのまま書くことも可能
println({re: 1.0, im: 2.5} + {re: 3.0, im: 1.5}) // 4 + 4i
}
実行すると想定通り結果が表示されました。0割りのコーナーケースは見なかったことにする
====== Compilation Statistics ======
Wasm size: 1494B
Time cost: 5ms
---
8 + 4i
4 + 2i
9 + 12i
3 + 0i
4 + 4i
できなかったこと
最後に、現時点でできなかったことの紹介です。今後のアップデートでできるようになるかもしれません。
ジェネリクスを用いたメソッドの一括定義
レシーバに型パラメータを使うことで、インターフェースの全実装にメソッドを定義...しようとしたのですが、コンパイルエラーになりました。T
を型コンストラクタにする必要があるようですがやり方が分からず...
interface Addable {
op_add(Self, Self) -> Self
}
func twice[T: Addable](self: T) -> T { // Invalid receiver type: must be a type constructor
self + self
}
変数以外の文字列埋め込み
これはパーサーが作りこまれたら対応してもらえるかもしれません3。
println("\(x+1)") // Lexing error: unrecognized character u32:0x13c
おわりに
以上、MoonBitの文法の紹介でした。型推論やVSCode上での補完が効いていて気持ちよく実装できました。OSS化されて詳細な実装が見られるようになるのが楽しみです。