Chapter 27. スタックに確保されたクロージャ関数

高階関数は、コードの形を再利用するのにとても便利なプログラミングの機能です。 高階関数呼び出しへの引数として渡された関数はしばしば実行時にヒープ上に生成されたクロージャ関数で、その呼び出しの後ではもはや使われないことを意図しています。 もしそのクロージャ関数が線形なら、それはプログラマが明示的に解放する必要があります (そうでなければ型検査中に型エラーが発生します)。 もしそのクロージャ関数が非線形なら、そのメモリはガベージコレクション (GC) によって回収されなければなりません (そうでなければメモリリークになります)。

ヒープに確保されたクロージャ関数の生成は動的なメモリ確保 (DMA) を必要としています。 制限された環境 (例: 組み込みプログラミング) では、DMA は (完全には) サポートされていないかもしれません。 DMA のサポートのない環境でのクロージャ関数の生成の1つの選択肢は、呼び出し関数のスタックフレームにそれを保管することです。 ATS にはこれを行なうための特殊な構文があります。

スタックに確保されたクロージャの実例を見てみましょう。 次のコードは高階関数テンプレートを実装しています:

// fun {res:t@ype} ifold{n:nat} ( n: int(n) , fopr: (res, natLt(n)) -<cloref1> res, ini: res ) : res = let // fun loop {i:nat | i <= n} .<n-i>. (i: int(i), res: res): res = if i < n then loop(i+1, fopr(res, i)) else res // in loop(0, ini) end // end of [ifold] //

本質的に、ifold(n, fopr, ini) は式 fopr(...fopr(fopr(ini, 0), 1)..., n-1) を評価します。 例えば、2つのベクトルのドット積 (もしくは内積) を計算する 関数 dotprodifold 呼び出しを使って実装できます:

// typedef res = double // fun dotprod {n:nat} ( n: int(n) , A: arrayref(res, n) , B: arrayref(res, n) ) : res = ( ifold<res>(n, lam(res, i) => res + A[i]*B[i], 0.0) ) //

ifold 呼び出しへの第二引数は、実行時にヒープ上に生成されたクロージャで、この呼び出しが戻った後はもう使用できません。 また、関数 dotprod は次のようにも実装できます:

// fun dotprod {n:nat} ( n: int(n) , A: arrayref(res, n) , B: arrayref(res, n) ) : res = let // var fopr = lam@(res: res, i: natLt(n)): res => res + A[i]*B[i] // in ifold<res>(n, $UNSAFE.cast(addr@fopr), 0.0) end // end of [dotprod] //

(lam ではなく) キーワード lam@ は与えられた位置にアンボックス化クロージャを生成します。 上記の例では、変数 foprdotprod のスタックフレームに位置していて、生成されたクロージャは fopr で予約されたメモリに保管されます。 ATS コンパイラは、クロージャを保管するのにメモリが十分大きいことを保証します。 (安全でない) キャスト呼び出しは、ifold に渡せるようにするため、fopr のアドレスをボックス化クロージャに変化させます。

dotprod の実装における (安全でない) キャスト削除するために、次のような ifold の変種を実装する必要があります:

// fun {res:t@ype} ifold_{n:nat} ( n: int(n) , fopr: &(res, natLt(n)) -<clo1> res, ini: res ) : res = let // fun loop {i:nat | i <= n} .<n-i>. ( i: int(i) , fopr: &(res, natLt(n)) -<clo1> res, res: res ) : res = if i < n then loop(i+1, fopr, fopr(res, i)) else res // end of [if] // in loop(0, fopr, ini) end // end of [ifold_] // (* ****** ****** *) // fun dotprod_ {n:nat} ( n: int(n) , A: arrayref(res, n) , B: arrayref(res, n) ) : res = let // var fopr = lam@(res: res, i: natLt(n)): res => res + A[i]*B[i] // in ifold_<res>(n, fopr, 0.0) end // end of [dotprod_] //

ifold_ の第二引数は参照渡しであることに注意してください。 注釈付きに矢印 -<clo1> はアンボックス化されたクロージャを表わす関数の型を作るのに使われます。 (アンボックス化されたクロージャを表わす) 左辺値は ifold_ 呼び出しの第二引数として渡され、このとき実行時に実際に渡されるのはこの左辺値のアドレスです。 ifold_ 呼び出しを使うことで、関数 dotproddotprod_ のような安全な実装ができるのです。

この章で用いたコード全体とテストコードは オンライン から入手できます。