7.3. 構文的拡張

7.3.1. Unicode構文

言語拡張-XUnicodeSyntaxは、特定のASCII文字列をUnicode文字を使って表すことを可能にする。以下の代替記法が提供される。

ASCIIUnicodeによる代替コードポイント名前
::::0x2237PROPORTION
=>0x21D2RIGHTWARDS DOUBLE ARROW
forall0x2200FOR ALL
->0x2192RIGHTWARDS ARROW
<-0x2190LEFTWARDS ARROW
-<0x2919LEFTWARDS ARROW-TAIL
>-0x291ARIGHTWARDS ARROW-TAIL
-<< 0x291BLEFTWARDS DOUBLE ARROW-TAIL
>>- 0x291CRIGHTWARDS DOUBLE ARROW-TAIL
*0x2605BLACK STAR

7.3.2. 魔法の井桁(magic hash)

-XMagicHashという言語拡張は、識別子に対する後置修飾子として「#」を認めるものである。つまり、「x#」が変数として有効に、「T#」が型構築子やデータ構築子として有効になる。

この井桁記号はまったく意味論に影響を与えない。非ボックス化された値や型に「#」で終わる名前を付ける(たとえばInt#)傾向があるが、必須ではない。これらはただの通常の変数に過ぎないのである。また、拡張-XMagicHashが何かをスコープに導入することもない。例えば、Int#をスコープに導入するためにはGHC.Prim(7.2. 非ボックス化型とプリミティブ演算を見よ)をインポートしなければならない。その後で、スコープに導入されたInt#言及することを可能にするのが-XMagicHashオプションである。

また、-XMagicHashは新しい形式のリテラルを何種類か有効にする。(7.2.1. 非ボックス化型 を見よ)

  • 'x'#の型はChar#

  • "foo"#の型はAddr#

  • 3#の型はInt#である。一般に、なんらかのHaskellの整数lexemeの後に#が付いたものはInt#になる。例えば-0x3A#32#がそうである。

  • 3##の型はWord#である。一般に、なんらかの非負なHaskellの整数lexemeの後に##が付いたものはWord#になる。

  • 3.2#の型はFloat#

  • 3.2##の型はDouble#

7.3.3. 階層的モジュール

GHCは、モジュール名の構文について、ある小さな拡張をサポートしている。すなわち、モジュール名はドット「.」を含むことができる。これは「階層的モジュール名前空間」拡張とも呼ばれる。これは、通常平坦なHaskellモジュールの名前空間を拡張して、より柔軟な、モジュールの階層をつくり出すからである。

この拡張は言語そのものにはほとんど影響を与えない。モジュール名は常に完全修飾されるので、完全修飾されたモジュール名を「真のモジュール名」だと考えることができる。従って、特に、モジュールの先頭のmoduleキーワードの後には、完全なモジュール名を与えなければならない。例えば、A.B.Cというモジュールは次のように始まらなければならない。

module A.B.C

階層的モジュールを使っていて、修飾名を使いたいときは、asキーワードを使ってタイプ数を節約するのが常套手段である。例えば、次のようにである。

import qualified Control.Monad.ST.Strict as ST

階層的モジュールが使われているときにGHCがどのようにソースファイルやインタフェースファイルを探索するかについては、4.7.3. 探索パスを見よ。

GHCには、階層的に配置された大規模なライブラリ群が付属している。これについては、付属のライブラリ説明書(訳注: 未訳。web上の最新版)を見てほしい。また、HackageDBから別のライブラリを入手してインストールすることもできる。

7.3.4. パターンガード

以下の議論はSimon Peyton Jonesの元提案を短くしたものである。(この提案はパターンガードが実装される前に書かれたので、これを未実装の機能として扱っていることに注意)

有限写像を表す抽象データ型と、それについてのlookup操作があったとしよう。

lookup :: FiniteMap -> Int -> Maybe Int

lookupは、与えられたキーが写像の定義域に含まれなければNothingを返し、そうでなければ(Just v)を返す。ここでvはそのキーが対応する値である。ここで次の定義を考えよう。

clunky env var1 var2 | ok1 && ok2 = val1 + val2
| otherwise  = var1 + var2
where
  m1 = lookup env var1
  m2 = lookup env var2
  ok1 = maybeToBool m1
  ok2 = maybeToBool m2
  val1 = expectJust m1
  val2 = expectJust m2

補助関数は次のとおりである。

maybeToBool :: Maybe a -> Bool
maybeToBool (Just x) = True
maybeToBool Nothing  = False

expectJust :: Maybe a -> a
expectJust (Just x) = x
expectJust Nothing  = error "Unexpected Nothing"

clunkyは何をしているのか?ガードであるok1 && ok2は両方のlookupが成功したことを確かめている。このために、maybeToBoolを使ってMaybeを真偽値に変換している。expectJustの呼び出し(遅延評価される)は、lookupの結果から値を抽出し、返った値をval1val2にそれぞれ束縛している。もしどちらかのlookupが失敗すると、clunkyはotherwiseの選択肢を選び、引数の和を返す。

これは確かに合法なHaskellだが、欲する結果を得るのに非常に冗長で自明でないやりかたをしている。おそらく、case式を使った方がclunkyをもっと直接的に書けるだろう。

clunky env var1 var2 = case lookup env var1 of
  Nothing -> fail
  Just val1 -> case lookup env var2 of
    Nothing -> fail
    Just val2 -> val1 + val2
where
  fail = var1 + var2

これで少し短くなったが、改善とは言えないだろう。もちろん、パターン照合やガードのついた等式をcase式に書き換えることは常にできる。これはまさに、複数の等式を持つ定義をコンパイルするときにコンパイラが行っていることである。Haskellにガードつきの等式があるのは、場合分けを一つ一つ独立に書き下していくことができるようにである。この構造はcaseを使った版では明らかでない。右辺のうちふたつは同じ(fail)だし、式全体がどんどんインデントされていっている。

