Je suis intrigué par la façon dont le compilateur haskell déduit parfois des types moins polymorphes que ce à quoi je m'attendais, par exemple lors de l'utilisation de définitions sans point.
Il semble que le problème soit la "restriction du monomorphisme", qui est activée par défaut sur les anciennes versions du compilateur.
Considérez le programme haskell suivant:
{-# LANGUAGE MonomorphismRestriction #-}
import Data.List(sortBy)
plus = (+)
plus' x = (+ x)
sort = sortBy compare
main = do
print $ plus' 1.0 2.0
print $ plus 1.0 2.0
print $ sort [3, 1, 2]
Si je compile ceci avec ghc
, je n'obtiens aucune erreur et la sortie de l'exécutable est:
3.0
3.0
[1,2,3]
Si je change le corps de main
en:
main = do
print $ plus' 1.0 2.0
print $ plus (1 :: Int) 2
print $ sort [3, 1, 2]
Je ne reçois aucune erreur de temps de compilation et la sortie devient:
3.0
3
[1,2,3]
comme prévu. Cependant, si j'essaye de le changer en:
main = do
print $ plus' 1.0 2.0
print $ plus (1 :: Int) 2
print $ plus 1.0 2.0
print $ sort [3, 1, 2]
J'obtiens une erreur de type:
test.hs:13:16:
No instance for (Fractional Int) arising from the literal ‘1.0’
In the first argument of ‘plus’, namely ‘1.0’
In the second argument of ‘($)’, namely ‘plus 1.0 2.0’
In a stmt of a 'do' block: print $ plus 1.0 2.0
La même chose se produit lorsque vous essayez d'appeler sort
deux fois avec différents types:
main = do
print $ plus' 1.0 2.0
print $ plus 1.0 2.0
print $ sort [3, 1, 2]
print $ sort "cba"
produit l'erreur suivante:
test.hs:14:17:
No instance for (Num Char) arising from the literal ‘3’
In the expression: 3
In the first argument of ‘sort’, namely ‘[3, 1, 2]’
In the second argument of ‘($)’, namely ‘sort [3, 1, 2]’
ghc
pense-t-il soudain que plus
n'est pas polymorphe et nécessite un argument Int
? La seule référence à Int
se trouve dans une application de plus
, comment cela peut-il avoir une importance lorsque la définition est clairement polymorphe ?ghc
pense-t-il soudainement que sort
nécessite un Num Char
exemple?De plus, si j'essaie de placer les définitions de fonctions dans leur propre module, comme dans:
{-# LANGUAGE MonomorphismRestriction #-}
module TestMono where
import Data.List(sortBy)
plus = (+)
plus' x = (+ x)
sort = sortBy compare
J'obtiens l'erreur suivante lors de la compilation:
TestMono.hs:10:15:
No instance for (Ord a0) arising from a use of ‘compare’
The type variable ‘a0’ is ambiguous
Relevant bindings include
sort :: [a0] -> [a0] (bound at TestMono.hs:10:1)
Note: there are several potential instances:
instance Integral a => Ord (GHC.Real.Ratio a)
-- Defined in ‘GHC.Real’
instance Ord () -- Defined in ‘GHC.Classes’
instance (Ord a, Ord b) => Ord (a, b) -- Defined in ‘GHC.Classes’
...plus 23 others
In the first argument of ‘sortBy’, namely ‘compare’
In the expression: sortBy compare
In an equation for ‘sort’: sort = sortBy compare
ghc
n'est-il pas en mesure d'utiliser le type polymorphe Ord a => [a] -> [a]
pour sort
?ghc
traite-t-il plus
et plus'
autrement? plus
devrait avoir le type polymorphe Num a => a -> a -> a
et je ne vois pas vraiment en quoi cela diffère du type de sort
et pourtant seulement sort
soulève une erreur.Dernière chose: si je commente la définition de sort
le fichier se compile. Cependant, si j'essaie de le charger dans ghci
et de vérifier les types que j'obtiens:
*TestMono> :t plus
plus :: Integer -> Integer -> Integer
*TestMono> :t plus'
plus' :: Num a => a -> a -> a
Pourquoi le type de plus
n'est-il pas polymorphe?
C'est la question canonique sur la restriction du monomorphisme dans Haskell comme discuté dans la méta question .
La restriction du monomorphisme comme indiqué par le wiki Haskell est:
une règle contre-intuitive dans l'inférence de type Haskell. Si vous oubliez de fournir une signature de type, cette règle remplira parfois les variables de type libre avec des types spécifiques en utilisant des règles de "défaut de type".
Cela signifie que, dans certaines circonstances, si votre type est ambigu (c'est-à-dire polymorphe), le compilateur choisira de - instancier ce type à quelque chose de pas ambigu.
Tout d'abord, vous pouvez toujours fournir explicitement une signature de type et cela évitera le déclenchement de la restriction:
plus :: Num a => a -> a -> a
plus = (+) -- Okay!
-- Runs as:
Prelude> plus 1.0 1
2.0
Alternativement, si vous définissez une fonction, vous pouvez éviterstyle sans point , et par exemple écrire:
plus x y = x + y
Il est possible de désactiver simplement la restriction afin que vous n'ayez rien à faire avec votre code pour le corriger. Le comportement est contrôlé par deux extensions: MonomorphismRestriction
l'activera (qui est la valeur par défaut) tandis que NoMonomorphismRestriction
le désactivera.
Vous pouvez mettre la ligne suivante tout en haut de votre fichier:
{-# LANGUAGE NoMonomorphismRestriction #-}
Si vous utilisez GHCi, vous pouvez activer l'extension à l'aide de la commande :set
:
Prelude> :set -XNoMonomorphismRestriction
Vous pouvez également dire à ghc
d'activer l'extension depuis la ligne de commande:
ghc ... -XNoMonomorphismRestriction
Remarque: Vous devriez vraiment préférer la première option au choix de l'extension via les options de ligne de commande.
Reportez-vous à page de GHC pour une explication de cette extension et d'autres.
J'essaierai de résumer ci-dessous tout ce que vous devez savoir pour comprendre quelle est la restriction du monomorphisme, pourquoi elle a été introduite et comment elle se comporte.
Prenez la définition triviale suivante:
plus = (+)
vous penseriez pouvoir remplacer chaque occurrence de +
par plus
. En particulier, depuis (+) :: Num a => a -> a -> a
, Vous vous attendez également à avoir plus :: Num a => a -> a -> a
.
Malheureusement, ce n'est pas le cas. Par exemple, dans GHCi, nous essayons ce qui suit:
Prelude> let plus = (+)
Prelude> plus 1.0 1
Nous obtenons la sortie suivante:
<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
Vous devrez peut-être :set -XMonomorphismRestriction
Dans les nouvelles versions de GHCi.
Et en fait, nous pouvons voir que le type de plus
n'est pas ce que nous attendons:
Prelude> :t plus
plus :: Integer -> Integer -> Integer
Ce qui s'est passé, c'est que le compilateur a vu que plus
avait le type Num a => a -> a -> a
, Un type polymorphe. De plus, il arrive que la définition ci-dessus relève des règles que j'expliquerai plus tard et il a donc décidé de rendre le type monomorphe par défaut la variable de type a
. La valeur par défaut est Integer
comme nous pouvons le voir.
Notez que si vous essayez de compiler le code ci-dessus en utilisant ghc
vous n'obtiendrez aucune erreur. Cela est dû à la façon dont ghci
gère (et doit gérer) les définitions interactives. Fondamentalement, chaque instruction entrée dans ghci
doit être vérifiée complètement avant de prendre en compte les éléments suivants; en d'autres termes, c'est comme si chaque instruction était dans un module séparé. Plus tard, je vais expliquer pourquoi cette question.
Considérez les définitions suivantes:
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
Nous nous attendrions à ce que toutes ces fonctions se comportent de la même manière et aient le même type, c'est-à-dire le type de show
: Show a => a -> String
.
Pourtant, lors de la compilation des définitions ci-dessus, nous obtenons les erreurs suivantes:
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
Donc f2
Et f4
Ne se compilent pas. De plus, lorsque nous essayons de définir ces fonctions dans GHCi, nous obtenons aucune erreur, mais le type pour f2
Et f4
Est () -> String
!
La restriction du monomorphisme est ce qui fait que f2
Et f4
Nécessitent un type monomorphe, et le comportement différent entre ghc
et ghci
est dû à différents défaut règles.
Dans Haskell, tel que défini par le rapport , il existe deux type distinct de liaisons . Liaisons de fonctions et liaisons de motifs. Une liaison de fonction n'est rien d'autre qu'une définition d'une fonction:
f x = x + 1
Notez que leur syntaxe est:
<identifier> arg1 arg2 ... argn = expr
Protections Modulo et déclarations where
. Mais cela n'a pas vraiment d'importance.
où il doit y avoir au moins un argument.
Une liaison de modèle est une déclaration de la forme:
<pattern> = expr
Encore une fois, les gardes modulo.
Notez que les variables sont des motifs, donc la liaison:
plus = (+)
est une liaison motif. Il lie le modèle plus
(une variable) à l'expression (+)
.
Lorsqu'une liaison de modèle se compose uniquement d'un nom de variable, elle est appelée une liaison de modèle simple.
La restriction du monomorphisme s'applique aux liaisons de motifs simples!
Eh bien, formellement, nous devrions dire que:
Un groupe de déclarations est un ensemble minimal de liaisons mutuellement dépendantes.
Section 4.5.1 du rapport .
Et puis (Section 4.5.5 du rapport ):
un groupe de déclarations donné est illimité si et seulement si:
chaque variable du groupe est liée par une liaison de fonction (par exemple
f x = x
) ou une simple liaison de modèle (par exempleplus = (+)
; Section 4.4.3.2), etune signature de type explicite est donnée pour chaque variable du groupe qui est liée par une simple liaison de modèle. (par exemple
plus :: Num a => a -> a -> a; plus = (+)
).
Exemples ajoutés par moi.
Ainsi, un groupe de déclarations restreint est un groupe où, soit il existe des liaisons de modèle non simple (par exemple (x:xs) = f something
Ou (f, g) = ((+), (-))
) ou il existe une simple liaison de modèle sans signature de type (comme dans plus = (+)
).
La restriction du monomorphisme affecte les groupes de déclaration restreints .
La plupart du temps, vous ne définissez pas de fonctions récurrentes mutuelles et donc un groupe de déclarations devient juste a contraignant.
La restriction du monomorphisme est décrite par deux règles dans la section 4.5.5 du rapport .
La restriction Hindley-Milner habituelle sur le polymorphisme est que seules les variables de type qui ne se produisent pas librement dans l'environnement peuvent être généralisées. De plus, les variables de type contraint d'un groupe de déclaration restreint peuvent ne pas être généralisées dans l'étape de généralisation pour ce groupe. (Rappelons qu'une variable de type est contrainte si elle doit appartenir à une classe de type, voir Section 4.5.2.)
La partie en surbrillance est ce que la restriction du monomorphisme introduit. Il dit que si le type est polymorphe (c'est-à-dire qu'il contient une variable de type) et cette variable de type est contrainte (c'est-à-dire qu'elle a une contrainte de classe: par exemple, le type Num a => a -> a -> a
est polymorphe car il contient a
et également contraint parce que a
a la contrainte Num
par-dessus.) alors il ne peut pas être généralisé.
En termes simples ne pas généraliser signifie que les utilisations de la fonction plus
peuvent changer de type.
Si vous aviez les définitions:
plus = (+)
x :: Integer
x = plus 1 2
y :: Double
y = plus 1.0 2
alors vous obtiendrez une erreur de type. Parce que lorsque le compilateur voit que plus
est appelé sur un Integer
dans la déclaration de x
il unifiera la variable de type a
avec Integer
et donc le type de plus
devient:
Integer -> Integer -> Integer
mais ensuite, quand il tapera check la définition de y
, il verra que plus
est appliqué à un argument Double
, et les types ne correspondent pas.
Notez que vous pouvez toujours utiliser plus
sans obtenir d'erreur:
plus = (+)
x = plus 1.0 2
Dans ce cas, le type de plus
est d'abord déduit être Num a => a -> a -> a
Mais ensuite son utilisation dans la définition de x
, où 1.0
Nécessite un Fractional
contrainte, le changera en Fractional a => a -> a -> a
.
Le rapport dit:
La règle 1 est requise pour deux raisons, toutes deux assez subtiles.
La règle 1 empêche la répétition inattendue des calculs. Par exemple,
genericLength
est une fonction standard (dans la bibliothèqueData.List
) Dont le type est donné pargenericLength :: Num a => [b] -> a
Considérez maintenant l'expression suivante:
let len = genericLength xs in (len, len)
Il semble que
len
ne doit être calculé qu'une seule fois, mais sans la règle 1, il peut être calculé deux fois, une fois à chacune des deux surcharges différentes. Si le programmeur souhaite réellement que le calcul soit répété, une signature de type explicite peut être ajoutée:let len :: Num a => a len = genericLength xs in (len, len)
Pour ce point, l'exemple du wiki est, je crois, plus clair. Considérez la fonction:
f xs = (len, len)
where
len = genericLength xs
Si len
était polymorphe, le type de f
serait:
f :: Num a, Num b => [c] -> (a, b)
Ainsi, les deux éléments du Tuple (len, len)
Pourraient en fait être des valeurs différentes! Mais cela signifie que le calcul effectué par genericLength
must doit être répété pour obtenir les deux valeurs différentes.
La justification est la suivante: le code contient un appel de fonction, mais ne pas introduire cette règle pourrait produire deux appels de fonction cachés, ce qui est contre-intuitif.
Avec la restriction du monomorphisme, le type de f
devient:
f :: Num a => [b] -> (a, a)
De cette façon, il n'est pas nécessaire d'effectuer le calcul plusieurs fois.
La règle 1 empêche toute ambiguïté. Par exemple, considérons le groupe de déclarations
[(n, s)] = lit t
Rappelons que
reads
est une fonction standard dont le type est donné par la signaturereads :: (Read a) => String -> [(a, String)]
Sans la règle 1,
n
se verrait attribuer le type∀ a. Read a ⇒ a
Ets
le type∀ a. Read a ⇒ String
. Ce dernier est un type non valide, car il est intrinsèquement ambigu. Il n'est pas possible de déterminer à quelle surcharge utilisers
, et cela ne peut pas être résolu en ajoutant une signature de type pours
. Par conséquent, lorsque des liaisons de modèle non simples sont utilisées (section 4.4.3.2), les types inférés sont toujours monomorphes dans leurs variables de type contraintes, indépendamment du fait qu'une signature de type soit fournie ou non. Dans ce cas,n
ets
sont monomorphes dansa
.
Eh bien, je crois que cet exemple va de soi. Il existe des situations où l'application de la règle entraîne une ambiguïté de type.
Si vous désactivez l'extension comme suggéré ci-dessus, vous sera obtenez une erreur de type lorsque vous essayez de compiler la déclaration ci-dessus. Cependant, ce n'est pas vraiment un problème: vous savez déjà que lorsque vous utilisez read
, vous devez en quelque sorte dire au compilateur quel type il doit essayer d'analyser ...
- Toutes les variables de type monomorphes qui restent lorsque l'inférence de type pour un module entier est terminée, sont considérées comme ambiguës et sont résolues en types particuliers en utilisant les règles par défaut (Section 4.3.4).
Cela signifie que. Si vous avez votre définition habituelle:
plus = (+)
Cela aura un type Num a => a -> a -> a
Où a
est une variable de type monomorphic en raison de la règle 1 décrite ci-dessus. Une fois le module entier déduit, le compilateur choisira simplement un type qui remplacera ce a
selon les règles par défaut.
Le résultat final est: plus :: Integer -> Integer -> Integer
.
Notez que cela se fait après tout le module est déduit.
Cela signifie que si vous avez les déclarations suivantes:
plus = (+)
x = plus 1.0 2.0
à l'intérieur d'un module, avant type par défaut le type de plus
sera: Fractional a => a -> a -> a
(voir la règle 1 pour savoir pourquoi cela se produit). À ce stade, suivant les règles par défaut, a
sera remplacé par Double
et nous aurons donc plus :: Double -> Double -> Double
Et x :: Double
.
Comme indiqué précédemment, il existe des règles par défaut, décrites dans Section 4.3.4 du rapport , que l'inférenceur peut adopter et qui remplaceront un type polymorphe par un type monomorphe une. Cela se produit chaque fois qu'un type est ambigu.
Par exemple dans l'expression:
let x = read "<something>" in show x
ici l'expression est ambiguë car les types de show
et read
sont:
show :: Show a => a -> String
read :: Read a => String -> a
Ainsi, le x
a le type Read a => a
. Mais cette contrainte est satisfaite par de nombreux types: Int
, Double
ou ()
Par exemple. Lequel choisir? Il n'y a rien qui puisse nous dire.
Dans ce cas, nous pouvons résoudre l'ambiguïté en indiquant au compilateur le type que nous voulons, en ajoutant une signature de type:
let x = read "<something>" :: Int in show x
Maintenant, le problème est: puisque Haskell utilise la classe de type Num
pour gérer les nombres, il y a beaucoup de cas où les expressions numériques contiennent des ambiguïtés.
Considérer:
show 1
Quel devrait être le résultat?
Comme auparavant, 1
A le type Num a => a
Et il existe de nombreux types de nombres qui peuvent être utilisés. Lequel choisir?
Avoir une erreur de compilation presque chaque fois que nous utilisons un nombre n'est pas une bonne chose, et donc les règles par défaut ont été introduites. Les règles peuvent être contrôlées à l'aide d'une déclaration default
. En spécifiant default (T1, T2, T3)
, nous pouvons changer la façon dont l'inférenceur utilise par défaut les différents types.
Une variable de type ambigu v
est par défaut si:
v
n'apparaît que dans les contraintes du type C v
où C
est une classe (c'est-à-dire si elle apparaît comme dans: Monad (m v)
alors c'est pas par défaut).Num
ou une sous-classe de Num
.Une variable de type par défaut est remplacée par le type en premier dans la liste default
qui est une instance de toutes les classes de variables ambiguës.
La déclaration default
par défaut est default (Integer, Double)
.
Par exemple:
plus = (+)
minus = (-)
x = plus 1.0 1
y = minus 2 1
Les types déduits seraient:
plus :: Fractional a => a -> a -> a
minus :: Num a => a -> a -> a
qui, par défaut, deviennent:
plus :: Double -> Double -> Double
minus :: Integer -> Integer -> Integer
Notez que cela explique pourquoi dans l'exemple de la question, seule la définition sort
soulève une erreur. Le type Ord a => [a] -> [a]
Ne peut pas être défini par défaut car Ord
n'est pas une classe numérique.
Notez que GHCi est livré avec étendu règles par défaut (ou ici pour GHC8 ), qui peut également être activé dans les fichiers en utilisant le ExtendedDefaultRules
extensions.
Les variables de type par défaut ne doivent pas seulement apparaître dans les contraintes où toutes les classes sont standard et il doit y avoir au moins une classe parmi Eq
, Ord
, Show
ou Num
et ses sous-classes.
De plus, la déclaration default
par défaut est default ((), Integer, Double)
.
Cela peut produire des résultats étranges. Prenant l'exemple de la question:
Prelude> :set -XMonomorphismRestriction
Prelude> import Data.List(sortBy)
Prelude Data.List> let sort = sortBy compare
Prelude Data.List> :t sort
sort :: [()] -> [()]
dans ghci, nous n'obtenons pas d'erreur de type mais les contraintes Ord a
entraînent une valeur par défaut de ()
, ce qui est pratiquement inutile.
Il y a beaucoup de ressources et de discussions sur la restriction du monomorphisme.
Voici quelques liens que je trouve utiles et qui peuvent vous aider à comprendre ou approfondir le sujet: