1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Wasmに特化したプログラミング言語MoonBitで遊んでみる

Posted at

はじめに

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

010_operator_overload.mbt
// pubを付けたときのみパッケージ外から参照可能
pub struct T {
  // フィールドには型を明示
  x:Int
}

構造体は以下のように初期化します。型は推論できるので指定不要です。

010_operator_overload.mbt
let a = { x:0 }

tuple

011_tuple_accessors.mbt
let x = (1, 2) // (Int, Int) に推論される
println(x.0) // 1
println(x.1) // 2

enum

単なる整数ではなく代数的データ型のため、要素ごとに別々の型の値を持たせることができます。

008_primitive_types.mbt
// 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関数)

matchif が式であることと相まって、式指向なプログラミングができるのが嬉しいです。

002_fib.mbt
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化されて詳細な実装が見られるようになるのが楽しみです。

  1. MojoPower Fx も安定化するまでOSS化しないという方針でした。最近の言語のトレンドなのでしょうか...?

  2. これは単なる比喩です。データと振る舞いがオブジェクトとして結合していないMoonBitにとって組み込みメソッドも自作メソッドも関数呼び出しに過ぎないため、そもそも「structを拡張」という概念がありません。

  3. 文字列埋め込みのパースは見た目のわりに複雑なので、あえて変数しか許可していないのかもしれません。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?