基本言語仕様

mimiumの言語仕様 #

このページでは、mimium言語の基本的文法(シンタックスおよびセマンティクス)について説明します。

コメントアウト #

Rust、C++やJavaScriptと同様、行中の//より右側はコメントとして扱われます。 また/* */のように囲むと、複数行をまとめてコメントアウトできます。

変数宣言、代入 #

letキーワードに続けて名前、=、代入したい値を記述すると、変数が作られます。

let mynumber = 1000

すでにスコープ中に同名の変数があった場合、そのスコープ内で一番新しくlet宣言された変数が参照されます。(元々の変数に対しては影響を与えません。)これは、シャドーイングと呼ばれます。

fn dsp(x){
  let x = 1.0
  x //x is always 1.0 whatever argument is given
}

letなしで変数を代入すると、すでに宣言されている変数に新たに値を代入します。

let mynumber = 1000
mynumber = 2000 // 2000 is newly assigned to mynumber
Note

mimiumでletで作成される変数は原則的にミュータブル(常に破壊的代入が可能)です。ただし、mimiumの関数評価は値呼びという戦略に基づいているので、例えば関数の引数に対して破壊的代入を行っても関数の外側に影響を与えることはありません。また、mimiumにはforループのような命令型の構文が存在しないため、実際は破壊的代入を積極的に使用する意味がそこまでありません。

ただし、クロージャによってキャプチャされた変数が関数の外側に飛び出すことによって、限定的に共有された値への読み書きを行うことができます。(これがどういう意味なのかよくわからないうちは、変数への再代入は特に意味なしというぐらいに考えても大丈夫です。)

#

とは変数などのデータを数値や文字列など目的に応じて区別するための概念です。 mimiumは静的型付け言語と呼ばれる、コンパイル時に(音を実際に鳴らす前)すべての型が決定される言語です。

静的型付け言語は一般的に、実行中に型をチェックする言語よりも実行速度の面で有利です。その一方、型の指定を手動で行う場合は記述が長くなりがちというデメリットも存在しますが、mimiumでは型推論と呼ばれる、文脈から型が自動的に決定できる場合は型注釈を省略できる機能が存在しているので、コードを簡潔に保つことが可能です。

型にはそれ以上分解できない最小単位であるプリミティブ型と、複数の型を組み合わせて作る合成型(aggregate type)が存在します。

型の明示的な注釈は変数の宣言と関数の宣言時に可能です。 変数および関数のパラメータでは名前に続けて:(コロン)を挟み型名を書くことで指定可能です。

let myvar:float = 100

以下のように異なる型へ代入した場合はコンパイル時にエラーが発生します。

let myvar:string = 100

関数での型宣言では返り値をパラメータの括弧に続けて->を挟んで書くことで指定できます。

fn add(x:float,y:float)->float{
  x + y
}

このadd関数の場合、文脈からxとyがfloatであることを予測できる1ので以下のように省略できます。

fn add(x,y){
  x+y
}

プリミティブ型 #

mimiumにおけるプリミティブ型はfloatstringvoidのみです。

mimiumでは数値型はfloat(内部的には64bit float)のみとなっています。 整数を利用するにはroundceilfloor関数などを利用します。

string型の値は"hoge"のようにダブルクオーテーションで囲った文字列リテラルから生成できます。 現在は文字列の切り出しや結合には対応しておらず、用途は基本的には

  1. Probe!マクロに渡してデバッグ用途に使う
  2. Sampler_mono!マクロに渡してオーディオファイルを読み込む
  3. includeに渡して他のソースファイルを読み込む

のいずれかに限られています。

voidは値を持たない型で、関数の返り値が存在しないことを明示するのに使用します。

合成型 #

関数型 #

関数型は(T1,T2,...)->Tのようなシグネチャで表記します。

配列 #

配列は、同じ型の値を複数個連続して格納できる型です。[](アングルブラケット)で囲んだカンマ区切りの値で生成できます。

let myarr = [1,2,3,4,5,6,7,8,9,10]

後述するタプルのような合成型も配列の要素にできますが、配列のそれぞれの要素は全て同じ型でなければいけません。

let tuparr = [(1,2),(3,4)]

配列型の値にmyarr[0]のようにアングルブラケットで0基準のインデックスを指定することで配列の値を取り出すことができます。

let arr_content = myarr[0] //arr_content should be 1

length_array()という関数を使用することで、配列の要素数を実行中に取得することができます。

let len = length_array(myarr) // len should be 10.0

配列は現在、サイズ固定かつイミュータブルです。配列の後ろに値を追加していくような操作はできません。また境界チェックもないため範囲外へのアクセスはクラッシュを引き起こします。

タプル #

タプルは、異なる型を1つにまとめた値です。変数を()(丸括弧)で囲んでカンマ区切りの変数を入れることで生成できます。 タプルは配列とも似ていますが、各要素で異なる型を持つことができます。

