有没有一种方法可以从IO monad中解包类型?

3
我有一个非常简单的函数
import qualified Data.ByteString.Lazy as B

getJson :: IO B.ByteString
getJson = B.readFile jsonFile

readJFile :: IO (Maybe Response)
readJFile =  parsing >>= (\d ->
             case d of
                 Left err -> return Nothing
                 Right ps -> return (Just ps))
    where parsing = fmap eitherDecode getJson :: IO (Either String Response)

jsonFile是我硬盘上文件的路径(抱歉没有使用do-notation,但我发现这样更清晰)。

我的问题是:是否有一种方法可以摆脱 IO 部分,以便我可以仅使用bytestring处理?

我知道您可以在某些单子上进行模式匹配,例如 Either Maybe ,以获取它们的值,但您是否可以使用 IO 进行类似的操作?

或者换句话说:是否有一种方法使 readJFile 在没有IO的情况下返回 Maybe Response


我不是在谈论内置的东西,我只是想要一种方法来做到这一点,也许我可以自己构建;现在我的代码看起来,我必须通过5个函数拖着那个IO单子,公平地说,它会使代码变得混乱不堪,特别是考虑到我只在那一个函数中执行IO操作而没有其他地方。 - Electric Coffee
1
如果其他函数不涉及IO操作,那么它们就不应该在IO中。让它们依赖于bytestring(即类型为B.ByteString -> ...),然后使用单个IO操作将读取和处理组合起来。 - kosmikus
更新了代码以便更加清晰明了。 - Electric Coffee
1
readJFile这个函数涉及到IO操作,因此它必须有一个IO类型。但是你可以按照我之前所说的,使用bytestring对其进行抽象处理。 - kosmikus
但是它不执行io操作,getJson函数执行io操作,readJFile函数只是解释接收到的数据。 - Electric Coffee
显示剩余3条评论
4个回答

4
你永远不需要在任何函数中 "drag a monad",除非所有函数实际上都需要进行 IO 操作。只需使用 fmap(或者 liftM / liftM2 等)将整个链提升到 Monad 中。
例如,
f1 :: B.ByteString -> K
f2 :: K -> I
f3 :: K -> J
f4 :: I -> J -> M

你的整个事情应该是这样的

m :: M
m = let k = "f1 getJson"
    in f4 (f2 k) (f3 k)

您可以简单地执行以下操作。
m = fmap (\b -> let k = f1 b
                in f4 (f2 k) (f3 k) )
    getJson

顺便提一下,使用do符号可能会更美观:

m = do
  b <- getJson
  return $ let k = f1 b
           in f4 (f2 k) (f3 k)

关于您的编辑和问题

有没有办法让readJFile在不使用IO的情况下返回Maybe Response

不行,这是不可能的,因为readJFile确实需要进行IO操作。这就是IO存在的全部意义!(当然,像Ricardo所说,可以使用unsafePerformIO,但这绝对不是它的有效应用。)

如果您觉得在IO中解包Maybe值并带有括号的签名很笨重,那么您可以看看MaybeT转换器

readJFile' :: MaybeT IO Response
readJFile' = do
   b <- liftIO getJson
   case eitherDecode b of
     Left err -> mzero
     Right ps -> return ps

4

以下是我对此的进一步解释,这是如何实现的:

getJson :: IO B.ByteString
getJson = B.readFile jsonFile -- as before

readJFile :: B.ByteString -> Maybe Response -- look, no IO
readJFile b = case eitherDecode b of
                Left err -> Nothing
                Right ps -> Just ps

最后,您将所有内容再次合并为一个IO操作:
getAndProcess :: IO (Maybe Response)
getAndProcess = do
  b <- getJson
  return (readJFile b)

它很简洁,很简单,我喜欢它!不幸的是,它根本不起作用。 - Electric Coffee
@ElectricCoffee,你发布的错误似乎与我的代码无关。它提到了一行 Left err -> return Nothing,这在我的代码中并没有出现,然后又提到了另一行 Right ps -> return Just ps,这也不在我的代码中出现。 - kosmikus

2

不,没有安全的方法可以从IO单子中获取值。相反,您应该通过应用带有fmap或bind(>>=)的函数来在IO单子内部完成工作。此外,当您希望结果为Maybe时,应使用decode而不是eitherDecode。

getJson :: IO B.ByteString
getJson = B.readFile jsonFile

parseResponse :: B.ByteString -> Maybe Response
parseResponse = decode

readJFile :: IO (Maybe Response)
readJFile = fmap parseResponse getJSON

如果你更喜欢使用 do 表示法,也可以这样写:

readJFile :: IO (Maybe Response)
readJFile = do
    bytestring <- getJson
    return $ decode bytestring

请注意,由于readJFile指定了类型,因此您甚至不需要使用parseResponse函数。

2
一般来说,是有方法的。虽然会有很多“但是”的限制,但确实存在这样的方法。你正在询问一个叫做不安全IO操作的东西:System.IO.Unsafe。它通常用于在调用外部库时编写包装器,而不是在常规Haskell代码中使用。
基本上,你可以调用unsafePerformIO :: IO a -> a,它会精确地执行你想要的操作,去掉了IO部分并将类型为a的封装值返回给你。但是,如果你查看文档,会发现有许多要求,你必须保证系统满足这些要求,这些要求都归结为同一思想:即使你通过IO执行了操作,答案也应该是函数的结果,与任何其他没有在IO中进行操作的Haskell函数一样:它应该始终具有相同的结果,没有副作用,仅基于输入值。
在这里,鉴于你的代码,显然情况并非如此,因为你正在从文件中读取。你应该继续在IO单子内工作,通过调用带有结果类型IO something的另一个函数来调用readJFile。然后,你将能够在IO包装器中读取值(自己处于IO中),对其进行操作,然后在返回时重新包装结果在另一个IO中。

5
不要向初学者推荐unsafePerformIO,即使只是提及它也会让它在某些情况下似乎是一个合理的选择,但事实并非如此。初学者永远不需要使用或知道unsafePerformIO。我提到这个是因为对于我和其他人来说,在学习Haskell时了解unsafePerformIO实际上使学习更加困难,因为这给人一种印象,即你应该在某个时候“跳出”IO monad,但实际上不是这样的。 - kqr
1
你真的读了我的回答吗,易于点踩先生?我没有推荐它,我只是陈述了一个事实。我的回答既不是在推广它,也不是错误的。如果你不喜欢它,就不要点赞。在某些情况下,需要进行不安全的操作,例如当你必须与外部库交互(也许编写包装器)。好吧,这不是初学者的东西,但只要是正确的,你就无权决定我应该告诉人们什么和不告诉人们什么。 - Riccardo T.
1
我想要强调的是,SO旨在成为答案仓库,这对于正在寻找信息的任何人都是如此。即使他是一名初学者并且不需要使用unsafePerformIO,在参考中提到它可能会对实际需要它的人有所帮助,@kqr。 - Riccardo T.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接