GHCはいくつかのプラグマ(ソースコード中に置かれるコンパイラへの指示)に対応している。プラグマは通常プログラムの意味には影響を与えないが、生成されるコードの効率性には影響し得る。
全てのプラグマは{-#
という形を取る。ここで、word
... #-}word
はプラグマの種類を表す。必要なら、この後に続けて、その種類のプラグマに特有の情報を書く。word
では大文字小文字の区別はなされない。GHCが理解する種々のプラグマは以下の節で解説されている。認識できないword
を持ったプラグマは無視される。プラグマ中ではレイアウト規則が適用されるので、閉じ括弧#-}
は開き括弧{-#
よりも右のカラムで始まっていなければならない。
ある種のプラグマはファイルヘッダプラグマである。
ファイルヘッダプラグマは、ファイル中でmodule
キーワードよりも前になければならない。
ファイルヘッダプラグマはいくつあっても良いし、コメントの前にあっても後にあっても良い。
ファイルヘッダプラグマは、そのファイルを(例えばcppで)前処理する前に、一回だけ読まれる。
ファイルヘッダプラグマは以下のものである。{-# LANGUAGE #-}
、{-# OPTIONS_GHC #-}
、{-# INCLUDE #-}
。
このプラグマは、言語拡張を、可搬性のある方法で有効にできるようにするものである。意図としては、全てのコンパイラが同じ構文のLANGUAGE
に対応する、というものである。もちろん、全ての拡張が全てのコンパイラで使える訳ではないが。可能なら、OPTIONS_GHC
プラグマの代わりにLANGUAGE
を使うべきである。
例えば、FFIと、CPPを使った前処理を有効にするには、次のようにする。
{-# LANGUAGE ForeignFunctionInterface, CPP #-}
LANGUAGE
はファイルヘッダプラグマ(7.18. プラグマを見よ)である。
全ての言語拡張は、前に「-X
」を付けることでコマンド行フラグになる。例えば-XForeignFunctionInterface
のように。(同様に、全ての「-X
」フラグはLANGUAGE
プラグマとして書ける)
対応している言語拡張の一覧は、ghc --supported-extensions
を実行することで得られる。(4.5. 実行モードを見よ)
Language.Haskell.Extension
で定義されている型Extension
の構築子ならどれを使っても良い。指定された拡張がGHCでサポートされていないなら、エラーが報告される。
OPTIONS_GHC
プラグマは、そのソースファイルをコンパイルするときにコンパイラに与える追加のオプションを指定するのに使う。詳細は4.2.2. ソースファイル中のコマンド行オプションを見よ。
GHCの古い版ではOPTIONS_GHC
ではなくOPITONS
を受け付けていたが、これはもはや非推奨である。
OPTIONS_GHC
はファイルヘッダプラグマ(7.18. プラグマを見よ)である。
過去には、FFIを使うとき、Cを介してコンパイルしているなら、INCLUDE
プラグマを使ってどのヘッダファイルをincludeする必要があるか指定しなければならなかった。これはもはやGHCにとって必要でないが、他のコンパイラとの互換性のために受け付けられる(そして無視される)。
WARNINGプラグマを使うと、特定の関数やクラスや型に任意の警告を付属させることができる。DEPRECATEDプラグマを使うと、特定の関数やクラスや型が、非推奨・廃止予定であると指定できる。これらのプラグマを使うには二つの方法がある。
モジュール全体を相手にすることができる。
module Wibble {-# DEPRECATED "Use Wobble instead" #-} where ...
あるいは、
module Wibble {-# WARNING "This is an unstable interface." #-} where ...
Wibble
をインポートしているモジュールをコンパイルするときはいつでも、GHCは指定されたメッセージを印字する。
次のような最上位の宣言を使うことで、関数、クラス、型、データ構築子に警告を付属させることができる。
{-# DEPRECATED f, C, T "Don't use these" #-} {-# WARNING unsafePerformIO "This is unsafe; I hope you know what you're doing" #-}
指定された実体をインポートして使用しているモジュールをコンパイルするとき、GHCは指定されたメッセージを印字する。
付属させることができるのは、コンパイル中のモジュールの最上位で宣言されている実体だけである。また、実体を宣言する際には未修飾名を使わなければならない。T
のように大文字から始まる名前は、型構築子T
かデータ構築子T
かのいずれかであり、両方がスコープにあるなら両方である。両方がスコープにあるとき、片方だけを指定することは今のところできない。(7.4.3. 中置型構築子、中置クラス、中置型変数と比較せよ)
警告と非推奨報告は次のものに対しては為されない。(a) 定義されたモジュール中での使用、および (b) エクスポートリスト中での使用。後者のおかげで、一つのモジュールが複数のモジュールがエクスポートしているものを集めて再エクスポートするという構造のライブラリで、余計な文句を言うことがない。
フラグ-fno-warn-warnings-deprecations
を使ってこの警告を抑制することができる。
これらのプラグマは関数定義のインライン化を制御する。
GHCは、(いつもと同様、-O
が指定されているときだけ)「十分に小さい」関数・値をインライン化(または「展開(unfold)」)して、呼び出しのオーバーヘッドを回避し、場合によってはより素晴らしい最適化を可能にしようとする。通常、GHCがある関数をインライン化するのが「高くつきすぎる」と判断したときは、インライン化は行わないし、他のモジュールで使うために展開候補をエクスポートすることもない。
ここで使える強力な武器がINLINE
プラグマであり、次のように使う。
key_function :: Int -> String -> (Bool, Double) {-# INLINE key_function #-}
INLINE
プラグマの主要な効果は、ある関数の「コスト」がとても低いと宣言することである。これによって、通常の展開機構がインライン化に非常に積極的になる。一方、関数「f
」に関するINLINE
プラグマには、他の効果がいくつかある。
GHCは関数のインライン化について熱心であるが、盲目的に行なう訳ではない。例えば、次のように書いたとする。
map key_function xs
これをインライン化して以下のようにしても、実際得られるものは何もない。
map (\x -> body
) xs
一般的に言って、GHCが関数をインライン化するのは、そうすることが有益だと考えるなんらかの理由が(どんなに軽微なものでもいいが)ある場合だけである。
さらに、GHCは関数が完全に適用されている場合にしかインライン化しない。ここで「完全に適用されて」いるというのは、その関数の定義の左辺に(構文的に)出現する引数の数と同じだけの引数に適用されているということである。例を挙げる。
comp1 :: (b -> c) -> (a -> b) -> a -> c {-# INLINE comp1 #-} comp1 f g = \x -> f (g x) comp2 :: (b -> c) -> (a -> b) -> a -> c {-# INLINE comp2 #-} comp2 f g x = f (g x)
comp1
とcomp2
の二つの関数は同じ意味論を持つが、comp1
が二個の引数に適用されたときにインライン化されるのに対して、comp2
は三個を必要とする。これは次のような場合に重大な違いを生むことがある。
map (not `comp1` not) xs
これは、同様に「comp2」を使った場合よりも良く最適化されることになるだろう。
INLINE関数f
の非インライン版が最終的に使われる場合を考慮して、通常の非インライン関数と同様にf
の定義を最適化することが、GHCにとって有用である。しかし、f
の最適化版をインライン化したくはない。INLINEプラグマを使うことの大きな理由の一つは、f
の右辺にある関数が書き換え規則を持っている場合にそれを露出することだが、これらの関数が最適化によって消えてしまっては良くない。
そのため、GHCは、書かれたコードをそのままの形でインライン化することを保証する。過不足なく。そのために、インライン用に関数定義のコピー("inline-RHS"と呼んでいる)を取っておき、それには手をつけずに、通常の右辺をいつものように最適化する。外部から可視である関数については、(最適化された右辺ではなく)この"inline-RHS"がインタフェースファイルに記録される。
INLINE関数は正格性解析によってworker/wrapperされることがない。そうでなく、全部まとめてインライン化される。
GHCはインライン化が永遠に続くことがないことを保証する。相互再帰的な一団はすべて、一個以上の決してインライン化されないループ破りによって切り離される。( Secrets of the GHC inliner, JFP 12(4) July 2002を見よ)。GHCはループ破りとしてINLINEプラグマのついた関数を選ばないことを試みるが、選択肢がないときはINLINE関数でも選択され得、この場合INLINEプラグマは無視される。例えば、自己再帰的な関数では、ループ破りはその関数自体でしかありえないので、INLINEプラグマは常に無視される。
構文的には、ある関数についてのINLINE
プラグマは、その関数の型シグネチャが置けるところならどこに置いても良い。
INLINE
プラグマはモナドのthen
/return
(またはbind
/unit
)関数に特に有用である。例えば、GHCのUniqueSupply
モナドのコードには次のものが含まれている。
#ifdef __GLASGOW_HASKELL__ {-# INLINE thenUs #-} {-# INLINE returnUs #-} #endif
NOINLINE
プラグマ(7.18.5.3. NOINLINEプラグマ)とINLINABLE
プラグマ(7.18.5.2. INLINABLEプラグマ)も見よ。
注意: HBCコンパイラはINLINE
プラグマと相性が悪いので、コードをHBC互換にしたいなら#ifdef __GLASGOW_HASKELL__
...#endif
というCプリプロセッサ指令でプラグマを囲む必要があるだろう。
関数f
に対する{-# INLINABLE f #-}
プラグマは、以下のように振る舞う。
INLINE
は「これをインライン化してくれ」と言うものだが、INLINABLE
は「インライン化は御自由に、あなたの裁量で」というものである。つまり、選択はGHCに委ねられ、プラグマのない関数と同じ規則が使われる。INLINEと異なり、その選択は呼び出し地点で行なわれ、したがってインライン化閾値や、最適化水準などの影響を受ける。
INLINE
と同様に、INLINABLE
プラグマは元々の右辺のコピーを保持し、その右辺の大きさにかかわらずそれをインタフェースファイルに保存する。
INLINABLE
プラグマの使い道の一つは、特殊関数inline
(7.20. 特殊な組込み関数)と組み合わせることである。inline f
という呼び出しは、極めて積極的にf
をインライン化しようとする。f
がインライン化できることを確実にするためにf
をINLINABLE
と標示するのは良い考えである。そうすることで、GHCがそれの展開候補を(どれだけ大きくても)露出させることを保証するようにできるからである。さらに、f
がINLINABLE
であるという注釈をすることで、GHCの最適化器が作り出した何らかのf
の最適化版ではなく、f
の元々の右辺がインライン化されることが確かになる。
INLINABLE
プラグマはSPECIALISE
プラグマとも共同する。すなわち、関数f
をINLINABLE
と標示した場合、後で別モジュールでSPECIALISE
することができる(7.18.8. SPECIALIZEプラグマを見よ)。
INLINE
プラグマと異なり、INLINABLE
プラグマを再帰関数に使うのは問題ない。これをするのは主に後でSPECIALISE
を使えるようにするためである。
NOINLINE
プラグマはまさに想像される通りのことを行う。すなわち、指定された関数がコンパイラによってインライン化されるのを防ぐ。コードの大きさについて非常に用心深くある場合をのぞけば、これが必要になることはないはずである。
NOTINLINE
はNOINLINE
の同義名である。(NOINLINE
は、Haskell 98によって、インライン化を無効にするための標準的な方法として定められているので、コードの可搬性を気にするならこちらを使うべきである)
INLINEプラグマとNOINLINEプラグマはCONLIKE修飾子を持つことができる。これは、RULEの照合に(のみ)影響する。7.19.3. 書き換え規則と、INLINE/NOINLINEおよびCONLIKEプラグマとの間の相互作用を見よ。
GHCのパイプライン中のどの段階でINLINEプラグマが有効になるかを制御したいことがあるだろう。インライン化が行われるのは、単純化器の実行過程だけである。単純化器は、毎回異なる段階番号で実行される。段階番号は零に向かって減少する。-dverbose-core2core
を使えば、単純化器が連続して実行されるに際しての段階番号を見ることができる。次のように、INLINEプラグマに段階番号を指定することができる。
"INLINE[k] f
" : 段階k
まではf
をインライン化しないが、段階k
以降は非常に積極的にインライン化する。
"INLINE[~k] f
" : 段階k
まではf
を非常に積極的にインライン化するが、段階k
以降はインライン化しない。
"NOINLINE[k] f
" : 段階k
まではf
をインライン化しないが、段階k
以降は(プラグマがないかのように)インライン化しようとする。
"NOINLINE[~k] f
" : 段階k
まではf
をインライン化しようとするが、段階k
以降はインライン化しない。
以下に、上の情報をまとめる。
-- 段階2より前 段階2以降 {-# INLINE [2] f #-} -- しない する {-# INLINE [~2] f #-} -- する しない {-# NOINLINE [2] f #-} -- しない 場合による {-# NOINLINE [~2] f #-} -- 場合による しない {-# INLINE f #-} -- する する {-# NOINLINE f #-} -- しない しない
「場合による」というのは、インライン化についての通常のヒューリスティクス(関数本体が小さいなら、とか、興味深い見た目の引数に適用されているなら、など)が適用されるということである。この規則は次のように捉えることもできる。
INLINEとNOINLINEの両方について、段階番号はインライン化が少しでも許されるかどうかを言っている。
これに加えて、INLINEプラグマには、関数本体を小さく見せる効果がある。したがって、インライン化が許されているときには、インライン化が発生する可能性が極めて高い。
これと同じ段階番号制御はRULES(7.19. 書き換え規則 )についても使える。
このプラグマはCの#line
プラグマに似ていて、自動生成されたHaskellコードで使うことを主に意図したものである。これを使うと、元々のコードの行番号とファイル名を指定することができる。例えば、ファイルが、Foo.vhs
というファイルから生成され、その行が元のファイルの42行目に当たるなら、以下のように書けば良い。
{-# LINE 42 "Foo.vhs" #-}
エラーメッセージが報告されるとき、LINE
プラグマで指定された行・ファイルを参照するようになる。
RULESプラグマを使うと書き換え規則を指定することができる。これは7.19. 書き換え規則 で解説されている。
(英式にSPECIALISEでも良い) 鍵となる多重定義関数について、特定の型に特殊化された版を作ることができる。(注意: コードサイズは増大する)。次のような多重定義関数があったとしよう。
hammeredLookup :: Ord key => [(key, value)] -> key -> value
この関数が、keyの型をWidget
として特に良く使われるなら、次のようにして特殊化することができる。
{-# SPECIALIZE hammeredLookup :: [(Widget, value)] -> Widget -> value #-}
関数についてのSPECIALIZE
プラグマは、その関数の型シグネチャが書けるところならどこにでも書ける。さらに、定義の地点でINLINABLE
プラグマを与えられている(7.18.5.2. INLINABLEプラグマ)関数であれば、インポートした関数をSPECIALIZE
することができる。
SPECIALIZE
の効果は、その関数の特殊化版を生成することと、およびその関数の未特殊化版の呼び出しを特殊化版の呼び出しに書き換える規則(7.19. 書き換え規則
を見よ)を生成することである。さらに、関数f
にSPECIALIZE
が与えられていると、f
によって呼ばれている(型クラスによる)多重定義関数全てについて、もしそれがこのSPECIALIZE
プラグマと同じモジュールにあるかINLINABLE
であるならば、GHCは自動的にそれの特殊化版を作る。これは推移的に繰り返される。
SPECIALIZE
プラグマによって生成されたRULEに段階制御(7.18.5.5. 段階管理)を付け加えることができる。RULEを直接書いた場合と全く同様である。例を示す。
{-# SPECIALIZE [0] hammeredLookup :: [(Widget, value)] -> Widget -> value #-}
これは、段階0(最後の段階)にのみ発火する特殊化規則を生成する。SPECIALIZE
中に段階制御を指定しなかった場合、段階制御はその関数のインラインプラグマ(あれば)から継承される。例。
foo :: Num a => a -> a foo = ...blah... {-# NOINLINE [0] foo #-} {-# SPECIALIZE foo :: Int -> Int #-}
このNOINLINE
は、段階0になるまでfoo
をインライン化しないようにGHCに指示する。そしてこの性質は特殊化規則に継承されるので、これも段階0にのみ発火する。
特殊化に対して段階制御を使うことの主な理由は、まずコンパイルパイプラインの初期に発火する最適化RULESを書いて、その後で関数の呼び出しを特殊化できるからである。特殊化が行なわれるのが早すぎると最適化RULESが発火しないかもしれない。
SPECIALIZEプラグマ中の型は元の関数の型より多相性が低いものならなんでも良い。ちゃんとした言い方では次のようになる。元の関数がf
だとする。
{-# SPECIALIZE f :: <type> #-}
このプラグマが正当なのは、以下の定義が正当な時だけである。
f_spec :: <type> f_spec = f
いくつか例を示す。(元の関数については型シグネチャだけを示し、コードは省略した)
f :: Eq a => a -> b -> b {-# SPECIALISE f :: Int -> b -> b #-} g :: (Eq a, Ix b) => a -> b -> b {-# SPECIALISE g :: (Eq a) => a -> Int -> Int #-} h :: Eq a => a -> a -> a {-# SPECIALISE h :: (Eq a) => [a] -> [a] -> [a] #-}
最後の例では、生成されたRULSの左辺がかなり複雑なものになる(試してみよ)ので、あまりうまく発動しないかもしれない。この手の特殊化を使うことがあったら、どの程度うまくいったか知らせてほしい。
SPECIALIZE
の後にはINLINE
かNOINLINE
プラグマを続けることができる。さらに、7.18.5. INLINEおよびNOINLINEプラグマにあるように段階を指定することもできる。このようなINLINE
プラグマはその関数の特殊化版(のみ)に影響し、その関数が再帰的であっても適用される。動機となる例はこれである。
-- type-indexedな表現を持つ配列のGADT data Arr e where ArrInt :: !Int -> ByteArray# -> Arr Int ArrPair :: !Int -> Arr e1 -> Arr e2 -> Arr (e1, e2) (!:) :: Arr e -> Int -> e {-# SPECIALISE INLINE (!:) :: Arr Int -> Int -> Int #-} {-# SPECIALISE INLINE (!:) :: Arr (a, b) -> Int -> (a, b) #-} (ArrInt _ ba) !: (I# i) = I# (indexIntArray# ba i) (ArrPair _ a1 a2) !: i = (a1 !: i, a2 !: i)
ここでは、(!:)
はArr e
型の配列の添字演算を行う再帰関数である。(Int,Int)
での(!:)
の呼び出しを考えてみよう。二番目の特殊化が発動し、その特殊化された関数がインライン化される。それには(!:)
の呼び出しが二つあり、どちらもInt
型についてのものである。これらの呼び出しは両方とも最初の特殊化を発動させ、その本体も再びインライン化される。結果として、添字演算関数についての型によるアンロールができたことになる。
INLINE
プラグマにするのと同様に、段階制御(7.18.5.5. 段階管理)をSPECIALISE INLINE
プラグマに加えることができる。そうした場合、書き換え規則と、特殊化される関数のINLINE制御の両方に、同じ段階が使われる。
警告: SPECIALISE INLINE
を非多相再帰な関数に対して使うと、GHCは発散する。
一般に、SPECIALIZE
プラグマは同じモジュールで定義された関数にのみ与え得る。しかし関数f
がその定義場所でINLINABLE
プラグマを与えられているなら、インポート先のモジュールで後から特殊化することができる(7.18.5.2. INLINABLEプラグマを見よ)。例を挙げる。
module Map( lookup, blah blah ) where lookup :: Ord key => [(key,a)] -> key -> Maybe a lookup = ... {-# INLINABLE lookup #-} module Client where import Map( lookup ) data T = T1 | T2 deriving( Eq, Ord ) {-# SPECIALISE lookup :: [(T,a)] -> T -> Maybe a
ここで、lookup
がINLINABLE
と宣言されているが、この定義位置ではT
が存在しないので、これをT
に関して特殊化することができない。代わりに、利用者のモジュールがT
を定義して、lookup
をその型に関して特殊化することができる。
さらに、Client
をインポートする(あるいは推移的に、Client
をインポートするモジュールをインポートする)全てのモジュールからは、このlookup
の特殊化版が「見え」て、利用されることになる。各モジュールにSPECIALIZE
プラグマを置く必要はない。
さらに、そもそもSPECIALIZE
プラグマすら必要ないこともある。モジュールMをコンパイルする際、GHCの最適化器(-Oで)は、Mで定義された全ての最上位の多重定義関数について、Mで呼ばれている型に関して特殊化するかどうか検討する。加えて最適化器はMにインポートされたINLINABLE
の多重定義関数について、Mで呼ばれている型に関して特殊化するかどうか検討する。よってこの例では、以下のように、lookup
をT
型で呼ぶだけで十分だろう。
module Client where import Map( lookup ) data T = T1 | T2 deriving( Eq, Ord ) findT1 :: [(T,a)] -> Maybe a findT1 m = lookup m T1 -- T型でのlookupの呼び出し
しかし、このような呼び出しがないこともあるので、そういう場合にこのプラグマは便利になり得る。
参考: 古いGHCでは、特定の型についての特殊化を自分で指定することができた。
{-# SPECIALIZE hammeredLookup :: [(Int, value)] -> Int -> value = intLookup #-}
より一般的なRULES
プラグマ(7.19.5. 特殊化
を見よ)ができたので、この機能は削除された。
考え方は同じで、対象がインスタンス宣言になっただけである。例を示す。
instance (Eq a) => Eq (Foo a) where { {-# SPECIALIZE instance Eq (Foo [(Int, Bar)]) #-} ... 通常と同じ ... }
このプラグマはインスタンス宣言のwhere
部に現れなければならない。
ところで、これはHBCと互換性がある。あるいはプラグマの配置場所については非互換かもしれないが。
UNPACK
は、コンパイラに対し、構築子フィールドの内容を構築子に直に収めることで、一段階の間接参照を排除することを指示するものである。例を示す。
data T = T {-# UNPACK #-} !Float {-# UNPACK #-} !Float
これにより、二つの非ボックス化Floatを保持する構築子T
ができる。これは常に最適化になっているとは限らない。例えば、構築子T
の内容を調べて、そのfloatを非正格な関数に渡す場合、それらを再びボックス化しなければならない。(これはコンパイラによって自動的に行われる)
構築子のアンパックは専ら-O
と組み合わせて使われるべきである[13]。再ボックス化をなるべく排除できるように展開候補をコンパイラに露出するためである。次の例を考える。
f :: T -> Float f (T f1 f2) = f1 + f2
コンパイラは、floatについての+
をインライン化することで、f1
とf2
を再ボックス化することを避けるが、これは-O
が有効なときだけである。
単一構築子のデータはどんなものでもアンパックし得る。
data T = T {-# UNPACK #-} !(Int,Int)
この場合、構築子T
は、対を平坦化して、二つのInt
を直接保持することになる。複数水準のアンパックもサポートされている。
data T = T {-# UNPACK #-} !S data S = S {-# UNPACK #-} !Int {-# UNPACK #-} !Int
この場合、二つの非ボックス化Int#
が構築子T
に直接置かれる。アンパックはnewtypeを透過して起こる。
-funbox-strict-fields
フラグも見よ。これは、簡単に言うと、あらゆる正格な構築子フィールドに{-# UNPACK #-}
を加えるのと同じ効果がある。
NOUNPACK
はコンパイラに、構築子フィールドの中身をアンパックするべきでないということを伝える。例。
data T = T {-# NOUNPACK #-} !(Int,Int)
フラグ-funbox-strict-fields
および-O
を使っていても、構築子T
のフィールドはアンパックされない。
{-# SOURCE #-}
は専らimport
宣言の中で使われ、モジュールのループを断ち切る役割を果たす。詳しい説明は4.7.9. 相互再帰的なモジュールをコンパイルするにはにある。