2016-08-17 1 views
15

Ich versuche, einen einfachen Interpreter von einem Transformer-basierten Monad-Stack auf freiere Effekte umzuschreiben, aber ich stehe vor Schwierigkeiten, meine Absicht dem GHC-Typ-System mitzuteilen.Wie komponiere ich 'freiere' Effekte in Haskell?

Ich verwende derzeit nur die State und Fresh Effekte. Ich bin mit zwei Zuständen und meine Wirkung Läufer sieht wie folgt aus:

runErlish g ls = run . runGlobal g . runGensym 0 . runLexicals ls 
    where runGlobal = flip runState 
     runGensym = flip runFresh' 
     runLexicals = flip runState 

Hinzu kommt, habe ich eine Funktion FindMacro mit dieser Art definiert:

findMacro :: Members [State (Global v w), State [Scope v w]] r 
      => Arr r Text (Maybe (Macro (Term v w) v w)) 

All dies bisher funktioniert perfekt fein. Das Problem kommt, wenn ich versuche, macroexpand2 zu schreiben (na ja, macroexpand1, aber ich bin vereinfacht es so ist die Frage leichter folgen):

macroexpand2 s = 
    do m <- findMacro s 
    return $ case m of 
     Just j -> True 
     Nothing -> False 

Dies erzeugt den folgenden Fehler:

Could not deduce (Data.Open.Union.Member' 
        (State [Scope v0 w0]) 
        r 
        (Data.Open.Union.FindElem (State [Scope v0 w0]) r)) 
from the context (Data.Open.Union.Member' 
        (State [Scope v w]) 
        r 
        (Data.Open.Union.FindElem (State [Scope v w]) r), 
        Data.Open.Union.Member' 
        (State (Global v w)) 
        r 
        (Data.Open.Union.FindElem (State (Global v w)) r)) 
    bound by the inferred type for `macroexpand2': 
      (Data.Open.Union.Member' 
       (State [Scope v w]) 
       r 
       (Data.Open.Union.FindElem (State [Scope v w]) r), 
       Data.Open.Union.Member' 
       (State (Global v w)) 
       r 
       (Data.Open.Union.FindElem (State (Global v w)) r)) => 
      Text -> Eff r Bool 
    at /tmp/flycheck408QZt/Erlish.hs:(79,1)-(83,23) 
The type variables `v0', `w0' are ambiguous 
When checking that `macroexpand2' has the inferred type 
    macroexpand2 :: forall (r :: [* -> *]) v (w :: [* -> *]). 
        (Data.Open.Union.Member' 
        (State [Scope v w]) 
        r 
        (Data.Open.Union.FindElem (State [Scope v w]) r), 
        Data.Open.Union.Member' 
        (State (Global v w)) 
        r 
        (Data.Open.Union.FindElem (State (Global v w)) r)) => 
        Text -> Eff r Bool 
Probable cause: the inferred type is ambiguous 

Ok , kann ich eine Members Anmerkung der Art hinzufügen:

macroexpand2 :: Members [State (Global v w), State [Scope v w]] r 
       => Text -> Eff r Bool 

Und jetzt bekomme ich diese:

Overlapping instances for Member (State [Scope v0 w0]) r 
    arising from a use of `findMacro' 
Matching instances: 
    instance Data.Open.Union.Member' 
      t r (Data.Open.Union.FindElem t r) => 
      Member t r 
    -- Defined in `Data.Open.Union' 
There exists a (perhaps superclass) match: 
    from the context (Members 
         '[State (Global v w), State [Scope v w]] r) 
    bound by the type signature for 
       macroexpand2 :: Members 
           '[State (Global v w), State [Scope v w]] r => 
           Text -> Eff r Bool 
    at /tmp/flycheck408QnV/Erlish.hs:(79,17)-(80,37) 
(The choice depends on the instantiation of `r, v0, w0' 
To pick the first instance above, use IncoherentInstances 
when compiling the other instance declarations) 
In a stmt of a 'do' block: m <- findMacro s 
In the expression: 
    do { m <- findMacro s; 
     return 
     $ case m of { 
      Just j -> True 
      Nothing -> False } } 
