web-dev-qa-db-fra.com

Quand est -XAllowAmbiguousTypes approprié?

J'ai récemment publié une question sur syntactic-2. 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 -XAllowAmbiguousTypes pour compiler avec cette signature. Alternativement, je peux remplacer le fi par

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

qui est le type impliqué par le fundep sur 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 la documentation , 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 éventuellement sup) devrait être ambigu ici, mais il compile sans l'extension. Pourquoi sugarSym est-il sans ambiguïté alors que share l'est? Puisque share est une application de sugarSym, les contraintes share viennent toutes directement de sugarSym.

211
crockeea

Je ne vois aucune version publiée de syntaxique dont la signature pour sugarSym utilise ces noms de type exacts, donc je vais utiliser la branche de développement à commit 8cfd02 ^ , la dernière version qui encore utilisé ces noms.

Alors, pourquoi GHC se plaint-il du fi dans votre signature de type mais pas celui de 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, sig et f, et parmi 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 sup et fi. Et en effet, la dépendance fonctionnelle f -> internal Dans SyntacticN utilise notre f pour lever l'ambiguïté de notre fi, puis la dépendance fonctionnelle f -> sig sym Dans ApplySym utilise notre nouvellement désambiguïsé fi pour lever l'ambiguïté sup (et sig, qui n'était déjà pas ambigu). Cela explique pourquoi sugarSym ne nécessite pas l'extension AllowAmbiguousTypes.

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 chevauchements 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 fi Et 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 que signifie [overlap ok]?

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 SyntacticN f fi Ni SyntacticN (a -> f) (AST sym (Full ia) -> fi) ne conviennent mieux que les autres. Si je change cette partie de votre signature de type en SyntacticN a fi Ou 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.

12
gelisam

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

Pour utiliser cette fonction, j'ai pu écrire natVal (Proxy::Proxy5). Un autre style consiste à utiliser TypeApplications: natVal @5 Proxy. Le type de Proxy est 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 AmbiguousTypes et é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 !

2
crockeea