GHCの融合変換を理解する(前編)

Posted on February 15, 2019
Tags: Haskell, GHC

今回のお題

GHCでsum [1,2,3]はどのようにコンパイルされるでしょうか。

コンパイルしてみると定数6となっていることがわかります。

今回の記事では、GHCはどのように6を計算しているのか解説します。 ポイントはリストリテラルの脱糖と、fold/build変換です。

リストリテラル

ghc-ddump-dsオプションを渡すと脱糖の結果をみることができます。

-Oオプションがない場合、GHCは[1,2,3]1 : 2: 3 : []に脱糖します。

一方で、-Oがある場合はbuild (\c n -> c 1 (c 2 (c 3 n)))という式に脱糖します。

ここでbuildGHC.Baseで定義される関数です。

g c nは、とあるリストの (:)コンストラクタをcに、[]コンストラクタをnにそれぞれ置き換える関数と考えると 良いです。buildはそのようなgを受け取り、cとして(:)nとして[]を渡すことで リストを生成します。

つまり、脱糖の結果、

となります。

次に、sum関数です。これは(Foldable型クラスを具象化すると)以下のような定義になります。

次にfoldl関数の定義を見てみましょう。

ソース

驚くべきことにfoldlfoldrを使って書かれています。 その理由はコメントで書かれています。

Note [Left folds via right fold]

Implementing foldl et. al. via foldr is only a good idea if the compiler can optimize the resulting code (eta-expand the recursive “go”). See #7994. We hope that one of the two measure kick in:

  • Call Arity (-fcall-arity, enabled by default) eta-expands it if it can see all calls and determine that the arity is large.
  • The oneShot annotation gives a hint to the regular arity analysis that it may assume that the lambda is called at most once. See [One-shot lambdas] in CoreArity and especially [Eta expanding thunks] in CoreArity. The oneShot annotations used in this module are correct, as we only use them in arguments to foldr, where we know how the arguments are called. oneShot

要は、コンパイラがη展開してくれるから途中で生成されるクロージャは消えるので 問題ないということです。そして、 foldlfoldrで書かれていることにはある重要な理由があります。 それはfold/build変換が効くということです。これについては後述します。

さて、ghcはこれらの定義に従ってコードをインライン展開します。

fold/build変換

さて、fold/build変換はここで定義されています。

まずRULESプラグマについて説明します。これは左辺で書かれた式を右辺の式に書き換える というGHCの持つ強力な(黒魔術)最適化機構です。 今回の場合は「foldr k z (build g)の形をした式をg k zに書き換える」ということを意味しています。 この書き換えが正しいこと、および書き換えが停止することは完全に ライブラリ製作者の責任です。 実際、このfold/build変換が正しいかどうかは自明ではありません。 ほとんどの場合は問題ないことが知られていますが、 sequndefinedをうまく使うと左辺と右辺で結果が異なる例が存在します。参考

さて、今回の式でもfold/build変換が適用できる形をしているので変換してみましょう。

実際の変換はghcに-ddump-rule-rewritesオプションを渡すと確認できます。

fold/build変換の結果β簡約できる箇所が見つかりました。GHCは(簡約結果が大きくなりすぎない限り)このような簡約を自動で行います。 人手でやるのはしんどいですが実際にやってみましょう。

最後にidがインライン展開され、定数同士の演算がコンパイル時に行われることで、 sum123 ==> 6となります。

結論

ghcではリストリテラルはbuild関数を使った定義に脱糖されるため、fold/build変換の対象となります。 最後に変換の全体結果を示します。

後半ではfold/build変換についてより詳しく解説します。