let mytup = (100,200,300)

左辺値にカンマ区切りの変数を置くことでタプルの値を取り出すことができます。

let (one,two,three) = mytup

この場合、型を明示するときは各要素にコロンをつけるのではなく、パターン全体からコロンに続けて方を表示する必要があります。

左辺値にカンマ区切りの変数を置くことでタプルの値を取り出すことができます。

let (one,two,three):(float,float,float) = mytup
Note

今後、左辺値で分解するだけではなく、mytup.1のようにインデックスで取り出す記法も実装される予定です。

タプルはmimiumの中では典型的に信号処理でステレオやマルチチャンネルなどのオーディオ信号のチャンネルをまとめて扱うために利用されています。

型エイリアス #

型エイリアスはmimium v2では現在実装中です。

構造体(レコード型) #

mimium v3では、Elmに着想を得た構文を持つレコード型(他言語の構造体に相当)を導入しています。レコードを使うことで、名前付きフィールドで関連するデータをグループ化でき、コードの可読性と保守性が向上します。

基本構文:

// 名前付きフィールドを持つレコードリテラル
let myadsr_param = { 
   attack = 100.0,
   decay = 200.0,
   sustain = 0.6,
   release = 2000.0,
}

// 型注釈はオプション
let myrecord = {
  freq:float = 440.0,
  amp = 0.5,
}

// 単一フィールドのレコードには末尾のカンマが必要
let singlerecord = {
  value = 100,
}

レコードフィールドへのアクセス:

// ドット演算子によるフィールドアクセス
let attack_time = myadsr_param.attack

// let束縛でのパターンマッチング
let {attack, decay, sustain, release} = myadsr_param

// アンダースコアを使った部分適用と組み合わせ
let myattack = myadsr_param |> _.attack

レコード更新構文:

レコードの変更版を作成することは関数型プログラミングでは一般的なパターンです。mimium v3では、レコードを更新するためのクリーンな構文を導入しています:

let myadsr = { attack = 0.0, decay = 10.0, sustain = 0.7, release = 10.0 }

// 特定のフィールドを更新し、他は変更しない
let newadsr = { myadsr <- attack = 4000.0, decay = 2000.0 }
// newadsr は { attack = 4000.0, decay = 10.0, sustain = 0.7, release = 10.0 }

// 元のレコードは変更されない(イミュータブルなセマンティクス)
// myadsr はまだ { attack = 0.0, decay = 10.0, sustain = 0.7, release = 10.0 }

レコード更新構文はシンタックスシュガーとして実装されており、関数型プログラミングのセマンティクスを保証します。既存のものを変更するのではなく、新しいレコードを作成します。

多段階計算(マクロ) #

mimium v3では、型安全なマクロシステムとして多段階計算機能が導入されています。これにより、コンパイル時にコードを生成し、効率的なオーディオ処理を実現できます。

多段階計算の詳細については、多段階計算(マクロ)のページを参照してください。

関数 #

関数は、複数の値を取って新しい値を返すような、再利用可能な手続きをまとめたものです。

例として2つの値を足算して返すだけのadd関数を考えます。

fn add(x,y){
  x+y
}

mimiumでは関数が第一級の値として扱えます。これは、関数を変数に代入したり、関数を引数として受け取ったりできるということです。

たとえば、先ほどのadd関数の型注釈は(float,float)->floatのようになっています。先ほどのadd関数を変数に代入する場合は以下のように書けます。関数を関数のパラメータとして代入する場合は高階関数の項を参照してください。

let my_function:(float,float)->float = add

無名関数(ラムダ式) #

実は先ほどの関数宣言は以下のような、無名関数を変数に格納する構文へのエイリアスです。

let add = |x:float,y:float|->float {x+y}

このような関数を変数に代入しないまま直接呼び出すことも可能です。

println(|x,y|{x + y}(1,2)) //print "3"

パラメータパック #

mimium v3では、パラメータパック機能により、関数がタプルやレコードを引数として受け取り、自動的に個別のパラメータにアンパックできます。

タプルの場合:

fn add(a:float, b:float)->float {
  a + b
}

// 個別の引数での直接呼び出し
add(100, 200)  // 300を返す

// タプルの自動アンパック
add((100, 200))  // 300を返す

// パイプ演算子とシームレスに連携
(100, 200) |> add  // 300を返す

レコードの場合:

fn adsr(attack:float, decay:float, sustain:float, release:float)->float {
  // ADSRエンベロープの実装...
}

// レコードで呼び出し - フィールドの順序は任意
let params = { attack = 100, decay = 200, sustain = 0.7, release = 1000 }
adsr(params)

// またはインライン
adsr({ decay = 200, attack = 100, release = 1000, sustain = 0.7 })

