言語拡張

はじめに

言語拡張の分類を識別するのは大事なことです。

言語拡張を一般特殊の部門に分類すること自体に潜んでいる問題は、これは主観的な分類であるということです。型の宇宙遊泳を行う Haskeller は、データベースプログラミングを行う人とは Haskell の解釈が大きく異なっています。ここで提示するもの自体は保守的な評価です。恣意的な基準として、FlexibleInstancesOverloadedStrings は”日常的”、GADTsTypeFamilies は”特殊”としましょう。

要点

  • 無難は、その拡張を導入しても使用しない限りモジュールの意味論は変わらない、ということを表します。
  • 歴史的は、その拡張は使うべきでなく、単に後方互換性だけのために GHC にある、ということを表します。
言語拡張 無難 歴史的 構文拡張 用途 用途 手引き
AllowAmbiguousTypes 特殊 型レベル 参照
Arrows 特殊 構文の拡張 参照
AutoDeriveTypeable 特殊 メタ 参照
BangPatterns 一般 正格性注釈 参照
CApiFFI 特殊 FFI 参照
ConstrainedClassMethods 特殊 型レベル 参照
ConstraintKinds 特殊 型レベル 参照
CPP 一般 プリプロセッサ 参照
DataKinds 特殊 型レベル 参照
DatatypeContexts 非推奨 非推奨 参照
DefaultSignatures 特殊 ジェネリック 参照
DeriveDataTypeable 一般 ジェネリック 参照
DeriveFoldable 一般 ジェネリック 参照
DeriveFunctor 一般 ジェネリック 参照
DeriveGeneric 一般 ジェネリック 参照
DeriveTraversable 一般 ジェネリック 参照
DisambiguateRecordFields 特殊 構文の拡張 参照
DoRec 特殊 構文の拡張 参照
EmptyCase 特殊 構文の拡張 参照
EmptyDataDecls 一般 構文の拡張 参照
ExistentialQuantification 特殊 型レベル 参照
ExplicitForAll 特殊 型レベル 参照
ExplicitNamespaces 特殊 構文の明確化 参照
ExtendedDefaultRules 特殊 ジェネリック 参照
FlexibleContexts 一般 型クラスの拡張 参照
FlexibleInstances 一般 型クラスの拡張 参照
ForeignFunctionInterface 一般 FFI 参照
FunctionalDependencies 一般 型クラスの拡張 参照
GADTs 一般 型レベル 参照
GADTSyntax 一般 構文の拡張 参照
GeneralizedNewtypeDeriving 一般 型クラスの拡張 参照
GHCForeignImportPrim 特殊 FFI 参照
ImplicitParams 特殊 型レベル 参照
ImpredicativeTypes 特殊 型レベル 参照
IncoherentInstances 特殊 型レベル 参照
InstanceSigs 特殊 型レベル 参照
InterruptibleFFI 特殊 FFI 参照
KindSignatures 特殊 型レベル 参照
LambdaCase 一般 構文の拡張 参照
LiberalTypeSynonyms 特殊 型クラスの拡張 参照
MagicHash 特殊 GHC 内部 参照
MonadComprehensions 特殊 構文の拡張 参照
MonoPatBinds 特殊 型の明確化 参照
MultiParamTypeClasses 一般 型クラスの拡張 参照
MultiWayIf 特殊 構文の拡張 参照
NamedFieldPuns 特殊 構文の拡張 参照
NegativeLiterals 一般 型の明確化 参照
NoImplicitPrelude 特殊 インポートの明確化 参照
NoMonoLocalBinds 一般 型の明確化 参照
NoMonomorphismRestriction 一般 型の明確化 参照
NPlusKPatterns 非推奨 非推奨 参照
NullaryTypeClasses 特殊 型クラスの拡張 参照
NumDecimals 一般 型の明確化 参照
OverlappingInstances 特殊 型クラスの拡張 参照
OverloadedLists 一般 構文の拡張 参照
OverloadedStrings 一般 構文の拡張 参照
PackageImports 一般 インポートの明確化 参照
ParallelArrays 特殊 DPH 参照
ParallelListComp 一般 構文の拡張 参照
PatternGuards 一般 構文の拡張 参照
PatternSynonyms 一般 構文の拡張 参照
PolyKinds 特殊 型レベル 参照
PolymorphicComponents 特殊 非推奨 参照
PostfixOperators 特殊 構文の拡張 参照
QuasiQuotes 特殊 メタ 参照
Rank2Types 特殊 歴史の産物 参照
RankNTypes 特殊 型レベル 参照
RebindableSyntax 特殊 メタ 参照
RecordWildCards 一般 構文の拡張 参照
RecursiveDo 特殊 構文の拡張 参照
RelaxedPolyRec 特殊 型の明確化 参照
RoleAnnotations 特殊 型の明確化 参照
Safe 特殊 安全性の検査 参照
SafeImports 特殊 安全性の検査 参照
ScopedTypeVariables 特殊 型レベル 参照
StandaloneDeriving 一般 型クラスの拡張 参照
TemplateHaskell 特殊 メタ 参照
TraditionalRecordSyntax 特殊 歴史の産物 参照
TransformListComp 特殊 構文の拡張 参照
Trustworthy 特殊 安全性の検査 参照
TupleSections 一般 構文の拡張 参照
TypeFamilies 特殊 型レベル 参照
TypedHoles 一般 対話的型付け 参照
TypeOperators 特殊 型レベル 参照
TypeSynonymInstances 一般 型クラスの拡張 参照
UnboxedTuples 特殊 FFI 参照
UndecidableInstances 特殊 型レベル 参照
UnicodeSyntax 特殊 構文の拡張 参照
UnliftedFFITypes 特殊 FFI 参照
Unsafe 特殊 安全性の検査 参照
ViewPatterns 一般 構文の拡張 参照

