Quand -XAllowAmbiguousTypes est-il approprié?

212

J'ai récemment posté une question sur syntactic-2.0 concernant la définition de share. J'ai eu ce travail dans GHC 7.6 :

{-# LANGUAGE GADTs, TypeOperators, FlexibleContexts #-}

import Data.Syntactic
import Data.Syntactic.Sugar.BindingT

data Let a where
    Let :: Let (a :-> (a -> b) :-> Full b)

share :: (Let :<: sup,
          sup ~ Domain b, sup ~ Domain a,
          Syntactic a, Syntactic b,
          Syntactic (a -> b),
          SyntacticN (a -> (a -> b) -> b) 
                     fi)
           => a -> (a -> b) -> b
share = sugarSym Let

Cependant, GHC 7.8 veut -XAllowAmbiguousTypescompiler avec cette signature. Alternativement, je peux remplacer le fipar

(ASTF sup (Internal a) -> AST sup ((Internal a) :-> Full (Internal b)) -> ASTF sup (Internal b))

qui est le type impliqué par le fundep on SyntacticN. Cela me permet d'éviter l'extension. Bien sûr, c'est

  • un type très long à ajouter à une signature déjà volumineuse
  • fastidieux à dériver manuellement
  • inutile en raison du fundep

Mes questions sont:

  1. Est-ce une utilisation acceptable de -XAllowAmbiguousTypes?
  2. En général, quand utiliser cette extension? Une réponse ici suggère "ce n'est presque jamais une bonne idée".
  3. Bien que j'aie lu les documents , j'ai toujours du mal à décider si une contrainte est ambiguë ou non. En particulier, considérez cette fonction de Data.Syntactic.Sugar:

    sugarSym :: (sub :<: AST sup, ApplySym sig fi sup, SyntacticN f fi) 
             => sub sig -> f
    sugarSym = sugarN . appSym
    

    Il me semble que fi(et peut-être sup) devrait être ambigu ici, mais il compile sans l'extension. Pourquoi est-il sugarSymsans ambiguïté share? Puisque sharec'est une application de sugarSym, les sharecontraintes viennent toutes directement sugarSym.

crockeea
la source
4
Y a-t-il une raison pour laquelle vous ne pouvez pas simplement utiliser le type déduit pour sugarSym Let, qui est (SyntacticN f (ASTF sup a -> ASTF sup (a -> b) -> ASTF sup b), Let :<: sup) => fet n'implique pas de variables de type ambiguë?
kosmikus
3
@kosmikus Sorrt il a fallu si longtemps pour répondre. Ce code ne compile pas avec la signature inférée pour share, mais ne compile lorsque l'une des signatures mentionnées dans la question est utilisée. Votre question a également été posée dans les commentaires d'un post précédent
crockeea
3
Un comportement indéfini n'est probablement pas le terme le plus approprié. Il est difficile de comprendre sur la base d'un seul programme. Le problème est la fiabilité et GHCI ne pouvant pas prouver les types de votre programme. Il y a une longue discussion qui pourrait vous intéresser uniquement sur ce sujet. haskell.org/pipermail/haskell-cafe/2008-April/041397.html
BlamKiwi
6
Quant à (3), ce type n'est pas ambigu en raison des dépendances fonctionnelles dans la définition de SyntacticN (c'est-à-dire, f - »fi) et ApplySym (en particulier, fi -> sig, sup). De là, vous obtenez que fseul est suffisant pour pleinement désambiguïser sig, fiet sup.
user2141650
3
@ user2141650 Désolé, il a fallu si longtemps pour répondre. Vous dites que le fundep on SyntacticNrend fisans ambiguïté en sugarSym, mais alors pourquoi est-ce pas la même chose pour fien share?
crockeea

Réponses:

12

Je ne vois aucune version publiée de syntaxique dont la signature sugarSymutilise ces noms de type exacts, donc j'utiliserai la branche de développement à commit 8cfd02 ^ , la dernière version qui utilisait toujours ces noms.

Alors, pourquoi GHC se plaint-il de la fisignature de votre type mais pas de celle pour sugarSym? La documentation à laquelle vous avez lié explique qu'un type est ambigu s'il n'apparaît pas à droite de la contrainte, sauf si la contrainte utilise des dépendances fonctionnelles pour déduire le type par ailleurs ambigu à partir d'autres types non ambigus. Comparons donc les contextes des deux fonctions et recherchons les dépendances fonctionnelles.

class ApplySym sig f sym | sig sym -> f, f -> sig sym
class SyntacticN f internal | f -> internal

sugarSym :: ( sub :<: AST sup
            , ApplySym sig fi sup
            , SyntacticN f fi
            ) 
         => sub sig -> f

share :: ( Let :<: sup
         , sup ~ Domain b
         , sup ~ Domain a
         , Syntactic a
         , Syntactic b
         , Syntactic (a -> b)
         , SyntacticN (a -> (a -> b) -> b) fi
         )
      => a -> (a -> b) -> b

