GHC拡張ノック(Part 1)

n番煎じのよく使うGHC拡張の紹介

Posted by mizunashi_mana on May 15, 2018Tags: GHC, Language Extensions

Haskell1では各処理系で言語拡張を提供し,LANGUAGEプラグマというものを利用することで,言語拡張を利用することが許容されています.Haskellのデファクト標準的な処理系GHCも多くの言語拡張を提供しており,その拡張はGHC拡張と呼ばれています.

今回は,このGHC拡張の簡単な紹介と,個人的に良く使う拡張についての簡単な紹介を,全3回に分けて行いたいと思います.対象としては,GHCHaskellプログラミングをしたことがあり,通常のHaskellの構文や動作方法が分かっている人を考えています.また,この記事はあくまで簡単な紹介に留めるもので,付随する留意点や詳細な機能説明は,大事な箇所は漏らさないよう注意するつもりですが,全てを網羅するつもりはありませんのでその点は注意してください.もし,実際にGHC拡張を使用する際は,GHCのユーザーガイドをよく読んでから使用するのが良いでしょう.

Link to
here
GHC拡張について

Link to
here
Haskellの言語拡張

Haskellには,言語拡張を取り込む方法が標準で提供されています.Haskell標準では,コンパイラプラグマというものが策定されており,これを通してコンパイラに追加情報を提供することができます.コンパイラプラグマは{-##-}で囲まれ,字句的にはコメントとして扱われます.標準では,インラインプラグマや特殊化プラグマの他に,LANGUAGEプラグマというものが策定されており,このプラグマを通して言語拡張を指定することができます.

例えば,実装によってCPPScopedTypeVariablesという名前の言語拡張が提供されており,それを使いたい場合,次のような文をモジュールの開始前に指定することで,言語拡張が有効になります.

{-# LANGUAGE CPP, ScopedTypeVariables #-}

module A where

また,LANGUAGEプラグマを複数指定することもできます.

{-# LANGUAGE CPP                 #-}
{-# LANGUAGE ScopedTypeVariables #-}

module A where

この機能を通して,多くのHaskell処理系では言語拡張を提供しています.

Link to
here
GHC拡張

Haskellのデファクト標準な処理系GHCも,多数の拡張を提供しており,この拡張がGHC拡張と呼ばれるものです.GHC拡張は,バージョン8.4.2現在,以下の数が提供されています2

$ ghc --supported-extensions | wc -l
235

--supported-extensionsオプションは,現在のGHCで使用できるGHC拡張を表示してくれるオプションです.ただ,GHC拡張は全てが独立した拡張ではなく,互いに依存しあった拡張が多く存在します.また,先頭にNoがついている拡張は,そのGHC拡張を無効にするような拡張になっています 3 4(例えば,NoImplicitPrelude拡張はImplicitPrelude拡張を無効にする拡張です)

また,デフォルトで有効になっている拡張などもあります.例えば,ImplicitPreludeという拡張はデフォルトで有効になります.現在デフォルトのHaskell 2010をベースにしたモードでGHC 8.4.2を使用する場合,以下の拡張がデフォルトで有効になります 5 6 7

  • NondecreasingIndentation: Haskellのレイアウトルールを変更する拡張です.この拡張を有効にすると,ネストされたdo式の場合,インデントをしなくていいようになります.
  • ImplicitPrelude: 暗黙的にPreludeモジュールがインポートされるようになる拡張です.
  • MonomorphismRestriction: 単相性制限を課すようにする拡張です.この制限により,関数束縛でなく型注釈もない束縛変数の型は,デフォルティングルールによって単相化されます.
  • TraditionalRecordSyntax: レコード構文を有効にする拡張です.この拡張では,名前付きのフィールドを持つデータ型を定義し,それを使用することが可能になります.
  • EmptyDataDecls: コンストラクタを持たないデータ型の定義を許容する拡張です.
  • ForeignFunctionInterface: FFIが使えるようになる拡張です.この拡張により,foreign import構文を使用することで,HaskellからCの関数を読み込むことができるようになります.
  • PatternGuards: case式において,通常のパターンに加えて,<-を使用してガードの中でさらにマッチした条件下でパターンマッチができるようになる拡張です.例えば,case (x, y) of { (True, y) | False <- y -> True; _ -> False }というような式が書けるようになります.
  • DoAndIfThenElse: if式の構文を,thenelseの前に;を許容するよう変更する拡張です.これにより,do式においてthenelseをインデントする必要がなくなります.

歴史的経緯で生まれ,互換性のために残されているものの,現状使用が推奨されていない拡張もあります.他に実験的な拡張やかなり大胆な拡張も存在するため,GHC拡張を使用する際はGHCのユーザーガイドをよく読んでから使用するのが良いでしょう.

Link to
here
GHC拡張の使い方

GHCGHC拡張を使用する方法は,Haskell標準のLANGUAGEプラグマを使用する他に,幾つかあります.まず,GHCにオプションを渡して有効にする方法です.例えば,NoImplicitPrelude拡張とStrict拡張を有効にした状態でMain.hsをコンパイルしたい場合,次のように書けます.

ghc -XNoImplicitPrelude -XStrict --make Main.hs

GHCでは-Xの後に拡張名を続けることで,言語拡張を有効にしてコンパイルすることができます.通常は,LANGUAGEプラグマを使用するのが良いですが,何らかの事情でLANGUAGEプラグマを使用できない場合や,デフォルトで有効にしたい言語拡張がある場合などに便利でしょう.特にGHCiで言語拡張を有効にしたくなった場合,このオプションをsetコマンドで指定すると良いでしょう.

>>> :set -XNoImplicitPrelude -XStrict

他にGHC拡張を有効にする方法として,Cabalの機能を活用する方法があります.cabalファイルのビルド情報欄には,default-extensionsというフィールドを指定することができ,そこにデフォルトで有効にしたい言語拡張のリストを書くことで,その拡張を有効にした状態でCabalがビルドを行ってくれます.例えば,NoImplicitPrelude拡張とStrict拡張をデフォルトで有効にしてビルドしたい場合,次のように書きます.

name:           TestPackage
version:        0.0
synopsis:       Small package with a program
author:         Angela Author
license:        BSD3
build-type:     Simple
cabal-version:  >= 1.2

executable program1
  build-depends:      base
  main-is:            Main.hs
  default-extensions: NoImplicitPrelude, Strict

Link to
here
主要なGHC拡張

以下では,個人的にデフォルトで有効化して使っている拡張を幾つか紹介します.なお,GHCのバージョンは8.4.2Haskell2010モードで使用することを前提にしています.

Link to
here
Preludeの暗黙的な使用を抑制する

この節では,以下の拡張を紹介します.

Haskellでは,Preludeモジュールが暗黙的にimportされます.つまり,Haskellプログラムは暗黙に

import Prelude

と書いてあると,解釈されるということです.Preludeモジュールには,Int/IOといった基本的なデータ型や,Eq/Functorといった基本的な型クラス,zip/putStrLnといった基本的な関数が含まれています.

Preludeモジュールの暗黙的なimportは,Haskellプログラムを簡潔に書く上では便利ですが,これを無効にしたい場合もあります.

  1. Preludeモジュールにあるデータ型や関数と同じ名前の,別モジュールの関数を使いたい時
  2. 別の代替となるpreludeパッケージを使う時

といった場合です.NoImplicitPrelude拡張はまさしくこのような場合に,Preludeモジュールを暗黙的にimportしないようにするGHC拡張です.1番目の理由の場合,この拡張をデフォルトで入れずモジュール度に指定すればいいと思いますが,私的には2番目の理由でこの拡張を使うためデフォルトで有効にしています.代替となるpreludeパッケージは幾つか存在しますが,主に

などがあります8.これらのパッケージを探すにはHackagePreludeカテゴリを参照するといいでしょう.

私の場合,classy-preludeを使っていますが,それも生で使用しているわけではなく,パッケージごとにpreludeモジュールを作って使用しています.Preludeは,最もよく使うものが提供されているモジュールですから,APIの変更の影響を最も強く受けます.それを外部パッケージに依存させると,パッケージ保守が結構大変です.もし,パッケージごとにpreludeモジュールを作っておけば,パッケージ側やGHCのバージョン変更の影響などでAPIが変更されても,そのモジュール内でフォールバックを設定することで他のモジュールに変更を持ち越す必要がなくなります.これをNoImplicitPrelude拡張と組み合わせ,

{-# LANGUAGE NoImplicitPrelude #-}

module A where

import MyPrelude

...

と書くことで,保守がかなりしやすくなります.

Link to
here
便利な構文の導入

Link to
here
新たなリテラル表記を可能にする

この節では,以下の3つの拡張を紹介します.

Haskellには幾つかのリテラルが存在します.例えば,'c'は文字cを表すChar型のリテラルです.100は整数100を表すNum a => a型のリテラルで,100.1は浮動小数点数100.1を表すFractional a => a型のリテラルになります.Haskell標準には他にも幾つかリテラルが存在しますが,特に数値は非常に多様な使われ方がなされるため,他の多くの言語はより強力なリテラル表現を持つことがあります.GHC拡張ではこの背景を元に,リテラルに対する幾つかの拡張を提供しています.BinaryLiteralsNum a => a型のリテラルに対して,HexFloatLiteralsFractional a => a型のリテラルに対して,NegativeLiteralsはどちらに対してもの拡張を,それぞれ提供します.

数値型に対するリテラルは,既存のものでも数種類存在します.通常の数値表現20,オクテット(8進数)表現0o24,ヘックス(16進数)表現0x143つです.BinaryLiterals拡張は,これに加え0bを接頭辞に付けることでバイナリ(2進数)表現0b10100を可能にする拡張です.

これらのオクテット表現やヘックス,バイナリ表現は浮動小数点数の表現はできません.しかし,浮動小数点数は実際にはIEEEの規格に則ったデータ表現になりますから,10進数表現よりも16進数表現の方が実態として分かりやすい場合があります.このためHexFloatLiterals拡張では,接頭に0xの付くヘックス表現でも浮動小数点数のリテラルを記述できるようにしています.この拡張によって,0.250x0.4と表記できるようになります.また,指数表記も10進方式のものではなく,ビット方式のものになります.指数表記にはeではなくpを使い,何ビット移動させるか(つまり,2の何乗を掛けるか)を書くようにします.例えば,1.00x0.4p2と表記できます.また,0.1250x0.4p-1と表記できます.

さて,Haskellには唯一の単項演算子-があります.この演算子を使用することでnegate 1の代わりに-1という表記が可能になります.しかし,この演算子の結合度は非常に弱く,また二項演算子の-も存在することからf -1という表記は(f) - (1)というように解釈されてしまうなどの問題があり,非常に使い勝手が悪い演算子となっていました.また,Haskellの仕様上,-128という表現は最終的にnegate (fromInteger 128)という式に脱糖されますが,例えばInt8などの,負数は-128まで扱えるが正数は+127までしか扱えないといったデータ型の場合に,この式はfromIntegerで一度+128の値になってしまいオーバーフローを起こしてしまうという問題がありました.これを解決するため導入されたのがNagativeLiterals拡張です.この拡張を導入することで空白を挟まない-1.0などは1つのリテラルと解釈されるようになります.この拡張を導入後は,次のようになります.

>>> max -1 2 == max (-1) 2 -- before: max -1 2 == max - (1 2)
True
>>> data SamplePZ = SamplePZ deriving (Eq, Show)
>>> instance Num SamplePZ where { fromInteger i | i <= 0 = SamplePZ }
>>> -100 :: SamplePZ -- before: raise error
SamplePZ
>>> - 100 :: SamplePZ
*** Exception: ...
>>> instance Fractional SamplePZ where { fromRational r | r <= 0 = SamplePZ }
>>> -100.10 :: SamplePZ -- before: raise error
SamplePZ
>>> - 100.10 :: SamplePZ
*** Exception: ...

Link to
here
空のデータ型に対するより強力なサポートを導入する

この節では,以下の2つの拡張を紹介します.

Haskellでは,コンストラクタを一切持たない型を定義できます.これは空のデータ型と呼ばれ,次のように書けます.

data Empty

このような型はbaseパッケージのData.Voidモジュールでも提供されており,有用な場合があります.しかし,Haskell標準ではこのようなデータ型に対するサポートが薄く,使用する上で不便な場面があります.このサポートを強化する拡張が,EmptyCase拡張とEmptyDataDeriving拡張です.

EmptyCase拡張は,空のパターンマッチを書けるようにする拡張です.Haskell標準では,空のパターンマッチは書けません.つまり,case x of {}というような式が書けないということです.通常はデータ型は何らかのコンストラクタを持っていますから,このようなパターンマッチを書きたいと思う場面はないでしょう.しかし,空のデータ型においてこのようなパターンマッチを書きたいと思うことがあります.

f :: Empty -> a
f x = case x of {}

このような表記を可能にするのがEmptyCase拡張です.なお,このケース式は次のように書くのと同値になります.

f :: Empty -> a
f x = x `seq` error "Non-exhaustive patterns in case"

もう1つのEmptyDataDeriving拡張は,空のデータ型に対してderiving構文を使用できるようにする拡張です.空のデータ型は,通常のデータ型と違いEqShowなどの型クラスインスタンスをderivingすることができません.つまり以下のようなことができません.

data Empty
  deriving (Eq, Ord, Show)

しかし,これでは不便な場合があります.それを可能にするのがEmptyDataDeriving拡張です.この拡張では,Eq/Ord/Show/Read4つがderiving可能になり,それぞれは次のようなインスタンスを生成します.

instance Eq Empty where
  _ == _ = True

instance Ord Empty where
  compare _ _ = EQ

instance Read Empty where
  readPrec = pfail

instance Show Empty where
  showsPrec _ x = case x of {}

Link to
here
新たな基本構文を導入する

この節では,以下の3つの拡張を紹介します.

Haskellでは,タプルやラムダ抽象,セクション,if式やcase式といった構文が導入されていますが,これらを組み合わせて多用する場合,幾つか冗長な表現が生まれる場合があります.その中でも頻出する表現に対して,新たな構文を提供するGHC拡張があります.それが,TupleSectionsMultiWayIfLambdaCase3つの拡張です.

Haskellには,セクションと呼ばれる二項演算子の部分適用を表す構文があります.また,Haskellではタプルにも独自の構文が充てがわれています.このタプルを使用する際,セクションのように部分適用を簡潔に書きたい場合があります.例えば,\x -> (1, x)という表現をもっと簡潔に書きたい場合があります.この場合は(,) 1というな表記が可能ですが,2番目に部分適用したい場合や,3つ組のタプルに部分適用したい場合などは非常に面倒です.このため,TupleSections拡張は(1, )という表記でタプルの部分適用を書ける構文を提供します.2つ以上空きがある場合は,左から引数を受け取っていくようになります.例えば,(True, , "str", )\x y -> (True, x, "str", y)と同等です.

MultiWayIfは名前の通り複数の条件をガード構文のように指定できるif式を提供する拡張です.つまり,以下のようなことがかけます.

f :: [Int] -> IO ()
f xs = sequence_ $ do
  x <- xs
  pure $ if
    | x <= 0          -> fail "non-positive number"
    | x `mod` 15 == 0 -> putStrLn "FizzBuzz"
    | x `mod` 3  == 0 -> putStrLn "Fizz"
    | x `mod` 5  == 0 -> putStrLn "Buzz"
    | otherwise       -> print x

このMultiWayIfは次のようにcase式で書き換えることが可能です.

f :: [Int] -> IO ()
f xs = sequence_ $ do
  x <- xs
  pure $ case () of
    _ | x <= 0          -> fail "non-positive number"
    _ | x `mod` 15 == 0 -> putStrLn "FizzBuzz"
    _ | x `mod` 3  == 0 -> putStrLn "Fizz"
    _ | x `mod` 5  == 0 -> putStrLn "Buzz"
    _ | otherwise       -> print x

3つ目のLambdaCase拡張は,ラムダ抽象とcase式を組み合わせた際に良く使う表現をより簡潔に書けるようにする拡張です.この拡張を使うと,\x -> case x of (a, b) -> a + bというようなラムダ抽象を,\case (a, b) -> a + bと書けるようになります.もちろんレイアウトルールもcase-of式と同じように作用するため,改行を含んだ式も書けます.

f :: Maybe Int -> Int
f = negate . \case
  Nothing -> 0
  Just x  -> x

Link to
here
正格化に対するサポートを導入する

この節では,以下の3つの拡張を紹介します.

Haskellはデフォルトの評価戦略として,グラフ簡約の遅延評価を採用しています.これはリストや再帰に関する表現を非常に豊かにする反面,パフォーマンスを悪化させたりデバッグを困難にさせる場面が多いなどの負の面もあります.このためHaskell標準では,seq関数や正格フラグといった正格評価へのサポートも提供しています.しかし,このサポートは表現が冗長な場合が多く,使い勝手が悪い側面があります.この面を解決するための拡張が,BangPatternsStrictDataStrict3つの拡張です.

再帰関数において,累積引数は多くの場合正格に計算した方が効率が良いですが,Haskell標準では以下のように書く必要がありました.

sum :: [Int] -> Int -> Int
sum xs y = y `seq` case xs of
  x:xs' -> sum xs' (x + y)
  []    -> y

このようなseqによる評価をより簡潔に書けるよう,BangPatterns拡張というものが提供されています.これはパターンを拡張し,バンパターンというものを導入します.このバンパターンは,通常のパターンに!を付けることで書けます.例えば,上の例はバンパターンを使うと以下のように書けます.

sum :: [Int] -> Int -> Int
sum xs !y = case xs of
  x:xs' -> sum xs' (x + y)
  []    -> y

バンパターンはパターンの1つですから,もちろんlet式やcase式でもlet !y = f x in ycase f x of !y -> yというように使えます.また,case x of (!y, z) -> y + zというように部分パターンとしても有効です.バンパターンはHaskellcase式の翻訳ルールに次の規則を加えることで実現されます.

case v of { !pat -> e; _ -> e' }
≡ v `seq` case v of { pat -> e; _ -> e' }

Haskell標準では,データ型の宣言において,コンストラクタの引数に正格フラグというものを付けることが許容されています.このフラグをつけた引数は,正格に評価された後コンストラクタに渡されます.ただ,一般にデータ型の引数は正格な方が効率が良いため,データ型宣言時に正格フラグを付けるという慣習がありました.この慣習を打破するために導入されたのが,StrictData拡張です.StrictData拡張下のモジュールでは,データ型宣言時,コンストラクタの引数は全て正格フラグをつけているものとして扱われます.また,~というフラグが新たに導入され,このフラグをつけた引数の場合はHaskell標準化のデフォルトの動作,つまり引数は正格に評価されず遅延されるようになります.StrictData下で宣言された

data T = Normal Int | Strict !Int | Lazy ~Int

というデータ型は,通常のHaskellの以下のデータ型と同等になります.

data T = Normal !Int | Strict !Int | Lazy Int

Strict拡張は,StrictData拡張に加え,ほとんどのパターンを暗黙的にバンパターンにする拡張です.つまり,殆どの評価を正格にする拡張です.バンパターンに変わる箇所は,関数の引数,let/where句の束縛変数,case式のパターンマッチなどです.これらのパターンには,最外の場所に!が暗黙的に付与されます.例えば,Strict拡張下で定義された

f :: Int -> (Int, Int) -> Int
f x (z, y) = let zy = z * y in case x - z of z' -> z' ^ z

という関数は,BangPatterns拡張下のHaskellの以下の関数と同等になります.

f :: Int -> (Int, Int) -> Int
f !x !(z, y) = let !zy = z * y in case x - z of !z' -> z' ^ z

注意して欲しいのは,このバンパターンはseqに置き換わるため,WHNFまでしか評価されないということです.つまり,!(z, y)というパターンは単なる(z, y)と完全に同じです.またトップレベルの束縛にバンパターンを付与することは許されておらず,遅延されるということにも注意が必要です.

Link to
here
パターンマッチをより柔軟に扱えるようにする

この節では,以下の2つの拡張を紹介します.

GHC拡張では,Haskell標準のパターンをさらに強力なものにする拡張があります.ViewPatternsはビューパターンという新たなパターンを導入します.また,PatternSynonymsはパターンの別名を付けることができるようにする拡張です.

Haskell標準にあるパターンガードは,非常に強力ですが,表現が非常に冗長になる場合があります.これを短縮して書けるように,ViewPatterns拡張はビューパターンというものを導入します.ビューパターンは,->の左側に式を,右側にパターンを書くことで,左の式に対象を適用して結果が右側のパターンにマッチした時,マッチするようなパターンです.例えば,

f ((`mod` 2) -> 0) = Nothing
f x                = Just x

というように使用でき,f 0Nothingを,f 3Just 3をそれぞれ返すようになります.この関数宣言は,以下のパターンガードを用いて書いた関数と一致します.

f x | 0 <- x `mod` 2 = Nothing
f x                  = Just x

ビューパターンはHaskellcase式の翻訳ルールに次の規則を加えることで実現されます.

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

PatternSynonyms拡張は,非常に強力で大きな拡張です9PatternSynonyms拡張は名前の通り,パターンに別名を与えるパターンシノニム機能を提供します.パターンシノニムは通常の関数と同じように,次のように定義できます.

pattern Nil :: [a]
pattern Nil = []

pattern Cons :: a -> [a] -> [a]
pattern Cons x xs = x : xs

{-# COMPLETE Nil, Cons #-}

このように定義したパターンは,以下のように使用できます.

len :: [a] -> Int
len (Cons _ xs) = 1 + len xs
len Nil         = 0

パターンシノニムは非常に便利な機能ですが,一方で注意する事項も幾つかあります.

まず,パターンシノニムの定義は関数定義と非常に似ていますが,パターンの別名であることに注意してください.パターンシノニムの定義において変数が出現する場合,関数の引数のように錯覚してしまいがちですが,この変数にはパターンにマッチした時そのマッチした部分が当てがわれます.つまり,右の式でマッチしたものが左の変数に束縛されるため,左の変数に束縛された後右の式を実行する関数と,流れが逆になるということです.このため,パターンシノニムの引数の変数は必ず右に出現する必要があります.また,パターンシノニムの右側には変数を含むパターンしかかけません.そのため,式を書きたい場合,ViewPatterns拡張などを用いなければなりません.さらにパターンシノニムは,デフォルトではパターンの網羅性検査が非常に難しいため,網羅性検査を行わないようになっています.ただし,COMPLETEプラグマを用いてパターンシノニムの網羅条件を与えることで,その範囲で網羅性検査を行うようになります.

パターンシノニムはパターンの種類に応じて3種類の書き方が存在します.上の単純なパターンシノニムは,双方向(bidirectional)パターンシノニムと呼ばれ,暗黙的にパターンの名前と等しい関数が作られます.この関数を用いることで,[0, 1, 2]の代わりにCons 0 (Cons 1 (Cons 2 Nil))といった式も書くことができるようになります.ただし,このような関数が単純には作れないパターンも存在します.例えば,(x, _)というパターンに,First xというパターンシノニムを与えたい場合,このFirstに対する関数は_の部分に入れるべき値が分からないため,作りようがありません.このような関数が単純に作れないパターンシノニムは単方向(unidirectional)パターンシノニムと呼ばれ,双方向パターンシノニムが=を使って定義されるのに対し,次のように<-を使って書きます.

pattern First :: Int -> (Int, Bool)
pattern First x <- (x, _)

このパターンシノニムはFirstという関数は作らず,単純にパターンの別名だけを提供します.ただし,First関数の定義を次のように与えることが可能になっています.

pattern First :: Int -> (Int, Bool)
pattern First x <- (x, _)
  where
    First x | x < 0 = (x, False)
    First x         = (x, True)

また,パターンシノニムはパターンの評価順序にも注意する必要があります.例えば,次の例をみてください.

data Pair a b = Pair a b

type Pair3 a b c = Pair a (Pair b c)

pattern Pair3 :: a -> b -> c -> Pair3 a b c
pattern Pair3 x y z = Pair x (Pair y z)

f :: Pair3 Bool Bool Bool -> Bool
f (Pair3 True True True) = True
f _                      = False

f' :: Pair3 Bool Bool Bool -> Bool
f' (Pair True (Pair True True)) = True
f' _                            = False

このff'は評価順が異なり,f (Pair False undefined)が例外を投げるのに対し,f' (Pair False undefined)Falseを返します.これは,パターンシノニムを使ったパターンマッチでは,自身のパターンを先に調べ,次に引数のパターンマッチを行うからです.つまり,fは以下と同等になります.

f :: Pair3 Bool Bool Bool -> Bool
f (Pair x (Pair y z)) | True <- x, True <- y, True <- z = True
f _                                                     = False

パターンシノニムは,モジュールエクスポートを書く際にも注意が必要で,module A (pattern Cons, pattern Nil) where ...というように接頭にpatternをつける必要があります.

Link to
here
レコードに対するサポートを強化する

この節では,以下の4つの拡張を紹介します.

Haskellのレコード構文は,便利な反面幾つか機能が劣る場面もあります.このため,GHCでは,レコードをより扱いやすくするための拡張を幾つか提供しています.それが,DuplicateRecordFieldsOverloadedLabelsNamedFieldPunsRecordWildCards4つの拡張です10

Haskell標準では,同じモジュール内で同じフィールド名を持つ複数のレコード構文を使用したデータ型の定義を行うことができません.これはどのデータ型のフィールドかが曖昧であるようなプログラムを書けてしまうからですが,そういう状況に遭遇するとこの制約は非常に不便です.これを解決するのが,DuplicateRecordFields拡張です.DuplicateRecordFields拡張は,曖昧になるような式を書けなくする代わりに,同一モジュールの複数のデータ型が同じフィールド名を持つことを許容する拡張です.つまり,以下のようなことが可能になります.

data A = A { d :: Int }
data B = B { d :: Bool }

ただし,この拡張下では,曖昧なフィールドを用いたレコードのアップデート構文やフィールドの選択関数の使用の際は型を明記する必要があったり,モジュールのエクスポートリストで選択関数をエクスポートすることが出来なくなったりします.

OverloadedLabels拡張は,#fooというような#から始まる新たな構文を導入します.#fooGHC.OverloadedLabelsモジュールのfromLabelメソッドにおいてIsLabel "foo" a => aというような型を持つ場合と同等になります.これを用いることで,同じフィールドを持つデータ型に対する選択関数を次のように書けます11

{-# LANGUAGE OverloadedLabels       #-} -- the main extension
{-# LANGUAGE DataKinds              #-} -- for Symbol kind
{-# LANGUAGE KindSignatures         #-} -- for HasField's `l` parameter
{-# LANGUAGE MultiParamTypeClasses  #-} -- for HasField and IsLabel classes
{-# LANGUAGE FunctionalDependencies #-} -- for HasField class
{-# LANGUAGE FlexibleInstances      #-} -- for HasField instances
{-# LANGUAGE ScopedTypeVariables    #-} -- for the IsLabel instance
{-# LANGUAGE DuplicateRecordFields  #-} -- for A and B data types

import GHC.OverloadedLabels (IsLabel(..))
import GHC.TypeLits (Symbol)
import Data.Proxy (Proxy(..))

data A = A { d :: Int }
data B = B { d :: Bool }

class HasField a (l :: Symbol) b | a l -> b where
  selectField :: Proxy l -> a -> b

instance HasField A "d" Int where
  selectField _ (A x) = x

instance HasField B "d" Bool where
  selectField _ (B x) = x

instance HasField a l b => IsLabel l (a -> b) where
  fromLabel = selectField (Proxy :: Proxy l)

これを使うことで,#d A { d = 0 }0を,#d B { d = True }Trueを返してくるようになります.また,#dには型を明記しなくても型推論が働くようになります.

さて他にレコードのパターンマッチやコンストラクトを非常に便利にしてくれる拡張として,NamedFieldPuns拡張とRecordWildCards拡張があります.レコードのパターンマッチは多くの場合冗長になりがちで,次のようなボイラープレートを書きがちです.

data A = A { x :: Int, y :: Bool }

f :: A -> Int
f A{ x = x } = x + 1

NamedFieldPuns拡張は,同等のことを次のように書けるようにする拡張です.

f :: A -> Int
f A{ x } = x + 1

また,このパターンは旧来の書き方と合わせて書くこともできます.

g :: A -> Int
g A{ x, y = False } = - x
g A{ x }            = x

さらにこの拡張は,コンストラクトの際も役に立ちます.let x = 1 in A { x, y = True }と書くとこの式は,A { x = 1, y = True }と書くのと同等になります.

NamedFieldPuns拡張ではフィールド名を明記する必要がありましたが,RecordWildCards拡張はさらにフィールド名を明記する必要がなくなります.以下のように{..}と書くことで,全てのフィールドを展開してくれるようになります.

f :: A -> Int
f A{..} = x + 1

また,部分的に明記することも可能で,その場合以下のように書きます.

g :: A -> Int
g A{ y = False, ..} = -x
g A{..}             = x

コンストラクトの際も,この拡張は有効です.let x = 1 in A { y = True, ..}と書いた場合,A { x = 1, y = True }と書くのと同等になります.

Link to
here
型演算子を導入する

この節では,以下の拡張を紹介します.

Haskellではユーザー定義の関数やデータ型のコンストラクタにおいて,演算子表記のものも定義できるようになっています.例えば,以下のようにです.

data Pair a b = a :*: b
infixl 7 :*:

(&) :: a -> (a -> b) -> b
x & f = f x
infixl 1 &

しかしHaskell標準では,型を定義する場合そのようなことはできません.これを可能にするのが,TypeOperators拡張です.この拡張の有効下では,

type a + b = Either a b
infixr 5 +

ということが可能になります.ただし,このように定義した型演算子は,同じ名前の値としての演算子があった場合区別ができません.このため,モジュールのエクスポートリストを書く際,型演算子か値レベルの演算子かの区別が付かなくなった場合,値レベルの方が優先されます.この時,型演算子を明示したい場合,typeを付けます12

{-# LANGUAGE TypeOperators #-}

module A
  ( type (+)
  ) where

type a + b = Either a b

Link to
here
型クラスを拡張する

この節では,以下の4つの拡張を紹介します.

Haskellの型クラスは非常に強力な機構です.しかしながら,Haskell標準の型クラスの構文は非常に制約がきつく,これらを緩和したいと思うことがよくあります.このため,GHCでは制約を緩和する拡張をいくつか提供しています.それが,MultiParamTypeClassesFlexibleContextsFlexibleInstancesInstanceSigs4つの拡張です.

Haskell標準では,クラスは1つの変数しか持てません.なので,次のような型クラスは作れません.

class C a b

これは非常に不便な制約なため,複数のパラメータを使うような型クラスを許容する拡張がMultiParamTypeClasses拡張です.この拡張により,上のコードが許容されるようになる他,以下のように変数が全くない型クラスも宣言することができるようになります.

class Nullary

また,Haskell標準では,メソッドにおいてクラスの型変数に型制約をかけるということも許容されていませんが,MultiParamTypeClasses拡張ではこれも可能にします13.これによって以下のようなクラス定義も書けるようになります.

class Setable s a where
  elem :: Eq a => a -> s a -> Bool

Haskell標準では,型制約の解決を安全に,しかも単純にするために,型注釈における制約の書き方クラス定義,インスタンス定義の際の制約の書き方を大きく制限しています.しかし,より複雑な型制約を書きたい時が往々にしてあります.そこで,この制限を緩め,クラス階層が非循環である場合には許容するようにする拡張が,FlexibleContexts拡張です.この拡張下では,

-- valid
class (Monad m, Monad (t m)) => Transform t m where
  lift :: m a -> (t m) a

-- valid
f :: Functor Maybe => ()
f = ()

-- invalid
class A a => B a
class B a => A a

となります.

FlexibleInstances拡張もFlexibleContexts拡張と同じく,Haskell標準での型クラスインスタンスの書き方の制限を,停止制限を守る場合に許容するというように緩和する拡張です.停止制限は簡単に言ってしまえば,インスタンス宣言において,型制約がインスタンスより小さく14,型関数を使っていないというものです15.この拡張下では,

-- valid
instance C1 (Maybe [a])

-- valid
instance C2 a a => C2 [a] [a]

-- valid
instance (Eq a, Show b) => C3 a b

-- valid
instance (Show a, Show (s a)) => Show (S s a)

-- invalid
instance C4 a => C4 a

-- invalid
instance C2 a a => C1 [a]

-- invalid
instance Functor [] => C1 a

となります.また,この拡張下では,型シノニムをインスタンスにすることもできます16

type List a = [a]

-- Instead of `instance C [a]`
instance C (List a)

ただし,型シノニムを使う場合そのシノニムの引数は全て適用しなければならないことに注意が必要です.

Haskell標準では,型クラスインスタンスの定義時,そのメソッドの型注釈は書けないようになっています.しかし,複雑な型クラスインスタンスを書く際,メソッドの型注釈を書きたい場合があります17.これを可能にするのがInstanceSigs拡張です.InstanceSigs拡張の元では,以下のようなインスタンス宣言が書けます.

data A = A

instance Eq A where
  (==) :: A -> A -> Bool
  A == A = True

Link to
here
型ワイルドカードをより柔軟に扱う

この節では,以下の拡張を紹介します.

GHCには型ワイルドカードという機能があります.この機能は,_と型シグネチャ上で書いておくと,そこの部分の型を推論してエラーメッセージとして表示してくれる機能です.この機能は,以下のように部分的に記述したり複数指定したりすることも可能です.

-- Inferred type: (a, b) -> (a, Maybe a1)
ignoreSecond :: _ -> _
ignoreSecond (x, _) = (x, Nothing)

これを活用すれば,複雑な型をある程度ヒントを与えた状態で推論してもらい,型を追記するプログラミングスタイルや,GHCが実際に型をどう推論するかを見るための補助に応用できます.しかし,例えばignoreSecondが引数と返り値で型が同じであるという情報が分かっていた場合に,これをヒントとして伝えたい場合がありますが,型ワイルドカードでそれを伝える方法はありません.これを解決するのがNamedWildCards拡張です.この拡張を使うと,以下のようなプログラムに対しても,接頭に_が付いている型をワイルドカードとみなして,エラーメッセージで型の推論結果を表示してくれるようになります.

-- Inferred type: (a, Maybe a1) -> (a, Maybe a1)
ignoreSecond :: _a -> _a
ignoreSecond (x, _) = (x, Nothing)

Link to
here
新たな表記法の導入

この節では,以下の2つの拡張を紹介します.

Haskellでは,モナドを扱いやすくするための,do構文という専用の構文が用意されています.この構文はHaskellプログラミングにおいて広く利用されています.GHCでは,これに加えArrowMonadFixというクラスに対しての専用の構文も提供しています.これはGHC拡張で実装されており,それぞれArrows拡張,RecursiveDo拡張を有効にすることで使用可能です.

Arrowクラスは,モナドの一般化として導入されました18.このクラスには,モナドのdo構文と同様に,クラスメソッドだけの式に脱糖できる構文が考案され,GHC拡張として実装されています.それがArrows拡張で利用できるproc構文です.

例えば,Arrowクラスのメソッドを使った次のような関数は,

doSomething :: Arrow a => a Int Int -> a Int Int -> a Int Int -> a Int Int
doSomething f g h
  =   arr (\x -> (x + 1, x))
  >>> first (f >>> (arr (\y -> 2 * y) >>> g) &&& returnA >>> arr snd)
  >>> arr (\(y, x) -> (x, x + y))
  >>> arr (\(x, z) -> (z, x * z))
  >>> second h
  >>> arr (\(z, t) -> t + z)

proc構文を使うと,

doSomething :: Arrow a => a Int Int -> a Int Int -> a Int Int -> a Int Int
doSomething f g h = proc x -> do
  y <- f -< x + 1
  g -< 2 * y
  let z = x + y
  t <- h -< x * z
  returnA -< t + z

というように書けます19.また,ArrowLoopクラスのloopメソッドに変換される,rec構文も搭載されており次のようなフィードバック制御を相互再帰で行うプログラムを書くことができます.

counter :: ArrowLoop a => (Int -> a Int Int) -> a Bool Int
counter delay = proc reset -> do
  rec output <- returnA -< if reset then 0 else next
      next <- delay 0 -< output + 1
  returnA -< output

proc構文についてはArrow syntaxのページにまとめられている他,提案論文にて変換規則を確認することが可能です.

さて,もう1つのMonadFixクラスは,モナドを拡張し,再帰的なバインディングを許すようなものです.このクラスを元に,RecursiveDo拡張はdo構文をさらに拡張します.具体的には,次のように使用できるrecという構文を新たに導入します.

doSomething :: [Int]
doSomething = do
  rec x <- [y, y * 10]
      y <- [1, 2]
  pure $ x + y

この関数は,次のようにMonadFixクラスのメソッドmfixを使った関数と同等です.

doSomething :: [Int]
doSomething = do
  (x, y) <- mfix $ \~(x, y) -> do
    x <- [y, y * 10]
    y <- [1, 2]
    pure (x, y)
  pure $ x + y

また,recを省略して書けるmdoという構文も提供されます.

doSomething :: [Int]
doSomething = mdo
  x <- [y, y * 10]
  y <- [1, 2]
  pure $ x + y

mdo構文は,それぞれの文と変数の依存関係を解析し,自動的にrecブロックに分けてくれます.後は,その分けられたrec文をmfixに翻訳することで,通常のdo構文に翻訳することができます.例えば,

mdo
  a <- m
  b <- f a c
  c <- f b a
  z <- h a b
  d <- g d e
  e <- g a z
  pure c

という式は,

do
  a <- m
  (b, c) <- mfix $ \~(b, c) -> do
    b <- f a c
    c <- f b a
    pure (b, c)
  z <- h a b
  (d, e) <- mfix $ \~(d, e) -> do
    d <- g d e
    e <- g a z
    pure (d, e)
  pure c

という式に翻訳されます.mdorecの変換規則は,提案論文にて確認が可能です.

Link to
here
次回予告

今回は,GHC拡張の簡単な紹介と使い方について,それから個人的にデフォルトで有効化している,Preludeの暗黙的なインポートを抑制する拡張,新たな構文を導入する拡張を紹介しました.

次回は,他のデフォルトで有効化している拡張について紹介したいと思います.

Link to
here
参考文献


  1. この記事では特に断らない限り,Haskell2010を「Haskell標準」または「Haskell」と呼称します.↩︎

  2. このオプションは,拡張を無効にするGHC拡張(例えば,NoImplicitPrelude拡張など)も含めて表示します.実際にはNoが付いている拡張を抜くと,提供されている数は120個になります.↩︎

  3. Haskell標準では,ある拡張を無効にするといった機能は提供されていません.このため,GHCでは無効にする機能を1つの拡張として,Haskell標準に則った形で提供しています.↩︎

  4. 有効にする拡張と無効にする拡張を両方指定した場合,GHCは指定された順番に沿って最後に指定された方を拡張として採用します.↩︎

  5. Haskell2010標準では,Haskell2010というプラグマをサポートすること,またHaskell98から新たにHaskell2010までに取り込まれた機能を切り離したPatternGuards/NoNPlusKPatterns/RelaxedPolyRec/EmptyDataDecls/EmptyDataDeclsという拡張をそれぞれサポートすることが望ましいと規定されています.GHCHaskell2010という拡張を指定できるようになっており,ここにあるほとんどはこの拡張を有効にした場合にも有効になります.↩︎

  6. デフォルトで有効になる拡張のほとんどは,Haskell 2010を元にしたものです.ただし全てがそうというわけではありません.NondecreasingIndentationHaskell標準にはない機能です.またGHCHaskell 2010で規定されている仕様を全てデフォルトで取り込んでいる訳でもありません.特にHaskell標準ではデータ型の宣言に型制約を書くことができますが,GHCではデフォルトではできません.これを有効にする場合,DatatypeContexts拡張を有効にする必要があります.↩︎

  7. GHCの内部ではRelaxedPolyRecという拡張も一緒に有効になります.しかし,現在この拡張は実装上の問題でGHC上で無効にすることができないため,ドキュメント上からも削除されています.この記事でもGHCの方針に従って,この拡張は特に扱いませんのでご留意ください.↩︎

  8. 現在,Preludeの代替を目指す,rioというパッケージが作成されています.このパッケージは現在まだprereleaseの段階で,stackにおいて実験的に使用されています.様々な最新のHaskellプログラミングの知見を取り入れており,標準のPreludeに大きく拡張を施しているため,Haskellで大規模な開発を行う場合注目する価値があるかもしれません.↩︎

  9. GHC 8.2.2の段階では,パターンシノニムはコンパイラがクラッシュするなどの非常に多くのバグを抱えていました.私は8.4.2をまだあまり試していませんが,パターンシノニムの仕様が非常に複雑なため,8.4.2でもまだバグを多く抱えている可能性があります.パターンシノニムをプロダクトで多用する場合,その点に注意した方が良いでしょう.↩︎

  10. GHCのレコードシステムの拡張は非常に強力ですが,その反面システムが非常に複雑になっています.このため,8.2.2の段階でコンパイラがクラッシュするなど非常に多くのバグを抱えていました.レコードシステムの仕様の改良は現在も進んでいますが,8.4.2でもまだバグを多く抱えている可能性があります.これらの拡張をプロダクトで多用する場合,その点に注意した方が良いでしょう.特に,GHC 8.0以降に導入された拡張には注意が必要です.↩︎

  11. OverloadedLabels拡張はかなり最近入った拡張で,多数のGHC拡張,特に強力な型システムを前提にして書かれています.このため,選択関数の実装にもかなり多くのGHC拡張を使用しています.ここでは,特に解説しないのでそういうものだと思っておいてください.なお,このプログラムはプロダクションで使うことを前提にしていませんので,そこはご注意ください.↩︎

  12. この機能は型演算子を定義しないで再エクスポートなどをする場合にも使用されるため,ExplicitNamespaces拡張として切り離されています.↩︎

  13. この機能はConstrainedClassMethods拡張として切り離されており,MultiParamTypeClasses拡張を有効にすると一緒に有効になります.↩︎

  14. 型制約が小さいとは,型変数とコンストラクタと変数の組の出現が少ないということです.↩︎

  15. より正確には,FunctionalDependenciesに対する制限もありますが,ここでは割愛します.↩︎

  16. この拡張は,TypeSynonymInstances拡張として切り離されており,FlexibleInstances拡張を有効にすると一緒に有効になります.↩︎

  17. 特にScopedTypeVariables拡張を指定する場合,型注釈は必要です.↩︎

  18. Generalising Monads to Arrows, John Hughes, in Science of Computer Programming 37, pp. 67111, May 2000↩︎

  19. 一見,この構文は単純な脱糖を行うと脱糖後のプログラムが非常に冗長になるように思えます.しかし,Arrowクラスのメソッドに設けられている書き換え規則によって,最終的に妥当な大きさまで脱糖後のプログラムが小さくなってくれます.↩︎