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