2011-03-30 9 views
6

Asumamos que tenemos una acción IO comoReentrante almacenamiento en caché de "referencialmente transparente" IO llama

lookupStuff :: InputType -> IO OutputType 

que podría ser algo tan simple como la búsqueda de DNS, o alguna llamada de servicio web contra un conjunto de datos invariables en el tiempo.

Vamos a suponer que:

  1. La operación nunca se lanza ninguna excepción y/o nunca diverge

  2. Si no fuera por la IO mónada, la función sería pura, es decir, el resultado es siempre igual para los parámetros de entrada iguales

  3. La acción es reentrante, es decir, se puede invocar desde varios hilos al mismo tiempo de forma segura.

  4. La operación lookupStuff es bastante (costosa).

El problema que estoy enfrentando es la manera correcta (y w/o utilizando cualquier unsafe*IO* tramposo) implementar una memoria caché de reentrada, que pueden ser llamados desde varios subprocesos, y coalesce varias consultas para los mismos parámetros de entrada- en una sola solicitud.

Supongo que estoy buscando algo similar al concepto de agujero negro de GHC para cálculos puros pero en el contexto de "cálculo" IO.

¿Cuál es la solución Haskell/GHC idiomática para el problema indicado?

+4

Las suposiciones 1 , 2 y 3 parecen implicar que la función es realmente pura y que la impureza es simplemente un detalle de implementación.En ese caso, no creo que haya nada de malo en usar inseguroPerformIO. De hecho, creo que InseguroPerformIO existe exactamente para tales casos. –

+1

De acuerdo. 1, 2, 3 son suposiciones muy fuertes que casi nunca son válidas para el código en IO, pero si de hecho se puede garantizar que esto no sea seguroPerformIO es bastante razonable. –

+1

Ok, pero ¿cómo puedo garantizar que el efecto IO nunca se realice más de una vez por el mismo argumento de entrada? – hvr

Respuesta

4

Sí, básicamente reimplante la lógica. Aunque parece similar a lo que GHC ya está haciendo, esa es la elección de GHC. Haskell se puede implementar en máquinas virtuales que funcionan de manera muy diferente, por lo que en ese sentido no está hecho para ti.

Pero sí, solo use un MVar (Map InputType OutputType) o incluso un IORef (Map InputType OutputType) (asegúrese de modificarlo con atomicModifyIORef), y simplemente almacene el caché allí. Si esta solución imperativa parece incorrecta, es la restricción "si no fuera por el IO, esta función sería pura". Si se tratara de una acción arbitraria IO, entonces la idea de que tendrías que mantener el estado para saber qué ejecutar o no parece perfectamente natural. El problema es que Haskell no tiene un tipo para "IO puro" (que, si depende de una base de datos, simplemente se comporta puro bajo ciertas condiciones, que no es lo mismo que ser hereditariamente puro).

import qualified Data.Map as Map 
import Control.Concurrent.MVar 

-- takes an IO function and returns a cached version 
cache :: (Ord a) => (a -> IO b) -> IO (a -> IO b) 
cache f = do 
    r <- newMVar Map.empty 
    return $ \x -> do 
     cacheMap <- takeMVar r 
     case Map.lookup x cacheMap of 
      Just y -> do 
       putMVar r cacheMap 
       return y 
      Nothing -> do 
       y <- f x 
       putMVar (Map.insert x y cacheMap) 
       return y 

Sí, es feo en el interior. Pero afuera, mira eso! Es como el tipo de una función de memoria pura, a excepción de que tiene IO manchado por todas partes.

+0

Mente, como se implementó, esto es un caché sincronizado. Es decir. cada llamada a ella está bloqueada por un 'MVar'. La alternativa posiblemente permita que la misma acción se ejecute más de una vez. Entonces usaría un 'IORef' en su lugar, y' atomicModifyIORef'. – luqui

+0

De hecho, este es el enfoque semántico. Siempre puede poner una 'forma de riesgo inseguro' alrededor de los resultados si así lo incluye. –

+0

Bueno, parece que su implementación serializa las llamadas a la función IO, incluso si la función 'cache'-wrapped se invoca concurrentemente desde diferentes subprocesos. ¿Cómo lo haría sin serializar pero sin permitirle ejecutar la "misma" acción más de una vez? – hvr

2

Aquí hay un código implementando más o menos lo que yo buscaba en mi pregunta original:

import   Control.Concurrent 
import   Control.Exception 
import   Data.Either 
import   Data.Map   (Map) 
import qualified Data.Map   as Map 
import   Prelude   hiding (catch) 

-- |Memoizing wrapper for 'IO' actions 
memoizeIO :: Ord a => (a -> IO b) -> IO (a -> IO b) 
memoizeIO action = do 
    cache <- newMVar Map.empty 
    return $ memolup cache action 

    where 
    -- Lookup helper 
    memolup :: Ord a => MVar (Map a (Async b)) -> (a -> IO b) -> a -> IO b 
    memolup cache action' args = wait' =<< modifyMVar cache lup 
     where 
     lup tab = case Map.lookup args tab of 
      Just ares' -> 
      return (tab, ares') 
      Nothing -> do 
      ares' <- async $ action' args 
      return (Map.insert args ares' tab, ares') 

El código anterior se basa en Async abstracción de Simon Marlow como se describe en Tutorial: Parallel and Concurrent Programming in Haskell:

-- |Opaque type representing asynchronous results. 
data Async a = Async ThreadId (MVar (Either SomeException a)) 

-- |Construct 'Async' result. Can be waited on with 'wait'. 
async :: IO a -> IO (Async a) 
async io = do 
    var <- newEmptyMVar 
    tid <- forkIO ((do r <- io; putMVar var (Right r)) 
       `catch` \e -> putMVar var (Left e)) 
    return $ Async tid var 

-- |Extract value from asynchronous result. May block if result is not 
-- available yet. Exceptions are returned as 'Left' values. 
wait :: Async a -> IO (Either SomeException a) 
wait (Async _ m) = readMVar m 

-- |Version of 'wait' that raises exception. 
wait' :: Async a -> IO a 
wait' a = either throw return =<< wait a 

-- |Cancels asynchronous computation if not yet completed (non-blocking). 
cancel :: Async a -> IO() 
cancel (Async t _) = throwTo t ThreadKilled 
+1

¡Muy bonito! ¿Prevé algún problema al reemplazar Data.Map con HashMap? –

+0

@ circular-ruin, no se puede pensar en ningún – hvr

+0

excelente, gracias :) –

Cuestiones relacionadas