私ならclunkyを次のように書く。

clunky env var1 var2
  | Just val1 <- lookup env var1
  , Just val2 <- lookup env var2
  = val1 + val2
...clunkyの他の等式...

意味は十分明快だろう。修飾子は順番に照合される。<-修飾子(パターンガードと呼ぼう)については、右辺が評価され、左辺のパターンと照合される。照合が失敗するとガードが全体として失敗し、次の等式が試みられる。成功すると、それに沿った束縛が行われ、次の修飾子が、拡張された環境の下で照合される。ただし、リスト内包表記の場合と違って、<-の右辺の式の型は左辺のパターンの型と同じである。パターンガードによって導入された束縛のスコープは、残りのガード修飾子と、その等式の右辺にわたる。

リスト内包表記の場合と同様に、パターンガード間に自由に真偽式を混ぜることができる。例えば、次のようにである。

f x | [y] <- x
    , y > 3
    , Just z <- h y
    = ...

従って、現在のHaskellのガードは、修飾子がただ一つの要素からなり、その要素が真偽式であるような、特別な場合とみなされる。

7.3.5. ビューパターン

ビューパターンを有効にするフラグは-XViewPatternsである。ビューパターンに関するさらなる情報と実例はWikiのページにある。

ビューパターンは、他のパターンの中に入れ子にして使えるパターンガードに少々似ていて、抽象型に対するパターン照合の方法として便利である。例えば、プログラミング言語の実装において、その言語の型の構文を以下のように表現することがあるかもしれない。

type Typ

data TypView = Unit
             | Arrow Typ Typ

view :: Type -> TypeView

-- さらに、Typを構築するための演算が続く...

Typの表現は抽象的なままにされているので、実装では手の込んだ表現(例えば共有を管理するためのhash-consing)を使うこともできる。 ビューパターンがないと、このシグネチャを使うのは少々不便である。

size :: Typ -> Integer
size t = case view t of
  Unit -> 1
  Arrow t1 t2 -> size t1 + size t2

等式を使った関数定義は使えず、このcaseを繰り返すしかない。さらに、tに関する照合が別のパターンの深くに埋まっている場合、状況はもっと悪くなる。

ビューパターンを使うと、関数viewをパターンの中で呼んで、その結果に対して照合を行うことができる。

size (view -> Unit) = 1
size (view -> Arrow t1 t2) = size t1 + size t2

つまり、expression -> patternと書かれる新しい形式のパターンを追加したのである。これは、「照合対象にexpressionを適用し、その適用の結果をpatternに対して照合せよ」という意味である。expressionは関数の型を持つ任意のHaskellの式であり、ビューパターンはパターンが使えるところならどこにでも使える。

