Wann ist -XAllowAmbiguousTypes angemessen?

212

Ich habe kürzlich eine Frage zu syntactic-2.0 bezüglich der Definition von gestellt share. Ich habe dies in GHC 7.6 arbeiten lassen :

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

GHC 7.8 möchte -XAllowAmbiguousTypesjedoch mit dieser Signatur kompilieren. Alternativ kann ich die ersetzen fimit

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

Welches ist der Typ, der vom Fundep auf impliziert wird SyntacticN. Dadurch kann ich die Erweiterung vermeiden. Das ist natürlich so

  • Ein sehr langer Typ, der zu einer bereits großen Signatur hinzugefügt werden kann
  • lästig manuell abzuleiten
  • unnötig wegen der fundep

Meine Fragen sind:

  1. Ist dies eine akzeptable Verwendung von -XAllowAmbiguousTypes?
  2. Wann sollte diese Erweiterung im Allgemeinen verwendet werden? Eine Antwort hier schlägt vor, "es ist fast nie eine gute Idee".
  3. Obwohl ich die Dokumente gelesen habe , habe ich immer noch Probleme zu entscheiden, ob eine Einschränkung nicht eindeutig ist oder nicht. Betrachten Sie insbesondere diese Funktion aus Data.Syntactic.Sugar:

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

    Es scheint mir, dass fi(und möglicherweise sup) hier mehrdeutig sein sollte, aber es kompiliert ohne die Erweiterung. Warum ist sugarSymeindeutig, während shareist? Da sharees sich um eine Anwendung von handelt sugarSym, sharekommen alle Einschränkungen direkt von sugarSym.

Crockeea
quelle
4
Gibt es einen Grund, warum Sie nicht einfach den abgeleiteten Typ für verwenden können sugarSym Let, bei dem es (SyntacticN f (ASTF sup a -> ASTF sup (a -> b) -> ASTF sup b), Let :<: sup) => fsich um mehrdeutige Typvariablen handelt und nicht?
Kosmikus
3
@kosmikus Sorrt es hat so lange gedauert zu antworten. Dieser Code nicht mit der abgeleiteten Signatur kompilieren für share, aber tut der Kompilierung , wenn eine der Signaturen in der Frage erwähnt verwendet wird.
Ihre
3
Undefiniertes Verhalten ist wahrscheinlich nicht der passendste Begriff. Es ist schwer zu verstehen, nur basierend auf einem Programm. Das Problem ist die Entscheidbarkeit und GHCI kann die Typen in Ihrem Programm nicht beweisen. Es gibt eine lange Diskussion, die Sie nur zu diesem Thema interessieren könnte. haskell.org/pipermail/haskell-cafe/2008-April/041397.html
BlamKiwi
6
Was (3) betrifft, so ist dieser Typ aufgrund der funktionalen Abhängigkeiten in der Definition von SyntacticN (dh f - »fi) und ApplySym (insbesondere fi -> sig, sup) nicht mehrdeutig. Daraus fergibt sich, dass allein ausreicht, um vollständig zu sigunterscheiden fi, und sup.
user2141650
3
@ user2141650 Die Antwort hat leider so lange gedauert. Sie sagen, der Fundep on SyntacticNmacht in fieindeutig sugarSym, aber warum gilt das Gleiche nicht für fiin share?
Crockeea

Antworten:

12

Ich sehe keine veröffentlichte Version der Syntaktik, deren Signatur sugarSymgenau diese Typnamen verwendet. Daher verwende ich den Entwicklungszweig bei Commit 8cfd02 ^ , der letzten Version, in der diese Namen noch verwendet wurden.

Warum beschwert sich GHC über die fiSignatur in Ihrem Typ, aber nicht über die für sugarSym? In der Dokumentation, auf die Sie verlinkt haben, wird erläutert, dass ein Typ nicht eindeutig ist, wenn er nicht rechts von der Einschränkung angezeigt wird, es sei denn, die Einschränkung verwendet funktionale Abhängigkeiten, um den ansonsten nicht mehrdeutigen Typ von anderen nicht mehrdeutigen Typen abzuleiten. Vergleichen wir also die Kontexte der beiden Funktionen und suchen nach funktionalen Abhängigkeiten.

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

