2015-02-05 8 views
16

Ich arbeite an einem Haskell-Server mit scotty und persistent. Viele Handler benötigen Zugriff auf den Datenbankverbindungspool, also hat mich genommen, den Pool, das um überall in der App in dieser Art der Mode:Wann ist eine generische Funktion nicht generisch?

main = do 
    runNoLoggingT $ withSqlitePool ":memory:" 10 $ \pool -> 
     liftIO $ scotty 7000 (app pool) 

app pool = do 
    get "/people" $ do 
     people <- liftIO $ runSqlPool getPeople pool 
     renderPeople people 
    get "/foods" $ do 
     food <- liftIO $ runSqlPool getFoods pool 
     renderFoods food 

wo getPeople und getFoods sind entsprechende persistent Datenbankaktionen, die Rückkehr [Person] bzw. [Food].

Das Muster liftIO und runSqlPool auf einen Pool von Aufruf nach einer Weile ermüdend wird - wäre es nicht toll, wenn ich sie in einer einzigen Funktion Refactoring könnte, wie Yesod des runDB, die nur die Abfrage nehmen würde und geben die entsprechenden Art. Mein Versuch, so etwas wie dieses zu schreiben ist:

runDB' :: (MonadIO m) => ConnectionPool -> SqlPersistT IO a -> m a 
runDB' pool q = liftIO $ runSqlPool q pool 

Jetzt kann ich schreiben:

