有没有办法在 Haskell 中链接纯函数和 IO 函数?

Is there a way to chain pure and IO functions in Haskell?

提问人:rex 提问时间:2/28/2023 更新时间:2/28/2023 访问量:106

问:

我目前正在学习Haskell。我在使用纯函数编写一些 IO 函数时遇到了困难(我不确定使用我正在使用的结构是否可以做到这一点)。

我在下面包含了复制我实际问题的完整玩具代码。

import Data.Function ((&))

data MyType = I Int | S String | B Bool deriving (Show)

debugPrint :: Bool -> MyType -> IO (Either String MyType)
debugPrint flag mt =
  if flag
    then do
      print mt
      return (Left "Debug mode: short circuiting further steps")
    else return (Right mt)

strToInt :: String -> Either String MyType
strToInt s =
  if sLen >= 10
    then Left "String is too long"
    else Right (I sLen)
  where
    sLen = length s

intToBool :: MyType -> Either String MyType
intToBool (I i) =
  if i <= 0
    then Left "Integer is <= 0"
    else Right (B (i > 5))
intToBool _ = undefined

boolToStr :: MyType -> Either String MyType
boolToStr (B b) =
  if not b
    then Left "True expected"
    else Right (S "Final output")
boolToStr _ = undefined


finalHandler :: Either String MyType -> IO ()
finalHandler (Left err) =  putStrLn $ "ERROR: " ++ err
finalHandler (Right myType) = print myType

main :: IO ()
main = do
  s <- getLine
  strToInt s
  (_) debugPrint True
  (_) intToBool
  (_) debugPrint True
  (_) boolToStr
  (_) debugPrint True
  & finalHandler

我对上述片段有 3 个问题。

  • 我想知道是否有任何预定义的运算符可用于链接 中的上述函数。main
  • 有没有更好的方法来表示此代码中的错误流?如果中间输出中有任何值,则控件应跳转到并让它处理该值。LeftfinalHandler
  • 我正在处理的主要问题是用 . 我想将值传递给并使用参数有条件地打印它,如果未打印,则返回相同的值。debugPrintstrToInt sRightdebugPrintflag

请忽略未定义的模式匹配,因为它们在我的实际代码中处理。

我试过使用 和 。但我仍然无法了解如何更改抽象以改善代码片段中的数据流。bind(>=>)liftIO

Haskell IO 组成

评论

2赞 lsmor 2/28/2023
在任何 Monad 中(当然包括),你可以用 调用一个纯函数。例如,你应该写 .现在,该变量在函数中可用。IOletstrToInt slet my_int = strToInt smy_intmain
1赞 lsmor 2/28/2023
现在,整个错误处理。有一种更好的方法可以做到这一点,但是由于您正在学习,因此我建议您首先进行模式匹配,并且每次调用函数都会返回。如果你这样做,你可能最终会得到一个非常嵌套的单调函数。之后,您应该阅读有关单子的信息,这与您描述的模式无关:“在短路之后”LeftRightEitherEitherLeft
1赞 lsmor 2/28/2023
提示:既然您已经知道了,请尝试使用该运算符;)来探索错误处理问题(>=>)

答:

2赞 leftaroundabout 2/28/2023 #1

有几种方法可以做到这一点,包括显式模式匹配。当然,您始终可以为任何任务定义您的 on 运算符。两者也许都是一个很好的练习。

但是,我认为惯的解决方案是让所有工作都由 monad 链接完成。

我不知道你对单子的了解有多深;我只想说,“mon”指的是一种奇异感。可以这么说,这意味着一切都是一体的。好吧,这是一个非常模糊的说法;在实践中,您应该记住的是,要单元地链接函数/操作,结果(即在所有人的右侧)需要在所有函数/操作中具有相同的外部类型构造函数。例如,您已经知道这一点 和 ,但它与任何其他单子一样有效。->IO IntIO ()

所以这就是你在这里需要实现的,然后你可以只使用普通的 / do 块语法。首先,你需要弄清楚什么是合适的单子,你可以将所有这些操作投射到其中。最简单的选择实际上是只使用 ,毕竟您可以在其中抛出和捕获异常。你可以要求你所有的函数都加入进来——这同样是一个有用的练习,但不是惯用的。>>=IOIO

一个更受约束的选项是将错误情况作为显式 monad 转换器放在 之上。您可能希望它被称为 ,它确实存在,但更常用的版本称为 ExceptT。它仍然是一样的,从字面上看只是一个 newtype 包装器,这是一种通用形式,例如您在代码中拥有的。IOEitherTm (Either e a)IO (Either String MyType)

这个新类型包装器的要点是避免嵌套在(两个单子)内部,而是将它们组合成一个单子,因此可以再次使用旧的组合器。Either ...IO ...

您仍然需要开始的是一种将结果提升到 monad 的方法。库有 except,它正是这样做的。Either ...ExceptTtransformers

对于 ,我将更改其签名中的类型权限:debugPrint

debugPrint :: Bool -> MyType -> ExceptT String IO MyType

对于其他人,我不会更改类型,而是在使用它们时将它们包装起来。exceptmain

我将实际代码作为练习。