【问题标题】:Writing a flexible "string fetcher"编写灵活的“字符串提取器”
【发布时间】:2019-08-06 06:31:16
【问题描述】:

我正在尝试编写一个模块,该模块允许从任意来源(例如,内存列表,或文件,数据库......等等)获取字符串:

type StringFetcher m = String -> m String

listStringFetcher :: [(String, String)] -> StringFetcher Maybe
listStringFetcher list key = fst <$> (listToMaybe $ filter ((key ==) . fst) list)

fileStringFetcher :: FilePath -> StringFetcher (MaybeT IO)
fileStringFetcher fp key = undefined

然后我想当我在我的应用程序中使用它时,我可以拥有如下功能:

usage :: (MonadIO m) => StringFetcher m -> m ()
usage fetch = (fetch "usage") >>= (liftIO . putStrLn)

但后来我有点卡住了,当我尝试运行 usage (listStringFetcher [("usage", "asdf")]) 时,我得到一个“由于使用 usage 而导致(MonadIO Maybe)没有实例”错误。我不太确定我应该如何访问StringFetcher 的“内部”字符串。所以我觉得这种方法可能不可行。有没有更合理的方法来做这样的事情?

编辑:为了更清楚地说明我想要实现的目标,这是我的应用程序中的一个实际功能:

usage :: [Command] -> ExceptT String IO String
usage c =  pure . ununlines $ [
  "usage: xyz <command>",
  "Commands:",
  unlines $ fmap (\x ->"\t" ++ name (x :: Command) ++ ": " ++ description (x :: Command)) c
  ]

我不想像这样硬编码字符串"usage: xyz &lt;command&gt;""Commands:"。我想做的是向函数添加另一个参数,其工作是使用键获取这些字符串。但我希望可以将“String fetcher”与不同的实现(可能涉及也可能不涉及 IO)互换。

【问题讨论】:

  • I'm not quite sure how I'm supposed to get access to the string "inside" of the StringFetcher. - 如果我对您的理解正确,那么您通常不能。没有一般的方法可以访问 Monad “内部”的值。我不太确定fetch 的意图是什么,我知道硬编码的"usage" 只是为了说明,但是除非这是您将使用几次的一般模式,否则编写函数是没有意义的,在在哪种情况下该怎么做取决于你如何使用它,这对我来说并不清楚。
  • PS in listStringFetcher 我想你希望外部的fstsnd
  • 这是我想在多个地方使用的一般模式。这个想法是从“任何地方”获取字符串以在程序中使用。我的想法被赋予了 StringFetcher 的类型,我应该可以将它与任何 monad 一起使用,并在该 monad 中使用 &gt;&gt;=(和朋友)对字符串做一些有用的事情。
  • 明确地说,我根本不接受这种方法。或者,也许我在想整个事情是完全错误的。目标是拥有可以提供任意 StringFetcher 的函数。

标签: haskell io monads


【解决方案1】:

总结

这个答案比我预期的要长,所以我认为总结一下。对于这样一个笼统的问题和相当模糊的表达意图,解决方案的范围相当广泛。我鼓励不要使代码过于复杂,而是专注于手头解决方案的可读性。使用像Reader 这样的现有原语也可以让其他阅读代码的人不那么惊讶。如果这还不够,最后的解决方案可以在任何情况下提供最大的灵活性。


详情

您必须了解,您的 StringFetcher 只是一种可能提供您想要的值的计算,但如果不实际评估该计算,您将无法获得该值。

您直接遇到的问题是,根据所使用的 StringFetcher 的类型,评估的上下文将取决于传递的值,这意味着原则上,您不能在每种情况下都使用每个 StringFetcher。如果您假设这样一个部分应用的 fetcher,这更容易可视化:

command :: forall m. StringFetcher m -> m String
command fetch = fetch "command"

你还没有真正在这里做任何工作,你只是把工作推得更远。剩下的 m String 需要提取 m。然后,您需要将 m 作为您签名的一部分,有点像:

usage :: forall m. MonadIO m => StringFetcher m -> m ()
usage fetch = do
    cmdStr <- command fetch
    liftIO . putStrLn $ cmdStr

