7.18. プラグマ

GHCはいくつかのプラグマ(ソースコード中に置かれるコンパイラへの指示)に対応している。プラグマは通常プログラムの意味には影響を与えないが、生成されるコードの効率性には影響し得る。

全てのプラグマは{-# word ... #-}という形を取る。ここで、wordはプラグマの種類を表す。必要なら、この後に続けて、その種類のプラグマに特有の情報を書く。wordでは大文字小文字の区別はなされない。GHCが理解する種々のプラグマは以下の節で解説されている。認識できないwordを持ったプラグマは無視される。プラグマ中ではレイアウト規則が適用されるので、閉じ括弧#-}は開き括弧{-#よりも右のカラムで始まっていなければならない。

ある種のプラグマはファイルヘッダプラグマである。

7.18.1. LANGUAGEプラグマ

このプラグマは、言語拡張を、可搬性のある方法で有効にできるようにするものである。意図としては、全てのコンパイラが同じ構文の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でサポートされていないなら、エラーが報告される。

7.18.2. OPTIONS_GHCプラグマ

OPTIONS_GHCプラグマは、そのソースファイルをコンパイルするときにコンパイラに与える追加のオプションを指定するのに使う。詳細は4.2.2. ソースファイル中のコマンド行オプションを見よ。

GHCの古い版ではOPTIONS_GHCではなくOPITONSを受け付けていたが、これはもはや非推奨である。

OPTIONS_GHCはファイルヘッダプラグマ(7.18. プラグマを見よ)である。

7.18.3. INCLUDEプラグマ

過去には、FFIを使うとき、Cを介してコンパイルしているなら、INCLUDEプラグマを使ってどのヘッダファイルをincludeする必要があるか指定しなければならなかった。これはもはやGHCにとって必要でないが、他のコンパイラとの互換性のために受け付けられる(そして無視される)。

7.18.4. WARNINGおよびDEPRECATEDプラグマ

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を使ってこの警告を抑制することができる。

7.18.5. INLINEおよびNOINLINEプラグマ

これらのプラグマは関数定義のインライン化を制御する。

7.18.5.1. INLINEプラグマ

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)
    

    comp1comp2の二つの関数は同じ意味論を持つが、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プリプロセッサ指令でプラグマを囲む必要があるだろう。

7.18.5.2. INLINABLEプラグマ

関数fに対する{-# INLINABLE f #-}プラグマは、以下のように振る舞う。

  • INLINEは「これをインライン化してくれ」と言うものだが、INLINABLEは「インライン化は御自由に、あなたの裁量で」というものである。つまり、選択はGHCに委ねられ、プラグマのない関数と同じ規則が使われる。INLINEと異なり、その選択は呼び出し地点で行なわれ、したがってインライン化閾値や、最適化水準などの影響を受ける。

  • INLINEと同様に、INLINABLEプラグマは元々の右辺のコピーを保持し、その右辺の大きさにかかわらずそれをインタフェースファイルに保存する。

  • INLINABLEプラグマの使い道の一つは、特殊関数inline(7.20. 特殊な組込み関数)と組み合わせることである。inline fという呼び出しは、極めて積極的にfをインライン化しようとする。fがインライン化できることを確実にするためにfINLINABLEと標示するのは良い考えである。そうすることで、GHCがそれの展開候補を(どれだけ大きくても)露出させることを保証するようにできるからである。さらに、fINLINABLEであるという注釈をすることで、GHCの最適化器が作り出した何らかのfの最適化版ではなく、fの元々の右辺がインライン化されることが確かになる。

  • INLINABLEプラグマはSPECIALISEプラグマとも共同する。すなわち、関数fINLINABLEと標示した場合、後で別モジュールでSPECIALISEすることができる(7.18.8. SPECIALIZEプラグマを見よ)。

  • INLINEプラグマと異なり、INLINABLEプラグマを再帰関数に使うのは問題ない。これをするのは主に後でSPECIALISEを使えるようにするためである。

7.18.5.3. NOINLINEプラグマ

NOINLINEプラグマはまさに想像される通りのことを行う。すなわち、指定された関数がコンパイラによってインライン化されるのを防ぐ。コードの大きさについて非常に用心深くある場合をのぞけば、これが必要になることはないはずである。

NOTINLINENOINLINEの同義名である。(NOINLINEは、Haskell 98によって、インライン化を無効にするための標準的な方法として定められているので、コードの可搬性を気にするならこちらを使うべきである)

7.18.5.4. CONLIKE修飾子

INLINEプラグマとNOINLINEプラグマはCONLIKE修飾子を持つことができる。これは、RULEの照合に(のみ)影響する。7.19.3. 書き換え規則と、INLINE/NOINLINEおよびCONLIKEプラグマとの間の相互作用を見よ。

7.18.5.5. 段階管理

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. 書き換え規則 )についても使える。

7.18.6. LINEプラグマ