In an equation for `macroexpand2': 
    macroexpand2 s 
     = do { m <- findMacro s; 
      return 
      $ case m of { 
       Just j -> True 
      Nothing -> False } } 

Ich wurde auf IRC aufgefordert, forall r v w. zu versuchen, was keinen Unterschied machte. Aus Neugierde versuchte ich IncoherentInstances beim kompilieren dieser Code (ich wollte nicht eine Gabel von freier und spielen) um zu sehen, ob es vielleicht einen Hinweis geben würde, was los war. Es hat nicht:

Could not deduce (Data.Open.Union.Member' 
        (State [Scope v0 w0]) 
        r 
        (Data.Open.Union.FindElem (State [Scope v0 w0]) r)) 
    arising from a use of `findMacro' 
from the context (Members 
        '[State (Global v w), State [Scope v w]] r) 
    bound by the type signature for 
      macroexpand2 :: Members 
           '[State (Global v w), State [Scope v w]] r => 
          Text -> Eff r Bool 
    at /tmp/flycheck408eru/Erlish.hs:(79,17)-(80,37) 
The type variables `v0', `w0' are ambiguous 
Relevant bindings include 
    macroexpand2 :: Text -> Eff r Bool 
    (bound at /tmp/flycheck408eru/Erlish.hs:81:1) 
Note: there are several potential instances: 
    instance (r ~ (t' : r'), Data.Open.Union.Member' t r' n) => 
      Data.Open.Union.Member' t r ('Data.Open.Union.S n) 
    -- Defined in `Data.Open.Union' 
    instance (r ~ (t : r')) => 
      Data.Open.Union.Member' t r 'Data.Open.Union.Z 
    -- Defined in `Data.Open.Union' 
In a stmt of a 'do' block: m <- findMacro s 
In the expression: 
    do { m <- findMacro s; 
     return 
     $ case m of { 
      Just j -> True 
      Nothing -> False } } 
