先行例
基礎知識
mimiumと多段階計算
mimium新内部表現の構想での検討材料
こういうのが計算できなかった(要するにfixpointの中でselfを使うと必要なselfのサイズを確定できない)
fn filterbank(N,input,lowestfreq, margin,Q,filter){<!-- line:28 -->
if(N>0){<!-- line:29 -->
return filter(input,lowestfreq+N*margin,Q)<!-- line:30 -->
+ filterbank(N-1,input, lowestfreq,margin,Q,filter)<!-- line:31 -->
}else{<!-- line:32 -->
return 0<!-- line:33 -->
}<!-- line:34 -->
}<!-- line:35 -->仮に多段階計算だとしてちょっと型を明示してみる まあ〜書きづらいよな
fn gen_filterbank(N:int,lowestfreq:float, margin:float,Q:float,filter:(float,float,float)->float)-> <(float,float,float)->float> {<!-- line:42 -->
// level:0<!-- line:43 -->
if(N>0){<!-- line:44 -->
< //level:1<!-- line:45 -->
|x| {<!-- line:46 -->
filter(x,~(lowestfreq+N*margin),~(Q))<!-- line:47 -->
+ ~( //level:0<!-- line:48 -->
gen_filterbank(N-1,x, lowestfreq,margin,Q,filter)<!-- line:49 -->
)<!-- line:50 -->
}<!-- line:51 -->
><!-- line:52 -->
<!-- line:53 -->
}else{<!-- line:54 -->
<|x|{0}><!-- line:55 -->
}<!-- line:56 -->
}<!-- line:57 -->
<!-- line:58 -->
fn filterbank(input:float, N:int,lowestfreq:float, margin:float,Q:float,filter:(float,float,float)->float)-> float{<!-- line:59 -->
~(gen_filterbank(N,lowestfreq,margin,filter))(input)<!-- line:60 -->
}<!-- line:61 -->filter同士の足し算を+のinfixでやりたいよねえ
let多相じゃないよなあこれは
ていうかこれそもそも普通に高階関数じゃダメなんだろうか?
fn gen_filterbank(N,lowestfreq, margin,Q,filter)->(float->float){<!-- line:70 -->
|input|{<!-- line:71 -->
if(N>0){<!-- line:72 -->
return filter(input,lowestfreq+N*margin,Q)<!-- line:73 -->
+ gen_filterbank(N-1,lowestfreq,margin,Q,filter)(input)<!-- line:74 -->
}else{<!-- line:75 -->
return 0<!-- line:76 -->
}<!-- line:77 -->
}<!-- line:78 -->
}<!-- line:79 -->いやーこれでもif文が展開されるのは実行時だから意味ないのか
---
あー、つまり
- ステージ0ではfixpointが使えてfeedが使えない
- ステージ1ではfeedが使えてfixpointが使えない
みたいな定義ができればいいんじゃなかろうか(というか、fixpointが使えるステージとfeedが使えるステージが交互に繰り返し出てくる、とかになればいけるのか?)
ステージ0でfixpointを使ったものを評価しきってステージ1に行く時には再帰は有限回の計算に展開されていることになる・・・のでは?
これ、同じように
- ステージ0でn個の要素数のものとして評価されたリストはステージ1からn要素のタプルとして見える
という前から考えていたこととも繋がるのでは?(というか、これがKronosでやっている高階ラムダ計算というか、System Fなのか?)
問題はそれをユーザーに意識させずに書かせる方法かあ、結局Faustはそれができているからこそ強いわけで(いや、Faustのパターンマッチは意味論破壊してHygienicとはいえマクロになってるから結局2段階なのか)
fn lowpass(input,fb){<!-- line:106 -->
(1-fb)*input + fb*self<!-- line:107 -->
}<!-- line:108 -->Faustとかと比べたときのメリット
マクロ部分の生成段階も含めてCやC++のコードとして生成できるのなら、juceのprepareToPlayのような関数内でのDSPグラフのパラメトリックな変形(フィルターの次数を変えるとか)が実現できるかも(そこまでのことが求められてるかというと微妙な気もするが・・・)
というか果たしてそんなことはできるのだろうか
あとは、Faustのエラーメッセージで入出力の数が合わないとかのエラーはマクロ展開後に発生するので、マクロを多用するほどエラーが読みづらくなる問題があるが、それは多分多段階計算の方が読みやすくなる・・・はず
実は無理に多段階計算じゃなくてもいい説
解決方法は実はもう2パターンくらいある気がしてきた。
1.あくまでコンパイラの最適化に任せる方向
意味論としては、fixとfeedが混在してても(めちゃくちゃ非効率的とはいえ)コンパイルを通せるような仕組みをとにかく作る。具体的には、fix+feedを使う場合は渡す内部状態変数を固定サイズではなく、例えばリンクドリストで作るようにし、fixを深く実行するほどリンクリストを奥に辿っていき、まだ未確保な場合は適宜メモリをアロケートしてリストを伸ばしていくような形にする。
現実的にはリンクドリストではなく動的ベクターとかで実装する。こうすると、例えば実際には定数で一定の深さまでしか展開されないことが自明な関数は、一回ダミーで1サンプルだけ実行すれば必要なメモリサイズは伺い知れる。
もちろん、ちゃんとやるなら再帰関数の部分の定数畳み込みは実施するようにすればいいし、そうでないとしても、再帰関数が残ったままCのコードをエクスポートするような形でも実行直前に必要なメモリサイズは確定できるということになる。
2.再帰を有限回しか行えないような意味論を定義する
例えば、Faustでパターンマッチ使って再帰を行う場合は基本的にフィルタの次数とかをnとか、n,mとかで定義して0まで減らしていく、といったのがほとんど。ならばfixは上限の決まったInt型(ディレイの読み出しインデックスに使うのと同じ型)にしてしまえば、最低限必要なメモリサイズはコンパイル時に決定できる。
ただ、そうするとユーザーからは、「なんだかよくわからんけど再帰する関数の第1引数には上限の決まったIntしか使えんらしい」という不条理感が残る
こっちは筋が悪いかなー
これら2つではカバーできないケース
再帰の深さによって返したい型が変わるような多相を考えると厳しいかも。
実際、Faustのパターンマッチングでの項書き換えは書き換えを適用するたびに異なる入出力数のノードを返すことができる(だからこそtakeみたいな関数が定義できるわけだし)
要素数がなアダマール積の定義とか
bus(n) = par(i,n,_); // There is si.bus(n) in the \FL s<!-- line:148 -->
<!-- line:149 -->
// hmtx(2) = _,_ <: +,-; // scalar butterfly<!-- line:150 -->
hmtx(2) = _,_ <: (bus(2):>_),(_,*(-1):>_) ; // prettier drawing<!-- line:151 -->
hmtx(n) = bus(n) <: (bus(n):>bus(n/2)) , // vector butterfly<!-- line:152 -->
((bus(n/2),(bus(n/2):par(i,n/2,*(-1)))) :> bus(n/2)) : (hmtx(n/2) , hmtx(n/2));<!-- line:153 -->
<!-- line:154 -->
process = hmtx(16);<!-- line:155 -->この辺は、多段階計算を使ったとしても生成したコードの型を変えるみたいなことは出来ないのでは?
fn hmtx(n:int)-> <[float;n]->[float;n]>{<!-- line:161 -->
assert!(n>=2 && n%2 == 0);<!-- line:162 -->
if(n==2){<!-- line:163 -->
`|list|{<!-- line:164 -->
let l = list[0];<!-- line:165 -->
let r = list[1]; <!-- line:166 -->
[l+r, l-r] <!-- line:167 -->
}<!-- line:168 -->
}else{<!-- line:169 -->
`|list:[float]|{ <!-- line:170 -->
let mixer = interleave <!-- line:171 -->
|> |(l1,l2)| {zipwith(l1,l2).map(|(a,b)| a + b)}<!-- line:172 -->
|> ~hmtx(n/2);<!-- line:173 -->
let upper = list |> mixer;<!-- line:174 -->
let lower = list<!-- line:175 -->
|> split(_,2)<!-- line:176 -->
|>|(l1,l2)| {(l1,l2.map(|a| -a))}<!-- line:177 -->
|> mixer;<!-- line:178 -->
join(upper,lower)<!-- line:179 -->
}<!-- line:180 -->
}<!-- line:181 -->
}<!-- line:182 -->項でインデックスされた型=依存型な気がするのよな
どのみち、拡張していくとしたら
- 現状のmimiumのシンタックスのまま再帰とfeedを混ぜてもコンパイル通せるように頑張る
- 多段階計算導入
- 依存型導入?
という順番になると思うので、どのみち一旦Rustでのmimiumコンパイラをシンプルに作りに行けばそれでいいのではないか?後から拡張するということで
継時再帰の方をどう定義するか&feedの項とどう使い分けるのかの問題はまだ残されているが…
継時再帰はKronosのMeta-Sequencerでも使われてたはずだし、きちんと意味論を定義することはできるのかな?
---
VMのインストラクションを考えてるけど。
ワンパスのコンパイラだと、関数呼び出しした時に値がFeedになるかどうかは、Substituteを直接的にするわけじゃないから判別できないのか
となると、やっぱり値に対して性質が決まってるから型として判別した方がいいんじゃないのか?つまりfilterの値の方に関してはメモリサイズが不定だよなあ
fn filterbank(N,input,lowestfreq, margin,Q,filter){<!-- line:211 -->
if(N>0){<!-- line:212 -->
return filter(input,lowestfreq+N*margin,Q)<!-- line:213 -->
+ filterbank(N-1,input, lowestfreq,margin,Q,filter)<!-- line:214 -->
}else{<!-- line:215 -->
return 0<!-- line:216 -->
}<!-- line:217 -->
}<!-- line:218 -->---
プリミティブとしてはlistとarrayの2種類を持っておき、型強制で<List 'a>をArray 'a として使えるようにする