haskell - trabajo - programacion generica ejemplos
¿Cuándo una función genérica no es genérica? (3)
Parece que GHC está diciendo que, de hecho, el tipo de runDB se especializa de alguna manera.
Tu conjetura es correcta. Su tipo original era app :: (MonadIO m) => (SqlPersistT IO a -> ma) -> ScottyM ()
. Esto significa que su argumento runDB
de tipo SqlPersistT IO a -> ma
se puede utilizar en cualquier tipo a
. Sin embargo, el cuerpo de la app
quiere usar el argumento runDB
en dos tipos diferentes ( Person
y Food
), por lo que necesitamos pasar un argumento que pueda funcionar para cualquier número de tipos diferentes en el cuerpo. Así, la app
necesita el tipo
app :: MonadIO m => (forall a. SqlPersistT IO a -> m a) -> ScottyM ()
(Yo sugeriría mantener la restricción de MonadIO
fuera de la general, pero también puede MonadIO
).
EDITAR:
Lo que está pasando detrás de escena es lo siguiente:
(F a -> G a) -> X
significa para todos forall a. (F a -> G a) -> X
forall a. (F a -> G a) -> X
, que significa //a -> (F a -> G a) -> X
//
es la lambda a nivel de tipo. Es decir, la persona que llama puede pasar en un solo tipo a
y una función de tipo F a -> G a
para esa elección particular de a
.
(forall a. F a -> G a) -> X
significa (//a -> F a -> G a) -> X
y la persona que llama debe pasar en una función en la cual el usuario puede especializarse en muchas opciones de .
Estoy trabajando en un servidor Haskell usando scotty
y persistent
. Muchos manejadores necesitan acceso al grupo de conexión de la base de datos, por lo que he decidido pasar el grupo por toda la aplicación, de esta manera:
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
donde getPeople
y getFoods
son acciones de base de datos persistent
apropiadas que devuelven [Person]
y [Food]
respectivamente.
El patrón de llamar a liftIO
y runSqlPool
en un pool se vuelve agotador después de un tiempo, ¿no sería genial si pudiera refactorizarlos en una sola función, como runDB
de Yesod, que simplemente tomaría la consulta y devolvería el tipo apropiado? Mi intento de escribir algo como esto es:
runDB'' :: (MonadIO m) => ConnectionPool -> SqlPersistT IO a -> m a
runDB'' pool q = liftIO $ runSqlPool q pool
Ahora, puedo escribir esto:
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
Excepto que GHC se queja:
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''
Parece que GHC está diciendo que, de hecho, el tipo de runDB
se especializa de alguna manera. Pero entonces, ¿cómo se definen las funciones como runSqlPool
? Su tipo de firma es similar al mío:
runSqlPool :: MonadBaseControl IO m => SqlPersistT m a -> Pool Connection -> m a
pero se puede usar con consultas de base de datos que devuelven muchos tipos diferentes, como lo estaba haciendo originalmente. Creo que hay algo fundamental que no entiendo bien sobre los tipos aquí, ¡pero no tengo idea de cómo descubrirlo! Cualquier ayuda sería muy apreciada.
EDITAR:
A sugerencia de Yuras, he añadido esto:
type DBRunner m a = (MonadIO m) => SqlPersistT IO a -> m a
runDB'' :: ConnectionPool -> DBRunner m a
app :: forall a. DBRunner ActionM a -> ScottyM ()
que requería -XRankNTypes
para el typedef. Sin embargo, el error del compilador sigue siendo idéntico.
EDITAR:
Victoria a los comentaristas. Esto permite que el código se compile:
app :: (forall a. DBRunner ActionM a) -> ScottyM ()
Por lo cual estoy agradecido, pero todavía desconcertado!
El código actualmente se ve así y this .
Juguemos el juego:
Prelude> let f str = (read str, read str)
Prelude> f "1" :: (Int, Float)
(1,1.0)
Funciona como se espera.
Prelude> let f str = (read1 str, read1 str) where read1 = read
Prelude> f "1" :: (Int, Float)
(1,1.0)
Trabaja tambien
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)
Pero esto no lo hace. ¿Cuál es la diferencia?
La última f
tiene el siguiente tipo:
Prelude> :t f
f :: (t1 -> t) -> t1 -> (t, t)
Por lo tanto, no funciona por una razón clara, ambos elementos de la tupla deben tener el mismo tipo.
La solución es así:
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)
Es poco probable que pueda venir con una buena explicación de RankNTypes
, así que ni siquiera lo intentaría. Hay suficientes recursos en la web.
Para responder realmente la pregunta del título que aparentemente continúa desconcertándote: Haskell siempre elige el tipo de rango 1 más genérico para una función, cuando no proporcionas una firma explícita. Así que para la app
en la app (runDB'' pool)
expresión app (runDB'' pool)
, GHC intentaría tener el tipo
app :: DBRunner ActionM a -> ScottyM ()
que es de hecho taquigrafía para
app :: forall a. ( DBRunner ActionM a -> ScottyM () )
Esto es polimórfico de rango 1, porque todas las variables de tipo se introducen fuera de la firma (no hay ninguna cuantificación en curso en la propia firma; el argumento DBRunner ActionM a
es de hecho monomorfo, ya que a
se fija en ese punto). En realidad, es el tipo más genérico posible: puede funcionar con un argumento polimórfico como (runDB'' pool)
, pero también estaría bien con los argumentos monomórficos.
Pero resulta que la implementación de la app
no puede ofrecer esa generalidad: necesita una acción polimórfica, de lo contrario no puede alimentar dos tipos diferentes de valores a esa acción. Por lo tanto necesitas solicitar manualmente el tipo más específico.
app :: (forall a. DBRunner ActionM a) -> ScottyM ()
que es rango 2, porque tiene una firma que contiene un argumento polimórfico de rango 1. GHC no puede realmente saber que este es el tipo que desea, no existe el "tipo de clasificación-n más general posible" bien definido para una expresión, ya que siempre puede introducir cuantificadores adicionales. Por lo que debe especificar manualmente el tipo de rango 2.