web-dev-qa-db-fra.com

Quelle est la restriction du monomorphisme?

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]’
  • Pourquoi 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 ?
  • Pourquoi 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
  • Pourquoi ghc n'est-il pas en mesure d'utiliser le type polymorphe Ord a => [a] -> [a] pour sort?
  • Et pourquoi 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 .

67
Bakuriu

Quelle est la restriction du monomorphisme?

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.

Comment je le répare?

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

Désactiver

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.

Une explication complète

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.

Un exemple

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.

Un autre exemple

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.

Quand est-ce que cela arrive?

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:

  1. 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 exemple plus = (+); Section 4.4.3.2), et

  2. une 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.

Qu'est ce que ça fait?

La restriction du monomorphisme est décrite par deux règles dans la section 4.5.5 du rapport .

Première règle

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.

Raisonnement

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èque Data.List) Dont le type est donné par

    genericLength :: 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 signature

    reads :: (Read a) => String -> [(a, String)]

    Sans la règle 1, n se verrait attribuer le type ∀ a. Read a ⇒ a Et s 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 utiliser s, et cela ne peut pas être résolu en ajoutant une signature de type pour s. 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 et s sont monomorphes dans a.

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 ...

Deuxième règle

  1. 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 -> aa 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.

Défaut

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 vC est une classe (c'est-à-dire si elle apparaît comme dans: Monad (m v) alors c'est pas par défaut).
  • au moins une de ces classes est Num ou une sous-classe de Num.
  • toutes ces classes sont définies dans le Prelude ou une bibliothèque standard.

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.

Défaut étendu

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.

Liens utiles

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:

83
Bakuriu