参照:

安全なもの

どの言語拡張が一般に良く使われているかはあまりはっきりとは分かりませんが、これらの言語拡張は無難であり広範囲で使用しても安全である、と言っても差し支えありません。

  • NoMonomorphismRestriction
  • FlexibleContexts
  • FlexibleInstances
  • GeneralizedNewtypeDeriving
  • GADTs
  • FunctionalDependencies
  • OverloadedStrings
  • TypeSynonymInstances
  • BangPatterns
  • DeriveGeneric
  • DeriveDataTypeable
  • ScopedTypeVariables

危険なもの

GHC の型検査器は時々、ある種の問題を解決できない時に、言語拡張を有効にするように何気なく教えてくれます。それらの中にはこれが含まれます。

  • DatatypeContexts
  • OverlappingInstances
  • IncoherentInstances
  • ImpredicativeTypes

これらはたいてい設計上の欠陥を暗に示しています。GHC が使うように提案しているとしても、エラーをその場で解決するために頼るのはやめましょう!

型推論

Haskell での型推論は一般にはかなり正確ですが、問題を引き起こしがちな境界的なケースがいくつかあります。以下の 2 つの関数を考えてみましょう。

相互再帰の束縛グループ

f x = const x g
g y = f 'A'

推論された型シグニチャは使用上は正しいですが、最も一般性のあるシグニチャにはなりません。GHC がモジュールを分析するとき、式の相互の依存関係を分析し、それらをグループ化し、相互に定義されているグループ全体に単一化をかけて、置換を行います。推論される型は、それ自体は可能な限りで最も一般性のある型にはならないかもしれないので、明示的にシグニチャを付けるのが良いでしょう。

-- Inferred types
f :: Char -> Char
g :: t -> Char

-- Most general types
f :: a -> a
g :: a -> Char

多相再帰

data Tree a = Leaf | Bin a (Tree (a, a))

size Leaf = 0
size (Bin _ t) = 1 + 2 * size t

この式の問題は、size の推論される型変数 a が 2 つの型の可能性(a(a,a))を含むということです。これら 2 つの型は型検査器の出現検査を通過せず、誤った型が推論されてしまいます。

    Occurs check: cannot construct the infinite type: t0 = (t0, t0)
    Expected type: Tree t0
      Actual type: Tree (t0, t0)
    In the first argument of `size', namely `t'
    In the second argument of `(*)', namely `size t'
    In the second argument of `(+)', namely `2 * size t'

型シグニチャを単に明示するだけで解決できます。多相再帰を使う型推論は、一般には決定不能です。

size :: Tree a -> Int
size Leaf = 0
size (Bin _ t) = 1 + 2 * size t

参照:

単相性制限

型推論の境界的なケースで最も良く見かけるものは、世にも恐ろしい単相性制限として知られています。

モジュールのトップレベルの宣言が型の一般性を持つとき、単相性の制限を施すと、Prelude の Num のサブクラスを含む型を含むトップレベルの値(即ち、ラムダに入っていない式)は一般化されず、代わりに default で指定されたリスト(通常、Integer の次に Double)により順々に検査されていった単相型により実体化されます。

-- Double は型推論器が推論した型です。
example1 :: Double
example1 = 3.14

-- ラムダがあるだけで、違う型が推論されるのです!
example2 :: Fractional a => t -> a
example2 _ = 3.14

default (Integer, Double)

GHC 7.8 の時点で、GHCi ではデフォルトで単相性制限は無効にされています。

λ: set +t

λ: 3
3
it :: Num a => a

λ: default (Double)

λ: 3
3.0
it :: Num a => a

セーフハスケル