この機能は、多くのパラメータを持つことが多いオーディオ処理関数で特に有用です。

デフォルト引数 #

関数でパラメータのデフォルト値を指定できます:

fn foo(x = 100, y = 200) {
  x + y + 1
}

fn bar(x = 100, y) {
  x + y
}

fn dsp() {
  // 空のレコード{}を使ってすべてのデフォルトを受け入れる
  foo({}) +           // x=100, y=200を使用、301を返す
  bar({y = 300})      // x=100, y=300を使用、400を返す
  // 合計: 701
}

デフォルト値はパラメータパック構文と連携します。{}を使ってすべてのデフォルト値を受け入れ、上書きしたいパラメータのみを指定できます。

パイプ(|>)演算子 #

mimiumではパイプ演算子|>利用することでa(b(c(d)))のようにネストした関数呼び出しをd |> c |> b |> aのように書き換えることができます。

パイプ演算子は他のどの演算子よりも低い結合順序を持っています。また、パイプの前後で改行が許されます。次の部分適用と組み合わせることで、データフローをわかりやすく表すことができます。

Note

mimium v3では、パラメータパック機能により、タプルやレコードを受け取る関数でもパイプ演算子を使用できます。

アンダースコア(_)による部分適用 #

関数適用の引数にアンダースコア(_)を使用すると、その部分を新たな引数とした関数を作れます。例えば足し算のadd関数の、片方の引数を1で固定した新たな関数addoneを作るとしましょう。

let addone = add(_,1)

これはコンパイラによる特別なマクロ展開のような実装(シンタックスシュガー)で、以下の構文と同等です。

let addone = |lambda_a1| add(lambda_a1,1)

パイプ演算子と組み合わせると、次のような形でデータフローを表せます。

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)
}

パイプ演算子は直前、直後での改行が許されています。

再帰によるループ #

名前のついている関数は自分自身を呼び出すことも可能です。

階乗を計算するfact関数は以下のように定義できます。

fn fact(input:float){
  if(input>0) 1 else input * fact(input-1)
}

再帰関数は無限ループを発生させる可能性があるので注意して使用してください。

letrec #

再帰関数はトップレベルでの関数定義のみで許され、letとラムダ式では表現することができません。ネストされた関数定義の中で再帰関数を定義したい場合、letの代わりにletrecを使用することで再帰関数を定義できます。

letrec fact = |input|{
  if(input>0) 1 else input * fact(input-1)
}

これはfnの構文と内部的に完全に等価です。注意点として、letrecで宣言する変数ではletのようにタプルを分解するようなパターンは受け取れません。

クロージャ #

TBD

式(expression)、文(statement)、ブロック #

関数などで使われていた中括弧{}で囲まれたの集まりはブロックと呼ばれる単位です。文(statement)はほとんどの場合let a = b,x = yのようなの代入をする構文で構成されています。式(expression)1000のような数字、mynumberのような変数シンボル、1+2*3のような演算式、add(x,y)のような返り値を持つ関数呼び出しなどで構成される単位です。

ブロックは実はの1つです。 ブロックには複数の文を置くことができ、最後の1行の式を返り値として持ちます。

//mynumber should be 6
let mynumber = {
  let x = 2
  let y = 4
  x+y
}

条件分岐 #

mimiumの条件分岐はif (condition) then_expression else else_expressionという構文を持っています。

conditionthen_expressionelse_expressionはすべて式です。 conditionの値が0より大きい時then_expression部分が、そうでなければelse_expressionが評価されます。

then/elseの部分をブロックとして表現すれば、以下のようにできます。

fn fact(input:float){
  if(input>0){
    1
  }else{
    input * fact(input-1)
  }
}

一方でif文自体も式として扱えるので、同じ構文を以下のように書き換えることもできます。

fn fact(input:float){
  if (input>0) 1 else input * fact(input-1)
}
Note

mimiumのシンタックスはRustを参考にしていますが、Rustのif文では条件部分の括弧が省略でき、then、else節の中括弧が必須なのに対して、mimiumでは逆に条件部分の括弧が必須で、then、else節の中括弧はブロック構文として必要なときは使い、不要なところでは省略可能です。

include #

include("path/to/file.mmm")という構文を用いると他のファイルをそのファイル内で読み込むことができます。

ファイルパスは絶対パスで指定された場合そのパスを、相対パスの場合、標準ライブラリ(~/.mimium/lib)を探してから、見つからなければそのファイルからの相対パスを検索します。

現在は読み込まれたファイルの名前空間の分割などはなく、純粋にinclude文をそのファイルのテキストに置換するだけになっています。相互依存するincludeの場合無限ループが発生することがあるので注意してください。

BNFによる文法定義、演算子の優先順位など #

TBD


  1. mimiumでは+*などの算術演算子を数値型にしか使えないため。今後変更になる可能性もあります。 ↩︎