Was ist die Monomorphismusbeschränkung?
Die im Haskell-Wiki angegebene Monomorphismus-Einschränkung lautet:
eine kontraintuitive Regel in der Haskell-Typinferenz. Wenn Sie vergessen, eine Typensignatur anzugeben, füllt diese Regel manchmal die freien Typvariablen mit bestimmten Typen unter Verwendung von "Typstandard" -Regeln.
Dies bedeutet, dass der Compiler unter bestimmten Umständen , wenn Ihr Typ mehrdeutig (dh polymorph) ist , diesen Typ auf etwas nicht mehrdeutiges instanziiert .
Wie behebe ich das?
Zunächst können Sie immer explizit eine Typensignatur angeben, um das Auslösen der Einschränkung zu vermeiden:
plus :: Num a => a -> a -> a
plus = (+)
Prelude> plus 1.0 1
2.0
Wenn Sie eine Funktion definieren, können Sie alternativ einen punktfreien Stil vermeiden und beispielsweise schreiben:
plus x y = x + y
Ausschalten
Sie können die Einschränkung einfach deaktivieren, damit Sie nichts an Ihrem Code ändern müssen, um sie zu beheben. Das Verhalten wird durch zwei Erweiterungen gesteuert:
MonomorphismRestriction
Aktiviert es (dies ist die Standardeinstellung) und
NoMonomorphismRestriction
deaktiviert es.
Sie können die folgende Zeile ganz oben in Ihre Datei einfügen:
{-# LANGUAGE NoMonomorphismRestriction #-}
Wenn Sie GHCi verwenden, können Sie die Erweiterung mit dem folgenden :set
Befehl aktivieren :
Prelude> :set -XNoMonomorphismRestriction
Sie können ghc
die Erweiterung auch über die Befehlszeile aktivieren:
ghc ... -XNoMonomorphismRestriction
Hinweis: Sie sollten die erste Option der Auswahl der Erweiterung über Befehlszeilenoptionen vorziehen.
Auf der Seite von GHC finden Sie eine Erläuterung dieser und anderer Erweiterungen.
Eine vollständige Erklärung
Ich werde versuchen, im Folgenden alles zusammenzufassen, was Sie wissen müssen, um zu verstehen, was die Monomorphismusbeschränkung ist, warum sie eingeführt wurde und wie sie sich verhält.
Ein Beispiel
Nehmen Sie die folgende triviale Definition:
plus = (+)
Sie würden denken, in der Lage zu sein, jedes Vorkommen von +
durch zu ersetzen plus
. Insbesondere , da (+) :: Num a => a -> a -> a
würde man erwarten , dass auch haben plus :: Num a => a -> a -> a
.
Dies ist leider nicht der Fall. Zum Beispiel versuchen wir in GHCi Folgendes:
Prelude> let plus = (+)
Prelude> plus 1.0 1
Wir erhalten folgende Ausgabe:
<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
Möglicherweise müssen Sie :set -XMonomorphismRestriction
in neueren GHCi-Versionen.
Und tatsächlich können wir sehen, dass die Art von plus
nicht das ist, was wir erwarten würden:
Prelude> :t plus
plus :: Integer -> Integer -> Integer
Was passiert ist, ist, dass der Compiler sah, dass plus
Typ Num a => a -> a -> a
, ein polymorpher Typ. Darüber hinaus fällt die obige Definition unter die Regeln, die ich später erläutern werde, und so hat er beschlossen, den Typ monomorph zu machen, indem er die Typvariable standardmäßig verwendet a
. Die Standardeinstellung ist, Integer
wie wir sehen können.
Beachten Sie, dass beim Versuch , den obigen Code mit zu kompilierenghc
, keine Fehler auftreten. Dies liegt daran, wie die interaktiven Definitionen ghci
behandelt werden (und behandelt werden müssen ). Grundsätzlich kann jede Anweisung eingegeben in ghci
müssen vollständig geprüft Typ vor Folgendes berücksichtigt wird; Mit anderen Worten, es ist, als ob jede Anweisung in einem separaten
Modul wäre . Später werde ich erklären, warum das so ist.
Ein anderes Beispiel
Betrachten Sie die folgenden Definitionen:
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
Wir würden erwarten, dass sich alle diese Funktionen gleich verhalten und den gleichen Typ haben, dh den Typ show
: Show a => a -> String
.
Beim Zusammenstellen der obigen Definitionen erhalten wir jedoch die folgenden Fehler:
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
Also f2
und f4
nicht kompilieren. Darüber hinaus , wenn sie versuchen , diese Funktion in GHCi wir bekommen definieren keine Fehler , aber der Typ für f2
und f4
ist () -> String
!
Monomorphie Einschränkung ist , was macht f2
und f4
einen monomorphen Typ erfordern, und das unterschiedliche Verhalten bewteen ghc
und ghci
ist aufgrund unterschiedlicher
säumige Regeln .
Wann passiert es?
In Haskell gibt es, wie im Bericht definiert , zwei verschiedene Arten von Bindungen . Funktionsbindungen und Musterbindungen. Eine Funktionsbindung ist nichts anderes als eine Definition einer Funktion:
f x = x + 1
Beachten Sie, dass ihre Syntax lautet:
<identifier> arg1 arg2 ... argn = expr
Modulo Wachen und where
Erklärungen. Aber sie spielen keine Rolle.
wo es mindestens ein Argument geben muss .
Eine Musterbindung ist eine Deklaration des Formulars:
<pattern> = expr
Wieder Modulo Wachen.
Beachten Sie, dass Variablen Muster sind , also die Bindung:
plus = (+)
ist eine Musterbindung . Es bindet das Muster plus
(eine Variable) an den Ausdruck (+)
.
Wenn eine Musterbindung nur aus einem Variablennamen besteht, spricht man von einer
einfachen Musterbindung.
Die Monomorphismus-Einschränkung gilt für einfache Musterbindungen!
Nun, formal sollten wir das sagen:
Eine Deklarationsgruppe ist eine minimale Menge von voneinander abhängigen Bindungen.
Abschnitt 4.5.1 des Berichts .
Und dann (Abschnitt 4.5.5 des Berichts ):
Eine bestimmte Deklarationsgruppe ist genau dann uneingeschränkt, wenn:
Jede Variable in der Gruppe ist durch eine Funktionsbindung (z. B. f x = x
) oder eine einfache Musterbindung (z plus = (+)
. B. Abschnitt 4.4.3.2) gebunden
Für jede Variable in der Gruppe, die durch einfache Musterbindung gebunden ist, wird eine explizite Typensignatur angegeben. (zB plus :: Num a => a -> a -> a; plus = (+)
).
Beispiele von mir hinzugefügt.
Eine eingeschränkte Deklarationsgruppe ist also eine Gruppe, in der entweder
nicht einfache Musterbindungen (z. B. (x:xs) = f something
oder (f, g) = ((+), (-))
) oder eine einfache Musterbindung ohne Typensignatur (wie in plus = (+)
) vorhanden sind.
Die Monomorphismusbeschränkung betrifft eingeschränkte Deklarationsgruppen.
Meistens definieren Sie keine gegenseitigen rekursiven Funktionen und daher wird eine Deklarationsgruppe nur zu einer Bindung.
Was tut es?
Die Monomorphismusbeschränkung wird in Abschnitt 4.5.5 des Berichts durch zwei Regeln beschrieben .
Erste Regel
Die übliche Hindley-Milner-Einschränkung des Polymorphismus besteht darin, dass nur Typvariablen verallgemeinert werden dürfen, die in der Umgebung nicht frei vorkommen. Darüber hinaus können die eingeschränkten Typvariablen einer eingeschränkten Deklarationsgruppe im Generalisierungsschritt für diese Gruppe möglicherweise nicht verallgemeinert werden.
(Denken Sie daran, dass eine Typvariable eingeschränkt ist, wenn sie zu einer Typklasse gehören muss; siehe Abschnitt 4.5.2.)
Der hervorgehobene Teil ist das, was die Monomorphismusbeschränkung einführt. Es heißt, wenn der Typ polymorph ist (dh eine Typvariable enthält)
und diese Typvariable eingeschränkt ist (dh eine Klassenbeschränkung hat: z. B. ist der Typ Num a => a -> a -> a
polymorph, weil er enthält, a
und auch eingeschränkt, weil der Typ a
die Einschränkung Num
darüber hat .)
dann kann es nicht verallgemeinert werden.
In einfachen Worten bedeutet nicht verallgemeinern , dass die Verwendung der Funktion plus
ihren Typ ändern kann.
Wenn Sie die Definitionen hätten:
plus = (+)
x :: Integer
x = plus 1 2
y :: Double
y = plus 1.0 2
dann würden Sie einen Tippfehler bekommen. Denn wenn der Compiler sieht, dass plus
über ein Integer
in der Deklaration von aufgerufen x
wird, wird die Typvariable a
mit vereinheitlicht Integer
und daher wird der Typ von plus
:
Integer -> Integer -> Integer
Wenn dann die Definition von typ überprüft y
wird, plus
wird angezeigt, dass sie auf ein Double
Argument angewendet wird und die Typen nicht übereinstimmen.
Beachten Sie, dass Sie weiterhin verwenden können, plus
ohne einen Fehler zu erhalten:
plus = (+)
x = plus 1.0 2
In diesem Fall plus
wird zuerst auf den Typ von geschlossen , Num a => a -> a -> a
aber dann wird seine Verwendung in der Definition von x
, wo 1.0
eine Fractional
Einschränkung erforderlich ist , in geändert Fractional a => a -> a -> a
.
Begründung
Der Bericht sagt:
Regel 1 ist aus zwei Gründen erforderlich, die beide ziemlich subtil sind.
Regel 1 verhindert, dass Berechnungen unerwartet wiederholt werden. Zum Beispiel genericLength
ist eine Standardfunktion (in der Bibliothek Data.List
), deren Typ durch gegeben ist
genericLength :: Num a => [b] -> a
Betrachten Sie nun den folgenden Ausdruck:
let len = genericLength xs
in (len, len)
Es sieht so aus, als ob len
es nur einmal berechnet werden sollte, aber ohne Regel 1 könnte es zweimal berechnet werden, jeweils einmal bei zwei verschiedenen Überladungen.
Wenn der Programmierer tatsächlich möchte, dass die Berechnung wiederholt wird, kann eine explizite Typensignatur hinzugefügt werden:
let len :: Num a => a
len = genericLength xs
in (len, len)
Für diesen Punkt ist das Beispiel aus dem Wiki meines Erachtens klarer. Betrachten Sie die Funktion:
f xs = (len, len)
where
len = genericLength xs
Wenn len
polymorph f
wäre, wäre die Art von :
f :: Num a, Num b => [c] -> (a, b)
Die beiden Elemente des Tupels (len, len)
können also tatsächlich
unterschiedliche Werte sein! Dies bedeutet jedoch, dass die Berechnung von wiederholt werden genericLength
muss , um die beiden unterschiedlichen Werte zu erhalten.
Das Grundprinzip hier ist: Der Code enthält einen Funktionsaufruf, aber wenn diese Regel nicht eingeführt wird, können zwei versteckte Funktionsaufrufe erzeugt werden, was nicht intuitiv ist.
Mit der Monomorphismus-Beschränkung wird die Art von f
:
f :: Num a => [b] -> (a, a)
Auf diese Weise muss die Berechnung nicht mehrmals durchgeführt werden.
Regel 1 verhindert Mehrdeutigkeiten. Betrachten Sie beispielsweise die Deklarationsgruppe
[(n, s)] = liest t
Denken Sie daran, dass dies reads
eine Standardfunktion ist, deren Typ durch die Signatur angegeben wird
liest :: (Lesen Sie a) => String -> [(a, String)]
Ohne Regel 1 n
würde der Typ ∀ a. Read a ⇒ a
und s
der Typ zugewiesen ∀ a. Read a ⇒ String
. Letzteres ist ein ungültiger Typ, da er von Natur aus mehrdeutig ist. Es ist nicht möglich zu bestimmen, bei welcher Überladung verwendet werden soll s
, und dies kann auch nicht durch Hinzufügen einer Typensignatur für gelöst werden s
. Wenn also nicht einfache Musterbindungen verwendet werden (Abschnitt 4.4.3.2), sind die abgeleiteten Typen in ihren eingeschränkten Typvariablen immer monomorph, unabhängig davon, ob eine Typensignatur bereitgestellt wird. In diesem Fall sind beide n
und s
monomorph in a
.
Ich glaube, dieses Beispiel ist selbsterklärend. Es gibt Situationen, in denen die Nichtanwendung der Regel zu einer Mehrdeutigkeit des Typs führt.
Wenn Sie die Erweiterung wie oben vorgeschlagen deaktivieren, wird beim Kompilieren der obigen Deklaration ein Typfehler angezeigt. Dies ist jedoch kein wirkliches Problem: Sie wissen bereits, dass read
Sie dem Compiler bei der Verwendung irgendwie mitteilen müssen, welchen Typ er zu analysieren versuchen soll ...
Zweite Regel
- Alle monomorphen Typvariablen, die verbleiben, wenn die Typinferenz für ein gesamtes Modul abgeschlossen ist, werden als mehrdeutig betrachtet und unter Verwendung der Standardregeln in bestimmte Typen aufgelöst (Abschnitt 4.3.4).
Das bedeutet, dass. Wenn Sie Ihre übliche Definition haben:
plus = (+)
Dies hat einen Typ, Num a => a -> a -> a
bei dem a
es sich aufgrund der oben beschriebenen Regel 1 um eine
monomorphe Typvariable handelt. Sobald das gesamte Modul abgeleitet ist, wählt der Compiler einfach einen Typ aus, der diesen a
gemäß den Standardregeln ersetzt.
Das Endergebnis ist : plus :: Integer -> Integer -> Integer
.
Beachten Sie, dass dies erfolgt, nachdem das gesamte Modul abgeleitet wurde.
Dies bedeutet, wenn Sie die folgenden Erklärungen haben:
plus = (+)
x = plus 1.0 2.0
innerhalb eines Moduls, bevor geben Sie den Typ des säumigen plus
wird:
Fractional a => a -> a -> a
(siehe Regel 1 , warum dies geschieht). An diesem Punkt wird das Befolgen der Standardregeln a
durch ersetzt Double
und wir haben plus :: Double -> Double -> Double
und x :: Double
.
Standardmäßig
Wie bereits erwähnt , bevor einige gibt es säumige Regeln, beschrieben in Abschnitt 4.3.4 des Berichts , dass die Rückschließer annehmen können , und das wird einen polymorphen Typen mit einem monomorphic ersetzen. Dies geschieht immer dann, wenn ein Typ nicht eindeutig ist .
Zum Beispiel im Ausdruck:
let x = read "<something>" in show x
hier ist der Ausdruck mehrdeutig, weil die Typen für show
und read
sind:
show :: Show a => a -> String
read :: Read a => String -> a
Das x
hat also Typ Read a => a
. Aber diese Einschränkung wird durch eine Menge von Arten erfüllt ist :
Int
, Double
oder ()
zum Beispiel. Welches soll ich wählen? Nichts kann uns sagen.
In diesem Fall können wir die Mehrdeutigkeit beheben, indem wir dem Compiler mitteilen, welchen Typ wir möchten, und eine Typensignatur hinzufügen:
let x = read "<something>" :: Int in show x
Das Problem ist nun: Da Haskell die Num
Typklasse verwendet, um Zahlen zu verarbeiten, gibt es viele Fälle, in denen numerische Ausdrücke Mehrdeutigkeiten enthalten.
Erwägen:
show 1
Was soll das Ergebnis sein?
Wie zuvor 1
hat Typ Num a => a
und es gibt viele Arten von Zahlen, die verwendet werden könnten. Welches soll ich wählen?
Fast jedes Mal, wenn wir eine Zahl verwenden, einen Compilerfehler zu haben, ist keine gute Sache, und daher wurden die Standardregeln eingeführt. Die Regeln können mithilfe einer default
Deklaration gesteuert werden . Durch Angabe können default (T1, T2, T3)
wir ändern, wie der Inferencer die verschiedenen Typen standardmäßig verwendet.
Eine mehrdeutige Typvariable v
ist standardmäßig verfügbar, wenn:
v
erscheint nur in Beschränkungen der Art, in der C v
es sich C
um eine Klasse handelt (dh wenn es wie Monad (m v)
folgt aussieht : dann ist es nicht standardmäßig).
- Mindestens eine dieser Klassen ist
Num
oder eine Unterklasse von Num
.
- Alle diese Klassen sind im Prelude oder in einer Standardbibliothek definiert.
Eine voreingestellte Typvariable wird durch den ersten Typ in der default
Liste ersetzt, der eine Instanz aller Klassen der mehrdeutigen Variablen ist.
Die Standarddeklaration default
lautet default (Integer, Double)
.
Zum Beispiel:
plus = (+)
minus = (-)
x = plus 1.0 1
y = minus 2 1
Die abgeleiteten Typen wären:
plus :: Fractional a => a -> a -> a
minus :: Num a => a -> a -> a
die standardmäßig zu folgenden Regeln werden:
plus :: Double -> Double -> Double
minus :: Integer -> Integer -> Integer
Beachten Sie, dass dies erklärt, warum im Beispiel in der Frage nur die sort
Definition einen Fehler auslöst. Der Typ Ord a => [a] -> [a]
kann nicht als Standard festgelegt werden, da Ord
es sich nicht um eine numerische Klasse handelt.
Erweiterte Standardeinstellung
Beachten Sie, dass GHCi kommt mit erweiterten säumigen Regeln (oder hier für GHC8 ), die in Dateien aktiviert werden können , als auch die Verwendung von ExtendedDefaultRules
Erweiterungen.
Der ausfall Typ Variablen muß nicht nur in contraints erscheinen , wo alle Klassen Standard und es muss mindestens eine Klasse sein , die unter ist
Eq
, Ord
, Show
oder Num
und ihre Unterklassen.
Darüber hinaus default
lautet die Standarddeklaration default ((), Integer, Double)
.
Dies kann zu merkwürdigen Ergebnissen führen. Nehmen Sie das Beispiel aus der Frage:
Prelude> :set -XMonomorphismRestriction
Prelude> import Data.List(sortBy)
Prelude Data.List> let sort = sortBy compare
Prelude Data.List> :t sort
sort :: [()] -> [()]
In ghci erhalten wir keinen Typfehler, aber die Ord a
Einschränkungen führen zu einer Standardeinstellung, ()
die so gut wie nutzlos ist.
Nützliche Links
Es gibt viele Ressourcen und Diskussionen über die Monomorphismusbeschränkung.
Hier sind einige Links, die ich nützlich finde und die Ihnen helfen können, das Thema zu verstehen oder weiter zu vertiefen: