4

我正在使用 warp、wai 和 acid-state 在 haskell 中编写 Web 服务。到目前为止,我有两个需要数据库交互的处理函数,后者给我带来了麻烦。

首先是注册:

registerUser :: AcidState UserDatabase -> Maybe (Map.Map String String) -> Response
registerUser db maybeUserMap =
  case maybeUserMap of
    (Just u) -> let _ = fmap (\id -> update db (StoreUser (toString id) u)) (nextRandom)
                in resPlain status200 "User Created."
    Nothing  -> resPlain status401 "Invalid user JSON."

如您所见,我设法IO通过在let _ = ...

在登录功能(目前只返回用户地图)中,我无法避免IO,因为我需要在响应中实际发回结果:

loginUser :: AcidState UserDatabase -> String -> Response
loginUser db username = do
  maybeUserMap <- (query db (FetchUser username))
  case maybeUserMap of
    (Just u) -> resJSON u
    Nothing  -> resPlain status401 "Invalid username."

这会导致以下错误:

src/Main.hs:40:3:
    Couldn't match type ‘IO b0’ with ‘Response’
    Expected type: IO (EventResult FetchUser)
                   -> (EventResult FetchUser -> IO b0) -> Response
      Actual type: IO (EventResult FetchUser)
                   -> (EventResult FetchUser -> IO b0) -> IO b0
    In a stmt of a 'do' block:
      maybeUserMap <- (query db (FetchUser username))
    In the expression:
      do { maybeUserMap <- (query db (FetchUser username));
           case maybeUserMap of {
             (Just u) -> resJSON u
             Nothing -> resPlain status401 "Invalid username." } }
    In an equation for ‘loginUser’:
        loginUser db username
          = do { maybeUserMap <- (query db (FetchUser username));
                 case maybeUserMap of {
                   (Just u) -> resJSON u
                   Nothing -> resPlain status401 "Invalid username." } }

src/Main.hs:42:17:
    Couldn't match expected type ‘IO b0’ with actual type ‘Response’
    In the expression: resJSON u
    In a case alternative: (Just u) -> resJSON u

src/Main.hs:43:17:
    Couldn't match expected type ‘IO b0’ with actual type ‘Response’
    In the expression: resPlain status401 "Invalid username."
    In a case alternative:
        Nothing -> resPlain status401 "Invalid username."

我相信该错误是由 db 查询返回一个IO值引起的。我的第一个想法是Response将类型签名更改为IO Response,但随后顶层函数抱怨它需要一个Response,而不是一个IO Response

在类似的注释中,我本来希望这样写registerUser

registerUser :: AcidState UserDatabase -> Maybe (Map.Map String String) -> Response
registerUser db maybeUserMap =
  case maybeUserMap of
    (Just u) -> do uuid <- (nextRandom)
                   update db (StoreUser (toString uuid) u)
                   resPlain status200 (toString uuid)
    Nothing  -> resPlain status401 "Invalid user JSON."

但这会导致非常相似的错误。

为了完整起见,这里是调用registerUserand的函数loginUser

authRoutes :: AcidState UserDatabase -> Request -> [Text.Text] -> String -> Response
authRoutes db request path body =
  case path of
    ("register":rest) -> registerUser db (decode (LB.pack body) :: Maybe (Map.Map String String))
    ("login":rest) -> loginUser db body
    ("access":rest) -> resPlain status404 "Not implemented."
    _ -> resPlain status404 "Not Found."

如何避免这些 IO 错误?

4

2 回答 2

5

您似乎无法在 Haskell 中使用 IO 类型。您的问题与翘曲,wai 或酸状态无关。我将尝试在您提出问题的上下文中进行解释。

所以你需要知道的第一件事是,当你实际执行时,你不能避免IO感染你的类型IO。与数据库对话本质上是IO操作,因此它们会被感染。您的第一个示例实际上从未向数据库添加任何内容。你可以去GHCI试试:

> let myStrangeId x = let _ = print "Haskell is fun!" in x

现在检查这个函数的类型:

>:t myStrangeId
myStrangeId :: a -> a

现在尝试运行它:

