mimium v2における言語仕様の変更 #
mimiumはこれまでの開発でのいくつかの反省を踏まえ、0から言語を再設計することとなりました。
コンパイラ・実行環境の実装言語がC++からRustになりました。
言語の基礎となる意味論を再設計し、またそれを実行するためのVMをRust上で定義することで、表現可能な範囲が大きく広がりました。一方で、これまでのLLVMを用いたJITコンパイルではなくRust上でバイトコードインタプリタを実行するため、実行パフォーマンスは全体的に以前よりも遅くなっています。これは、今後改めてバイトコードのJITコンパイラを実装することで改善する方針で検討しています。ただ現段階の実装でも、例えばサイン波オシレーターを100本ぐらい複製する分にはパフォーマンス的に問題なく実行可能です。
新しい言語仕様の設計に伴い、たとえば以下のような信号処理を行う状態付き関数(間接的にでもself
やdelay
を呼び出すもの)を高階関数を用いて複製するようなコードがコンパイル可能になりました。
let pi = 3.14159265359
let sr = 44100.0
fn phasor(freq){
(self + freq/sr)%1.0
}
fn osc(freq){
sin(phasor(freq)*pi*2.0)
}
fn amosc(freq,rate){
osc(freq + osc(rate)*4000.0)
}
fn replicate(n,gen:()->(float,float)->float){
let g = gen()
if (n>0.0){
let c = replicate(n - 1.0,gen)
|x,rate| g(x,rate) + c(x+100.0,rate+0.1)
}else{
|x,rate| 0.0
}
}
let n = 40.0
let mycounter = replicate(n,| |amosc);
fn dsp(){
let res = mycounter(4000.0,0.5) / n
(res,res)
}
構文の破壊的変更 #
let
キーワードの導入
#
mimium v2では、いくつか表面上の言語仕様(構文)にも変更が加わっています。例えば、これまでのmimiumでは変数の宣言と破壊的代入に同じ構文を用いていました。
hoge = 10.0 // new variable declaration
hoge = 20.0 // destructive assignment
最も大きな構文上の仕様変更として、mimium v2ではRustと同じように、let
キーワードを用いて変数を宣言し、let
なしでの代入を行うことで、一度宣言した変数への破壊的代入を行います。
let hoge = 10.0
hoge = 20.0
これは、人によっては構文を余計に複雑にしたように思えるかもしれません。ただ、let
キーワードによる変数の宣言は言語の意味論をより明確にすることができます。ネストされた関数定義の中では、let
によるローカルな変数の宣言でスコープを区切ること(シャドーイング)によって、短い単純な変数名を文脈に応じて再利用することがやりやすくなります。
return
キーワードの削除
#
mimium v0.4まではfn
を使った関数定義では返り値の指定にreturnが必須となっていました。
fn countup(active){
return if (active) (self+1) else 0
}
mimium v2以降では、単にブロック中の最後の式が返り値になります。これもRustに近い仕様ですが、mimiumでは現状loopやwhileのような構造によって要求されるearly returnの必要性がないため、そもそもreturnキーワードの存在自体が冗長なものでした。このためv2ではreturnキーワード自体を削除することにしました。
fn countup(active){
if (active) (self+1) else 0
}
オーディオファイル読み込みの方法の変更 #
オーディオファイルの読み込みは、loadwav
およびloadwavsize
で行っていましたが、これはgen_sampler_mono
という高階関数を用いたパターンに変更になっています。
//gen_sampler_mono returns higher-order function that takes playback position in samples as an argument
let sampler = gen_sampler_mono("myfile.wav")
fn counter(){
self+1.0
}
fn dsp(){
counter() |> sampler
}
ファイルのデコードにはRustのSymphoniaクレートを使用しています。そのためmp3やFLACなど、Symphoniaで対応しているオーディオファイルは全て読み込み可能です。
v0.4.0では実装されていたがv2.0.0では未実装の機能 #
- 構造体型と型エイリアスは未実装です。これは多相(ジェネリクス)の導入に関わるため今後の最優先の課題になっています。 レコード型の案はこちらで検討中です。 https://github.com/mimium-org/mimium-rs/issues/99
- 配列定義と読み出しのための構文は未実装です。これは配列の内部表現(アレイかリストか)とその実装におけるメモリ解放の問題の解決のためです。
非破壊的な変更(追加された仕様) #
letrec
宣言の追加
#
mimiumでは、fn
を用いた関数宣言の中でのみ再帰的関数呼び出しが許されます。インラインで関数を宣言するときは、let myf = |x| { ... }
のようにして宣言しますが、ここでは再帰呼び出しは許されません。またv2ではfn
を用いる関数宣言をネストすることを許さない仕様になりました。そのためネストした関数で再帰関数を書く方法がありませんでした。そこで、letrec myf = |x| { if(x>0.0) myf(x-1.0)+1.0 else 0.0 }
のような形で、インラインで再帰関数を定義できるようにしています。通常のlet
がlet (a,b) = tuple_value
のようにタプルのような複数の値を同時にバインドすることを可能にしてるのに対して、letrec
では一つの変数名にのみバインドできることに注意してください。
部分適用のためのプレースホルダー _
#
関数適用(もしくは基本の二項演算子)の引数としてアンダースコア_
を与えると、その部分のみを新たな引数として新たな関数を作ることができます。これはシンタックスシュガー(ある種の特別扱いされたマクロ)として実装されているため、以下の2つの構文は意味的に完全に等価です。
let f = foo(1.0, _, _)
let f = |a1,a2| foo(1.0,a1,a2)
この部分適用と、パイプ演算子(|>
)と組み合わせることでデータフローの表現をより簡潔にすることが可能になります。
fn foo(x, y, z) {
100.0 * x + 10.0 * y + z
}
let d2 = _ / _
let f = foo(1.0, _, 3.0)
fn dsp(){
let x = 3.0 |>
1.0 + _ |>
d2(_, 2.0) |>
f
let y = 3.0
|> 1.0 + _
|> |arg| d2(arg, 2.0)
|> f
(x, y)
}
現在のところ、関数の部分適用は実行時にクロージャのアロケーション/解放を伴うため僅かにパフォーマンス的なデメリットがあります。これは今後関数のインライン展開や定数畳み込みなどの簡易的な最適化で解決可能なものとなっています。
また、二つ以上の引数に対してパイプ演算子とタプルを用いて引数を展開する構文(パラメーターパック)も、今後レコード型の導入に合わせて実装予定です。