4

尽管阅读了 LYAH 中非常清楚的解释,然后是 Haskell Wiki 和其他一些东西,但我仍然对 state monad 的实现方式感到困惑。我想我明白它是什么,虽然我不自信。

所以假设我有一些微不足道的数据类型:

data Simple a = Top a 
  deriving ( Show )

还有这个:

newtype SimpleState a = SimpleState { applySimple :: Int -> ( a, Int ) }

然后我让 SimpleState 成为一个单子

instance Monad SimpleState where
    return x = SimpleState $ \s -> ( x, s )
    st >>= g = SimpleState $ \s -> let ( x, s' ) = applySimple st s in applySimple ( g x ) s' 

问题 1: lambda 如何将 s ( for state ) 作为参数?它是如何传入的?

问题2:如果applySimple在其函数签名中接受一个参数,为什么我applySimple st s在lambda里面?为什么要applySimple申请两次?

更令人困惑的是,这件事改变了状态:

tic :: SimpleState Int
tic = SimpleState $ \s -> ( s, s + 1 ) 

问题 3. 这是什么?为什么它对 SimpleState 执行某种操作,但它的签名不是函数?

所以现在我可以将 tic 传递给这个函数:

incr :: Simple a -> SimpleState ( Simple ( a, Int ) ) 
incr ( Top a ) = do 
  v <- tic
  return ( Top ( a, v ) )

问题 4:我可以/我如何将 tic 与 tic 一起使用>>=

并像这样使用它:

applySimple ( incr ( Top 1 ) ) 3

我明白了:

(Top (1,3),4)

同样,applySimple应用于两个参数,这让我感到困惑。

总而言之,我对构造函数SimpleState正在接受一个将 s 作为参数的函数,并且不知道它来自于它在上下文中的使用方式这一事实感到非常困惑。

4

3 回答 3

11

好吧,你已经把一堆问题塞进了一个帖子里……

1. lambda参数s从何而来?

return x = SimpleState $ \s -> ( x, s )

如果您查看您的构造函数SimpleState,您会注意到它需要一个类型的函数Int -> (a, Int)作为参数。所以使用 lambda 是为了给我们一个正确类型的函数。Lambda 只是创建函数的一种方式。

之所以需要一个函数,是因为只有通过函数参数才能访问状态单子的当前状态。

2. 为什么applySimple取两个参数?

那是因为它是一个字段访问器。

data Point = Point { x :: Int, y :: Int }

是什么类型的x?嗯,是的Point -> Int。它从一个值中提取一个字段Point

origin = Point 0 0
potOfGold = Point 15 3

main = putStrLn $ "Pot of gold is at (" ++ show (x potOfGold) ++ ", " ++
                  show (y potOfGold) ++ ")"

显然,x没有类型Int,因为每个Point. 同样地,

newtype MyState a = MyState { runState :: Int -> (a, Int) }

是什么类型的runState?嗯,是的MyState -> Int -> (a, Int)MyState a它从一个值中提取一个字段(唯一的字段) 。

为什么tic不是函数?

一元动作不必是函数。例如,考虑一下代码putStrLn

main = putStrLn "Hello, world."

做某事对你来说是有意义的,putStrLn因为它是一个函数,对吧?好吧,你是对的,它做了一些事情,但你的推理是错误的。您正在将命令式直觉用于功能语言。严格来说,putStrLn不打印任何东西,它是一个返回单子动作的函数,结果单子动作打印"Hello, world."

printHello :: IO ()
printHello = putStrLn "Hello, world."

显然,这会有所作为。它不是一个函数,因为它不带参数。如果您想了解 Haskell,请记住这一点:“做某事”是一元动作的属性,与函数无关。

4. 我可以/如何使用ticwith>>=吗?

就 Stack Overflow 上的问题而言,“我如何将功能 X 与功能 Y 一起使用”是我最讨厌的问题列表的顶部。这就像问“我如何在厨房里用水”,我说“你可以拖地”,但实际上你口渴了,想喝水。最好问一个问题,例如“我如何用水清洁地板?” 或“如何用水解渴?” 这些问题是可以回答的。

因此,问题 4 的答案是“是的,您可以使用ticwith >>=。” 而且,“你如何使用它取决于你想做什么。”

脚注:我建议您遵循主要的编码风格,例如,f (x y)而不是f ( x y ),因为它会帮助人们阅读您的代码。

于 2013-03-02T04:55:27.440 回答
10

问题 1: lambda 如何将 s ( for state ) 作为参数?它是如何传入的?

get让我们使用and的经典定义put,定义为:

put :: Int -> SimpleState ()
put n = SimpleState (\_ -> ((), n))

get :: SimpleState Int
get = SimpleState (\s -> (s, s))

当您调用时applySimple,您打开SimpleState,它公开了一个类型的函数Int -> (a, Int)。然后将该函数应用于初始状态。让我们用一些具体的例子来尝试一下。

首先,我们将运行命令put 1,初始状态为0

applySimple (put 1) 0

-- Substitute in definition of 'put'
= applySimple (SimpleState (\_ -> ((), 1))) 0

-- applySimple (Simple f) = f
(\_ -> ((), 1)) 0

-- Apply the function
= ((), 1)

请注意如何put忽略初始状态并仅将右侧状态槽替换为1,将左侧返回值槽留在后面()

现在让我们尝试运行 get 命令,使用的起始状态为0

applySimple get 0

-- Substitute in definition of 'get'
= applySimple (SimpleState (\s -> (s, s))) 0

-- applySimple (SimpleState f) = f
= (\s -> (s, s)) 0

-- Apply the function
= (0, 0)

get只是复制0到左边的返回值槽,而右边的状态槽保持不变。

因此,您将初始状态传递给该 lambda 函数的方式只是展开新SimpleState类型以公开底层 lambda 函数并将 lambda 函数直接应用于初始状态。

问题 2:如果 applySimple 在其函数签名中接受一个参数,为什么我在 lambda 中有 applySimple st s?为什么 applySimple 应用了两次?

那是因为applySimple's 类型不是Int -> (a, Int). 其实是:

applySimple :: SimpleState -> Int -> (a, Int)

这是 Haskell 记录语法的一个令人困惑的方面。每当您有如下记录字段时:

data SomeType { field :: FieldType }

... thenfield的类型实际上是:

field :: SomeType -> FieldType

我知道这很奇怪。

问题 3. 这是什么?为什么它对 SimpleState 执行某种操作,但它的签名不是函数?

newtypeSimpleState隐藏了它包装的函数。 newtypes可以隐藏任何东西,直到你打开它们。你SimpleState里面确实有函数,但是编译器看到的只是外部的新类型,直到你用applySimple.

问题 4:我可以/我如何将 tic 与 >>= 一起使用?

你在定义中犯了一个错误incr。正确的使用tic方法是这样的:

ticTwice :: SimpleState ()
ticTwice = do
    tic
    tic

...编译器将其转换为:

ticTwice =
    tic >>= \_ ->
    tic

使用(>>=)和 tic 的定义,您可以证明这会将值增加 2:

tic >>= \_ -> tic

-- Substitute in definition of `(>>=)`
SimpleState (\s ->
    let (x, s') = applySimple tic s
    in  applySimple ((\_ -> tic) x) s')

-- Apply the (\_ -> tic) function
SimpleState (\s ->
    let (x, s') = applySimple tic s
    in  applySimple tic s')

-- Substitute in definition of `tic`
SimpleState (\s ->
    let (x, s') = applySimple (SimpleState (\s -> (s, s + 1))) s
    in  applySimple (SimpleState (\s -> (s, s + 1))) s'

-- applySimple (SimpleState f) = f
SimpleState (\s ->
    let (x, s') = (\s -> (s, s + 1)) s
    in  (\s -> (s, s + 1)) s'

-- Apply both functions
SimpleState (\s ->
    let (x, s') = (s, s + 1)
    in  (s', s' + 1)

-- Simplify by getting rid of unused 'x'
SimpleState (\s ->
    let s' = s + 1
    in  (s', s' + 1)

-- Simplify some more:
SimpleState (\s -> s + 1, s + 2)

所以你会看到,当你链接两个tics using(>>=)时,它会将它们组合成一个有状态的函数,该函数将状态递增2,并返回状态 plus 1

于 2013-03-02T06:18:01.893 回答
4

Statemonad 中,bind 组合有状态函数(类似于通常的(.)组合。当你完成组合计算(即do块中的所有东西)时,你可以在初始状态value上运行东西。

问题 1: lambda 如何将 s ( for state ) 作为参数?它是如何传入的?

紧随其后的 lambda 绑定是您最终运行单子计算SimpleState初始状态值。如果您使用的是一种状态可以更改类型的构造,而不是仅限于s.

为什么 applySimple 应用了两次?

具体发生的事情是从包装器applySimple中提取你的;然后将该函数应用于 ( ) 后面的内容。这与做的想法相同:.Int -> ( a, Int )newtypes($) (+1) 1

为什么它对 SimpleState 执行某种操作,但它的签名不是函数?

我认为理解 bound\s->是你最初的TBD 状态,并且从 bind as composition 的角度思考会让你到达那里。这是一种与单子非常不同的野兽Maybe

FWIW 许多人似乎发现我写的这个旧教程很有帮助。

于 2013-03-02T04:55:12.493 回答