haskell - versión perezosa de mapM
io lazy-sequences (2)
La mónada IO tiene un mecanismo para diferir los efectos. Se llama unsafeInterleaveIO
. Puede usarlo para obtener el efecto deseado:
import System.IO.Unsafe
lazyMapM :: (a -> IO b) -> [a] -> IO [b]
lazyMapM f [] = return []
lazyMapM f (x:xs) = do y <- f x
ys <- unsafeInterleaveIO $ lazyMapM f xs
return (y:ys)
Así es como se implementa la IO lenta. No es seguro que el orden en que se ejecutarán realmente los efectos sea difícil de predecir y estará determinado por el orden en que se evalúan los elementos de la lista de resultados. Por esta razón, es importante que los efectos IO en f
sean benignos, en el sentido de que deben ser insensibles a los pedidos. Un buen ejemplo de un efecto normalmente benigno es leer de un archivo de solo lectura.
Supongamos que recibo una gran lista de elementos mientras trabajo con IO:
as <- getLargeList
Ahora, estoy tratando de aplicar fn :: a -> IO b
en:
as <- getLargeList
bs <- mapM fn as
mapM
tiene el tipo mapM :: Monad m => (a -> mb) -> [a] -> m [b]
, y eso es lo que necesito en términos de coincidencia de tipos. Pero construye toda la cadena en la memoria hasta devolver el resultado. Estoy buscando un análogo de mapM
, que funcionará perezosamente, para poder usar head of bs
mientras sigo construyendo tail.
No use unsafeInterleaveIO
o cualquier IO perezosa para ese asunto. Este es precisamente el problema que las iteraciones fueron creadas para abordar: evitar la IO lenta, lo que proporciona una administración de recursos impredecible. El truco es nunca compilar la lista y transmitirla constantemente usando repeticiones hasta que haya terminado de usarla. Usaré ejemplos de mi propia biblioteca, pipes
, para demostrar esto.
Primero, defina:
import Control.Monad
import Control.Monad.Trans
import Control.Pipe
-- Demand only ''n'' elements
take'' :: (Monad m) => Int -> Pipe a a m ()
take'' n = replicateM_ n $ do
a <- await
yield a
-- Print all incoming elements
printer :: (Show a) => Consumer a IO r
printer = forever $ do
a <- await
lift $ print a
Ahora seamos groseros con nuestro usuario y exijamos que nos generen una lista realmente grande:
prompt100 :: Producer Int IO ()
prompt100 = replicateM_ 1000 $ do
lift $ putStrLn "Enter an integer: "
n <- lift readLn
yield n
Ahora, vamos a ejecutarlo:
>>> runPipe $ printer <+< take'' 1 <+< prompt100
Enter an integer:
3<Enter>
3
Solo solicita al usuario un entero, ¡ya que solo pedimos un número entero!
Si desea reemplazar prompt100
con salida de getLargeList
, solo escriba:
yourProducer :: Producer b IO ()
yourProducer = do
xs <- lift getLargeList
mapM_ yield xs
... y luego ejecutar:
>>> runPipe $ printer <+< take'' 1 <+< yourProducer
Esto transmitirá de forma perezosa la lista y nunca construirá la lista en la memoria, todo sin usar ataques de IO
inseguros. Para cambiar la cantidad de elementos que exiges, simplemente cambia el valor que pasas para take''
Para ver más ejemplos como este, lea el tutorial de pipes
en Control.Pipe.Tutorial
.
Para obtener más información acerca de por qué el IO lento causa problemas, lea las diapositivas originales de Oleg sobre el tema, que puede encontrar aquí . Él hace un gran trabajo explicando los problemas con el uso de IO perezoso. Cada vez que te sientas obligado a usar IO perezoso, lo que realmente quieres es una biblioteca iteratee.