2012-07-20 77 views
11

只需从这个优秀的tutorial中了解State monad。但是,当我试图向非程序员解释时,他们有一个难题让我难住。为什么国家需要价值?

如果该国的目的是模拟可变的内存,这是为什么状态单子店是该类型的函数:

s -> (a, s) 

,而不是简单:

s -> s 

换句话说,“中间”价值的需求是什么?例如,在我们需要的情况下,我们不可能通过简单地将状态定义为(state, value)的元组来模拟它吗?

我确定我困惑的东西,任何帮助表示赞赏。

+2

我喜欢Roman的回答,但我会说'State'的目的不是模拟可变内存,而是模拟需要可变内存的计算。运行'State'的意义在于(理论上)获得计算结果,状态本身就是一个神器。当然,在真正的问题中,我们经常关心输出状态,这也是它可用的原因。 – 2012-07-21 13:06:00

回答

16

要使用命令式语言(如C)绘制并行,s -> s对应于返回类型为void的函数,该函数被调用纯粹是为了副作用(如改变内存)。它与State s()同构。

事实上,可以编写仅通过全局变量进行通信的C函数。但是,与C中一样,返回函数的值通常很方便。这就是a的用途。

当然有可能对于您的特定问题s -> s是一个更好的选择。虽然它不是Monad,但它是Monoid(当包裹在Endo中时)。因此,您可以使用<>mempty构造这些函数,这些函数对应Monad的>>=return

+0

也's - > s'不能很好地扩展。使用C并行:通过简单地定义新的全局变量,您总是可以将额外的信息添加到全局状态;在Haskell中,这需要改变底层类型's'。我将用一个例子来说明:让我们有一个状态'Stack - > Stack',我们希望收集堆栈顶部如何看待特定时刻。在C中,你可以添加新的全局变量,'堆栈 - >堆栈'你不能 - 你必须使用更具体的东西,比如'(Stack,[a]) - >(Stack,[a])' 。使用“正常”状态就像使用少量的<-'和'return [a,b,c]一样简单' – Vitus 2012-07-20 17:29:54

+0

对不起,但我给了这个答案一个-1。我担心,虽然真实且功能强大,但对于一位了解'状态'的新Haskell开发人员而言,Monoid对函数的看法可能会更困惑,而不是有帮助。最初,将函数组合直接作为函数组合(或者'Control.Category。(。)',如果你想要更通用一些)比单一的append更简单。 – mergeconflict 2012-07-23 17:23:37

5

为了扩大在尼克的回答有点:

s是国家。如果你所有的函数都是s -> s(状态到状态),你的函数将不能返回任何值。你可以将你的状态定义为(the actual state, value returned),但是这种状态与state-ful函数计算的值相混淆。还有一种常见的情况是,你需要函数实际计算并返回值...

1

大概你想用do表示法中的有状态计算吗?

你应该问自己Monad情况会是什么样子的定义有状态的计算由

newtype State s = { runState :: s -> s } 
+1

考虑如何编写'Functor'实例可能更具启发性。 'fmap ::(a - > b) - >状态a - >状态b'。虽然您可以轻松地转换“输出”状态的结束,但您没有足够的信息来转换输入结束。 – 2012-07-20 20:59:03

+0

我原本以“实际上,Functor'实例是什么样子结束了答案?”但决定不包括它:) – 2012-07-20 22:52:12

2

s' -> s'相当于(a, s) -> (a, s)。在这里,很显然你的State将需要一个初始的a开始除了s之外。

另一方面,s -> (a, s)只需要种子s开始的东西,根本不需要a值。

因此,s -> (a, s)的类型告诉您State不像(a, s) -> (a, s)那么复杂。 Haskell中的类型传达大量信息。

2

如果State的目的是模拟可变的内存,这是为什么状态单子店是该类型的函数:

s -> (a, s) 

,而不是简单:

s -> s 

单元State的目的不是模拟可变内存,而是模拟均为产生一个值有副作用。简单地说,给定s类型的初始状态,您的计算将产生一些类型为a的值以及更新的状态。

也许你的计算不会产生一个值...然后,容易:值类型a只是()。另一方面也许你的计算没有副作用。再次简单一点,您可能会想到您的状态转换函数(s -> s参数为modify)为id。但是你经常在同一时间处理两者。


实际上,你可以使用getput为相对简单的例子:

get :: State s s  -- s -> (s, s) 
put :: s -> State() -- s -> (s -> ((), s)) 
  • get是,鉴于目前的状态(第一s)的计算,将返回它既可作为一个值 - 也就是计算结果 - 和“新”(未修改)状态。

  • put是,给定一个新的状态(第一s)和当前状态(第二s)的计算,将简单地忽略当前状态。它会生成()作为计算值(因为它当然没有计算任何值!)并且挂在提供的新状态上。

0

要解决的问题是,您有一个输入和一系列函数,并且您希望按顺序将函数应用于输入。

如果功能是纯粹的状态,转变职能,s -> ss类型的输入,那你就不要需要State使用它们。 Haskell非常擅长将这些功能链接在一起,例如与标准组合运算符.或类似foldr (.) idfoldr id

但是,如果功能都发生变异的状态报告这样做,这样就可以给他们的类型s -> (s,a),然后再刷胶它们放在一起的一些结果是有点讨厌的。您必须解压缩结果元组并将新的状态值传递给下一个函数,在其他位置使用报告的值,然后解压缩结果,依此类推。将错误的状态传递给输入函数很容易,因为必须明确命名每个结果并明确输入才能进行解包。你最终的东西是这样的:

let 
    (res1, s1) = fun1 s0 
    (res2, s2) = fun2 s1 
    (res3, s3) = fun3 res1 res2 s1 
    ... 
    in resN 

在那里,我不小心经过s1,而不是s2,也许是因为我加入了第二条线的后面并没有意识到的第三行需要改变。当构成s -> s功能,不可能出现这个问题,因为没有名字得到的权利:

let 
    resN = fun1 . fun2 . fun3 . -- etc. 

因此,我们发明了State做同样的伎俩。 State本质上只是一种将s -> (s,a)等函数粘合在一起的方式,使得正确的状态总是传递给正确的函数。

所以它没有那么多的人去了“我们要使用State,让我们使用s -> (s,a)”,而是“我们正在编写功能,如s -> (s,a),让我们创造State做出那么容易”。功能s -> s,它已经很容易,我们不必发明任何东西。

作为s -> (s,a)自然产生的例子,考虑解析:解析器将被给予一些输入,消耗一些输入并返回一个值。在Haskell中,这被自然地建模为输入列表,并返回一对值和剩余的输入 - 即[Input] -> ([Input], a)State [Input]