【问题标题】:How to build a Monad with non-IO "exterior", but IO "interior"?如何构建一个非 IO“外部”但 IO“内部”的 Monad?
【发布时间】:2020-06-29 04:26:06
【问题描述】:

我正在尝试编写一个呈现一些 HTML 的 Monad,同时跟踪(和缓存)一些特定的函数调用。这是我尝试过的:

data TemplateM a = TemplateM
  { templateCache :: ![(Text, Text)]
  , templateResult :: !(IO a)
  }

这是我打算如何使用它:

renderCached :: Text -> TemplateM Text
renderCached k = 
  -- lookup templateCache from the monadic context, if it lacks the key, 
  -- then fetch the key from an external data source (which is where the
  -- "IO interior" comes from, and store it in templateCache (monadic context)

值得注意的是,我不希望通过liftliftIO 等在TemplateM 中执行任意IO 操作。在TemplateM 中应该发生的唯一IO 是通过renderCached 函数从缓存中获取一些东西。

我能够为此定义 FunctorApplicative 实例,但完全被 Monad 实例卡住了。以下是我的成绩:

instance Functor TemplateM where
  {-# INLINE fmap #-}
  fmap fn tmpl = tmpl{templateResult=fmap fn (templateResult tmpl)}

instance Applicative TemplateM where
  {-# INLINE pure #-}
  pure x = TemplateM
    { templateCache = []
    , templateResult = pure x
    }

  {-# INLINE (<*>) #-}
  fn <*> f =
    let fnCache = templateCache fn
        fnFunction = templateResult fn
        fCache = templateCache f
        fResult = templateResult f
    in TemplateM { templateCache = fnCache <> fCache
                 , templateResult = fnFunction <*> fResult
                 }

有没有办法为此编写Monad 实例而不将IO 内部暴露给外界?

【问题讨论】:

  • 您无需担心执行任意 IO 操作,因为您的 monad 中有一个 IO。只是不要派生 MonadIO 类(也不要导出 TemplateM 数据构造函数),你会没事的。
  • 至于编写你的 monad 实例,尝试阐明你是如何被卡住的可能是有益的。你不能写这个实例对我来说并不奇怪——你的类型的结构本质上是一对,所以Writer是你的灵感。但是WriterT 将这对放在 inside monad 中,而不是像您所做的那样放在外面。此外,由于您打算从缓存中读取,而不仅仅是写入它,因此 Writer(又名配对)将不支持您需要的内容。也许你需要StateT
  • 如果您对变形器还不满意,我建议您尝试自己实现一个状态单子newtype State s a = State (s -&gt; (a,s)),使用get :: State s sput :: s -&gt; State s (),以感受状态传递,然后回到这个,使用你从那个模式中学到的东西。
  • 我同意卢克的观点。您的TemplateM 将缓存作为其成员之一,这意味着一元函数a -&gt; TemplateM b 返回 缓存作为输出。您正在寻找的是一个将缓存作为输入(并可能对其进行修改)的 monad,即StateT

标签: haskell monads


【解决方案1】:

正如其他人所建议的,这里的基本解决方案是使用StateT。由于您不需要将IORef 存储在数据结构中或在线程之间共享它,因此您可以完全消除它。 (当然,如果情况发生了变化,而您确实最终希望在多个并发线程之间共享状态,那么您将不得不重新考虑这个选择。)

import Control.Monad.State.Strict
import Data.Text (Text)
import Data.Tuple (swap)

newtype TemplateM a = TemplateM {unTemplateM :: StateT [(Text, Text)] IO a}
  deriving (Functor, Applicative, Monad)

renderCached :: Text -> TemplateM Text
renderCached k = TemplateM $ do
  v <- pure $ "rendered template for " <> k
  modify ((k, v) :)
  pure v

runTemplateM :: [(Text, Text)]
             -> TemplateM a
             -> IO ([(Text, Text)], a)
runTemplateM initialCache x = fmap swap $ flip runStateT initialCache (unTemplateM x)

不用说,像这样的缓存几乎肯定应该存储为一个不是列表的结构。一个有希望的选择是使用text-trie,这是一种专门为此目的设计的数据结构 Wren Romano。您也可以考虑使用HashMap 甚至Map

【讨论】:

    【解决方案2】:

    我已经在ReaderT 的基础上制定了一个解决方案,但我真的很想让我最初的想法发挥作用:

    newtype TemplateM a = TemplateM { unTemplateM :: ReaderT (IORef [(Text, Text)]) IO a } deriving (Functor, Applicative, Monad)
    
    renderCached :: Text -> TemplateM Text
    renderCached k = TemplateM $ do
      -- this is just dummy code. The actual cache lookup has not
      -- been implemented, but the types align
      v <- pure $ "rendered template for " <> k
      cacheRef <- ask
      atomicModifyIORef' cacheRef (\x -> ((k, v):x, ()))
      pure v
    
    runTemplateM :: [(Text, Text)] 
                 -> TemplateM a 
                 -> IO ([(Text, Text)], a)
    runTemplateM initialCache x = do
      initialCacheRef <- newIORef initialCache
      (flip runReaderT) initialCacheRef $ do
        res <- unTemplateM x
        ref <- ask
        finalCache <- readIORef ref
        pure (finalCache, res)
    

    【讨论】:

      猜你喜欢
      • 2018-03-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-26
      • 2010-12-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多