誰もが結局は気づくことですが、GHC(Haskell 言語ではない)の実装の中には、型システムを破壊するのに使える関数がいくつかあり、それらは unsafe[危険]という接頭辞が付いています。これらの関数は、式の健全性を手で証明できるが、型システムではその性質を表現できない時だけのために存在します。証明の義務を果たさずこれらの関数を使うと、あらゆる尺度で未定義の、想像を超えた痛みと苦しみを与える振る舞いが起こるので、そうすることを避けることを強く推奨します。最初に Haskell を学んでいる場合、これらの関数を使う正当な理由は全く持ってありません。以上。

unsafeCoerce :: a -> b
unsafePerformIO :: IO a -> a

セーフハスケル言語拡張を使えば、危険な言語機能の使用を -XSafe を使って制限することができます。このフラグは、Safe とされたモジュールのみインポートするよう制限します。また、危険なコードを生み出すのに使える特定の言語拡張も(-XTemplateHaskell など)禁止します。これらの拡張の直接のユースケースはセキュリティー監査の下にあります。

{-# LANGUAGE Safe #-}
{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE Safe #-}

import Unsafe.Coerce
import System.IO.Unsafe

bad1 :: String
bad1 = unsafePerformIO getLine

bad2 :: a
bad2 = unsafeCoerce 3.14 ()
Unsafe.Coerce: Can't be safely imported!
The module itself isn't safe.

参照:

パターンガード

{-# LANGUAGE PatternGuards #-}

combine env x y
   | Just a <- lookup x env
   , Just b <- lookup y env
   = Just $ a + b

   | otherwise = Nothing

ビューパターン

{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NoMonomorphismRestriction #-}

import Safe

lookupDefault :: Eq a => a -> b -> [(a,b)] -> b
lookupDefault k _ (lookup k -> Just s) = s
lookupDefault _ d _ = d

headTup :: (a, [t]) -> [t]
headTup (headMay . snd -> Just n) = [n]
headTup _ = []

headNil :: [a] -> [a]
headNil (headMay -> Just x) = [x]
headNil _ = []

その他の構文上の拡張

タプルセクション

{-# LANGUAGE TupleSections #-}

first :: a -> (a, Bool)
first = (,True)

second :: a -> (Bool, a)
second = (True,)

多分岐 if 式

{-# LANGUAGE MultiWayIf #-}

operation x =
  if | x > 100   = 3
     | x > 10    = 2
     | x > 1     = 1
     | otherwise = 0

ラムダケース

{-# LANGUAGE LambdaCase #-}

data Exp a
  = Lam a (Exp a)
  | Var a
  | App (Exp a) (Exp a)

example :: Exp a -> a
example = \case
  Lam a b -> a
  Var a   -> a
  App a b -> example a

パッケージからのインポート

import qualified "mtl" Control.Monad.Error as Error
import qualified "mtl" Control.Monad.State as State
import qualified "mtl" Control.Monad.Reader as Reader

レコードのワイルドカード

レコードのワイルドカードを使えば、レコードの名前を、暗黙のうちにレコードの対応するラベルを調べる変数として展開することができます。

{-# LANGUAGE RecordWildCards #-}

data T = T { a :: Int , b :: Int }

f :: T -> Int
f (T {..} ) = a + b

パターンシノニム

型検査器を書いているとすると、関数のシグニチャを入れ子にするのを簡単にするために、特別に TArr を入れることはよくあることでしょう。GHC は実際コア言語でそうしています。しかし技術的には (->) コンストラクタのより基本的な運用によって書くこともできます。

data Type
  = TVar TVar
  | TCon TyCon
  | TApp Type Type
  | TArr Type Type
  deriving (Show, Eq, Ord)

パターンシノニムを使えば、矢印型に対するパターンマッチングを不便にしてしまうことなく、余計なコンストラクタを無くすことができます。

{-# LANGUAGE PatternSynonyms #-}

pattern TArr t1 t2 = TApp (TApp (TCon "(->)") t1) t2

すると、矢印型に対するエリミネータ(除去子)とコンストラクタはとても自然に書けます。

{-# LANGUAGE PatternSynonyms #-}

import Data.List (foldl1')

type Name  = String
type TVar  = String
type TyCon = String

data Type
  = TVar TVar
  | TCon TyCon
  | TApp Type Type
  deriving (Show, Eq, Ord)


pattern TArr t1 t2 = TApp (TApp (TCon "(->)") t1) t2

tapp :: TyCon -> [Type] -> Type
tapp tcon args = foldl TApp (TCon tcon) args

arr :: [Type] -> Type
arr ts = foldl1' (\t1 t2 -> tapp "(->)" [t1, t2]) ts

elimTArr :: Type -> [Type]
elimTArr (TArr (TArr t1 t2) t3) = t1 : t2 : elimTArr t3
elimTArr (TArr t1 t2) = t1 : elimTArr t2
elimTArr t = [t]

-- (->) a ((->) b a)
-- a -> b -> a
to :: Type
to = arr [TVar "a", TVar "b", TVar "a"]

from :: [Type]
from = elimTArr to

results matching ""

    No results matching ""