2015-02-05 32 views
16

Sto lavorando su un server Haskell utilizzando scotty e persistent. Molti gestori hanno bisogno di accedere al pool di connessione al database, così ho preso a passare alla piscina intorno in tutta l'applicazione, in questa sorta di moda:Quando una funzione generica non è generica?

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 

dove getPeople e getFoods sono opportune azioni persistent di database che restituiscono [Person] e [Food] rispettivamente.

Il modello di chiamare liftIO e runSqlPool su una piscina diventa stancante dopo un po '- non sarebbe bello se potessi refactoring in una singola funzione, come Yesod di runDB, che basta prendere la query e restituire il appropriata genere. Il mio tentativo di scrivere qualcosa di simile a questo è:

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

Ora, posso scrivere questo:

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 

Solo che GHC si lamenta:

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' 

Sembra GHC sta dicendo che in effetti il ​​tipo di runDB diventa specializzato in qualche modo. Ma come sono definite le funzioni come runSqlPool? La sua firma di tipo simile al mio:

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

, ma può essere utilizzato con le query di database che restituiscono molti tipi diversi, come stavo facendo in origine. Penso che ci sia qualcosa di fondamentale in cui sto fraintendendo i tipi qui, ma non ho idea di come scoprire di cosa si tratta! Qualsiasi aiuto sarebbe molto apprezzato.

EDIT:

su suggerimento Yuras', ho aggiunto questo:

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

che ha richiesto -XRankNTypes per il typedef. Tuttavia, l'errore del compilatore è ancora identico.

EDIT:

vittoria ai commentors. Questo permette al codice di compilare:

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

Per il quale sono grato, ma ancora disorientato!

Il codice è attualmente simile a this e this.

+0

Prova ad aggiungere il tipo di firma a 'app' con' forall 'esplicito e molto probabilmente vedresti cosa c'è che non va. – Yuras

+0

@Yuras Penso che parte di questo problema sia che attualmente ho una comprensione molto scarsa del 'forall' esplicito, ma cercherò di farlo. –

+0

Suppongo che dovrebbe essere 'app :: (per sempre a. DBRunner ActionM a) -> ScottyM()'. –

risposta

20

Sembra che GHC stia dicendo che in effetti il ​​tipo di runDB diventa specializzato in qualche modo.

La tua ipotesi è giusta. Il tuo tipo originale era app :: (MonadIO m) => (SqlPersistT IO a -> m a) -> ScottyM(). Ciò significa che l'argomento del tipo SqlPersistT IO a -> m a può essere utilizzato in qualsiasi uno tipo a. Tuttavia, il corpo di app desidera utilizzare l'argomento runDB in due tipi diversi (Person e Food), quindi è necessario passare un argomento che può funzionare per qualsiasi numero di tipi diversi nel corpo. Così app ha bisogno del tipo

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

(vorrei suggerire mantenendo il vincolo MonadIO al di fuori del forall, ma si può anche mettere dentro.)

EDIT:

Che cosa sta succedendo dietro le quinte è la seguente:

(F a -> G a) -> X significa forall a. (F a -> G a) -> X, che significa /\a -> (F a -> G a) -> X . /\ è il lambda di livello testo. Cioè, il chiamante deve passare in un singolo tipo a e una funzione di tipo F a -> G a per quella particolare scelta di a.

(forall a. F a -> G a) -> X significa (/\a -> F a -> G a) -> X e il chiamante deve passare in una funzione che la callee può specializzarsi per molte scelte di a.

+0

Ma come interagisce con il modo intuitivo in cui una funzione generica può agire su più tipi? Perché 'runSqlPool' può restituire diversi tipi? Esiste un 'forall' da qualche parte nelle viscere' persistent' che non riesco a vedere? –

+0

@DanielBuckmaster: Nah. 'runSqlPool' funziona per lo stesso motivo per cui' f = head ["Hi"] ++ (mostra $ head [1..5]) 'funziona. Tuttavia, se dovessi usare 'runSqlPool' come parametro per ottenere' a' diversi, incontrerai lo stesso problema - il 'a' viene risolto al primo incontro. – Zeta

+0

@Zeta Lo vedo, quindi passarlo come parametro corregge il suo tipo. Questo ha senso, e devo passare 'runDB' perché fa affidamento su un pool che è costruito in fase di runtime. Non potrei definire 'runDB' (senza' '') come funzione di libreria, solo 'runDB''. Questa è la differenza con "runSqlPool", immagino. –

7

Consente di riprodurre il gioco:

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

funziona come previsto.

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

Anche lavori.

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) 

Ma questo non. Quale differenza?

L'ultima f ha il tipo successivo:

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

in modo che non funziona per una ragione chiara, entrambi gli elementi di una tupla deve avere lo stesso tipo.

La correzione è così:

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) 

improbabile che posso venire con buona spiegazione di RankNTypes, quindi non mi piacerebbe anche provare. Ci sono abbastanza risorse nel web.

+0

Grazie, questo è il tipo di logica che sto cercando di attraversare nella mia testa. Cercherò 'RankNTypes' per vedere se riesco a capirlo. –

6

Per rispondere veramente alla domanda del titolo che a quanto pare continua a mistificarti: Haskell sceglie sempre il tipo di rank-1 più generico per una funzione, quando non si fornisce una firma esplicita.Così per app nell'espressione app (runDB' pool), GHC avrebbe tentato di avere tipo

app :: DBRunner ActionM a -> ScottyM() 

che è in realtà una scorciatoia per

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

Questo è rango-1 polimorfico, perché tutte le variabili di tipo vengono introdotti al di fuori della firma (non vi è alcuna quantificazione in atto nella firma stessa, l'argomento DBRunner ActionM a è in realtà monomorfico poiché a è stato risolto in quel punto). In realtà, è il tipo più generico possibile: può funzionare con un argomento polimorfo come (runDB' pool), ma anche con argomenti monomorfici.

ma si scopre l'attuazione app non possono offrire quella generalità: essa bisogno un'azione polimorfica, altrimenti non può alimentare due diversi tipi di a valori a tale azione. Pertanto è necessario richiedere manualmente il tipo più specifico

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

che è rango-2, perché ha una firma che contiene un rango-1 argomento polimorfico. GHC non può davvero sapere che questo è il tipo che si desidera – non è definito il tipo più generale possibile “ di tipo rank-n ” per un'espressione, poiché è sempre possibile inserire quantificatori aggiuntivi. Quindi devi specificare manualmente il tipo rank-2.

+0

Questo, combinato con la spiegazione di Tom Ellis di "forall' come introduzione di un lambda di livello testo, mi ha davvero aiutato. Grazie per andare al guaio! –

Problemi correlati