另一种方法是避免将 fetcher 作为参数传递,而是将其作为上下文的一部分。如果你知道如何使用ReaderT,这应该是显而易见的,但无论如何,对其他人来说......

假设我们的字符串存储在Map String String 中。然后我们可以利用Reader 明确地依赖这些字符串:

import qualified Data.Map as Map
type Strings = Map.Map String String 

usage :: Reader Strings ()
usage = do
    cmdStr <- fromMaybe "no description" . Map.lookup "command" <$> ask
    -- do whatever

由于我们不能在这个特定的函数中做很多“任何事情”,我们需要ReaderT 来增加一点:

usage :: ReaderT Strings IO ()
usage = do
    cmdStr <- fromMaybe "no description" . Map.lookup "command" <$> ask
    liftIO . putStrLn $ cmdStr

快!我们现在对Strings 和 IO 都有明确的依赖关系,并且可以根据需要从外部提供字符串。这就是魔法发生的地方:

main :: IO ()
main = do
    let strings = Map.fromList [("command", "this is a command")]
    runReaderT usage strings

对于您的情况,您可能只想使用 askForCommand 创建自己的类型,而不是 Reader Strings,这样可以稍微缩短此代码并使其更加明确。

问题是,如果您认为某处将存在 fetcher 的 IO 变体,那么跳过所有这些障碍有点愚蠢。如果是这种情况,您可能会简单地假设它总是需要 IO,并且在预定义字符串列表的情况下它只是不会触及它。这将大大简化实现,而不会造成任何实际损失。

如果您坚持基于 Fetcher 本身使用 Fetcher 更改代码的实际类型(例如,usage 使用纯 fetcher 是纯的,而 usage 使用IO fetcher 是 IO1),你需要一个 Transformer Monad 类:

usage :: forall m. => (MonadStringFetcher m, MonadIO) -> m ()

如果usage 无论如何都在使用 IO,这个例子无疑是非常愚蠢的(我之前也说过),但如果它没有,它就不必。

同样,MonadReader 类可以作为一个很好的模板来实现它。这种方法非常类似于“效果”(如在 Idris 中所见),并允许您直接从预期功能构建上下文类型。这是编写此类内容最灵活的方式,但一旦开始添加更多内容,它也是最耗时且很快变得复杂的方式。


1 这里讨论的是从 Fetcher 继承的 usage 的要求,而不是像我使用的 putStrLn 那样的内部结构。您可能会在这里想到两个不同的IOs。

【讨论】:

  • 读者似乎认为这可能是我想要的,但我想我在做一些愚蠢的事情,因为 GHC 似乎不喜欢usage :: ReaderT IO Strings () - “期待 IO 的一个或多个参数”编辑:看起来参数只是错误的方式,ReaderT String IO () 有效。
  • 是的,阅读器 monad 转换器看起来就像我想要的那样。我从来没有读过太多关于 reader monad 的内容,是时候研究一下了。谢谢!
【解决方案2】:

我知道它不会直接回答您的问题,但可以作为指南。您的函数示例的具体实例是:

usage ::  StringFetcher IO -> IO ()
usage fetch = (fetch "usage") >>= ((liftIO . putStrLn) :: String -> IO ())

这会导致错误:

error:
    • Couldn't match type ‘Maybe String’ with ‘IO String’
      Expected type: StringFetcher IO
        Actual type: StringFetcher Maybe
    • In the first argument of ‘usage’, namely
        ‘(listStringFetcher [("usage", "asdf")])’
      In the expression: usage (listStringFetcher [("usage", "asdf")])
      In an equation for ‘it’:
          it = usage (listStringFetcher [("usage", "asdf")])

意思是你当前对listStringFetcher 的定义永远不会与liftIO 一起工作,因为它的类型很好,IO,而不是Maybe。

要么使用其他函数,而不是liftIO,要么更改其他函数的定义

【讨论】:

  • 是的,我想这让我明白我的想法是错误的,而且这种方法无法按照我想要的方式工作。也许我需要考虑类型类什么的......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-02-07
  • 1970-01-01
  • 1970-01-01
  • 2011-02-12
  • 1970-01-01
  • 2011-02-26
  • 1970-01-01
相关资源
最近更新 更多