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:
MonomorphismRestrictionAktiviert es (dies ist die Standardeinstellung) und
NoMonomorphismRestrictiondeaktiviert 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 :setBefehl aktivieren :
Prelude> :set -XNoMonomorphismRestriction
Sie können ghcdie 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 -> awü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 plusnicht das ist, was wir erwarten würden:
Prelude> :t plus
plus :: Integer -> Integer -> Integer
Was passiert ist, ist, dass der Compiler sah, dass plusTyp 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, Integerwie 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 ghcibehandelt werden (und behandelt werden müssen ). Grundsätzlich kann jede Anweisung eingegeben in ghcimü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 f2und f4nicht kompilieren. Darüber hinaus , wenn sie versuchen , diese Funktion in GHCi wir bekommen definieren keine Fehler , aber der Typ für f2und f4ist () -> String!
Monomorphie Einschränkung ist , was macht f2und f4einen monomorphen Typ erfordern, und das unterschiedliche Verhalten bewteen ghcund ghciist 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 whereErklä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 somethingoder (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 -> apolymorph, weil er enthält, aund auch eingeschränkt, weil der Typ adie Einschränkung Numdarüber hat .)
dann kann es nicht verallgemeinert werden.
In einfachen Worten bedeutet nicht verallgemeinern , dass die Verwendung der Funktion plusihren 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 Integerin der Deklaration von aufgerufen xwird, wird die Typvariable amit vereinheitlicht Integerund daher wird der Typ von plus:
Integer -> Integer -> Integer
Wenn dann die Definition von typ überprüft ywird, plus
wird angezeigt, dass sie auf ein DoubleArgument angewendet wird und die Typen nicht übereinstimmen.
Beachten Sie, dass Sie weiterhin verwenden können, plusohne einen Fehler zu erhalten:
plus = (+)
x = plus 1.0 2
In diesem Fall pluswird zuerst auf den Typ von geschlossen , Num a => a -> a -> a
aber dann wird seine Verwendung in der Definition von x, wo 1.0eine 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 genericLengthist 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 lenes 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 lenpolymorph fwä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 readseine Standardfunktion ist, deren Typ durch die Signatur angegeben wird
liest :: (Lesen Sie a) => String -> [(a, String)]
Ohne Regel 1 nwürde der Typ ∀ a. Read a ⇒ aund 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 nund smonomorph 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 readSie 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 -> abei dem aes 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 pluswird:
Fractional a => a -> a -> a(siehe Regel 1 , warum dies geschieht). An diesem Punkt wird das Befolgen der Standardregeln adurch ersetzt Double
und wir haben plus :: Double -> Double -> Doubleund 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 showund readsind:
show :: Show a => a -> String
read :: Read a => String -> a
Das xhat also Typ Read a => a. Aber diese Einschränkung wird durch eine Menge von Arten erfüllt ist :
Int, Doubleoder ()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 NumTypklasse 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 1hat Typ Num a => aund 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 defaultDeklaration gesteuert werden . Durch Angabe können default (T1, T2, T3)wir ändern, wie der Inferencer die verschiedenen Typen standardmäßig verwendet.
Eine mehrdeutige Typvariable vist standardmäßig verfügbar, wenn:
verscheint nur in Beschränkungen der Art, in der C ves sich Cum eine Klasse handelt (dh wenn es wie Monad (m v)folgt aussieht : dann ist es nicht standardmäßig).
- Mindestens eine dieser Klassen ist
Numoder 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 defaultListe ersetzt, der eine Instanz aller Klassen der mehrdeutigen Variablen ist.
Die Standarddeklaration defaultlautet 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 Ordes 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 ExtendedDefaultRulesErweiterungen.
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, Showoder Numund ihre Unterklassen.
Darüber hinaus defaultlautet 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 aEinschrä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: