6.9 KiB
date
date |
---|
2025-07-02 11:40 |
#memo
Proposal: Record Syntax · Issue #99 · mimium-org/mimium-rs · GitHub
古い v0.4.0 では実験的なレコード(struct)型があったが、新たなレコード型の構文を実装する。
新構文のデザインは、名前ごとにデータをグルーピングすることで、プログラマの意図をより直接的にコードに反映できるようにするとともに、デフォルト値を設定することで記述量を減らすことを目指す。
また、タプル型で考案されているパラメータパックとデフォルト値付きレコード型を組み合わせることで、「UGen as 関数」という概念を型安全性を保ちながら直接表現でき、かつデフォルト引数付き関数にありがちな引数の数に関する混乱を回避できそう。
基本構文
構文のベースは Elm である。小竹は Rust 風にはしない。
匿名レコード型を正の文脈で使う。
let myadsr_param = { attack = 100.0, decay = 200.0, sustain:float = 0.6, // let 式同様に型注釈は省略可能だ release = 2000.0, // 末尾のカンマも許可されるべきだ } let singlerecord = { value = 100, // フィールドが1つだけのレコードは、ブロックや代入構文と区別するために末尾カンマが必須だ }
後で実装予定だが、型エイリアス/新規型宣言の中でもデフォルト値を設定できる。宣言内のデフォルト値はリテラルである必要がある。
type alias ADSR = { attack:float = 100.0,
decay:float = 200.0,
sustain:float = 0.6,
release:float = 2000.0, }
各要素へはドット演算子でアクセスするか、let パターンで展開できる。
let myattack = myadsr_param.attack let { attack .. } = myadsr_param // 変数名はレコード型宣言に依存する
アンダースコアを使った部分適用の構文糖衣を使えば、こんなコードでもたぶん動作する。
let myattack = myadsr_param |> _.attack // 型は正しく推論される
Elm 由来の「update」構文も加える。ただし区切りにはパイプではなく左アローを使う。
let newadsr = { myadsr_param <- attack = 4000.0, decay = 2000.0 } // attack フィールドのみ上書きして新しいレコードを生成
これは以下のような構文糖衣として実装できる。
let newadsr = { attack = 4000.0,
decay = myadsr_param.decay,
sustain = myadsr_param.sustain,
release = myadsr_param.release, }
この展開は型推論後にのみ行える。実装は mirgen 側で行えばいいはず。
関数宣言での引数デフォルト値
fn adsr(attack = 100, decay = 200, sustain:float = 0.7, release = 1000.0) -> float {
// 実装…
}
// もちろんこれは動作する
adsr(200,400,0.5,200)
// 以下は許可しない。次のパラメータパックの説明参照
adsr()
adsr(attack = 200)
adsr(200)
レコード型を使った自動パラメータパック
引数が 2 つを超える関数呼び出しでは、
- 宣言と同じ数の引数
- 型の統一ルールに合致する単一のレコード引数
のいずれも許可される。というか、多パラメーター関数は実質的に単一レコード引数関数のエイリアスになる。
// もちろんOK
adsr(myadsr_param)
// デフォルト引数ですべて評価する
adsr({ .. })
// オールデフォルトの場合はこれも許して良さそう
adsr(..)
デフォルトパラメーターに対する部分更新構文も許したい。デフォルト値付きと未設定引数を混在させた関数の場合とか。
fn myugen(freq, phase = 0.0, amp = 1.0) { // freq は必須
//実装…
}
myugen({ freq = 200.0 .. }) // これは許可されるはず
myugen({ phase = 0.05 .. }) // freq がないのでエラー
右辺値で使う { key = val .. }
構文は、let
パターン展開と同様に IncompleteRecord
AST ノードを生成し、その型は IncompleteRecord
となる。この IncompleteRecord
式は、部分適用のアンダースコア同様、引数としてのみ使える想定。
型システムレベルでは、Record
型は各フィールドにデフォルト値があるかどうかの情報だけを持ち、値自体は持たない。
型推論では以下のように Record
と IncompleteRecord
が統一(unify)される。
// 疑似コード
Record{ [(key:"freq", type:float, default_v:false),
(key:"phase", type:float, default_v:true),
(key:"amp", type:float, default_v:true)]
}
IncompleteRecord{ [(key:"freq", type:float)] }
=== unify ===>
Record{ [(key:"freq", type:float, default_v:true),
(key:"phase", type:float, default_v:true),
(key:"amp", type:float, default_v:true)] }
型推論の最後まで IncompleteRecord
が残っているとコンパイルエラーとする?。
そして、単一の IncompleteRecord
引数が与えられた場合(ケース2)、mirgen 前の構文糖衣アンラップフェーズで、以下のように更新構文を用いた AST 展開を行う。
let default_v = { freq = 0.0, phase = 0.0, amp = 1.0 }
// "myugen" の型情報をもとに生成。freq は上書きされるので何でもよい。
myugen({ default_v <- freq = 200.0 })
デフォルト値生成のロジックは、Rust の Default
トレイトのような制限付き型クラス(インターフェース)を模したものになると思われる。
部分型と型クラス
構造的部分型を採用するつもり。
つまり、
fn mysynth(freq,amp,gate){
...
}
みたいな関数に対して
let param = {
freq:1000,
amp:1.0,
gate: 1.0,
phase: 0.0, //余計なパラメーター
}
param |> mysynth //でも、部分型になるのでOK
ということができる。
そして、レコード型のメンバーに関数型を許せば、結局これは型クラスを作ってるのに等しいことになるはず
例えばデフォルト値が次の型クラスの実装となっているとすると
type ADSR = {
attack:float,
decay: float,
sustain: float,
release: float
}
trait Default{
fn default()->Self
}
impl Default for ADSR{
fn default()->Self{
{
attack:100,
decay: 100,
sustain: 0.7,
release: 100
}
}
}
type alias Default<T>{
default: ()->T
}
let adsr:ADSR = {..}//これが
let adsr = Default<ADSR>::default() //こうなる
隠れ引数として実装されれば、定数畳み込みはできそう
fn default(typeid, parama,paramb)->T{
match typeid{
...
}
}