使用状态monad隐藏显式状态

Har*_*sen 10 monads state haskell monomorphism-restriction

我正试图在Haskell写一个小游戏,并且有相当多的状态需要传递.我想尝试用状态monad隐藏状态

现在我遇到了一个问题:带状态和参数的函数很容易写入状态monad中.但也有一些函数只是将状态作为参数(并返回修改后的状态,或者可能是其他东西).

在我的代码的一部分中,我有这一行:

let player = getCurrentPlayer state
Run Code Online (Sandbox Code Playgroud)

我希望它不采取状态,而是写

player <- getCurrentPlayerM
Run Code Online (Sandbox Code Playgroud)

目前,它的实现看起来像这样

getCurrentPlayer gameState = 
  (players gameState) ! (on_turn gameState)
Run Code Online (Sandbox Code Playgroud)

通过这样编写它似乎很简单,使它在State monad中工作:

getCurrentPlayerM = do state <- get
                       return (players state ! on_turn state)
Run Code Online (Sandbox Code Playgroud)

然而,这引起了ghc的抱怨!它说,没有使用"get"产生的(MonadState GameState m0)实例.我已经重写了一个非常相似的函数,除了它的状态monad形式不是nullary,所以在预感中,我重写了这样:

getCurrentPlayerM _ = do state <- get
                         return (players state ! on_turn state)
Run Code Online (Sandbox Code Playgroud)

果然,它的确有效!但当然我必须把它称为getCurrentPlayerM(),我觉得有点傻.传递参数是我想要首先避免的!

另外一个惊喜:看看我得到的ghci类型

getCurrentPlayerM :: MonadState GameState m => t -> m P.Player
Run Code Online (Sandbox Code Playgroud)

但是如果我尝试在我的代码中明确地设置它,我会得到另一个错误:"约束MonadState GameState m中的非类型变量参数"以及允许它的语言扩展的提议.我想这是因为我的GameState是一种类型,而不是类型类,但是为什么它在实践中被接受,但是当我试图明确它时,我更加困惑.

总结一下:

  1. 为什么我不能在State monad中编写nullary函数?
  2. 为什么我不能声明我的解决方法函数实际具有的类型?

dfl*_*str 14

问题是您没有为函数编写类型签名,并且适用单态限制.

当你写:

getCurrentPlayerM = ...
Run Code Online (Sandbox Code Playgroud)

您正在编写没有类型声明的顶级一元约束值定义,因此Haskell编译器将尝试推断定义的类型.但是,单态限制(字面意思:单形限制)表明所有具有推断类型约束的顶级定义必须解析为具体类型,即它们不能是多态的.


为了解释我的意思,请举一个更简单的例子:

pi = 3.14
Run Code Online (Sandbox Code Playgroud)

在这里,我们定义pi没有类型,所以GHC推断出类型Fractional a => a,即"任何类型a,只要它可以被视为一个分数".但是,这种类型是有问题的,因为它意味着它pi不是常数,即使它看起来像.为什么?因为pi将根据我们想要的类型重新计算值.

如果我们有(2::Double) + pi,pi将是一个Double.如果我们有(3::Float) + pi,pi将是一个Float.每一次pi使用,因此它必须重新计算(因为我们不能存储的替代版本pi所有可能的分数类型的,对吗?).这对于简单的文字很好3.14,但如果我们想要更多的小数pi并使用计算它的奇特算法怎么办?我们不希望每次pi使用时重新计算它,是吗?

这就是为什么Haskell报告声明顶级一元类型约束定义必须具有单一类型(单态),以避免这个问题.在这种情况下,pi将获得default类型Double.如果需要,可以使用default关键字更改默认数字类型:

default (Int, Float)

pi = 3.14 -- pi will now be Float
Run Code Online (Sandbox Code Playgroud)

但是,在您的情况下,您将获得推断签名:

getCurrentPlayerM :: MonadState GameState m => m P.Player
Run Code Online (Sandbox Code Playgroud)

这意味着:"对于存储GameStates的任何州monad ,检索玩家." 但是,由于单态限制适用,Haskell被迫尝试通过选择具体类型来使这种类型成为非多态的m.然而,它找不到一个,因为没有类型默认状态monad像数字一样,所以它放弃了.

您要么为您的函数提供显式类型签名:

getCurrentPlayerM :: MonadState GameState m => m P.Player
Run Code Online (Sandbox Code Playgroud)

...但是你需要添加FlexibleContextsHaskell语言扩展才能工作,方法是在文件的顶部添加:

{-# LANGUAGE FlexibleContexts #-}
Run Code Online (Sandbox Code Playgroud)

或者,您可以明确指定所需的状态monad:

getCurrentPlayerM :: State GameState P.Player
Run Code Online (Sandbox Code Playgroud)

你也可以通过添加扩展名来禁用单态限制; 但是,添加类型签名要好得多.

{-# LANGUAGE NoMonomorphismRestriction #-}
Run Code Online (Sandbox Code Playgroud)

PS.如果您有一个将状态作为参数的函数,则可以使用:

value <- gets getCurrentPlayer
Run Code Online (Sandbox Code Playgroud)

您还应该考虑使用带状态monad的镜头,这样可以为隐式状态传递编写非常干净的代码.