记忆化IO功能



只是想知道如何重写下面的函数,使其在程序的生命周期中只被调用一次?

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

上面的函数是从各种函数中调用多次的。如果使用相同的参数(即文件名)调用函数,如何防止重新打开文件?

我鼓励您寻求一个功能更强大的解决方案,例如,通过预先加载所需的标头,并在一些数据结构中传递它们,例如Map。如果显式传递它不方便,可以使用ReaderState monad转换器来处理它。

也就是说,您可以使用unsafePerformIO创建一个全局可变引用来保存您的数据结构,从而以您想要的方式实现这一点。

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) 

为了线程安全,我在这里使用了MVar。如果你不需要,你可以使用IORef

另外,请注意memo上的NOINLINE杂注,以确保引用只创建一次。如果没有这一点,编译器可能会将其内联到getHeader中,每次都会给您一个新的引用。

最简单的方法是在main的开头调用它一次,然后将生成的String传递给所有其他需要它的函数:

main = do
    header <- getHeader
    bigOldThingOne header
    bigOldThingTwo header

您可以使用monad memo包将任何monad封装到MemoT转换器中。备忘录表将通过您的一元函数隐式传递。然后使用startEvalMemoT将存储的monad转换为普通的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

您不应该使用unsafePerformIO来解决此问题。正确的方法是创建一个IORef,它包含一个Maybe,最初包含Nothing。然后创建一个IO函数,该函数检查值,如果值为Nothing则执行计算,并将结果存储为Just。如果它找到Just,它将重用该值。

所有这些都需要传递IORef引用,这和传递字符串本身一样麻烦,这就是为什么每个人都直接建议只传递字符串本身,无论是显式还是隐式地使用Reader-monad。

unsafePerformIO的合法用途非常少,这不是其中之一。不要走这条路,否则当哈斯克尔不断做意想不到的事情时,你会发现自己在和它斗争。每一个使用unsafePerformIO作为"聪明技巧"的解决方案总是以灾难性的方式结束(包括readFile)。

附带说明-您可以简化getHeader函数:

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

getHeader path = take 13 <$> readFile path

最新更新