単型性制限とは何ですか?
Haskell wiki で述べられているモノモーフィズムの制限は次のとおりです。
Haskell の型推論における直感に反するルール。型シグネチャを提供するのを忘れた場合、この規則は「型のデフォルト設定」規則を使用して自由型変数を特定の型で満たすことがあります。
これが意味することは、状況によっては、型があいまいである (つまり、ポリモーフィックである) 場合、コンパイラはその型をあいまいではないものにインスタンス化することを選択するということです。
どうすれば修正できますか?
まず第一に、いつでも型シグネチャを明示的に提供でき、これにより制限のトリガーが回避されます。
plus :: Num a => a -> a -> a
plus = (+) -- Okay!
-- Runs as:
Prelude> plus 1.0 1
2.0
あるいは、関数を定義している場合は、ポイントフリー スタイルを避ける ことができます。たとえば、次のように記述します。
plus x y = x + y
オフにする
制限を修正するためにコードに何もする必要がないように、単に制限をオフにすることができます。動作は 2 つの拡張機能によって制御され
MonomorphismRestriction
ます。有効にする (デフォルト) 一方で、
NoMonomorphismRestriction
無効にします。
ファイルの一番上に次の行を追加できます。
{-# LANGUAGE NoMonomorphismRestriction #-}
GHCi を使用している場合は、次の:set
コマンドを使用して拡張機能を有効にできます。
Prelude> :set -XNoMonomorphismRestriction
ghc
コマンドラインから拡張機能を有効にするように指示することもできます。
ghc ... -XNoMonomorphismRestriction
注:コマンドライン オプションで拡張機能を選択するよりも、最初のオプションを優先する必要があります。
この拡張機能と他の拡張機能の説明については、GHC のページを参照してください。
完全な説明
単型性制限とは何か、なぜそれが導入されたのか、どのように動作するのかを理解するために知っておく必要があるすべてを以下に要約します。
例
次の簡単な定義を取ります。
plus = (+)
+
のすべての出現をで置き換えることができると思うでしょうplus
。特に(+) :: Num a => a -> a -> a
、plus :: Num a => a -> a -> a
.
残念ながら、そうではありません。たとえば、GHCi で次のことを試してみると:
Prelude> let plus = (+)
Prelude> plus 1.0 1
次の出力が得られます。
<interactive>:4:6:
No instance for (Fractional Integer) arising from the literal ‘1.0’
In the first argument of ‘plus’, namely ‘1.0’
In the expression: plus 1.0 1
In an equation for ‘it’: it = plus 1.0 1
:set -XMonomorphismRestriction
新しい GHCi バージョンでは必要になるかもしれません。
実際、 の型plus
が期待したものではないことがわかります。
Prelude> :t plus
plus :: Integer -> Integer -> Integer
何が起こったかというと、コンパイラは が多相型であるtype をplus
持っていることを認識したということです。Num a => a -> a -> a
さらに、上記の定義は後で説明するルールに該当する場合があるため、彼は型 variableをデフォルトa
にすることで型をモノモーフィックにすることにしました。デフォルトはInteger
ご覧の通りです。
を使用して上記のコードをコンパイルghc
しようとしても、エラーは発生しないことに注意してください。これは、インタラクティブな定義を処理する方法ghci
(および処理する必要がある) によるものです。基本的に、以下を検討する前に、入力されたすべてのステートメントを完全に型チェックするghci
必要があります。つまり、すべてのステートメントが個別の
モジュールにあるかのようです。後で、なぜこれが重要なのかを説明します。
他の例
次の定義を考慮してください。
f1 x = show x
f2 = \x -> show x
f3 :: (Show a) => a -> String
f3 = \x -> show x
f4 = show
f5 :: (Show a) => a -> String
f5 = show
これらの関数はすべて同じように動作し、同じ型、つまり : の型を持つことが期待されshow
ますShow a => a -> String
。
しかし、上記の定義をコンパイルすると、次のエラーが発生します。
test.hs:3:12:
No instance for (Show a1) arising from a use of ‘show’
The type variable ‘a1’ is ambiguous
Relevant bindings include
x :: a1 (bound at blah.hs:3:7)
f2 :: a1 -> String (bound at blah.hs:3:1)
Note: there are several potential instances:
instance Show Double -- Defined in ‘GHC.Float’
instance Show Float -- Defined in ‘GHC.Float’
instance (Integral a, Show a) => Show (GHC.Real.Ratio a)
-- Defined in ‘GHC.Real’
...plus 24 others
In the expression: show x
In the expression: \ x -> show x
In an equation for ‘f2’: f2 = \ x -> show x
test.hs:8:6:
No instance for (Show a0) arising from a use of ‘show’
The type variable ‘a0’ is ambiguous
Relevant bindings include f4 :: a0 -> String (bound at blah.hs:8:1)
Note: there are several potential instances:
instance Show Double -- Defined in ‘GHC.Float’
instance Show Float -- Defined in ‘GHC.Float’
instance (Integral a, Show a) => Show (GHC.Real.Ratio a)
-- Defined in ‘GHC.Real’
...plus 24 others
In the expression: show
In an equation for ‘f4’: f4 = show
したがってf2
、f4
コンパイルしないでください。さらに、GHCi でこれらの関数を定義しようとすると、エラーは発生しませんがf2
、 andの型f4
は() -> String
!
モノモーフィズムの制限は、モノモーフィック型を作成f2
および必要とするものであり、とf4
の間の異なる動作は、異なる
デフォルト規則によるものです。ghc
ghci
それはいつ起こりますか?
レポートで定義されているように、Haskellには2 つの異なるタイプのバインディングがあります。関数バインディングとパターン バインディング。関数バインディングは、関数の定義に他なりません。
f x = x + 1
構文は次のとおりです。
<identifier> arg1 arg2 ... argn = expr
モジュロガードとwhere
宣言。しかし、それらは実際には問題ではありません。
少なくとも 1 つの引数が必要です。
パターン バインディングは、次の形式の宣言です。
<pattern> = expr
繰り返しますが、モジュロ ガードです。
変数は patternであるため、バインディングは次のようになることに注意してください。
plus = (+)
パターンバインディングです。pattern plus
(変数) を expression にバインドしています(+)
。
パターン バインディングが変数名のみで構成される場合、
単純なパターン バインディングと呼ばれます。
単一型の制限は単純なパターン バインディングに適用されます。
正式には、次のように言う必要があります。
宣言グループは、相互に依存するバインディングの最小限のセットです。
レポートのセクション 4.5.1 。
次に (レポートのセクション 4.5.5 ):
次の場合にのみ、特定の宣言グループが制限されません。
グループ内のすべての変数は、関数バインディング (例f x = x
) または単純なパターン バインディング (例plus = (+)
; セクション 4.4.3.2 )によってバインドされます。
単純なパターン バインディングによってバインドされたグループ内のすべての変数に対して、明示的な型シグネチャが与えられます。(例plus :: Num a => a -> a -> a; plus = (+)
)。
私が追加した例。
したがって、制限された宣言グループは、
非単純なパターン バインディング ((x:xs) = f something
や など(f, g) = ((+), (-))
) があるか、型シグネチャのない単純なパターン バインディング ( など) があるグループplus = (+)
です。
単相性の制限は、制限された宣言グループに影響します。
ほとんどの場合、相互再帰関数を定義しないため、宣言グループは単なるバインディングになります。
それは何をするためのものか?
単形性制限は、レポートのセクション 4.5.5 で 2 つの規則によって説明されています。
最初のルール
Hindley-Milner のポリモーフィズムに関する通常の制限は、環境内で自由に発生しない型変数のみを一般化できるというものです。さらに、制限された宣言グループの制約された型変数は、そのグループの一般化ステップで一般化されない場合があります。
(型変数が何らかの型クラスに属さなければならない場合、型変数は制約されることを思い出してください。セクション 4.5.2 を参照してください。)
強調表示されている部分は、単型性制限が導入するものです。型がポリモーフィック (つまり、型変数を含む)
であり、その型変数が制約されている (つまり、クラス制約がある: たとえば、型Num a => a -> a -> a
が含まれているためポリモーフィックであり、制約がa
あるためa
制約さNum
れている) ことを示します。 .)
その場合、一般化することはできません。
簡単に言えば、一般化しないということは、関数の使用plus
によってその型が変わる可能性があることを意味します。
定義がある場合:
plus = (+)
x :: Integer
x = plus 1 2
y :: Double
y = plus 1.0 2
次に、型エラーが発生します。コンパイラがの宣言でplus
an を介して呼び出されていることを確認すると、型変数がと統合されるため、 の型は次のようになります。Integer
x
a
Integer
plus
Integer -> Integer -> Integer
しかし、その後、 の定義を型チェックすると、 が引数に
適用され、型が一致しないy
ことがわかります。plus
Double
plus
エラーが発生することなく引き続き使用できることに注意してください。
plus = (+)
x = plus 1.0 2
この場合、 の型plus
は最初に であると推測されますが、制約が必要Num a => a -> a -> a
な の定義で使用すると
、 に変更されます。x
1.0
Fractional
Fractional a => a -> a -> a
根拠
レポートは次のように述べています。
ルール 1 が必要な理由は 2 つありますが、どちらもかなり微妙です。
ルール 1 は、計算が予期せず繰り返されるのを防ぎます。たとえば、genericLength
( library 内の) 標準関数でありData.List
、その型は
genericLength :: Num a => [b] -> a
ここで、次の式を考えてみましょう。
let len = genericLength xs
in (len, len)
len
1 回だけ計算する必要があるように見えますが、ルール 1 がないと、2 つの異なるオーバーロードのそれぞれで 1 回ずつ、2 回計算される可能性があります。
プログラマーが実際に計算を繰り返したい場合は、明示的な型シグネチャを追加できます。
let len :: Num a => a
len = genericLength xs
in (len, len)
この点については、wikiの例の方がわかりやすいと思います。関数を考えてみましょう:
f xs = (len, len)
where
len = genericLength xs
len
がポリモーフィックである場合、タイプは次のf
ようになります。
f :: Num a, Num b => [c] -> (a, b)
したがって、タプルの 2 つの要素は、(len, len)
実際には
異なる値になる可能性があります。しかし、これは、2 つの異なる値を取得するために、 で行われる計算を繰り返さgenericLength
なければならないことを意味します。
コードには1 つの関数呼び出しが含まれていますが、このルールを導入しないと2 つの隠し関数呼び出しが生成される可能性があり、直感に反します。
単型性制限により、 の型は次のようにf
なります。
f :: Num a => [b] -> (a, a)
この方法では、計算を複数回実行する必要はありません。
ルール 1 はあいまいさを防ぎます。たとえば、宣言グループを考えてみましょう
[(n,s)] = reads t
reads
これは、シグネチャによって型が指定される標準関数であることを思い出してください。
reads :: (Read a) => String -> [(a,String)]
ルール 1 がなければ、 typeと
typen
が割り当てられます。後者は本質的にあいまいであるため、無効な型です。どのオーバーロードで を使用するかを決定することはできません。また、 の型シグネチャを追加することによってこれを解決することもできません。したがって、非単純なパターン バインディングが使用される場合 (セクション 4.4.3.2 )、推論される型は、型シグネチャが提供されるかどうかに関係なく、制約された型変数で常に単相です。この場合、 と の両方がで単形性です。∀ a. Read a ⇒ a
s
∀ a. Read a ⇒ String
s
s
n
s
a
まあ、この例は自明だと思います。ルールを適用しないと型があいまいになる場合があります。
上記のように拡張機能を無効にすると、上記の宣言をコンパイルしようとすると型エラーが発生します。ただし、これは実際には問題ではありません。使用するときにread
、解析しようとする型をコンパイラに何らかの方法で伝える必要があることは既にわかっています...
第二のルール
- モジュール全体の型推論が完了したときに残っているモノモーフィックな型変数は、あいまいであると見なされ、デフォルト規則 (セクション 4.3.4 ) を使用して特定の型に解決されます。
この意味は。通常の定義がある場合:
plus = (+)
これは、上記の規則 1 により、
単相Num a => a -> a -> a
型変数である型を持ちます。モジュール全体が推論されると、コンパイラは
、デフォルトの規則に従って、それを置き換える型を単に選択します。a
a
最終結果は次のとおりplus :: Integer -> Integer -> Integer
です。
これは、モジュール全体が推論された後に行われることに注意してください。
これは、次の宣言がある場合を意味します。
plus = (+)
x = plus 1.0 2.0
モジュール内では、型をデフォルトにする前にplus
、型は次のようになります:
Fractional a => a -> a -> a
(これが起こる理由については、ルール 1 を参照してください)。この時点で、デフォルトのルールに従って、 anda
に置き換えられるDouble
ため、 and にplus :: Double -> Double -> Double
なりx :: Double
ます。
不履行
前に述べたように、レポートのセクション 4.3.4 で説明されているいくつかのデフォルトルールが存在します。これは、推論器が採用でき、ポリモーフィック型をモノモーフィック型に置き換えるものです。これは、型があいまいな場合に発生します。
たとえば、式では次のようになります。
let x = read "<something>" in show x
show
ここでは、 andの型が次のread
とおりであるため、式があいまいです。
show :: Show a => a -> String
read :: Read a => String -> a
したがって、x
has type Read a => a
. しかし、この制約は多くのタイプで満たされています:
Int
、Double
または()
。どちらを選ぶ?私たちに言えることは何もありません。
この場合、必要な型をコンパイラに伝え、型シグネチャを追加することで、あいまいさを解決できます。
let x = read "<something>" :: Int in show x
問題は、Haskell がNum
型クラスを使用して数値を処理するため、数値式にあいまいさが含まれる場合が多いことです。
検討:
show 1
結果はどうあるべきですか?
前と同じように1
型Num a => a
があり、使用できる数の型がたくさんあります。どちらを選ぶ?
数値を使用するたびにコンパイラ エラーが発生するのは良くないため、デフォルトのルールが導入されました。default
ルールは、宣言を使用して制御できます。指定するdefault (T1, T2, T3)
ことで、推論器がさまざまな型をデフォルトにする方法を変更できます。
次の場合、あいまいな型変数v
はデフォルト可能です。
v
は、クラスであるという種類の制約でのみC v
表示C
されます (つまり、: のように表示される場合は、デフォルト可能Monad (m v)
ではありません)。
- これらのクラスの少なくとも 1 つが である
Num
か、そのサブクラスですNum
。
- これらのクラスはすべて Prelude または標準ライブラリで定義されています。
デフォルト可能な型変数は、あいまいな変数のすべてのクラスのインスタンスであるリストの最初の型に置き換えられます。default
デフォルトのdefault
宣言はdefault (Integer, Double)
.
例えば:
plus = (+)
minus = (-)
x = plus 1.0 1
y = minus 2 1
推論される型は次のとおりです。
plus :: Fractional a => a -> a -> a
minus :: Num a => a -> a -> a
これは、ルールをデフォルトにすることにより、次のようになります。
plus :: Double -> Double -> Double
minus :: Integer -> Integer -> Integer
sort
これは、質問の例で定義のみがエラーを発生させる理由を説明していることに注意してください。は数値クラスではないため、型Ord a => [a] -> [a]
をデフォルト設定できません。Ord
拡張デフォルト
GHCi には拡張されたデフォルト規則( GHC8 の場合はこちら) が付属していることに注意してください。これは、拡張機能を使用してファイル内で有効にすることもできますExtendedDefaultRules
。
デフォルト可能な型変数は、すべてのクラスが標準であり、 、、またはそのサブクラスの中に少なくとも 1 つのクラスがなければならない制約に現れる
必要があるだけではありません。Eq
Ord
Show
Num
さらに、デフォルトのdefault
宣言はdefault ((), Integer, Double)
.
これにより、奇妙な結果が生じる場合があります。質問から例を挙げます:
Prelude> :set -XMonomorphismRestriction
Prelude> import Data.List(sortBy)
Prelude Data.List> let sort = sortBy compare
Prelude Data.List> :t sort
sort :: [()] -> [()]
ghci では、型エラーは発生しませんが、Ord a
制約によってデフォルトが発生し、ほとんど役に立ちません()
。
便利なリンク
単型性の制限については、多くのリソースと議論があります。
以下は、私が役に立つと思ういくつかのリンクであり、トピックを理解したり、深く掘り下げたりするのに役立ちます。