このプラグマはCの#lineプラグマに似ていて、自動生成されたHaskellコードで使うことを主に意図したものである。これを使うと、元々のコードの行番号とファイル名を指定することができる。例えば、ファイルが、Foo.vhsというファイルから生成され、その行が元のファイルの42行目に当たるなら、以下のように書けば良い。

{-# LINE 42 "Foo.vhs" #-}

エラーメッセージが報告されるとき、LINEプラグマで指定された行・ファイルを参照するようになる。

7.18.7. RULESプラグマ

RULESプラグマを使うと書き換え規則を指定することができる。これは7.19. 書き換え規則 で解説されている。

7.18.8. SPECIALIZEプラグマ

(英式に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. 書き換え規則 を見よ)を生成することである。さらに、関数fSPECIALIZEが与えられていると、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の左辺がかなり複雑なものになる(試してみよ)ので、あまりうまく発動しないかもしれない。この手の特殊化を使うことがあったら、どの程度うまくいったか知らせてほしい。

7.18.8.1. SPECIALIZE INLINE

SPECIALIZEの後にはINLINENOINLINEプラグマを続けることができる。さらに、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は発散する。

7.18.8.2. インポートした関数のSPECIALIZE

一般に、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

ここで、lookupINLINABLEと宣言されているが、この定義位置ではTが存在しないので、これをTに関して特殊化することができない。代わりに、利用者のモジュールがTを定義して、lookupをその型に関して特殊化することができる。

さらに、Clientをインポートする(あるいは推移的に、Clientをインポートするモジュールをインポートする)全てのモジュールからは、このlookupの特殊化版が「見え」て、利用されることになる。各モジュールにSPECIALIZEプラグマを置く必要はない。

さらに、そもそもSPECIALIZEプラグマすら必要ないこともある。モジュールMをコンパイルする際、GHCの最適化器(-Oで)は、Mで定義された全ての最上位の多重定義関数について、Mで呼ばれている型に関して特殊化するかどうか検討する。加えて最適化器はMにインポートされたINLINABLEの多重定義関数について、Mで呼ばれている型に関して特殊化するかどうか検討する。よってこの例では、以下のように、lookupT型で呼ぶだけで十分だろう。

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の呼び出し

しかし、このような呼び出しがないこともあるので、そういう場合にこのプラグマは便利になり得る。

7.18.8.3. 旧式のSPECIALIZE構文

参考: 古いGHCでは、特定の型についての特殊化を自分で指定することができた。

{-# SPECIALIZE hammeredLookup :: [(Int, value)] -> Int -> value = intLookup #-}

より一般的なRULESプラグマ(7.19.5. 特殊化 を見よ)ができたので、この機能は削除された。

7.18.9. SPECIALIZE instanceプラグマ

考え方は同じで、対象がインスタンス宣言になっただけである。例を示す。

instance (Eq a) => Eq (Foo a) where {
   {-# SPECIALIZE instance Eq (Foo [(Int, Bar)]) #-}
   ... 通常と同じ ...
 }

このプラグマはインスタンス宣言のwhere部に現れなければならない。

ところで、これはHBCと互換性がある。あるいはプラグマの配置場所については非互換かもしれないが。

7.18.10. UNPACKプラグマ

UNPACKは、コンパイラに対し、構築子フィールドの内容を構築子に直に収めることで、一段階の間接参照を排除することを指示するものである。例を示す。

data T = T {-# UNPACK #-} !Float
           {-# UNPACK #-} !Float

これにより、二つの非ボックス化Floatを保持する構築子Tができる。これは常に最適化になっているとは限らない。例えば、構築子Tの内容を調べて、そのfloatを非正格な関数に渡す場合、それらを再びボックス化しなければならない。(これはコンパイラによって自動的に行われる)

構築子のアンパックは専ら-Oと組み合わせて使われるべきである[13]。再ボックス化をなるべく排除できるように展開候補をコンパイラに露出するためである。次の例を考える。

f :: T -> Float
f (T f1 f2) = f1 + f2

コンパイラは、floatについての+をインライン化することで、f1f2を再ボックス化することを避けるが、これは-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 #-}を加えるのと同じ効果がある。

7.18.11. NOUNPACKプラグマ

NOUNPACKはコンパイラに、構築子フィールドの中身をアンパックするべきでないということを伝える。例。

data T = T {-# NOUNPACK #-} !(Int,Int)

フラグ-funbox-strict-fieldsおよび-Oを使っていても、構築子Tのフィールドはアンパックされない。

7.18.12. SOURCEプラグマ

{-# SOURCE #-}は専らimport宣言の中で使われ、モジュールのループを断ち切る役割を果たす。詳しい説明は4.7.9. 相互再帰的なモジュールをコンパイルするにはにある。

[13]

実は、技術的な事情によりUNPACKプラグマは-Oなしでは何もしない。(tick 5252を見よ)