> myStrangeId "Hello"
"Hello"

如您所见,它实际上从不打印消息,它只是返回参数。所以实际上在 let 语句中定义的代码是完全死的,它根本没有做任何事情。在你的registerUser函数中也是如此。

所以,正如我上面所说,你不能避免你的函数有一个IO类型,因为你想IO在函数中做。这可能看起来像一个问题,但它实际上是一件非常好的事情,因为它非常明确地表明您的程序的哪些部分正在执行IO,哪些没有执行。你需要学习haskell方式,将IO动作组合在一起来制作一个完整的程序。

如果您查看其中的Application类型,Wai您会发现它只是一个类型同义词,如下所示:

type Application = Request -> IO Response

当你完成你的程序时,这就是你想要的类型签名。如您所见,它Response被包裹在IO此处。

因此,让我们从您的顶级功能开始authRoutes。它目前有这个签名:

authRoutes :: AcidState UserDatabase -> Request -> [Text.Text] -> String -> Response

我们实际上希望它有一个稍微不同的签名,Response应该是IO Response

authRoutes :: AcidState UserDatabase -> Request -> [Text.Text] -> String -> IO Response

包裹东西IO很容易。因为IO它是一个 monad,所以你可以使用这个return :: a -> IO a函数来完成它。要获得所需的签名,您可以在函数定义return之后添加。=但是,这并不能满足您的要求,因为loginUser并且registerUser 还会返回 a IO Response,因此您最终会得到一些双重包装的响应。相反,您可以从包装纯响应开始:

authRoutes :: AcidState UserDatabase -> Request -> [Text.Text] -> String -> IO Response
authRoutes db request path body =
  case path of
    ("register":rest) -> registerUser db (decode (LB.pack body) :: Maybe (Map.Map String String))
    ("login":rest)    -> loginUser db body
    ("access":rest)   -> return $ resPlain status404 "Not implemented."
    _                 -> return $ resPlain status404 "Not Found."

请注意,我return之前添加resPlain了将它们包装在 IO 中。

现在让我们看看registerUser。实际上,您可以按照自己的意愿编写它。我假设它nextRandom有一个看起来像这样的签名:nextRandom :: IO something,那么你可以这样做:

registerUser :: AcidState UserDatabase -> Maybe (Map.Map String String) -> IO Response
registerUser db maybeUserMap =
  case maybeUserMap of
    (Just u) -> do
        uuid <- nextRandom 
        update db (StoreUser (toString uuid) u)
        return $ resPlain status200 (toString uuid)
    Nothing  -> return $ resPlain status401 "Invalid user JSON."

你的loginUser功能只需要一些小的改动:

loginUser :: AcidState UserDatabase -> String -> IO Response
loginUser db username = do
  maybeUserMap <- query db (FetchUser username)
  case maybeUserMap of
    (Just u) -> return $ resJSON u
    Nothing  -> return $ resPlain status401 "Invalid username."

所以总而言之,IO当你真正想要做的时候,你无法避免感染你的类型IO。相反,您必须接受它,并将您的非 IO 值包装在IO. 最好的做法IO是尽可能限制应用程序的最小部分。如果您可以在没有签名的情况下编写函数IO,那么您应该return稍后再将其包装起来。然而,一个函数必须执行一些 IO 是非常合乎逻辑的loginUser,因此它具有该签名不是问题。

编辑:

因此,正如您在评论中所说,Wai 已将其应用程序类型更改为:

type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived

你可以在这里这里阅读为什么。

要使用您的IO Response类型,您可以执行以下操作:

myApp :: Application
myApp request respond = do
    response <- authRoutes db request path body
    respond response
于 2015-04-22T14:15:45.683 回答
-1

您在上下文中的值之间混合,即 IO (* -> ) 和值 ( )。您不能对值执行“do”类型语法。简单的解决方案是使用 unsafePerformIO。用法取决于您的上下文(注意“不安全”一词)。推荐的方法是在最后使用带有 IO 的 monad 转换器堆栈,然后使用 liftIO 来执行您的 IO 操作。

于 2015-04-22T11:23:28.577 回答