Also für sugarSymdie nicht-mehrdeutig Typen sind sub, sigund f, und von denen , sollten wir in der Lage sein, funktionale Abhängigkeiten zu folgen , um alle anderen Arten im Kontext verwendet eindeutig zu machen, nämlich supund fi. Und tatsächlich verwendet die f -> internalfunktionale Abhängigkeit in SyntacticNunsere f, um unsere zu disambiguieren fi, und danach die f -> sig symfunktionale Abhängigkeit in ApplySymverwendet unsere neu eindeutige, um fizu disambiguieren sup(und sig, was bereits nicht mehrdeutig war). Das erklärt also, warum sugarSymdie AllowAmbiguousTypesErweiterung nicht erforderlich ist .

Schauen wir uns jetzt an sugar. Das erste, was mir auffällt, ist, dass sich der Compiler nicht über einen mehrdeutigen Typ beschwert, sondern über überlappende Instanzen:

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

Wenn ich das richtig lese, ist es nicht so, dass GHC denkt, dass Ihre Typen mehrdeutig sind, sondern dass GHC bei der Überprüfung, ob Ihre Typen mehrdeutig sind, auf ein anderes, separates Problem gestoßen ist. Es sagt Ihnen dann, dass, wenn Sie GHC angewiesen hätten, die Mehrdeutigkeitsprüfung nicht durchzuführen, dieses separate Problem nicht aufgetreten wäre. Dies erklärt, warum das Aktivieren von AllowAmbiguousTypes das Kompilieren Ihres Codes ermöglicht.

Das Problem mit den überlappenden Instanzen bleibt jedoch bestehen. Die beiden von GHC ( SyntacticN f fiund SyntacticN (a -> f) ...) aufgelisteten Instanzen überschneiden sich. Seltsamerweise scheint sich die erste davon mit jeder anderen Instanz zu überschneiden, was verdächtig ist. Und was heißt [overlap ok]das?

Ich vermute, dass Syntactic mit OverlappingInstances kompiliert wurde. Und wenn man sich den Code ansieht , tut es das tatsächlich.

Wenn man ein bisschen experimentiert, scheint es, dass GHC mit überlappenden Instanzen einverstanden ist, wenn klar ist, dass eine streng allgemeiner ist als die andere:

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

Aber GHC ist nicht in Ordnung mit überlappenden Fällen, in denen keiner eindeutig besser passt als der andere:

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

Ihre Typensignatur wird verwendet SyntacticN (a -> (a -> b) -> b) fiund passt weder SyntacticN f finoch SyntacticN (a -> f) (AST sym (Full ia) -> fi)besser als die andere. Wenn ich diesen Teil Ihrer Typensignatur in SyntacticN a fioder ändere, SyntacticN (a -> (a -> b) -> b) (AST sym (Full ia) -> fi)beschwert sich GHC nicht mehr über die Überlappung.

Wenn ich Sie wäre, würde ich mir die Definition dieser beiden möglichen Instanzen ansehen und feststellen, ob eine dieser beiden Implementierungen die gewünschte ist.

Gelisam
quelle
2

Ich habe festgestellt, dass dies AllowAmbiguousTypessehr praktisch ist TypeApplications. Betrachten Sie die Funktion natVal :: forall n proxy . KnownNat n => proxy n -> Integervon GHC.TypeLits .

Um diese Funktion zu nutzen, könnte ich schreiben natVal (Proxy::Proxy5). Ein alternativer Stil ist TypeApplications:natVal @5 Proxy . Der Typ von Proxywird von der Typanwendung abgeleitet, und es ist ärgerlich, ihn jedes Mal schreiben zu müssen, wenn Sie anrufen natVal. So können wir aktivieren AmbiguousTypesund schreiben:

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

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

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

Beachten Sie jedoch, dass Sie nicht mehr zurückkehren können , wenn Sie einmal mehrdeutig sind !

Crockeea
quelle