(exp -> pat )というパターンの意味論は以下の通り。

  • スコープ規則:

    このビューパターンによって束縛される変数は、patによって束縛される変数である。

    exp中の変数は全て束縛された出現である(訳注: 変数はどれも束縛されていなければいけない、換言すればスコープにある変数しか使ってはいけないということ)が、「左の方」のパターン中で束縛された変数はスコープにある。この特徴によって、例えば、ある関数のある引数を、別の引数のビューの中で使うことができる。例えば、7.3.4. パターンガードに登場した関数clunkyは、ビューパターンを使って次のように書ける。

    clunky env (lookup env -> Just val1) (lookup env -> Just val2) = val1 + val2
    ...clunkyのその他の等式...
    

    より精密に言うと、スコープ規則は以下の通りである。

    • 単一のパターンの中で、ビューパターンの左にあるパターンで束縛された変数はスコープにある。例。

      example :: Maybe ((String -> Integer,Integer), String) -> Bool
      example Just ((f,_), f -> 4) = True
      

      さらに、関数定義において、カリー化された引数を照合することで束縛された変数は、その後の引数におけるビューパターン内で使うことができる。

      example :: (String -> Integer) -> String -> Bool
      example f (f -> 4) = True
      

      つまり、この場合のスコープ割り当ては、カリー化された引数をタプルにまとめた場合と同じになる。

    • letwhere、もしくは最上位のような相互再帰的な束縛において、ある宣言中のビューパターンが別の宣言によって束縛された変数に言及することはできない。つまり、それぞれの宣言が自己完結していなければならない。例えば、以下のプログラムは許されない。

      let {(x -> y) = e1 ;
           (y -> x) = e2 } in x
      

      (この設計上の決定について、よりはっきりと述べたものがTrac #4061にある。)

  • 型付け: もしexpの型がT1 -> T2patT2型の値に照合するなら、ビューパターン全体でT1型の値に照合する。

  • 照合: Haskell 98レポートの3.17.3節(和訳)の等式群に、以下のものを加える。

    case v of { (e -> p) -> e1 ; _ -> e2 }
     =
    case (e v) of { p -> e1 ; _ -> e2 }
    

    つまり、( exp -> pat )というパターンに変数vを照合するには、(exp v)を評価し、その結果をpatに対して照合する。

  • 効率性: ある関数定義やcase式の中で、同じビュー関数が複数の選択肢の中で使われている(例えば上のsize)場合、ビュー関数が一回しか適用されないように、GHCはその関数の適用を集約して一つのネストしたcase式にしようとする。GHCのパターンのコンパイルは、The Implementation of Functional Programming Languagesの第四章にある行列アルゴリズムに従っている。ある行列の最初の列の上部の行いくつか(訳注: top rows)が全て「同じ」式を持つビューパターンであった場合、それらのパターンは一つのネストされたcaseに変換される。これには、例えば、tuple中で整列したの隣接ビューパターンが含まれる。以下のような場合である。

    f ((view -> A, p1), p2) = e1
    f ((view -> B, p3), p4) = e2
    

    二つのビューパターンがどんなときに「同じ」であるかの現在の描像は非常に制限されたもので、完全な構文的同値性ですらない。それでも、変数、リテラル、適用、それにタプルを含んでいる。例えば、view ("hi", "there")が二つあった場合、それはまとめられる。一方、現在の実装はα同値性に従った比較を行わないので、(x, view x -> y)が二つあっても合体することはない。

7.3.6. n+kパターン

n+kパターンへの対応はデフォルトで無効になっている。有効にするには、-XNPlusKPatternsフラグが使える。

7.3.7. 伝統的なレコード構文

C {f = x}のような伝統的なレコード構文はデフォルトで有効になっている。無効にするには、-XNoTraditionalRecordSyntaxフラグが使える。

7.3.8. 再帰的do記法

Haskell 98のdo記法では、再帰的な束縛が許されない。すなわち、do式中で束縛された変数は、テキスト中でそれより後ろのコードブロックからのみ可視である。let式と比較せよ。let式では、束縛変数がその束縛グループ全体から可視である。

全てではないが色々なモナドに関して、do内でのこのような再帰的束縛が実際に意味のあることだということが分かった。特に、この意味での再帰は、使われているモナドについての不動点演算子を要求する。これはControl.Monad.Fixで次のように定義されているMonadFixクラスのmfixメソッドによって表現される。

class Monad m => MonadFix m where
   mfix :: (a -> m a) -> m a

HaskellのMaybe[] (リスト)、ST (正確版と遅延版の両方)、IOや他の多くのモナドはMonadFixインスタンスを持つ。一方、シグネチャ(a -> r) -> rを持つ継続モナドはこのインスタンスを持たない。

MonadFixに属するモナドについては、再帰的束縛を許すようなdo記法の拡張をGHCが提供する。-XRecursiveDo (言語プラグマ: RecursiveDo)がキーワードmdorecを含む必要な構文的サポートを提供する。この二つのキーワードはそれぞれ高水準・低水準の記法に使う。do式内の束縛とは異なり、mdorecによって導入される束縛は、ちょうど通常のlet式と同様に再帰的に定義される。mdoキーワードにちなんで、この記法をmdo記法とも呼ぶ。

以下は(人為的だが)単純な例である。

{-# LANGUAGE RecursiveDo #-}
justOnes = mdo { xs <- Just (1:xs)
               ; return (map negate xs) }

あるいは、以下も同等である

{-# LANGUAGE RecursiveDo #-}
justOnes = do { rec { xs <- Just (1:xs) }
              ; return (map negate xs) }

推測できるだろうが、justOnesJust [-1,-1,-1,...に評価される。

GHCにおけるmdo記法の実装は論文A recursive do for Haskellに記述されている元々の翻訳規則に良く則っている。この論文はValue Recursion in Monadic Computationsという仕事を基礎としている。さらに、GHCは前者の論文に記述されている構文を拡張し、recキーワードで示される低水準の構文を導入する。これを次に述べる。

7.3.8.1. 再帰的束縛グループ

フラグ-XRecursiveDoは、recという新しいキーワードを導入する。これは、相互再帰的なモナド文の集りをまとめて、一つの文を作るものである。

let文と同様に、recで束縛された変数はそのrecグループの全体と、そのrecの下で可視である。例として、次の二つを比較せよ。

    do { a <- getChar            do { a <- getChar
       ; let { r1 = f a r2          ; rec { r1 <- f a r2
       ;     ; r2 = g r1 }          ;     ; r2 <- g r1 }
       ; return (r1 ++ r2) }        ; return (r1 ++ r2) }

どちらの場合でも、r1r2letrecのブロック全体と、それ以降の文の中で使える。違いは、letが非モナド的であるのに対して、recはモナド的であることだ。(周知のように、Haskellにおいてletとは実際にはletrecのことである)

recの意味論はかなり単純である。GHCがrecグループを見付けると、それの束縛変数の集合を計算し、mfixへの呼び出しを適切に導入する。mfixは基礎となるモナド的な値再帰演算子であり、MonadFixクラスに所属する。例を挙げる。

rec { b <- f a c     ===>    (b,c) <- mfix (\~(b,c) -> do { b <- f a c
    ; c <- f b a }                                        ; c <- f b a
                                                          ; return (b,c) })

通常通り、bc等のメタ変数は任意のパターンであって良い。一般に、rec ssという文は、脱糖されて次のような文になる。

vs <- mfix (\~vs -> do { ss; return vs })

ただし、vsssによって束縛される変数群からなるタプルである。

recブロックの翻訳過程は、mfixへの呼び出しをラップしているに過ぎないことに特に注意せよ。束縛についてのその他の分析はなされない。これは次に述べるmdo記法の役割である。

7.3.8.2. mdo記法

recブロックは、再帰的な結び目を正確にどこに作るのかをコンパイラに指示する。しかし、結び目の位置決めがかなり繊細な問題になることが分かった。特に、結び目は可能な限り小さいグループを包んで欲しい。この過程は分割(segmentation)と呼ばれ、A recursive do for Haskellの3.2節に詳細な記述がある。分割によってよりよい多相性が得られ、再帰的な結び目の大きさが軽減される。最も重要なのは、これがモナド的再帰のいわゆるright-shrinking公理が持つ本質的な問題が引き起こす不必要な干渉を避ける点である。短かく言うと、意味のあるモナドの大部分(IO、正格Stateなど)はこの公理を満たす再帰演算子を持たないため、分割を行なわないと不要な干渉が発生し、最終的な翻訳結果の停止性を変えることがある。(詳細はValue Recursion in Monadic Computationsの3.1および7.2.2節にある)

mdo記法は、recブロックをコードに明示的に置くという負担を取り除く。文で束縛された変数がそれ以降の文からのみ可視になる通常のdo式と異なり、mdo式で束縛された変数はその式の全ての文から可視である。コンパイラは相互再帰する文からなる最小の分割単位を自動的に発見し、ユーザがその周りをrec修飾子で囲んだかのように扱う。

この定義は構文的である。

  • 生成子gが、字句的にそれより後にある生成子g'依存するのは、以下の場合である。

    • gによって使われる変数をg'が定義する、または

    • g'が字句的にgg''の間に現れる。ここでgg''に依存しているものとする。

  • 与えられたmdo式の分割単位(segment)とは、生成子の列であって、その列内のいかなる生成子も列外の生成子に依存しないような最小の列である。特別な場合として、mdo式の最後の式は、生成子ではないにもかかわらず、単独で分割単位を構成するとみなされる。

この意味での分割単位は強連結成分の解析に関係しているが、分割単位は並び換えることができず、連続していなければならないという点が違いである。

mdo式の例と、それのrecブロックへの翻訳を示す。

mdo { a <- getChar      ===> do { a <- getChar
    ; b <- f a c                ; rec { b <- f a c
    ; c <- f b a                ;     ; c <- f b a }
    ; z <- h a b                ; z <- h a b
    ; d <- g d e                ; rec { d <- g d e
    ; e <- g a z                ;     ; e <- g a z }
    ; putChar c }               ; putChar c }

与えられたmdo式が複数のrecブロックを作ることがあるのに注意。再帰的な依存関係がない場合、mdorecブロックを一つも導入しない。この場合、予想される通り、mdo式はdo式と全く同じである。

要約すると、mdo式を与えられたとき、GHCはまず分割を行ない、最小の再帰グループを包むrecを導入する、次に、結果として生成されたrecがそれぞれ、前の節で記述したようにControl.Monad.Fix.mfixへの呼び出しを使って脱糖される。最初のmdo式は、この脱糖済みのコードと全く同様に型検査される。

再帰的do記法を使うにあたって、他にもいくつか重要な点がある。

  • これは-XRecursiveDoフラグまたはLANGUAGE RecursiveDoプラグマで有効になる。(同じフラグが、mdo記法とdo記法内のrecの使用の両方を有効にする)

  • recブロックはmdo式の中でも使うことができ、単一の文として扱われる。ただし、一つの式ではmdorecブロックのどちらかを使うのが良いスタイルである。

  • あるモナドに再帰的な束縛を使う必要があるなら、そのモナドはMonadFixクラスのインスタンスとして宣言されていなければならない。

  • 次のMonadFixインスタンスは自動的に提供される。List, Maybe, IO。さらに、Control.Monad.STとControl.Monad.ST.Lazyモジュールは、Haskellの(それぞれ正格と遅延の)内部的な状態モナドについてのMonadFixインスタンスを提供する。

  • let束縛やwhere束縛と同様に、一つのrecの中での名前の覆い隠しは許されない。つまり、一つのrecで束縛される名前は全て異なっていなければならない(そうでなければGHCが文句を言う)。

7.3.9. 並行リスト内包表記

並行リスト内包表記はリスト内包表記を自然に拡張したものである。リスト内包表記は、mapとfilterを書くための扱いやすい構文と捉えることができる。並行内包表記はこれをzipWith系関数を含むように拡張するものである。

並行リスト内包表記は複数の独立した枝からなり、「|」で区切られる。それぞれの枝には修飾子が並べられる。例えば、以下のものは二つのリストをzipする。

   [ (x, y) | x <- xs | y <- ys ]

結果のリストは、最も短い枝と同じ長さになる。この点で、並行リスト内包表記の振る舞いはzipのものを踏襲している。

通常の内包表記への変換を規定することによって並行リスト内包表記を定義することができる。以下に示すのは基本的な考え方である。

次のような並行内包表記があったとする。

   [ e | p1 <- e11, p2 <- e12, ...
       | q1 <- e21, q2 <- e22, ...
       ...
   ]

これは次のように変換される。

   [ e | ((p1,p2), (q1,q2), ...) <- zipN [(p1,p2) | p1 <- e11, p2 <- e12, ...]
                                         [(q1,q2) | q1 <- e21, q2 <- e22, ...]
                                         ...
   ]

ここで、「zipN」は、枝の数に応じた適切なzipである。

7.3.10. 一般化(SQL風)リスト内包表記

一般化リスト内包表記は、SQLでおなじみのソートやグループ化といった操作を可能にするための、リスト内包表記という構文糖に対するさらなる強化である。これは、論文Comprehensive comprehensions: comprehensions with "order by" and "group by"で完全に記述されている。ただし、我々が使う構文は論文のものと僅かに異なる。

この拡張は-XTransformListCompというフラグによって有効になる。

例を示す。

employees = [ ("Simon", "MS", 80)
, ("Erik", "MS", 100)
, ("Phil", "Ed", 40)
, ("Gordon", "Ed", 45)
, ("Paul", "Yale", 60)]

output = [ (the dept, sum salary)
| (name, dept, salary) <- employees
, then group by dept using groupWith
, then sortWith by (sum salary)
, then take 5 ]

この例では、リストoutputの値は次のようになる。

[("Yale", 60), ("Ed", 85), ("MS", 180)]

新しいキーワードが三つある。groupbyusingである。(関数sortWithgroupWithはキーワードではない。GHC.Extsからエクスポートされている普通の関数である。)

内包表記修飾子の新しい形式が五つあり、すべて(既存の)キーワードthenで導入される。

  • then f
    
    この文は、fが型forall a. [a] -> [a]を持つことを要求する。これの使用例として、最初にあげた例ではtake 5を適用するのに使われている。
  • then f by e
    

    この形式は上のものに似ているが、fの最初の引数として渡される関数を作ることができる。そのため、fの型はforall a. (a -> t) -> [a] -> [a]でなければならない。型から分かるように、この関数は、変形対象のリストの要素からfがなんらかの情報を「射影抽出(project out)」できるようにするものである。

    ひとつの例が最初の例にある。この例では、変換されるリストの任意の要素についてsortWithsum salaryを見つけ出すのに使われる関数が、sortWithへの引数として与えられている。

  • then group by e using f
    

    グループ化系統の文のうち、最も一般的な形がこれである。この形式では、fの型がforall a. (a -> t) -> [a] -> [[a]]であることが要求される。上のthen f by eの場合と同様、最初の引数はコンパイラによってfに与えられる。これは、変換対象のリストの各要素についてfがeを計算することができるようにする関数である。しかし、グループ化以外の場合と異なり、fはさらに対象のリストをいくつかの部分リストに分割する。これによって、この文以降のあらゆる点において、内包表記中でこれ以前に現われた束縛は、単一の値ではなく可能な値のリストを指すようになる。これを理解する助けになるように、ひとつの例を見てみよう。

    -- これはGHC.ExtsのgroupWithと同様に働くが、最初に入力をソートしない
    groupRuns :: Eq b => (a -> b) -> [a] -> [[a]]
    groupRuns f = groupBy (\x y -> f x == f y)
    
    output = [ (the x, y)
    | x <- ([1..3] ++ [1..2])
    , y <- [4..6]
    , then group by x using groupRuns ]
    

    結果として、変数outputは次に示す値を取る。

    [(1, [4, 5, 6]), (2, [4, 5, 6]), (3, [4, 5, 6]), (1, [4, 5, 6]), (2, [4, 5, 6])]
    

    関数theを使って、xの型をリストから元の数値型に戻したのに注意。対照的に、変数yは、グループ化によって導入されたリスト形式のままにしてある。

  • then group using f
    

    この形式のgroup文では、fの型は単純にforall a. [a] -> [[a]]である必要があり、ここまでの内包を直接グループ化するのに使われる。この形式の例を以下に示す。

    output = [ x
    | y <- [1..5]
    , x <- "hello"
    , then group using inits]
    

    結果は、「hello」という単語を五回並べた文字列の、すべての前方部分列(prefix)を含むリストになる。

    ["","h","he","hel","hell","hello","helloh","hellohe","hellohel","hellohell","hellohello","hellohelloh",...]
    

7.3.11. Monad内包表記

モナド内包表記はリスト内包表記をあらゆるモナドに一般化したものである。これには並列内包表記(7.3.9. 並行リスト内包表記)と変換内包表記(7.3.10. 一般化(SQL風)リスト内包表記)を含む。

モナド内包表記は以下のものに対応する。

  • 束縛:

    [ x + y | x <- Just 1, y <- Just 2 ]
    

    束縛は、(>>=)returnを使って、次のような通常のdo記法に翻訳される。

    do x <- Just 1
       y <- Just 2
       return (x+y)
    
  • ガード:

    [ x | x <- [1..10], x <= 5 ]
    

    ガードはguard関数を使って翻訳される。これにはMonadPlusインスタンスが必要である。

    do x <- [1..10]
       guard (x <= 5)
       return x
    
  • 変換文(-XTransformListCompを付けた場合と同様)。

    [ x+y | x <- [1..10], y <- [1..x], then take 2 ]
    

    これは、以下のものに翻訳される。

    do (x,y) <- take 2 (do x <- [1..10]
                           y <- [1..x]
                           return (x,y))
       return (x+y)
    
  • グループ化文(-XTransformListCompを付けた場合と同様)

    [ x | x <- [1,1,2,2,3], then group by x using GHC.Exts.groupWith ]
    [ x | x <- [1,1,2,2,3], then group using myGroup ]
    
  • 並列な文(-XParallelListComp付きの場合と同様)

    [ (x+y) | x <- [1..10]
            | y <- [11..20]
            ]
    

    並列な文はmzip関数を使って翻訳される。これにはControl.Monad.Zipで定義されているMonadZipのインスタンスが必要である。

    do (x,y) <- mzip (do x <- [1..10]
                         return x)
                     (do y <- [11..20]
                         return y)
       return (x+y)
    

MonadComprehensions拡張が有効なら、これらの機能が全て有効になる。内包表記の種類と、より詳細な使い方については、前の章、7.3.10. 一般化(SQL風)リスト内包表記7.3.9. 並行リスト内包表記に説明がある。一般に、モナド内包表記用に型[a]を型Monad m => m aに置き換えるだけで良い。

注意: これらの例示ではほとんどリストモナドしか使っていないが、モナド内包表記はあらゆるモナドに対して働く。リストに関して必要なインスタンスは全てbaseパッケージが提供する。これによってMonadComprehensionsが組み込みのリスト内包表記、変換内包表記、並列内包表記と後方互換になる。

より形式的には、脱糖は以下のように行われる。以下ではモナド内包表記[ e | Q]を脱糖したものをD[ e | Q]と書く。

式: e
宣言: d
修飾子リスト: Q,R,S

-- 基本形
D[ e | ]               = return e
D[ e | p <- e, Q ]  = e >>= \p -> D[ e | Q ]
D[ e | e, Q ]          = guard e >> \p -> D[ e | Q ]
D[ e | let d, Q ]      = let d in D[ e | Q ]

-- 並列内包表記 (並列な枝が複数あるならこれを繰り返す)
D[ e | (Q | R), S ]    = mzip D[ Qv | Q ] D[ Rv | R ] >>= \(Qv,Rv) -> D[ e | S ]

-- 変換内包表記
D[ e | Q then f, R ]                  = f D[ Qv | Q ] >>= \Qv -> D[ e | R ]

D[ e | Q then f by b, R ]             = f (\Qv -> b) D[ Qv | Q ] >>= \Qv -> D[ e | R ]

D[ e | Q then group using f, R ]      = f D[ Qv | Q ] >>= \ys ->
                                        case (fmap selQv1 ys, ..., fmap selQvn ys) of
                                 	     Qv -> D[ e | R ]

D[ e | Q then group by b using f, R ] = f (\Qv -> b) D[ Qv | Q ] >>= \ys ->
                                        case (fmap selQv1 ys, ..., fmap selQvn ys) of
                                           Qv -> D[ e | R ]

ただし、QvはQで束縛された変数(のうち後で使われるもの)のタプルであり、
       selQviはQvをその第i成分に写すセレクタである

演算子       標準の束縛             期待される型
--------------------------------------------------------------------
return       GHC.Base               t1 -> m t2
(>>=)        GHC.Base               m1 t1 -> (t2 -> m2 t3) -> m3 t3
(>>)         GHC.Base               m1 t1 -> m2 t2         -> m3 t3
guard        Control.Monad          t1 -> m t2
fmap         GHC.Base               forall a b. (a->b) -> n a -> n b
mzip         Control.Monad.Zip      forall a b. m a -> m b -> m (a,b)

内包表記は、それを脱糖したものが型検査に通るなら型検査に通るべきである。

モナド内包表記は構文の再束縛(7.3.12. 再束縛可能な構文とPreludeの暗黙インポート)に対応している。構文の再束縛なしの場合、「標準の束縛」で定義された演算子が使われる。構文の再束縛が有効な場合、各演算子は現在の字句的スコープから引かれる。例えば、並列内包表記は、スコープにある"mzip"をなんであれ使って型検査・脱糖される。

再束縛される演算子は上の表にある「期待される型」を持っていなければならない。これらの型は驚くほど一般的である。例えば、次のような型を持つバインド演算子を使うことができる。

(>>=) :: T x y a -> (a -> T y z b) -> T x z b

変換内包表記の場合には、内包表記が任意のモナドに関する物であるだけでなく、グループが任意の(fmapを持つ)型nに関してパラメタ化されている。

7.3.12. 再束縛可能な構文とPreludeの暗黙インポート

GHCは通常Prelude.hiを自動的にインポートする。これが嫌なら、-XNoImplicitPreludeを使うと良い。こうすれば、自分自身のPreludeをインポートすることができる。(ただし、それにPreludeという名前をつけてはいけない。Haskellではモジュールの名前空間は平坦なので、Preludeモジュールと衝突を起こしてはならないのだ)

自分で数値クラスの階層を定義するために、自作のプレリュードを実装しているとしよう。しかし、リテラルの「1」が、Haskellレポートの指定通りにPrelude.fromInteger 1を意味するとしたら、これは完全な無駄骨である。このため、-XRebindableSyntaxフラグを使った場合には、以下に挙げる組込み構文は(Preludeのものではなく)スコープにあるものならなんでも使うようになる。

  • 整数リテラル368の意味は「fromInteger (368::Integer)」であり、「Prelude.fromInteger (368::Integer)」ではない。

  • 小数リテラルも全く同じように扱われる。変換はfromRational (3.68::Rational)である。

  • 多重定義された数値的パターンでの等値比較では、とにかくスコープにある(==)を使う。

  • n+kパターンにおける減算演算およびだいなりいこーる比較では、とにかくスコープにある(-)(>=)を使う。

  • 符号反転(例えば「- (f x)」)は、数値パターンでも式中でも「negate (f x)」を意味する。

  • 条件分岐(例えば、 "if e1 then e2 else e3")は"ifThenElse e1 e2 e3"を意味する。ただしcase式は影響を受けない。

  • do記法の変換時にはとにかくスコープにある(>>=)(>>)failが使われる。リスト内包表記、mdo(7.3.8. 再帰的do記法 )、並行配列内包表記は影響を受けない。

  • アロー記法(7.15. アロー記法 を見よ)では、とにかくスコープにあるarr(>>>)firstapp(|||)loopの各関数が使われる。ただし、他の構文要素の場合と異なり、これらの関数の型はPreludeのものとかなり良く近似していなければならない。詳細は固まっていので、もしこれを使いたいなら、声を掛けてほしい。

-XRebindableSyntaxは、-XNoImplicitPreludeを自動的に有効にする。

どの場合でも(アロー記法は例外)、コードの静的意味は脱糖された形でのそれと等しくなるはずである。これは少々予想に反するかもしれない。例えば、368というリテラルの静的意味はfromInteger (368::Integer)のそれとまったく同じである。従って、fromIntegerは下に挙げるどんな型をもっていても良い。

fromInteger :: Integer -> Integer
fromInteger :: forall a. Foo a => Integer -> a
fromInteger :: Num a => a -> Integer
fromInteger :: Integer -> Bool -> Bool

警告: これは実験的な機能であり、通常ほど検査がなされない。脱糖されたプログラムを型検査するには-dcore-lintを使う。Core Lintが満足しているなら、問題ないはずである。

7.3.13. 後置演算子

-XPostfixOperatorsフラグを使うと、演算子の左セクションの構文に小さな拡張が有効になり、これを使って後置演算子を定義することができるようになる。拡張とはこうである。以下のような左セクションがあったとしよう。

  (e !)

これは、(型検査と実行の両面において)以下の式と等しい。

  ((!) e)

(これは式eが何であっても、また演算子(!)が何であっても成り立つ)。Haskell 98の厳密な解釈では、このセクションは以下と同等だとされる。

  (\y -> (!) e y)

つまり、演算子は二引数の関数でなければならない。GHCでは一引数の関数であっても良く、結果として、関数を後置記法で書くことができるようになる。

この拡張は関数定義の左辺には及ばない。従って、このような関数を定義するときには前置形を使わなければならない。

7.3.14. タプルのセクション

-XTupleSectionsフラグは、タプルの構築子をPython風に部分適用することを有効にする。例として、以下のプログラム

  (, True)

は、次の不恰好な表記と同じ意味の書き方とみなされる。

  \x -> (x, True)

タプルの引数はどんな組み合わせで省略してもよい。以下のように。

  (, "I", , , "Love", , 1337)

これは、次のように翻訳される。

  \a b c d -> (a, "I", b, c, "Love", d, 1337)

unboxed tuplesを有効にしているなら、これについてもセクションが使える。例えば、

  (# , True #)

非ボックス化タプルにゼロ項のものはないので、次の式

  (# #)

は、変わらず非ボックス化単項タプルの構築子を意味する。

7.3.15. ラムダcase

-XLambdaCaseフラグは以下の形の式を有効にする。

  \case { p1 -> e1; ...; pN -> eN }

これは以下と同等である。

  \freshName -> case freshName of { p1 -> e1; ...; pN -> eN }

\caseはレイアウトを開始するので、以下のように書けることに注意。

  \case
    p1 -> e1
    ...
    pN -> eN

7.3.16. 多選択肢のif式

-XMultiWayIfフラグを使うと、GHCは以下のように複数の選択肢を持つ条件式を受け付ける。

  if | guard1 -> expr1
     | ...
     | guardN -> exprN

これは、だいたい次のものと同等である。

  case () of
    _ | guard1 -> expr1
    ...
    _ | guardN -> exprN

ただし、多選択肢のif式はレイアウトに変更を加えない。

7.3.17. レコードフィールドの曖昧性除去

レコードの構築とパターンマッチにおいては、仮に同じフィールド名を持つデータ型が二つスコープにあったとしても、どのフィールドが言及されているのか全く曖昧でない。例えば以下のように。

module M where
  data S = MkS { x :: Int, y :: Bool }

module Foo where
  import M

  data T = MkT { x :: Int }

  ok1 (MkS { x = n }) = n+1   -- 曖昧でない
  ok2 n = MkT { x = n+1 }     -- 曖昧でない

  bad1 k = k { x = 3 }  -- 曖昧
  bad2 k = x k          -- 曖昧

スコープには二つのxがあるが、ok1の定義中のパターンにおけるxは、型Sのフィールドを指す他にないということが明白である。関数ok2についても同様である。しかし、bad1におけるレコード更新と、bad2におけるレコード選択では、どちらの型が意図されているか明確でない。

Haskell 98はこの四つすべてを曖昧であると見做すが、-XDisambiguateRecordFieldsが与えられると、GHCは前者二つを認める。この規則は、Haskell 98でのインスタンス宣言の規則(インスタンス宣言中のメソッド束縛の左辺のメソッド名は曖昧さなくそのクラスのメソッドを(スコープにあれば)参照し、スコープに同名の別の変数があっても構わないとする)と全く同じである。これによって、異なるモジュールから同じフィールド名を使う二つのレコードをインポートしたときに、修飾名がそこらに散らばるのを軽減できる。

詳細をいくつか。

  • フィールドの曖昧性除去は同名利用(7.3.18. レコード同名利用 を見よ)と組み合わせることができる。例。

    module Foo where
      import M
      x=True
      ok3 (MkS { x }) = x+1   -- 曖昧性除去と同名利用を両方使っている
    

  • -XDisambiguateRecordFieldsが有効なら、対応する選択関数が修飾された形でしかスコープにない場合であっても、修飾されていないフィールド名を使うことができる。例えば、先に挙げた例と同じMモジュールがあると仮定すると、これは合法である。

    module Foo where
      import qualified M    -- qualifiedであることに注意
    
      ok4 (M.MkS { x = n }) = n+1   -- 曖昧でない
    

    構築子MkSは修飾された形でしかスコープにないので、M.MkSと呼ばなければならない。しかし、スコープにM.xがあってもxがないにもかかわらず、フィールドxを修飾する必要はない。(実質的に、これは構築子によって修飾されている)

7.3.18. レコード同名利用

レコード同名利用(訳注: record puns; punは「駄洒落」「語呂合わせ」の意)は、-XNamedFieldPunsフラグによって有効になる。

レコードを使うとき、フィールド名と同じ名前の変数を束縛するようなパターンを書くことがよくある。以下のような場合である。

data C = C {a :: Int}
f (C {a = a}) = a

レコード同名利用は、この変数名を省略することを可能にする。よって、上と同じことを単に次のように書くことができる。

f (C {a}) = a

つまり、レコードパターン中において、aというパターンはa = aというパターン(同じaという名前についての)に展開される。

以下のことに注意。

  • レコード同名利用は式中でも使える。例えば、次のように書く。

    let a = 1 in C {a}
    

    これは次のものと同等である。

    let a = 1 in C {a = a}
    

    この展開は純粋に構文上のものであるため、レコード同名利用の式は、そのフィールド名と同じ綴りを持つなかで、最も内側の変数を参照する。

  • 同じレコード中で、同名利用と通常のパターンを混ぜることができる。

    data C = C {a :: Int, b :: Int}
    f (C {a, b = 4}) = a
    

  • 同名利用は、レコードパターンが使えるところ(例えばlet束縛の中や最上位)ならどこでも使うことができる。

  • フィールド名が修飾されている場合は、その同名利用が展開されるときに修飾が取り除かれる。例えば、

    f (C {M.a}) = a
    

    は、

    f (M.C {M.a = a}) = a
    

    を意味する。(これは、構築子M.Cのフィールド選択関数aが修飾された形でのみスコープにある場合に便利である)

7.3.19. レコードワイルドカード

レコードワイルドカードは-XRecordWildCardsフラグによって有効になる。このフラグによって-XDisambiguateRecordFieldsも有効になる。

沢山のフィールドのあるレコードでは、レコードパターンにおいてフィールドをいちいち書き下すのが面倒なことがある。次のような場合である。

data C = C {a :: Int, b :: Int, c :: Int, d :: Int}
f (C {a = 1, b = b, c = c, d = d}) = b + c + d

レコードワイルドカード構文では、レコードパターン中に「..」を使えるようになる。そうすると、省略したそれぞれのff = fというパターンに置換される。例として、上のパターンは次のように書ける。

f (C {a = 1, ..}) = b + c + d

さらなる詳細。

  • ワイルドカードは他のパターン(同名利用(7.3.18. レコード同名利用 )も含む)と併用できる。例えばC {a = 1, b, ..})というパターンでのように。さらに、レコードワイルドカードはレコードパターンが使えるところならどこでも使えるので、let束縛や最上位でも使える。例えば、次の最上位の束縛は、bcdを定義する。

    C {a = 1, ..} = e
    

  • レコードワイルドカードは式中でも使える。次のように書くことができる。

    let {a = 1; b = 2; c = 3; d = 4} in C {..}
    

    これは以下のように書くのと同じである。

    let {a = 1; b = 2; c = 3; d = 4} in C {a=a, b=b, c=c, d=d}
    

    この展開は純粋に構文上のものなので、レコードワイルドカード式は、省略されたフィールド名と同じ綴りの変数のうちもっとも内側にあるものを参照する。

  • ..」は省略されたスコープにあるレコードフィールドに展開される。より精密には、"C {..}"の展開がfを含むのは、以下の条件が成り立つ場合、そしてその場合に限る。

    • fが構築子Cのレコードフィールドである。

    • なんらかの形でfがスコープにある(修飾形でも、非修飾形でも)。

    • (パターンでなく)式の場合、変数fが、レコードセレクタ自身の束縛とは別に、非修飾形でスコープにある。

    例を示す。

    module M where
      data R = R { a,b,c :: Int }
    module X where
      import M( R(a,b) )
      f b = R { .. }
    

    このR{..}R{M.a=a}に展開される。bはレコードフィールドがスコープにないので省略される。cは変数cがスコープにない(もちろん、レコード選択子cの束縛を除いて)ので省略される。

7.3.20. 局所結合性宣言

Haskell 98レポートを注意深く読むと、letwhereで導入される局所束縛の内部に結合性宣言(infixinfixlinfixr)が出現することが許されていることが分かる。しかし、このような束縛の意味論について、Haskellレポートはあまり詳しく規定していない。

GHCでは、次のように、局所束縛に結合性宣言が付属していてもよい。

let f = ...
    infixr 3 `f`
in
    ...

そして、この結合性宣言は、この束縛がスコープにあるようなあらゆる場所で適用される。例えば、let式なら、他のlet束縛の右辺とlet式の本体で適用される。また、再帰的do式(7.3.8. 再帰的do記法 )では、let文の局所結合性宣言は、束縛される名前と同様に、そのグループの全ての文に渡るスコープを持つ。

さらに、局所結合性宣言にはその名前についての局所束縛が付属「していなければならない」。次のように、別の場所で束縛された名前の結合性を設定しなおすことは不可能である。

let infixr 9 $ in ...

局所結合性宣言は技術的にHaskell 98なので、有効にするのにフラグは必要ない。

7.3.21. パッケージ修飾されたインポート

-XPackageImportsフラグが有効なら、GHCは、インポート宣言を、インポート先のモジュールが属しているべきパッケージ名で修飾することを認める。例をあげる。

import "network" Network.Socket

こうすると、networkパッケージ(のいずれかのバージョン)からNetwork.Socketをインポートすることになる。これは、同じモジュールが複数のパッケージから利用できたり、現在ビルド中のパッケージと外部のパッケージの両方にあったりする場合に、インポートの曖昧性を取り除くために使うことができる。

注意: おそらく、あなたがこの機能を使う必要はないだろう。この機能は、主に我々がAPIの変更に際して後方互換なパッケージのバージョンをビルドすることができるように追加されたものである。一般的な事例では、この機能を使うと脆い依存が発生しやすい。モジュールはあるパッケージから別のパッケージに移ることがあり、その場合パッケージ修飾されたインポートは壊れるからである。

7.3.22. safeインポート

-XSafe-XTrustworthy-XUnsafeのいずれかのフラグを付けると、GHCはimport宣言の構文を拡張してimportキーワードの後に任意でsafeキーワードを受け取るようにする。この機能はSafe HaskellというGHC拡張の一部である。例えば、

import safe qualified Network.Socket as NS

は、Network.Socketモジュールをインポートするが、Network.Socketが安全にインポートできる場合にしかコンパイルに成功しない。importがどういう場合に安全だとみなされるかの記述は7.25. Safe Haskellを見よ。

7.3.23. 盗まれた構文の概略

専用の構文を有効にするオプションを使うと、Haskell 98で動作していたコードがコンパイルできなくなる可能性がある。これは大抵、変数名として使われていたものが予約語になることが原因である。この節では、言語拡張によって「盗まれた」構文を列挙する。ここではHaskell 98の字句構文(Haskell 98レポートを見よ)の記法と非終端記号を使っている。構文の変更については、既存の正しいプログラムに影響する可能性のあるもの(「盗まれた」構文)のみを列挙する。多くの拡張は新しい文脈自由構文を導入するが、これらの場合は例外なく、新しい構文を使って書かれたプログラムは適当なオプションなしではコンパイルできない。

専用の構文には次の二種類がある。

  • 新しい予約語や予約シンボル。もはやプログラムで識別子として使うことのできない文字列である。

  • その他の専用構文。特定のオプションが有効になっていると別の意味を持つような文字の列。

盗まれるのは以下の構文である。

forall

-XExplicitForAllによって盗まれる(型中において)。従って、-XScopedTypeVariables-XLiberalTypeSynonyms-XRank2Types-XRankNTypes-XPolymorphicComponents-XExistentialQuantificationによっても盗まれる。

mdo

-XRecursiveDoに盗まれる。

foreign

-XForeignFunctionInterfaceに盗まれる。

recproc-<>--<<>>-、および(||)の括弧

-XArrowsによって盗まれる。

?varid, %varid

-XImplicitParamsによって盗まれる。

[|, [e|, [p|, [d|, [t|, $(, $varid

-XTemplateHaskellによって盗まれる。

[:varid|

-XQuasiQuotesによって盗まれる。

varid{#}, char#, string#, integer#, float#, float##, (#, #),

-XMagicHashによって盗まれる。