In an equation for `macroexpand2': 
    macroexpand2 s 
     = do { m <- findMacro s; 
      return 
      $ case m of { 
       Just j -> True 
       Nothing -> False } } 

Also, das ist, wo mein Verständnis von freier Interna abläuft und ich habe Fragen:

  1. Warum gibt es eine überlappende Instanz? Ich verstehe nicht, woher das kommt.
  2. Was macht InkoherentInstances eigentlich? Autoselection klingt sehr wahrscheinlich schwer zu debuggende Fehler.
  3. Wie nutze ich findMacro innerhalb einer anderen Funktion?

Prost!

Antwort

17

Typinferenz für erweiterbare Effekte war historisch schlecht. Lassen Sie uns ein paar Beispiele sehen:

{-# language TypeApplications #-} 

-- mtl 
import qualified Control.Monad.State as M 

-- freer 
import qualified Control.Monad.Freer as F 
import qualified Control.Monad.Freer.State as F 

-- mtl works as usual 
test1 = M.runState M.get 0 

-- this doesn't check 
test2 = F.run $ F.runState F.get 0 

-- this doesn't check either, although we have a known 
-- monomorphic state type 
test3 = F.run $ F.runState F.get True 

-- this finally checks 
test4 = F.run $ F.runState (F.get @Bool) True 

-- (the same without TypeApplication) 
test5 = F.run $ F.runState (F.get :: F.Eff '[F.State Bool] Bool) True 

Ich werde versuchen, das allgemeine Problem zu erklären und minimale Code-Darstellung bieten. Eine eigenständige Version des Codes kann found here sein.

Auf der untersten Ebene (optimierte Darstellungen abgesehen) wird Eff die folgende Art und Weise definiert:

{-# language 
    GADTs, DataKinds, TypeOperators, RankNTypes, ScopedTypeVariables, 
    TypeFamilies, DeriveFunctor, EmptyCase, TypeApplications, 
    UndecidableInstances, StandaloneDeriving, ConstraintKinds, 
    MultiParamTypeClasses, FlexibleInstances, FlexibleContexts, 
    AllowAmbiguousTypes, PolyKinds 
    #-} 

import Control.Monad 

data Union (fs :: [* -> *]) (a :: *) where 
    Here :: f a -> Union (f ': fs) a 
    There :: Union fs a -> Union (f ': fs) a 

data Eff (fs :: [* -> *]) (a :: *) = 
    Pure a | Free (Union fs (Eff fs a)) 

deriving instance Functor (Union fs) => Functor (Eff fs) 

Mit anderen Worten, ist Eff ein freies Monade von einer Vereinigung einer Liste von functors. Union fs a verhält sich wie ein n-ary Coproduct.Die binäre Coproduct ist wie Either für zwei functors:

data Coproduct f g a = InL (f a) | InR (g a) 

Im Gegensatz dazu Union fs a lässt uns einen Funktor aus einer Liste von functors wählen:

type MyUnion = Union [[], Maybe, (,) Bool] Int 

-- choose the first functor, which is [] 
myUnion1 :: MyUnion 
myUnion1 = Here [0..10] 

-- choose the second one, which is Maybe 
myUnion2 :: MyUnion 
myUnion2 = There (Here (Just 0)) 

-- choose the third one 
myUnion3 :: MyUnion 
myUnion3 = There (There (Here (False, 0))) 

Lassen Sie uns die State Wirkung als Beispiel implementieren. Zuerst müssen wir eine Functor Instanz für Union fs haben, da Eff nur eine Monad Instanz hat, wenn Functor (Union fs).

Functor (Union '[]) ist trivial, da die leere Vereinigung Werte nicht haben:

instance Functor (Union '[]) where 
    fmap f fs = case fs of {} -- using EmptyCase 

Ansonsten voranstellen wir einen Funktor der Gewerkschaft:

instance (Functor f, Functor (Union fs)) => Functor (Union (f ': fs)) where 
    fmap f (Here fa) = Here (fmap f fa) 
    fmap f (There u) = There (fmap f u) 

Nun ist die State Definition und die Läufer:

run :: Eff '[] a -> a 
run (Pure a) = a 

data State s k = Get (s -> k) | Put s k deriving Functor 

runState :: forall s fs a. Functor (Union fs) => Eff (State s ': fs) a -> s -> Eff fs (a, s) 
runState (Pure a)     s = Pure (a, s) 
runState (Free (Here (Get k))) s = runState (k s) s 
runState (Free (Here (Put s' k))) s = runState k s' 
runState (Free (There u))   s = Free (fmap (`runState` s) u) 

Wir können schon anfangen zu schreiben und ru unsere Eff Programme nning, obwohl es fehlt uns den ganzen Zucker und Bequemlichkeit:

action1 :: Eff '[State Int] Int 
action1 = 
    Free $ Here $ Get $ \s -> 
    Free $ Here $ Put (s + 10) $ 
    Pure s 

-- multiple state 
action2 :: Eff '[State Int, State Bool]() 
action2 = 
    Free $ Here $ Get $ \n ->   -- pick the first effect 
    Free $ There $ Here $ Get $ \b -> -- pick the second effect 
    Free $ There $ Here $ Put (n < 10) $ -- the second again 
    Pure() 

Jetzt:

> run $ runState action1 0 
(0,10) 
> run $ (`runState` False) $ (`runState` 0) action2 
(((),0),True) 

Es gibt nur zwei wesentliche Dinge fehlen hier.

Die erste ist die Monade Instanz für Eff die uns do -Notation statt Free und Pure, und auch können wir verwenden, um die vielen polymorphen monadischen Funktionen nutzen können. Wir werden es hier überspringen, weil es einfach zu schreiben ist.

Die zweite ist Inferenz/Überladung für die Auswahl von Effekten aus Listen von Effekten. Zuvor mussten wir Here x schreiben, um den ersten Effekt zu wählen, There (Here x), um den zweiten zu wählen, und so weiter. Stattdessen möchten wir Code schreiben, der in Effektlisten polymorph ist, also müssen wir nur spezifizieren, dass ein Effekt ein Element einer Liste ist, und einige verborgene Typklassenmagie fügt die entsprechende Anzahl von There -s ein.

Wir brauchen eine Member f fs Klasse, die f a es in Union fs a es injizieren kann, wenn f ein Element der fs ist. Historisch gesehen haben Menschen es auf zwei Arten implementiert.

Zuerst direkt mit OverlappingInstances:

class Member (f :: * -> *) (fs :: [* -> *]) where 
    inj :: f a -> Union fs a 

instance Member f (f ': fs) where 
    inj = Here 

instance {-# overlaps #-} Member f fs => Member f (g ': fs) where 
    inj = There . inj 

-- it works 
injTest1 :: Union [[], Maybe, (,) Bool] Int 
injTest1 = inj [0] 

injTest2 :: Union [[], Maybe, (,) Bool] Int 
injTest2 = inj (Just 0) 

Zweitens indirekt, indem man zuerst den Index f in fs mit einer Art Familie Berechnung, dann inj mit einer nicht-überlappenden Klasse Umsetzung, geführt von f es berechneter Index. Dies wird im Allgemeinen als besser angesehen, da Menschen überlappende Instanzen nicht mögen.

data Nat = Z | S Nat 

type family Lookup f fs where 
    Lookup f (f ': fs) = Z 
    Lookup f (g ': fs) = S (Lookup f fs) 

class Member' (n :: Nat) (f :: * -> *) (fs :: [* -> *]) where 
    inj' :: f a -> Union fs a 

instance fs ~ (f ': gs) => Member' Z f fs where 
    inj' = Here 

instance (Member' n f gs, fs ~ (g ': gs)) => Member' (S n) f fs where 
    inj' = There . inj' @n 

type Member f fs = Member' (Lookup f fs) f fs 

inj :: forall fs f a. Member f fs => f a -> Union fs a 
inj = inj' @(Lookup f fs) 

-- yay 
injTest1 :: Union [[], Maybe, (,) Bool] Int 
injTest1 = inj [0] 

Die freer Bibliothek verwendet die zweite Lösung, während extensible-effects die ersten für GHC Versionen verwenden älter als 7,8, und die zweite für neuere GHC-s.

Jedenfalls haben beide Lösungen die gleiche Inferenz-Einschränkung, nämlich dass wir fast immer Lookup nur konkrete monomorphe Typen, nicht Typen, die Typvariablen enthalten. Beispiele in GHCI:

> :kind! Lookup Maybe [Maybe, []] 
Lookup Maybe [Maybe, []] :: Nat 
= 'Z 

Das funktioniert, weil es keine Variablen vom Typ entweder Maybe oder [Maybe, []] sind.

> :kind! forall a. Lookup (Either a) [Either Int, Maybe] 
forall a. Lookup (Either a) [Either Int, Maybe] :: Nat 
= Lookup (Either a) '[Either Int, Maybe] 

Dieser Typ bleibt stecken, weil die a Variable die Reduktion blockiert.

> :kind! forall a. Lookup (Maybe a) '[Maybe a] 
forall a. Lookup (Maybe a) '[Maybe a] :: Nat 
= Z 

Dies funktioniert, weil das einzige, was wir über beliebige Art Variablen wissen ist, dass sie sich gleich sind, und a gleich a.

Im Allgemeinen wird der Typ family reduction bei Variablen hängen bleiben, da Constraint Solving sie später auf verschiedene Typen verfeinern kann, sodass GHC keine Annahmen über sie treffen kann (abgesehen davon, dass sie sich selbst gleich sind). Im Wesentlichen das gleiche Problem tritt mit der OverlappingInstances Implementierung auf (obwohl es keine Typenfamilien gibt).

Lassen Sie uns im Lichte dieser freer erneut besuchen.

import Control.Monad.Freer 
import Control.Monad.Freer.State 

test1 = run $ runState get 0 -- error 

GHC weiß, dass wir einen Stapel mit einem einzigen Effekt haben, da run Arbeiten auf Eff '[] a. Es weiß auch, dass dieser Effekt State s sein muss. Aber wenn wir schreiben get, GHC weiß nur, dass es einen State t Effekt für einige frische t Variable hat, und dass Num t halten muss, so wenn es versucht, das freer Äquivalent von Lookup (State t) '[State s] zu berechnen, wird es auf den Typ Variablen stecken, und weiter Die Instanzauflösung stolpert über den Ausdruck der Familie mit festgefahrenen Typen. Ein weiteres Beispiel:

foo = run $ runState get False -- error 

Dies scheitert auch, weil GHC Lookup (State s) '[State Bool] berechnen muss, und obwohl wir wissen, dass der Staat Bool sein muss, dies wird immer noch wegen des s Variable stecken. Diese

foo = run $ runState (modify not) False -- this works 

funktioniert, weil der Staat Art von modify not kann Bool gelöst werden, und Lookup (State Bool) '[State Bool] reduziert.

Jetzt, nach diesem großen Umweg, werde ich Ihre Fragen am Ende Ihrer Post adressieren.

  1. Overlapping instances ist kein Hinweis auf eine mögliche Lösung, nur eine Art Fehler Artefakt. Ich würde mehr Code-Kontext benötigen, um genau zu bestimmen, wie genau es entsteht, aber ich bin mir sicher, dass es nicht relevant ist, da sobald Lookup stecken bleibt, der Fall hoffnungslos wird.

  2. IncoherentInstances ist auch irrelevant und hilft nicht. Wir brauchen einen konkreten Effektpositionsindex, um Code für das Programm generieren zu können, und wir können keinen Index aus dem Nichts ziehen, wenn Lookup steckenbleibt.

  3. Das Problem mit findMacro ist, dass es State Effekte mit Typ Variablen innerhalb der Staaten hat. Wann immer Sie findMacro verwenden möchten, müssen Sie sicherstellen, dass die Parameter v und w für Scope und Global bekannte Betontypen sind. Sie können dies tun, indem Sie Anmerkungen eingeben, oder bequemer können Sie TypeApplications verwenden, und schreiben Sie findMacro @Int @Int für die Angabe v = Int und w = Int. Wenn Sie findMacro innerhalb einer polymorphen Funktion haben, müssen Sie ScopedTypeVariables aktivieren, v und w mit einer forall v w. Annotation für diese Funktion binden und findMacro @v @w schreiben, wenn Sie es verwenden. Sie müssen auch {-# language AllowAmbiguousTypes #-} für polymorphe v oder w aktivieren (wie in den Kommentaren hervorgehoben). Ich denke aber, dass es in GHC 8 eine sinnvolle Erweiterung ist, zusammen mit TypeApplications zu ermöglichen.


Nachtrag:

jedoch glücklicherweise neue GHC 8-Funktionen lassen Sie uns Typinferenz für erweiterbare Effekte reparieren, und wir können alles ableiten mtl kann, und auch einige Dinge mtl nicht umgehen kann. Die neue Typschlussfolgerung ist auch in Bezug auf die Reihenfolge der Effekte invariant.

Ich habe eine minimal implementation here zusammen mit einer Reihe von Beispielen. Es wird jedoch noch nicht in einer mir bekannten Effektbibliothek verwendet. Ich werde wahrscheinlich einen Bericht darüber schreiben und eine Pull-Anfrage machen, um sie zu freer hinzuzufügen.

+0

Ihre Antwort auf 3 ist nicht wirklich der Grund, warum dieses bestimmte Beispiel fehlschlägt (obwohl dies eine sehr vollständige Erklärung einer anderen allgemeinen Fallstrick ist) - in diesem Fall z. '\ x -> fromJust <$> findMacro x' Typ überprüft ohne zusätzliche Anmerkungen, während z. '\ x -> const True <$> findMacro x' nicht - das Problem ist, dass im OP-Beispiel der Rückgabewert (' Bool') die Typvariablen nicht erwähnt, was sie (per Definition) amüsant macht. In MTL gibt es einen fundep, so dass die Tyvars auch dann nicht mehrdeutig sind (d. H. "Const True <$> get" ist in Ordnung), aber das ist mit freierem nicht möglich. – user2407038

+0

@ user2407038 Ich glaube nicht. 'macroexpand2' scheitert vor der Mehrdeutigkeitsprüfung, und wir haben den 'Eff r Bool'-Rückgabetyp, und alle Abhängigkeiten können potentiell aus' r 'berechnet werden, so dass es keine unmittelbare Mehrdeutigkeit gibt. –

+0

@ user2407038 hier ist ein [kleines Beispiel] (http://lpaste.net/177990) mit einer ähnlichen Fehlermeldung wie im OP. –