main = do 
    runNoLoggingT $ withSqlitePool ":memory:" 10 $ \pool -> 
     liftIO $ scotty 7000 $ app (runDB' pool) 

app runDB = do 
    get "/people" $ do 
     people <- runDB getPeople 
     renderPeople people 
    get "/foods" $ do 
     food <- runDB getFoods 
     renderFoods food 

Außer, dass GHC klagt:

Couldn't match type `Food' with `Person' 
Expected type: persistent-2.1.1.4:Database.Persist.Sql.Types.SqlPersistT 
       IO 
       [persistent-2.1.1.4:Database.Persist.Class.PersistEntity.Entity 
        Person] 
    Actual type: persistent-2.1.1.4:Database.Persist.Sql.Types.SqlPersistT 
       IO 
       [persistent-2.1.1.4:Database.Persist.Class.PersistEntity.Entity 
        Food] 
In the first argument of `runDB', namely `getFoods' 

Es scheint wie GHC sagt, dass in der Tat der Typ von runDB irgendwie spezialisiert wird. Aber wie sind dann Funktionen wie runSqlPool definiert? Seine Art Signatur sieht ähnlich wie mir:

runSqlPool :: MonadBaseControl IO m => SqlPersistT m a -> Pool Connection -> m a 

aber es kann mit Datenbankabfragen verwendet werden, die viele verschiedene Arten zurückkehren, wie ich ursprünglich tat. Ich denke, es gibt etwas Grundlegendes, dass ich hier Typen falsch verstehe, aber ich habe keine Ahnung, wie ich herausfinden soll, was es ist! Jede Hilfe würde sehr geschätzt werden.

EDIT:

bei Yuras' Vorschlag, ich habe das hinzugefügt:

type DBRunner m a = (MonadIO m) => SqlPersistT IO a -> m a 
runDB' :: ConnectionPool -> DBRunner m a 
app :: forall a. DBRunner ActionM a -> ScottyM() 

die -XRankNTypes für die typedef erforderlich. Der Compilerfehler ist jedoch immer noch identisch.

EDIT:

Sieg der commentors. Dies ermöglicht den Code zu kompilieren:

app :: (forall a. DBRunner ActionM a) -> ScottyM() 

Für die ich dankbar bin, aber immer noch mystifiziert!

Der Code sieht derzeit wie this und this aus.

+0

Versuchen Sie, Typ-Signatur zu 'app' mit expliziten' forall' hinzuzufügen und wahrscheinlich würden Sie sehen, was falsch ist. – Yuras

+0

@Yuras Ich denke, ein Teil dieses Problems ist, dass ich derzeit ein sehr schlechtes Verständnis von expliziten "Forall" habe, aber ich werde mich bemühen, dies zu tun. –

+0

Ich denke, es sollte "app :: (forall a. DBRunner AktionM a) -> ScottyM()". –

Antwort

20

Es scheint, als ob GHC sagt, dass sich der Typ von runDB in der Tat irgendwie spezialisiert.

Ihre Vermutung ist richtig. Ihr ursprünglicher Typ war app :: (MonadIO m) => (SqlPersistT IO a -> m a) -> ScottyM(). Dies bedeutet, dass Ihr runDB Argument vom Typ SqlPersistT IO a -> m a bei jedem einen Typ a verwendet werden kann. Der Körper von app möchte jedoch das Argument runDB bei zwei verschiedenen Typen verwenden (Person und Food). Stattdessen müssen wir ein Argument übergeben, das für eine beliebige Anzahl verschiedener Typen im Körper funktionieren kann. So muss app den Typ

app :: MonadIO m => (forall a. SqlPersistT IO a -> m a) -> ScottyM() 

(Ich würde vorschlagen, die MonadIO Einschränkung außerhalb des forall halten, aber man kann es auch nach innen setzen.)

EDIT:

Was hinter den Kulissen vor sich geht ist das folgende:

(F a -> G a) -> X bedeutet forall a. (F a -> G a) -> X, was /\a -> (F a -> G a) -> X bedeutet . /\ ist der Typ-Level-Lambda. Das heißt, der Aufrufer bekommt einen einzigen Typ a und eine Funktion des Typs für diesen insbesondere Wahl von a.

(forall a. F a -> G a) -> X bedeutet (/\a -> F a -> G a) -> X und der Anrufer hat in einer Funktion zu übergeben, die die Rufenen-viele Auswahl von a spezialisieren können.

+0

Aber wie wirkt sich das auf die intuitive Art und Weise aus, wie eine generische Funktion auf mehrere Typen wirken kann? Warum kann 'runSqlPool' verschiedene Typen zurückgeben? Gibt es einen "Forall" irgendwo in "hartnäckigen" Eingeweiden, den ich einfach nicht sehen kann? –

+0

@DanielBuckmaster: Nein. 'runSqlPool' funktioniert aus dem gleichen Grund' f = head ["Hi"] ++ (show $ head [1..5]) 'funktioniert. Wenn Sie jedoch 'runSqlPool' als Parameter verwenden, um ein anderes' a' zu erhalten, würden Sie auf dasselbe Problem stoßen - das 'a' wird bei der ersten Begegnung behoben. – Zeta

+0

@ Zeta Ich sehe, so übergibt es als Parameter seinen Typ fixiert. Das macht Sinn, und ich habe 'runDB' zu übergeben, weil es auf einen Pool angewiesen ist, der zur Laufzeit erstellt wird. Ich konnte 'runDB' (ohne' '') nicht als Bibliotheksfunktion definieren, nur 'runDB'. Das ist der Unterschied zu 'runSqlPool', denke ich. –

7

Lässt das Spiel spielen:

Prelude> let f str = (read str, read str) 
Prelude> f "1" :: (Int, Float) 
(1,1.0) 

wie erwartet funktioniert.

Prelude> let f str = (read1 str, read1 str) where read1 = read 
Prelude> f "1" :: (Int, Float) 
(1,1.0) 

Funktioniert auch.

Prelude> let f read1 str = (read1 str, read1 str) 
Prelude> f read "1" :: (Int, Float) 

<interactive>:21:1: 
    Couldn't match type ‘Int’ with ‘Float’ 
    Expected type: (Int, Float) 
     Actual type: (Int, Int) 
    In the expression: f read "1" :: (Int, Float) 
    In an equation for ‘it’: it = f read "1" :: (Int, Float) 

Aber das tut nicht. Was ist der Unterschied?

Die letzte f hat den nächsten Typ:

Prelude> :t f 
f :: (t1 -> t) -> t1 -> (t, t) 

So ist es nicht für klaren Grund arbeiten, beide Elemente des Tupels sollten denselben Typ haben.

Das Update ist wie folgt aus:

Prelude> :set -XRankNTypes 
Prelude> let f read1 str = (read1 str, read1 str); f :: (Read a1, Read a2) => (forall a . Read a => str -> a) -> str -> (a1, a2) 
Prelude> f read "1" :: (Int, Float) 
(1,1.0) 

unwahrscheinlich, dass ich mit einem guten Erklärung RankNTypes kommen kann, also würde ich nicht einmal versuchen. Es gibt genügend Ressourcen im Web.

+0

Danke, das ist die Art von Logik, die ich versuche, in meinem Kopf durchzugehen. Ich werde 'RankNTypes' nachschlagen, um zu sehen, ob ich es verstehen kann. –

6

Um wirklich die Titelfrage zu beantworten, die Sie scheinbar weiter mystifiziert: Haskell wählt immer den allgemeinsten Rang-1-Typ für eine Funktion, wenn Sie keine explizite Signatur angeben.Also für app im Ausdruck app (runDB' pool), versuchen würde, GHC Typ

app :: DBRunner ActionM a -> ScottyM() 

zu haben, die für

app :: forall a. (DBRunner ActionM a -> ScottyM()) 

in der Tat eine Abkürzung ist Dies ist Rang-1-polymorphe, da alle Variablen vom Typ außerhalb des eingeführt werden Signatur (es gibt keine Quantifizierung in der Signatur selbst; das Argument DBRunner ActionM a ist tatsächlich monomorph, da a an diesem Punkt fixiert ist). Tatsächlich ist es der allgemeinste Typ, der möglich ist: Er kann mit einem polymorphen Argument wie (runDB' pool) arbeiten, wäre aber auch mit monomorphen Argumenten in Ordnung.

Aber es stellt sich die Umsetzung der app aus, kann an dieser Allgemeinheit bieten: es braucht eine polymorphe Aktion, sonst kann es nicht zwei verschiedene Arten von a Werte füttern zu dieser Aktion. Daher müssen Sie manuell die spezifischeren Typ

app :: (forall a. DBRunner ActionM a) -> ScottyM() 

das ist Rang-2, beantragen, weil es eine Signatur aufweist, die einen Rang-1 polymorphen Argument enthält. GHC kann nicht wirklich wissen, dass dies der Typ ist, den Sie wollen – Es gibt keine gut definierten “ allgemeinsten möglichen Rang-n-Typ ” für einen Ausdruck, da Sie immer zusätzliche Quantifikatoren einschieben können. Sie müssen also den Rang-2-Typ manuell angeben.

+0

Dies, kombiniert mit Tom Ellis Erklärung von 'forall' als Einführung eines type-level Lambda, hat mir wirklich geholfen. Danke, dass Sie sich die Mühe gemacht haben! –