Quando -XAllowAmbiguousTypes è appropriato?


212

Di recente ho pubblicato una domanda su syntactic-2.0 per quanto riguarda la definizione di share. Ho funzionato in 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

Tuttavia, GHC 7.8 vuole -XAllowAmbiguousTypescompilare con quella firma. In alternativa, posso sostituire il ficon

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

che è il tipo implicito dal fondo SyntacticN. Questo mi permette di evitare l'estensione. Certo che lo è

  • un tipo molto lungo da aggiungere a una firma già di grandi dimensioni
  • noioso da derivare manualmente
  • inutile a causa del fondo

Le mie domande sono:

  1. È un uso accettabile di -XAllowAmbiguousTypes?
  2. In generale, quando usare questa estensione? Una risposta qui suggerisce "non è quasi mai una buona idea".
  3. Anche se ho letto i documenti , ho ancora problemi a decidere se un vincolo è ambiguo o meno. In particolare, considerare questa funzione da Data.Syntactic.Sugar:

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

    Mi sembra che fi(e possibilmente sup) dovrebbe essere ambiguo qui, ma si compila senza l'estensione. Perché è sugarSyminequivocabile mentre lo shareè? Poiché shareè un'applicazione di sugarSym, i sharevincoli provengono tutti direttamente sugarSym.


4
C'è qualche motivo per cui non puoi semplicemente usare il tipo inferito per sugarSym Let, che è (SyntacticN f (ASTF sup a -> ASTF sup (a -> b) -> ASTF sup b), Let :<: sup) => fe non coinvolge variabili di tipo ambigue?
kosmikus,

3
@kosmikus Sorrt ci è voluto così tanto tempo per rispondere. Questo codice non viene compilato con la firma inferita per share, ma viene compilato quando viene utilizzata una delle firme menzionate nella domanda. La tua domanda è stata posta anche nei commenti di un precedente post
crockeea,

3
Il comportamento indefinito probabilmente non è il termine più appropriato. È difficile da capire basandosi solo su un programma. Il problema è la decabilità e GHCI non è in grado di provare i tipi nel tuo programma. C'è una lunga discussione che potrebbe interessarti solo su questo argomento. haskell.org/pipermail/haskell-cafe/2008-April/041397.html
BlamKiwi

6
Per quanto riguarda (3), quel tipo non è ambiguo a causa delle dipendenze funzionali nella definizione di SyntacticN (cioè, f - »fi) e ApplySym (in particolare, fi -> sig, sup). Da questo, si ottiene che fda solo è sufficiente a completamente disambiguate sig, fie sup.
user2141650

3
@ user2141650 Mi dispiace aver impiegato tanto tempo per rispondere. Stai dicendo che il fundep su SyntacticNrende fiinequivocabile in sugarSym, ma allora perché lo stesso non è vero per fiin share?
crockeea,

Risposte:


12

Non vedo alcuna versione pubblicata di sintattica la cui firma per sugarSymutilizza quei nomi esatti di tipi, quindi userò il ramo di sviluppo su commit 8cfd02 ^ , l'ultima versione che ancora utilizzava quei nomi.

Quindi, perché GHC si lamenta della fifirma del tuo tipo ma non di quella sugarSym? La documentazione a cui hai collegato spiega che un tipo è ambiguo se non appare a destra del vincolo, a meno che il vincolo non stia usando dipendenze funzionali per inferire il tipo altrimenti ambiguo da altri tipi non ambigui. Quindi confrontiamo i contesti delle due funzioni e cerchiamo dipendenze funzionali.

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

Quindi sugarSym, i tipi non ambigui sono sub, sige f, e da quelli che dovremmo essere in grado di seguire le dipendenze funzionali al fine di chiarire tutti gli altri tipi utilizzati nel contesto, vale a dire supe fi. E in effetti, la f -> internaldipendenza funzionale in SyntacticNusa il nostro fper chiarire le nostre fi, e in seguito la f -> sig symdipendenza funzionale in ApplySymusa il nostro appena chiarito fiper chiarire sup(e sig, che era già non ambiguo). Questo spiega perché sugarSymnon richiede l' AllowAmbiguousTypesestensione.

Vediamo ora sugar. La prima cosa che noto è che il compilatore non si lamenta di un tipo ambiguo, ma piuttosto di istanze sovrapposte:

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

Quindi, se sto leggendo bene, non è che GHC pensi che i tuoi tipi siano ambigui, ma piuttosto, mentre controlla se i tuoi tipi sono ambigui, GHC ha riscontrato un problema diverso e separato. Ti sta quindi dicendo che se avessi detto a GHC di non eseguire il controllo dell'ambiguità, non avrebbe riscontrato quel problema separato. Questo spiega perché abilitando AllowAmbiguousTypes è possibile compilare il codice.

Tuttavia, il problema con le istanze sovrapposte rimane. Le due istanze elencate da GHC ( SyntacticN f fie SyntacticN (a -> f) ...) si sovrappongono. Stranamente, sembra che il primo di questi dovrebbe sovrapporsi a qualsiasi altra istanza, il che è sospetto. E cosa [overlap ok]significa?

Sospetto che Syntactic sia compilato con OverlappingInstances. E guardando il codice , in effetti lo fa.

Sperimentando un po ', sembra che GHC stia bene con casi sovrapposti quando è chiaro che uno è strettamente più generale dell'altro:

{-# 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])

Ma GHC non va bene con casi sovrapposti in cui nessuno dei due è chiaramente migliore degli altri:

{-# 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])

La tua firma di tipo utilizza SyntacticN (a -> (a -> b) -> b) fie né SyntacticN f fiSyntacticN (a -> f) (AST sym (Full ia) -> fi)si adatta meglio dell'altra. Se cambio quella parte della firma del tuo tipo in SyntacticN a fio SyntacticN (a -> (a -> b) -> b) (AST sym (Full ia) -> fi), GHC non si lamenta più della sovrapposizione.

Se fossi in te, esaminerei la definizione di quei due possibili casi e determinerei se una di quelle due implementazioni è quella che desideri.


2

Ho scoperto che AllowAmbiguousTypesè molto comodo da usare con TypeApplications. Considera la funzione natVal :: forall n proxy . KnownNat n => proxy n -> Integerdi GHC.TypeLits .

Per usare questa funzione, potrei scrivere natVal (Proxy::Proxy5). Uno stile alternativo è quello di utilizzare TypeApplications: natVal @5 Proxy. Il tipo di Proxyviene dedotto dall'applicazione tipo ed è fastidioso doverlo scrivere ogni volta che chiami natVal. Quindi possiamo abilitare AmbiguousTypese scrivere:

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

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

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

Tuttavia, nota che una volta che diventi ambiguo, non puoi tornare indietro !

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.