2012-02-26 13 views
5

è solo curioso di sapere come riscrivere la seguente funzione per essere chiamata solo una volta durante la vita del programma?Funzione IO memorizzata?

getHeader :: FilePath -> IO String 
getHeader fn = readFile fn >>= return . take 13 

La funzione precedente viene richiamata più volte da varie funzioni. Come impedire la riapertura del file se la funzione viene chiamata con lo stesso parametro, es. nome del file ?

risposta

7

Vi incoraggio a cercare una soluzione più funzionale ad esempio caricando le intestazioni di cui hai bisogno in anticipo e facendole passare in una struttura dati come ad esempio uno Map. Se il passaggio esplicito è scomodo, è possibile utilizzare un trasformatore monad Reader o State per gestirlo direttamente.

Detto questo, è possibile eseguire ciò nel modo desiderato utilizzando unsafePerformIO per creare un riferimento mutabile globale per contenere la struttura dei dati.

import Control.Concurrent.MVar 
import qualified Data.Map as Map 
import System.IO.Unsafe (unsafePerformIO) 

memo :: MVar (Map.Map FilePath String) 
memo = unsafePerformIO (newMVar Map.empty) 
{-# NOINLINE memo #-} 

getHeader :: FilePath -> IO String 
getHeader fn = modifyMVar memo $ \m -> do 
    case Map.lookup fn m of 
    Just header -> return (m, header) 
    Nothing  -> do header <- take 13 `fmap` readFile fn 
         return (Map.insert fn header m, header) 

Ho usato un MVar qui per sicurezza thread. Se non ne hai bisogno, potresti essere in grado di scappare usando invece IORef.

Inoltre, si noti il ​​pragma NOINLINE su memo per garantire che il riferimento venga creato solo una volta. Senza questo, il compilatore potrebbe inserirlo in getHeader, dandovi un nuovo riferimento ogni volta.

+0

Grazie. Se desidero evitare unsafePerformIO in modo che 'memo' restituisca un'azione IO, funzionerà ancora? Immagino che ora verrà chiamato ogni volta che deve essere valutato. –

+1

@DavidUnric: No, la memoizzazione non funzionerebbe allora, dal momento che si otterrebbe una nuova 'Map' vuota ogni volta, quindi dovresti caricare il testo dal file ogni volta. Potresti creare il 'MVar' in un posto e poi passarlo, ma potresti anche passare direttamente la' Mappa'. – hammar

+1

hammar> Thx per la spiegazione. Ho rifattorizzato il codice in modo che l'intestazione venga ora passata dalla prima funzione IO appena prima che venga utilizzata. Anche se segnerò il tuo asnwer in quanto mostra un altro approccio di cui non ero a conoscenza. –

4

La cosa più semplice è quella di chiamare solo una volta all'inizio del main e passare il conseguente String intorno a tutte le altre funzioni che ne hanno bisogno:

main = do 
    header <- getHeader 
    bigOldThingOne header 
    bigOldThingTwo header 
+0

Grazie.Sapevo su questa strada, ma trovo un po 'scomodo, perché l'intestazione deve essere passata a tutte le funzioni concatenate anche se non vi è alcun uso di questo parametro per loro. –

+1

@DavidUnric: dovresti esaminare le monade dei lettori. Risolvono esattamente questo problema. – hammar

2

Non si dovrebbe usare unsafePerformIO per risolvere questo problema. Il modo corretto di fare esattamente quello che descrivi è creare uno IORef che contiene un Forse, contenente inizialmente Niente. Quindi crei una funzione IO che controlla il valore ed esegue il calcolo se è Nothing e memorizza il risultato come Just. Se trova un Just riutilizza il valore.

Tutto questo richiede passando intorno il riferimento IOREF, che è altrettanto ingombrante come passando attorno alla stringa stessa, che è il motivo per cui tutti direttamente raccomanda solo di passaggio intorno alla stringa stessa, né esplicitamente né implicitamente utilizzando la monade Reader.

Ci sono pochissimi usi legittimi per unsafePerformIO e questo non è uno di questi. Non seguire questa strada, altrimenti ti troverai a combattere Haskell quando continua a fare cose inaspettate. Ogni soluzione che usa unsafePerformIO come un "trucco intelligente" finisce sempre in modo catastrofico (e questo include readFile).

Nota a margine - è possibile semplificare la vostra funzione getHeader:

getHeader path = fmap (take 13) (readFile path) 

O

getHeader path = take 13 <$> readFile path 
4

È possibile utilizzare monad-memo pacchetto per avvolgere ogni monade in MemoT trasformatore. La tabella dei memo verrà passata implicitamente in tutte le tue funzioni monadiche. Quindi utilizzare startEvalMemoT per convertire monade memoized in azioni ordinarie IO:

{-# LANGUAGE NoMonomorphismRestriction #-} 

import Control.Monad.Memo 

getHeader :: FilePath -> IO String 
getHeader fn = readFile fn >>= return . take 13 

-- | 'memoized' version of getHeader 
getHeaderm :: FilePath -> MemoT String String IO String 
getHeaderm fn = memo (lift . getHeader) fn 

-- | 'memoized' version of Prelude.print 
printm a = memo (lift . print) a 

-- | This will not print the last "Hello" 
test = do 
    printm "Hello" 
    printm "World" 
    printm "Hello" 

main :: IO() 
main = startEvalMemoT test