如何在纯函数中跳过不必要的 IO?

Posted

技术标签:

【中文标题】如何在纯函数中跳过不必要的 IO?【英文标题】:How to skip unnecessary IOs in pure functions? 【发布时间】:2022-01-10 12:55:44 【问题描述】:

更新

这个问题有一个额外的限制, 也就是尽量避免IO

这个限制最初是放在我问题的最后, 这似乎很难被注意到。

我实际上知道如何在 Haskell 中实现我的目标, 就像我知道如何在其他命令式编程语言中做到这一点一样——命令式的方式。

但我没有使用任何其他命令式编程,对吗? 我正在使用 Haskell。 我想要一种 Haskell 方式,一种纯粹的方式。

我通过将额外限制重新定位在相对显眼的位置来重新组织我的问题。 那是我的错。 非常感谢这些快速响应。

原始问题

main :: IO ()
main =
    putStrLn =<< cachedOrFetched
        <$> getLine
        <*> getLine

cachedOrFetched :: String -> String -> String
cachedOrFetched cached fetched =
    if not $ null cached
    then cached
    else fetched

上面的代码执行了两次IO。 但期望的行为是跳过第二个 IO 当第一个 IO 的结果不为空时。

我知道我可以通过使用dowhen 来实现这一点。 鉴于使用太多dos 违反了我使用 Haskell 的初衷, 我可能会和when住在一起。

或者有更好的方法吗?更纯粹的方式?

这就是全部内容

大约两周前我开始学习 Haskell。 我不期待它的工作, 但只是被编程语言本身所吸引。 因为据我所知,它是“最纯粹的”。

起初,一切似乎都和我预期的一样好。 但后来我发现我必须在我的纯代码中写IOs。 我花了很长时间才找到一种方法来控制不纯污染。 Applicative Functor 似乎是救星。

有了它,我可以将不纯的 IO “curry” 到我的纯函数中, 这节省了很多 do&lt;- 和明确的 IO 符号。 但是,我遇到了这个问题 - 我无法跳过纯函数中不必要的 IO。

再次,大量搜索和阅读。 不幸的是,到目前为止还没有令人满意的答案。

参考文献

Avoiding IO How to get rid of IO

【问题讨论】:

这能回答你的问题吗? ***.com/q/47120384/126014 @MarkSeemann 从技术上讲,确实如此。我最欣赏的关于 Applicative Functor 的一件事是它提供的优雅。我需要检查我是否仍然可以使用帖子中提到的技术保持优雅。无论如何,非常感谢! 【参考方案1】:

要跳过 IO,您必须告诉cachedOrFetched 您正在执行 IO,而不是在 读取这两行之后将两个字符串传递给它。只有这样,它才能有条件地运行第二个getLine IO 操作。当然cachedOrFetched不一定需要和IO打交道,anymonad都可以:

main :: IO ()
main = putStrLn =<< cachedOrFetched getLine getLine

cachedOrFetched :: Monad m => m String -> m String -> m String
cachedOrFetched first second = do
    cached <- first
    if not $ null cached
        then return cached
        else second

我可能会写

main :: IO ()
main = getLine >>= ifNullDo getLine >>= putStrLn

ifNullDo :: Applicative f => f String -> String -> f String
ifNullDo fetch cached = 
    if not $ null cached
        then pure cached
        else fetch

我没有使用任何其他命令式编程,对吧?

是的,你是。如果您正在编写 main 函数或使用 IO 类型,那么您正在编写一个命令式程序,一个接一个的 IO 操作。 (纯)程序需要控制何时、是否以及以何种顺序运行putStrLngetLine 命令。

Haskell 只是明确了命令式编程,并允许非常容易地从中抽象出来,例如通过换出 monad 来运行模拟而不是实际的 IO。

我想要一种 Haskell 方式,一种纯粹的方式。

Haskell 方式是将纯应用逻辑与不纯边界分离到外界;非常像ports and adapters architecture。当然,这只能到此为止,对于像cachedOrFetched 这样的玩具示例,很难看出在哪里设置边界。但在更大的应用程序中,您通常会为业务逻辑中的“动作”提出自己的抽象,并且只处理它们而不是直接处理IO

【讨论】:

这看起来很有趣——使用Applicative 来避免IO,更抽象的方式!你觉得ifNullDo还能算纯函数吗? IO 本身并不是不纯的。例如,putStrLn :: String -&gt; IO () 是一个纯函数:给一个字符串,它总是返回相同的IO () 值。杂质来自 Haskell 运行时对 IO 值的执行【参考方案2】:

这是您的代码在使用do notation 编写时的样子:

main :: IO ()
main = do
  cached <- getLine
  fetched <- getLine
  putStrLn $ cachedOrFetched cached fetched

您可以看到,在调用 cachedOrFetched 之前,这两个 IO 操作都已完成。你可以像这样在main 中做所有事情:

main :: IO ()
main = do
  cached <- getLine
  if not $ null cached
    then putStrLn cached
    else do
      fetched <- getLine
      putStrLn fetched

原因是因为cachedOrFetched 必须能够访问这两个值(它们是其调用的参数!)才能决定选择哪个。

【讨论】:

以上是关于如何在纯函数中跳过不必要的 IO?的主要内容,如果未能解决你的问题,请参考以下文章

如何在函数调用中跳过可选参数?

如何在推送事件中跳过 GitHub Actions 作业?

如何在循环中跳过项目

如何根据某些条件在 MSSQL 游标中跳过一行(迭代)?

如何在 Moshi 中跳过 JSON 属性?

如何在 C++ 中跳过空格