Donc pour sugarSym, les types non ambigus sont sub, siget f, à partir de ceux-ci, nous devrions pouvoir suivre les dépendances fonctionnelles afin de lever l'ambiguïté de tous les autres types utilisés dans le contexte, à savoir supet fi. Et en effet, la f -> internaldépendance fonctionnelle dans SyntacticNutilise notre fpour lever l'ambiguïté fi, et par la suite la f -> sig symdépendance fonctionnelle dans ApplySymutilise notre nouvellement désambiguïsé fipour lever l' ambiguïté sup(et sig, ce qui était déjà non ambigu). Cela explique pourquoi sugarSymne nécessite pas l' AllowAmbiguousTypesextension.

Regardons maintenant sugar. La première chose que je remarque est que le compilateur ne se plaint pas d'un type ambigu, mais plutôt de superpositions d'instances:

Overlapping instances for SyntacticN b fi
  arising from the ambiguity check for share
Matching givens (or their superclasses):
  (SyntacticN (a -> (a -> b) -> b) fi1)
Matching instances:
  instance [overlap ok] (Syntactic f, Domain f ~ sym,
                         fi ~ AST sym (Full (Internal f))) =>
                        SyntacticN f fi
    -- Defined in ‘Data.Syntactic.Sugar’
  instance [overlap ok] (Syntactic a, Domain a ~ sym,
                         ia ~ Internal a, SyntacticN f fi) =>
                        SyntacticN (a -> f) (AST sym (Full ia) -> fi)
    -- Defined in ‘Data.Syntactic.Sugar’
(The choice depends on the instantiation of b, fi’)
To defer the ambiguity check to use sites, enable AllowAmbiguousTypes

Donc, si je lis bien, ce n'est pas que GHC pense que vos types sont ambigus, mais plutôt, qu'en vérifiant si vos types sont ambigus, GHC a rencontré un problème différent et distinct. Il vous indique ensuite que si vous aviez demandé à GHC de ne pas effectuer la vérification d'ambiguïté, il n'aurait pas rencontré ce problème distinct. Cela explique pourquoi l'activation de AllowAmbiguousTypes permet à votre code de se compiler.

Cependant, le problème avec les instances qui se chevauchent reste. Les deux instances répertoriées par GHC ( SyntacticN f fiet SyntacticN (a -> f) ...) se chevauchent. Curieusement, il semble que le premier d'entre eux devrait se chevaucher avec toute autre instance, ce qui est suspect. Et qu'est-ce que cela [overlap ok]signifie?

Je soupçonne que Syntactic est compilé avec OverlappingInstances. Et en regardant le code , c'est le cas.

En expérimentant un peu, il semble que GHC accepte les instances qui se chevauchent lorsqu'il est clair que l'une est strictement plus générale que l'autre:

{-# LANGUAGE FlexibleInstances, OverlappingInstances #-}

class Foo a where
  whichOne :: a -> String

instance Foo a where
  whichOne _ = "a"

instance Foo [a] where
  whichOne _ = "[a]"

-- |
-- >>> main
-- [a]
main :: IO ()
main = putStrLn $ whichOne (undefined :: [Int])

Mais GHC n'est pas d'accord avec des instances qui se chevauchent lorsque ni l'une ni l'autre n'est clairement mieux adaptée que l'autre:

{-# LANGUAGE FlexibleInstances, OverlappingInstances #-}

class Foo a where
  whichOne :: a -> String

instance Foo (f Int) where  -- this is the line which changed
  whichOne _ = "f Int"

instance Foo [a] where
  whichOne _ = "[a]"

-- |
-- >>> main
-- Error: Overlapping instances for Foo [Int]
main :: IO ()
main = putStrLn $ whichOne (undefined :: [Int])

Votre signature de type utilise SyntacticN (a -> (a -> b) -> b) fi, et ni l'un SyntacticN f fini l' autre ne SyntacticN (a -> f) (AST sym (Full ia) -> fi)convient mieux que l'autre. Si je change cette partie de votre signature de type en SyntacticN a fiou SyntacticN (a -> (a -> b) -> b) (AST sym (Full ia) -> fi), GHC ne se plaint plus du chevauchement.

Si j'étais vous, je regarderais la définition de ces deux instances possibles et déterminer si l'une de ces deux implémentations est celle que vous voulez.

gelisam
la source
2

J'ai découvert que AllowAmbiguousTypesc'est très pratique à utiliser avec TypeApplications. Considérez la fonction natVal :: forall n proxy . KnownNat n => proxy n -> Integerde GHC.TypeLits .

Pour utiliser cette fonction, je pouvais écrire natVal (Proxy::Proxy5). Un autre style est d'utiliser TypeApplications: natVal @5 Proxy. Le type de Proxyest déduit par l'application de type, et c'est ennuyeux de devoir l'écrire à chaque fois que vous appelez natVal. Ainsi, nous pouvons activer AmbiguousTypeset écrire:

{-# Language AllowAmbiguousTypes, ScopedTypeVariables, TypeApplications #-}

ambiguousNatVal :: forall n . (KnownNat n) => Integer
ambiguousNatVal = natVal @n Proxy

five = ambiguousNatVal @5 -- no `Proxy ` needed!

Cependant, notez qu'une fois que vous devenez ambigu, vous ne pouvez pas revenir en arrière !

crockeea
la source