【问题标题】:Memoized IO function?Memoized IO 功能?
【发布时间】:2012-02-26 15:19:35
【问题描述】:

只是好奇如何将以下函数重写为在程序的生命周期中只调用一次?

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

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

【问题讨论】:

    标签: haskell


    【解决方案1】:

    我鼓励您寻求更实用的解决方案,例如通过预先加载您需要的标头并将它们传递到某些数据结构中,例如 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,每次都会给你一个新的引用。

    【讨论】:

    • 谢谢。如果我想避免 unsafePerformIO 所以'备忘录'会返回一个 IO 动作这仍然有效吗?我想现在每次必须对其进行评估时都会调用它。
    • @DavidUnric:不,记忆化不会起作用,因为你每次都会得到一个新的空Map,所以你每次都会从文件中加载文本。你可以在一个地方创建MVar,然后传递它,但你也可以直接传递Map
    • hammar> 谢谢你的解释。我确实重构了代码,所以现在头在使用之前从第一个 IO 函数传递过来。虽然我要标记你的答案,因为它显示了另一种我不知道的方法。
    【解决方案2】:

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

    main = do
        header <- getHeader
        bigOldThingOne header
        bigOldThingTwo header
    

    【讨论】:

    • 谢谢。我知道这种方式,但觉得有点不舒服,因为 header 必须传递给所有链式函数,即使它们没有使用这个参数。
    • @DavidUnric:你应该看看 reader monads。他们正好解决了这个问题。
    【解决方案3】:

    您可以使用 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
    

    【讨论】:

      【解决方案4】:

      您不应使用 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 来获得正确的语义。
      猜你喜欢
      • 1970-01-01
      • 2016-09-28
      • 1970-01-01
      • 1970-01-01
      • 2017-08-28
      • 2019-05-25
      • 1970-01-01
      • 2012-08-17
      相关资源
      最近更新 更多