编写灵活的“字符串提取器”

时间:2019-08-06 06:31:16

标签: haskell io monads

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

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的字符串“ inside”。因此,我觉得这种方法可能不可行。有没有更合理的方式来做这样的事情?

编辑:为了帮助您更清楚地了解我要实现的目标,这是应用程序中的实际功能:

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 <command>""Commands:"进行硬编码。我想做的是向函数添加另一个参数,该函数的工作是使用键来获取那些字符串。但是我希望可以将“ String fetcher”与不同的实现方式互换(可能涉及或不涉及IO)。

2 个答案:

答案 0 :(得分:0)

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

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,也许不是。

您使用的不是liftIO,而是更改其他功能,或者更改其他功能的定义

答案 1 :(得分:0)

摘要

此答案的增长时间超出了我的预期,因此我认为需要进行总结。对于这样一个普遍的问题以及含糊其词的意图,解决方案的范围相当广泛。我鼓励不要将代码复杂化到绝对不必要的程度,而应着眼于解决方案的可读性。使用现有的Reader之类的原语也使该代码对于其他阅读它的人来说并不那么令人惊讶。如果这还不够,那么最后的解决方案将在任何情况下提供最大的灵活性。


详细信息

您必须了解StringFetcher只是可以提供所需值的计算,但是如果不实际评估该计算,您将无法获得该值。

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

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

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

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

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

问题是,如果您假设某处将存在某个访存程序的IO变体,那么跳过所有这些箍是很愚蠢的。如果是这种情况,您可以简单地假设它始终需要IO,而对于预定义的字符串列表,它只是不会使用它。这将大大简化实施过程,而不会造成任何实际损失。

如果您对基于Fetcher本身的Fetcher更改(此处关注的)代码的实际类型进行了 insist 操作(例如,使用纯fetcher的usage是纯净的,而{{ 1}}使用IO提取程序是IO 1 ),则需要一个Transmoner Monad类:

usage

如果usage :: forall m. => (MonadStringFetcher m, MonadIO) -> m () 仍在使用IO(当然也与我之前说的一样),则此示例无疑是很愚蠢的,但是如果没有使用它,则不必这样做。

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


1 此处讨论的是从提取程序继承来的MonadReader的要求,而不是像我使用的usage一样。您可能会在这里考虑两个不同的putStrLn