言語拡張-XUnicodeSyntax
は、特定のASCII文字列をUnicode文字を使って表すことを可能にする。以下の代替記法が提供される。
ASCII | Unicodeによる代替 | コードポイント | 名前 |
---|---|---|---|
::
|
:: | 0x2237 | PROPORTION |
=>
|
⇒ | 0x21D2 | RIGHTWARDS DOUBLE ARROW |
forall
|
∀ | 0x2200 | FOR ALL |
->
|
→ | 0x2192 | RIGHTWARDS ARROW |
<-
|
← | 0x2190 | LEFTWARDS ARROW |
-< | ↢ | 0x2919 | LEFTWARDS ARROW-TAIL |
>- | ↣ | 0x291A | RIGHTWARDS ARROW-TAIL |
-<< | 0x291B | LEFTWARDS DOUBLE ARROW-TAIL | |
>>- | 0x291C | RIGHTWARDS DOUBLE ARROW-TAIL | |
* | ★ | 0x2605 | BLACK STAR |
-XMagicHash
という言語拡張は、識別子に対する後置修飾子として「#」を認めるものである。つまり、「x#」が変数として有効に、「T#」が型構築子やデータ構築子として有効になる。
この井桁記号はまったく意味論に影響を与えない。非ボックス化された値や型に「#」で終わる名前を付ける(たとえばInt#
)傾向があるが、必須ではない。これらはただの通常の変数に過ぎないのである。また、拡張-XMagicHash
が何かをスコープに導入することもない。例えば、Int#
をスコープに導入するためにはGHC.Prim
(7.2. 非ボックス化型とプリミティブ演算を見よ)をインポートしなければならない。その後で、スコープに導入されたInt#
に言及することを可能にするのが-XMagicHash
オプションである。このオプションが有効だと、x#y = 0
の意味が変わることに注意。y
という引数を一つ取る関数x#
を定義することになる。#
という演算子を定義するには、空白を置けば良い。x # y = 0
。
また、-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#
-123
というリテラルは、Haskell98およびHaskell 2010によれば、negate (fromInteger 123)
のように脱糖される。言語拡張-XNegativeLiterals
は、これがfromInteger (-123)
に脱糖されるようにする。
ある数値的データ型が表現できる範囲が正と負で異なる場合、これによって違いが出ることがある。例えば、8ビット算術において-128は表現可能だが、+128はそうでない。よって、negate (fromInteger 128)
は予期しない整数リテラルオーバーフローのメッセージを引き起こす。
Haskell 2010およびHaskell 98は1.2e6
という構文の浮動小数点リテラルを定義している。このようなリテラルはFractional a => a
という型を持つ。
言語拡張-XNumDecimals
は、Integral
のインスタンスについても浮動小数点リテラルの構文を許し、(1.2e6 :: Num a => a)
のような値を使えるようにする。
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から別のライブラリを入手してインストールすることもできる。
以下の議論は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の結果から値を抽出し、返った値をval1
とval2
にそれぞれ束縛している。もしどちらかの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のガードは、修飾子がただ一つの要素からなり、その要素が真偽式であるような、特別な場合とみなされる。
ビューパターンを有効にするフラグは-XViewPatterns
である。ビューパターンに関するさらなる情報と実例はWikiのページにある。
ビューパターンは、他のパターンの中に入れ子にして使えるパターンガードに少々似ていて、抽象型に対するパターン照合の方法として便利である。例えば、プログラミング言語の実装において、その言語の型の構文を以下のように表現することがあるかもしれない。
type Typ data TypView = Unit | Arrow Typ Typ view :: Typ -> TypView -- さらに、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.6. パターンガードに登場した関数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
つまり、この場合のスコープ割り当ては、カリー化された引数をタプルにまとめた場合と同じになる。
let
やwhere
、もしくは最上位のような相互再帰的な束縛において、ある宣言中のビューパターンが別の宣言によって束縛された変数に言及することはできない。つまり、それぞれの宣言が自己完結していなければならない。例えば、以下のプログラムは許されない。
let {(x -> y) = e1 ; (y -> x) = e2 } in x
(この設計上の決定について、よりはっきりと述べたものがTrac #4061にある。)
型付け: もしexp
の型がT1
->
T2
でpat
がT2
型の値に照合するなら、ビューパターン全体で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)
が二つあっても合体することはない。
パターンシノニムは-XPatternSynonyms
フラグによって有効になる。このフラグは、パターンシノニムを使うのと定義するのの両方に必要である。ビューパターンについてのより多くの例はWikiページにある。
パターンシノニムは、特定の形のパターンをパラメタ化して、名前を与えることを可能にする。データの表現と結びつけられていない、抽象的な構築子だと思うこともできる。例えば、プログラミング言語の実装において、その言語の型が以下のように表現されているかもしれない。
data Type = App String [Type]
この表現の例をいくつか挙げる。Type
宇宙にある型のうちいくつか、以下のようにエンコードされるものを考えよ。
App "->" [t1, t2] -- t1 -> t2 App "Int" [] -- Int App "Maybe" [App "Int" []] -- Maybe Int
この表現は、どの型も特別扱いを受けないという点で非常に包括的である。しかし、関数によってはなんらかの既知の型を特別に扱う必要があるかもしれない。例えば、以下の関数はそれぞれ、(ネストされた)矢印型の引数の型を全て集めるものと、Int
型を認識するものである。
collectArgs :: Type -> [Type] collectArgs (App "->" [t1, t2]) = t1 : collectArgs t2 collectArgs _ = [] isInt :: Type -> Bool isInt (App "Int" []) = True isInt _ = False
直接App
に対する照合を行なうのは読みにくく、書くときは間違えやすい。照合が入れ子になると、状況はさらに悪化する。
isIntEndo :: Type -> Bool isIntEndo (App "->" [App "Int" [], App "Int" []]) = True isIntEndo _ = False
パターンシノニムを使うと、表現を抽象化した形で、構築子のようにパターン照合できる照合子を公開することができる。好きな既知の型に対して、その型の表現を変えることなく、パターンシノニムを作ることができる。(これらはType
型が定義されたのと同じモジュールで定義される必要がないことに注意)
pattern Arrow t1 t2 = App "->" [t1, t2] pattern Int = App "Int" [] pattern Maybe t = App "Maybe" [t]
これによって、上の関数をずっときれいに書くことができる。
collectArgs :: Type -> [Type] collectArgs (Arrow t1 t2) = t1 : collectArgs t2 collectArgs _ = [] isInt :: Type -> Bool isInt Int = True isInt _ = False isIntEndo :: Type -> Bool isIntEndo (Arrow Int Int) = True isIntEndo _ = False
この例では、パターンシノニムInt
およびArrow
を式としても使える(これらは双方向である)ことに注意。これは常には成り立たない。以下の構文を使うと、一方向のパターンシノニムを宣言できる。
pattern Head x <- x:xs
この場合、Head
x
を使うことができるのはパターン内だけで、式では使えない。なぜなら、これは右辺のxs
の値を決めないからである。
一方向パターンシノニムの宣言と使用の意味論は以下の通りである
パターンシノニム宣言は、一方向が双方向のいずれかである。一方向パターンシノニムの宣言は、
pattern Name args <- pat
双方向パターンシノニムの構文は、
pattern Name args = pat
パターンシノニム宣言はモジュールの最上位にしか出現できない。特に、これは局所定義としては許されない。現在GHCiでも使えないが、これは技術的な制限であり、将来のバージョンで撤廃されるであろう。
パターンシノニム自体の名前は、真のデータ構築子と同じ名前空間にある。前置形と中置形のどちらの構文も使える。インポート/エクスポート指定では、パターン名にpattern
を前置する必要がある。例えば、
module Example (pattern Single) where pattern Single x = [x]
定義の左辺に出てくる変数は、右辺のパターンによって束縛される。双方向パターンシノニムでは、右辺の変数は全て左辺にも出現しなければならない。さらに、ワイルドカードパターンとビューパターンは使用できない。一方向パターンシノニムについては、右辺のパターンに制約はない。
パターンシノニムを再帰的に定義することはできない。
以下の形のパターンシノニム宣言は、
pattern P var1 var2 ... varN <- pat
以下の形のパターン型を与えられる。
pattern CProv => P t1 t2 ... tN :: CReq => t
ここで、CProv
とCReq
は型文脈であり、t1
、t2
, ..., tN
、t
は型である。
この型を持つパターンシノニムは、具体化された後の(単相)型が、CReq
の制約を満たす場合に使える。この場合、照合の右辺において、利用可能な文脈をCProv
で拡張する。これは、存在量化された型が文脈を拡張できるのと同じである。
例として以下のプログラムを考える。
{-# LANGUAGE PatternSynonyms, GADTs #-} module ShouldCompile where data T a where MkT :: (Show b) => a -> b -> T a pattern ExNumPat x = MkT 42 x
ExNumPat
のパターン型は、
pattern (Show b) => ExNumPat b :: (Num a, Eq a) => T a
よって、これを使って以下のような関数を定義することができる。
f :: (Num t, Eq t) => T t -> String f (ExNumPat x) = show x
双方向パターンシノニムは、式として使われる場合には次の型を持つ。
(CProv, CReq) => t1 -> t2 -> ... -> tN -> t
よって、前の例において、式の中でExNumPat
が使われた場合、以下の型を持つ。
ExNumPat :: (Show b, Num a, Eq a) => b -> T t
パターン内にパターンシノニムが出現する場合、評価に際しては、まずそのパターンシノニム自体に対して照合が行なわれ、その後引数であるパターンに対して照合が行なわれる。例えば、以下のプログラムにおいて、f
とf'
は同等である。
pattern Pair x y <- [x, y] f (Pair True True) = True f _ = False f' [x, y] | True <- x, True <- y = True f' _ = False
f
の正格性は、以下のように定義されたg
とは異なることに注意。
g [True, True] = True g _ = False *Main> f (False:undefined) *** Exception: Prelude.undefined *Main> g (False:undefined) False
n+k
パターンへの対応はデフォルトで無効になっている。有効にするには、-XNPlusKPatterns
フラグが使える。
C {f = x}
のような伝統的なレコード構文はデフォルトで有効になっている。無効にするには、-XNoTraditionalRecordSyntax
フラグが使える。
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
)がキーワードmdo
とrec
を含む必要な構文的サポートを提供する。この二つのキーワードはそれぞれ高水準・低水準の記法に使う。do
式内の束縛とは異なり、mdo
やrec
によって導入される束縛は、ちょうど通常の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) }
推測できるだろうが、justOnes
はJust [-1,-1,-1,...
に評価される。
GHCにおけるmdo記法の実装は論文A recursive do for Haskellに記述されている元々の翻訳規則に良く則っている。この論文はValue Recursion in Monadic Computationsという仕事を基礎としている。さらに、GHCは前者の論文に記述されている構文を拡張し、rec
キーワードで示される低水準の構文を導入する。これを次に述べる。
フラグ-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) }
どちらの場合でも、r1
とr2
はlet
やrec
のブロック全体と、それ以降の文の中で使える。違いは、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) })
通常通り、b
、c
等のメタ変数は任意のパターンであって良い。一般に、rec
という文は、脱糖されて次のような文になる。ss
vs
<- mfix (\ ~vs
-> do {ss
; returnvs
})
ただし、vs
はss
によって束縛される変数群からなるタプルである。
rec
ブロックの翻訳過程は、mfix
への呼び出しをラップしているに過ぎないことに特に注意せよ。束縛についてのその他の分析はなされない。これは次に述べる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'
が字句的にg
とg''
の間に現れる。ここでg
はg''
に依存しているものとする。
与えられた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
ブロックを作ることがあるのに注意。再帰的な依存関係がない場合、mdo
はrec
ブロックを一つも導入しない。この場合、予想される通り、mdo
式はdo
式と全く同じである。
要約すると、mdo
式を与えられたとき、GHCはまず分割を行ない、最小の再帰グループを包むrec
を導入する、次に、結果として生成されたrec
がそれぞれ、前の節で記述したようにControl.Monad.Fix.mfix
への呼び出しを使って脱糖される。最初のmdo
式は、この脱糖済みのコードと全く同様に型検査される。
再帰的do記法を使うにあたって、他にもいくつか重要な点がある。
これは-XRecursiveDo
フラグまたはLANGUAGE RecursiveDo
プラグマで有効になる。(同じフラグが、mdo
記法とdo
記法内のrec
の使用の両方を有効にする)
rec
ブロックはmdo
式の中でも使うことができ、単一の文として扱われる。ただし、一つの式ではmdo
かrec
ブロックのどちらかを使うのが良いスタイルである。
あるモナドに再帰的な束縛を使う必要があるなら、そのモナドはMonadFix
クラスのインスタンスとして宣言されていなければならない。
次のMonadFix
インスタンスは自動的に提供される。List, Maybe, IO。さらに、Control.Monad.STとControl.Monad.ST.Lazyモジュールは、Haskellの(それぞれ正格と遅延の)内部的な状態モナドについてのMonadFixインスタンスを提供する。
let
束縛やwhere
束縛と同様に、一つのrec
の中での名前の覆い隠しは許されない。つまり、一つのrec
で束縛される名前は全て異なっていなければならない(そうでなければGHCが文句を言う)。
並行リスト内包表記はリスト内包表記を自然に拡張したものである。リスト内包表記は、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である。
一般化リスト内包表記は、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)]
新しいキーワードが三つある。group
、by
、using
である。(関数sortWith
とgroupWith
はキーワードではない。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)」できるようにするものである。
ひとつの例が最初の例にある。この例では、変換されるリストの任意の要素についてsortWith
がsum 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.12. 並行リスト内包表記)と変換内包表記(7.3.13. 一般化(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.13. 一般化(SQL風)リスト内包表記と7.3.12. 並行リスト内包表記に説明がある。一般に、モナド内包表記用に型[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.15. 再束縛可能な構文とPreludeの暗黙インポート)に対応している。構文の再束縛なしの場合、「標準の束縛」で定義された演算子が使われる。構文の再束縛が有効な場合、各演算子は現在の字句的スコープから引かれる。例えば、並列内包表記は、スコープにある"mzip
"をなんであれ使って型検査・脱糖される。
再束縛される演算子は上の表にある「期待される型」を持っていなければならない。これらの型は驚くほど一般的である。例えば、次のような型を持つバインド演算子を使うことができる。
(>>=) :: T x y a -> (a -> T y z b) -> T x z b
変換内包表記の場合には、内包表記が任意のモナドに関する物であるだけでなく、グループが任意の(fmap
を持つ)型n
に関してパラメタ化されている。
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.11. 再帰的do記法 )、並行配列内包表記は影響を受けない。
アロー記法(7.17. アロー記法 を見よ)では、とにかくスコープにあるarr
、(>>>)
、first
、app
、(|||)
、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が満足しているなら、問題ないはずである。
-XPostfixOperators
フラグを使うと、演算子の左セクションの構文に小さな拡張が有効になり、これを使って後置演算子を定義することができるようになる。拡張とはこうである。以下のような左セクションがあったとしよう。
(e !)
これは、(型検査と実行の両面において)以下の式と等しい。
((!) e)
(これは式e
が何であっても、また演算子(!)
が何であっても成り立つ)。Haskell 98の厳密な解釈では、このセクションは以下と同等だとされる。
(\y -> (!) e y)
つまり、演算子は二引数の関数でなければならない。GHCでは一引数の関数であっても良く、結果として、関数を後置記法で書くことができるようになる。
この拡張は関数定義の左辺には及ばない。従って、このような関数を定義するときには前置形を使わなければならない。
-XTupleSections
フラグは、タプルの構築子をPython風に部分適用することを有効にする。例として、以下のプログラム
(, True)
は、次の不恰好な表記と同じ意味の書き方とみなされる。
\x -> (x, True)
タプルの引数はどんな組み合わせで省略してもよい。以下のように。
(, "I", , , "Love", , 1337)
これは、次のように翻訳される。
\a b c d -> (a, "I", b, c, "Love", d, 1337)
unboxed tuplesを有効にしているなら、これについてもセクションが使える。例えば、
(# , True #)
非ボックス化タプルにゼロ項のものはないので、次の式
(# #)
は、変わらず非ボックス化単項タプルの構築子を意味する。
-XLambdaCase
フラグは以下の形の式を有効にする。
\case { p1 -> e1; ...; pN -> eN }
これは以下と同等である。
\freshName -> case freshName of { p1 -> e1; ...; pN -> eN }
\case
はレイアウトを開始するので、以下のように書けることに注意。
\case p1 -> e1 ... pN -> eN
-XEmptyCase
フラグは、選択肢を持たないcase式やラムダcase式を有効にする。次のように。
case e of { } -- 選択肢がない あるいは \case { } -- -XLambdaCaseも必要
これは、検査対象の式がボトム以外の型を持たないことが分かっている場合に便利なことがある。例を挙げる。
data Void f :: Void -> Int f x = case x of { }
依存型の諸機能と一緒に使うと、より便利である(Tracを見よ)。例えば、以下の二つのabsurd
の定義の候補を考えよ。
data a :==: b where Refl :: a :==: a absurd :: True :~: False -> a absurd x = error "absurd" -- (A) absurd x = case x of {} -- (B)
我々は(B)の方がずっと良いと思う。なぜなら、(True :~: False)
が空の型であることをGHCが発見できるからである。従って、(B)は部分関数でなく、GHCは-fwarn-incomplete-patterns
付きでこれをコンパイルできるはずである(ただし、パターンマッチ検査はまだ、これができるほど賢くない)。一方で(A)は危険な感じを与えるし、この関数が実際には決して呼ばれないことをGHCが検査しない。
-XMultiWayIf
フラグを使うと、GHCは以下のように複数の選択肢を持つ条件式を受け付ける。
if | guard1 -> expr1 | ... | guardN -> exprN
これは、だいたい次のものと同等である。
case () of _ | guard1 -> expr1 ... _ | guardN -> exprN
多選択肢のif式は新しいレイアウト文脈を導入する。よって、上の例は以下と同等である。
if { | guard1 -> expr1 ; | ... ; | guardN -> exprN }
以下の式では、
if | guard1 -> if | guard2 -> expr2 | guard3 -> expr3 | guard4 -> expr4
レイアウトが次のように翻訳されるので、意図通りに動く。
if { | guard1 -> if { | guard2 -> expr2 ; | guard3 -> expr3 } ; | guard4 -> expr4 }
多選択肢のif式でのレイアウトは、他のレイアウト文脈と同じように働くが、相違点として、多選択肢のifのガード間のセミコロンは省略可能である。よって、全てのガードを同じ桁に揃える必要はない。これは、関数定義やcase式のガードと同じ性質である。
レコードの構築とパターンマッチにおいては、仮に同じフィールド名を持つデータ型が二つスコープにあったとしても、どのフィールドが言及されているのか全く曖昧でない。例えば以下のように。
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.22. レコード同名利用 を見よ)と組み合わせることができる。例。
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
を修飾する必要はない。(実質的に、これは構築子によって修飾されている)
レコード同名利用(訳注: 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
が修飾された形でのみスコープにある場合に便利である)
レコードワイルドカードは-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
レコードワイルドカード構文では、レコードパターン中に「..
」を使えるようになる。そうすると、省略したそれぞれのf
がf = f
というパターンに置換される。例として、上のパターンは次のように書ける。
f (C {a = 1, ..}) = b + c + d
さらなる詳細。
ワイルドカードは他のパターン(同名利用(7.3.22. レコード同名利用 )も含む)と併用できる。例えばC {a = 1, b, ..})
というパターンでのように。さらに、レコードワイルドカードはレコードパターンが使えるところならどこでも使えるので、let
束縛や最上位でも使える。例えば、次の最上位の束縛は、b
とc
とd
を定義する。
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
の束縛を除いて)ので省略される。
Haskell 98レポートを注意深く読むと、let
やwhere
で導入される局所束縛の内部に結合性宣言(infix
、infixl
、infixr
)が出現することが許されていることが分かる。しかし、このような束縛の意味論について、Haskellレポートはあまり詳しく規定していない。
GHCでは、次のように、局所束縛に結合性宣言が付属していてもよい。
let f = ... infixr 3 `f` in ...
そして、この結合性宣言は、この束縛がスコープにあるようなあらゆる場所で適用される。例えば、let
式なら、他のlet
束縛の右辺とlet
式の本体で適用される。また、再帰的do
式(7.3.11. 再帰的do記法 )では、let
文の局所結合性宣言は、束縛される名前と同様に、そのグループの全ての文に渡るスコープを持つ。
さらに、局所結合性宣言にはその名前についての局所束縛が付属「していなければならない」。次のように、別の場所で束縛された名前の結合性を設定しなおすことは不可能である。
let infixr 9 $ in ...
局所結合性宣言は技術的にHaskell 98なので、有効にするのにフラグは必要ない。
-XPackageImports
フラグが有効なら、GHCは、インポート宣言を、インポート先のモジュールが属しているべきパッケージ名で修飾することを認める。例をあげる。
import "network" Network.Socket
こうすると、network
パッケージ(のいずれかのバージョン)からNetwork.Socket
をインポートすることになる。これは、同じモジュールが複数のパッケージから利用できたり、現在ビルド中のパッケージと他のパッケージの両方にあったりする場合に、インポートの曖昧性を取り除くために使うことができる。
特別なパッケージ名this
を使うと、現在ビルド中のパッケージを示すことができる。
注意: おそらく、あなたがこの機能を使う必要はないだろう。この機能は、主に我々がAPIの変更に際して後方互換なパッケージのバージョンをビルドすることができるように追加されたものである。一般的な事例では、この機能を使うと脆い依存が発生しやすい。モジュールはあるパッケージから別のパッケージに移ることがあり、その場合パッケージ修飾されたインポートは壊れるからである。
-XSafe
、-XTrustworthy
、-XUnsafe
のいずれかのフラグを付けると、GHCはimport宣言の構文を拡張してimport
キーワードの後に任意でsafe
キーワードを受け取るようにする。この機能はSafe HaskellというGHC拡張の一部である。例えば、
import safe qualified Network.Socket as NS
は、Network.Socket
モジュールをインポートするが、Network.Socketが安全にインポートできる場合にしかコンパイルに成功しない。importがどういう場合に安全だとみなされるかの記述は7.27. Safe Haskellを見よ。
次のようなインポートリストやエクスポートリストにおいて、
module M( f, (++) ) where ... import N( f, (++) ) ...
f
および(++)
の各実体は値である。しかし、型演算子(7.4.4. 型演算子)が有効だと、(++)
を型構築子として宣言できるようになる。この場合、これをインポートしたりエクスポートするにはどうすれば良いだろうか。
-XExplicitNamespaces
拡張は、この曖昧性を取り除くため、インポートリストやエクスポートリストの中の型構築子の名前に「type
」を前置することを許す。次のように。
module M( f, type (++) ) where ... import N( f, type (++) ) ... module N( f, type (++) ) where data family a ++ b = L a | R b
-XExplicitNamespaces
は、-XTypeOperators
によって有効になる。また、(なぜか)-XTypeFamilies
によっても有効になる。
専用の構文を有効にするオプションを使うと、Haskell 98で動作していたコードがコンパイルできなくなる可能性がある。これは大抵、変数名として使われていたものが予約語になることが原因である。この節では、言語拡張によって「盗まれた」構文を列挙する。ここではHaskell 98の字句構文(Haskell 98レポートを見よ)の記法と非終端記号を使っている。構文の変更については、既存の正しいプログラムに影響する可能性のあるもの(「盗まれた」構文)のみを列挙する。多くの拡張は新しい文脈自由構文を導入するが、これらの場合は例外なく、新しい構文を使って書かれたプログラムは適当なオプションなしではコンパイルできない。
専用の構文には次の二種類がある。
新しい予約語や予約シンボル。もはやプログラムで識別子として使うことのできない文字列である。
その他の専用構文。特定のオプションが有効になっていると別の意味を持つような文字の列。
盗まれるのは以下の構文である。
forall
-XExplicitForAll
によって盗まれる(型中において)。従って、-XScopedTypeVariables
、-XLiberalTypeSynonyms
、-XRankNTypes
、-XExistentialQuantification
によっても盗まれる。
mdo
-XRecursiveDo
に盗まれる。
foreign
-XForeignFunctionInterface
に盗まれる。
rec
、proc
、-<
、>-
、-<<
、>>-
、および(|
、|)
の括弧
-XArrows
によって盗まれる。
?varid
,
-XImplicitParams
によって盗まれる。
[|
, [e|
, [p|
, [d|
, [t|
, $(
, $$(
, [||
, [e||
, $varid
, $$varid
-XTemplateHaskell
によって盗まれる。
[varid
|
-XQuasiQuotes
によって盗まれる。
varid
{#
}, char
#
, string
#
, integer
#
, float
#
, float
##
-XMagicHash
によって盗まれる。
(#
, #)
-XUnboxedTuples
によって盗まれる。
varid
!
varid
-XBangPatterns
によって盗まれる。
pattern
-XPatternSynonyms
によって盗まれる。