【发布时间】:2012-02-26 15:19:35
【问题描述】:
只是好奇如何将以下函数重写为在程序的生命周期中只调用一次?
getHeader :: FilePath -> IO String
getHeader fn = readFile fn >>= return . take 13
从各种函数多次调用上述函数。 如果使用相同的参数调用函数,即如何防止重新打开文件,即。文件名?
【问题讨论】:
标签: haskell
只是好奇如何将以下函数重写为在程序的生命周期中只调用一次?
getHeader :: FilePath -> IO String
getHeader fn = readFile fn >>= return . take 13
从各种函数多次调用上述函数。 如果使用相同的参数调用函数,即如何防止重新打开文件,即。文件名?
【问题讨论】:
标签: haskell
我鼓励您寻求更实用的解决方案,例如通过预先加载您需要的标头并将它们传递到某些数据结构中,例如 Map。如果显式传递它不方便,您可以使用 Reader 或 State 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,每次都会给你一个新的引用。
【讨论】:
Map,所以你每次都会从文件中加载文本。你可以在一个地方创建MVar,然后传递它,但你也可以直接传递Map。
最简单的方法是在 main 的开头调用一次,然后将生成的 String 传递给所有其他需要它的函数:
main = do
header <- getHeader
bigOldThingOne header
bigOldThingTwo header
【讨论】:
您可以使用 monad-memo 包将任何 monad 包装到 MemoT 转换器中。备忘录表将在您的单子函数中隐式传递。然后使用startEvalMemoT将memoized 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 来解决此问题。完全按照您的描述进行操作的正确方法是创建一个包含 Maybe 的 IORef,最初包含 Nothing。然后创建一个 IO 函数来检查该值,如果它是 Nothing 则执行计算并将结果存储为 Just。如果它找到 Just 它会重用该值。
所有这些都需要传递 IORef 引用,这与传递字符串本身一样麻烦,这就是为什么每个人都直接建议只传递字符串本身,无论是显式还是隐式地使用 Reader monad。
unsafePerformIO 的合法用途非常少,这不是其中之一。不要走那条路,否则当 Haskell 一直在做意想不到的事情时,你会发现自己在与它战斗。每个使用 unsafePerformIO 作为“聪明把戏”的解决方案总是以灾难性的方式结束(包括 readFile)。
旁注 - 您可以简化 getHeader 函数:
getHeader path = fmap (take 13) (readFile path)
或者
getHeader path = take 13 <$> readFile path
【讨论】:
MVar 来获得正确的语义。