quartz-research-note/content/内部状態をもつ関数とクロージャの組み合わせ.md

2.5 KiB

date
2024-07-26 11:31

#mimium

mimiumの中間表現を考える の続き

fn onepole(x,g){
	x*(1.0-g) + self*g
}
fn filterbank(n,filter){
	let next = filterbank(n-1,filter);
	if (n>0){
		|x,freq| filter(x,freq+n*100) +next(x,freq)
	}else{
		|x,freq| filter(x,freq+n) 
	}
	
}
let myfilter = filterbank(3,onepole)
fn dsp(input){
	myfilter(input,1000)
}

この表現の場合は、filterbank関数が実行されるたびに1回ずつクロージャが生成されるので多分期待通りに動く。

なのだが、より単純な表現の場合の方が微妙な挙動について考えなければならない

再帰によるパラメトリックな複製ではなく、普通に2つに複製する場合を考える

fn onepole(x,g){
	x*(1.0-g) + self*g
}
fn replicate(filter){
	|x,freq| filter(x,freq) + filter(x+freq*2)
}
let myfilter = replicate(onepole)
fn dsp(input){
	myfilter(input,1000)
}

このサンプルコードにおいて、replicate関数の中で2回呼び出されているfilterは、同じクロージャのインスタンスか否か?

クロージャで内部状態をミュータブルに変更する目的を達成するのであれば、この2つは同じインスタンスに対する呼び出しであるが、信号処理としては別々の内部状態インスタンスを持つと考える方が自然に読める

解決策としては、そもそも変数に対して一切の再代入を認めない実装にしてしまうこと。そうすると、クロージャが生成された時にfilterのようなクロージャを含む上位値を全てdeep-copyしてしまう実装にできる。(まあ、こうすると実装にupvalueを使う旨みはほとんどなくなるが)

意味論を変えないまま2つのfilterを区別するならこんな感じにサンクを挟むことになる

fn onepole(x,g){
	x*(1.0-g) + self*g
}
fn replicate(filter:()-> (float,float)->float){
	let f1 = filter()
	let f2 = filter()
	|x,freq| f1(x,freq) + f2(x+freq*2)
}
let myfilter = replicate(||onepole)
fn dsp(input){
	myfilter(input,1000)
}

とりあえずこれで意味論は通った&実際コンパイラも動いてるが、エンドユーザーにはわかりづらそう。

中間的なアプローチとしては、内部状態のストレージはレキシカルな呼び出しに対応するが、クロージャはあくまで名前に対応するという考え方ができる。この場合、高階関数を使う以上は内部状態はツリー構造に戻さざるを得ないが