如何在纯函数中跳过不必要的 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 的结果不为空时。
我知道我可以通过使用do
或when
来实现这一点。
鉴于使用太多do
s 违反了我使用 Haskell 的初衷,
我可能会和when
住在一起。
或者有更好的方法吗?更纯粹的方式?
这就是全部内容
大约两周前我开始学习 Haskell。 我不期待它的工作, 但只是被编程语言本身所吸引。 因为据我所知,它是“最纯粹的”。
起初,一切似乎都和我预期的一样好。
但后来我发现我必须在我的纯代码中写IO
s。
我花了很长时间才找到一种方法来控制不纯污染。
Applicative Functor 似乎是救星。
有了它,我可以将不纯的 IO “curry” 到我的纯函数中,
这节省了很多 do
、<-
和明确的 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 操作。 (纯)程序需要控制何时、是否以及以何种顺序运行putStrLn
和getLine
命令。
Haskell 只是明确了命令式编程,并允许非常容易地从中抽象出来,例如通过换出 monad 来运行模拟而不是实际的 IO。
我想要一种 Haskell 方式,一种纯粹的方式。
Haskell 方式是将纯应用逻辑与不纯边界分离到外界;非常像ports and adapters architecture。当然,这只能到此为止,对于像cachedOrFetched
这样的玩具示例,很难看出在哪里设置边界。但在更大的应用程序中,您通常会为业务逻辑中的“动作”提出自己的抽象,并且只处理它们而不是直接处理IO
。
【讨论】:
这看起来很有趣——使用Applicative
来避免IO
,更抽象的方式!你觉得ifNullDo
还能算纯函数吗?
IO
本身并不是不纯的。例如,putStrLn :: String -> 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?的主要内容,如果未能解决你的问题,请参考以下文章