在State Monad中混合和匹配有状态的计算

14

我的程序状态由三个值组成,分别是类型为ABCabc。不同的函数需要访问不同的值。我想使用State单子编写函数,以便每个函数只能访问它需要访问的状态片段。

我有以下四个函数:

f :: State (A, B, C) x
g :: y -> State (A, B) x
h :: y -> State (B, C) x
i :: y -> State (A, C) x

这是我在函数 f 中调用函数 g 的方式:

f = do
    -- some stuff
    -- y is bound to an expression somewhere in here
    -- more stuff
    x <- g' y
    -- even more stuff

    where g' y = do
              (a, b, c) <- get
              let (x, (a', b')) = runState (g y) (a, b)
              put (a', b', c)
              return x

那个 g' 函数是一段丑陋的样板代码,除了连接类型 (A, B, C)(A, B) 之间的差距外,什么也没做。它基本上是在三元组状态上运行的 g 的一个版本,但保持第三个元素不变。我正在寻找一种不需要这种样板代码的编写 f 的方法。也许像这样:

f = do
    -- stuff
    x <- convert (0,1,2) (g y)
    -- more stuff

convert (0,1,2)将类型为State(a,b) x的计算转换为类型为State(a,b,c) x的计算。同样,对于所有类型abcd

  • convert(2,0,1)将类型为State(c,a) x的计算转换为类型为State(a,b,c) x的计算
  • convert(0,1)将类型为State b x的计算转换为类型为State(a,b) x的计算
  • convert (0,2,1,0)将类型为State(c,b) x的计算转换为类型为State(a,b,c,d) x的计算

我的问题:

  1. 除了将状态值放在元组中,有没有更好的解决方案?我考虑使用单子变换器堆栈。但是,我认为只有当任意两个函数fg满足FGGF时,才能够实现。其中Ff所需的状态值的集合,Gg所需的状态值的集合。我对此是否有误解?(请注意,我的示例不满足此属性。例如,G={a,b}H={b,c}。两者都不是另一个的子集。)
  2. 如果没有比元组更好的方法,那么有没有一种避免我提到的样板代码的好方法?我甚至愿意编写一个包含很多样板函数的文件(如下所示),只要该文件可以自动生成一次,然后被遗忘即可。是否有更好的方法?(我已经了解了透镜,但它们的复杂性、丑陋的语法、庞大的不必要功能集和似乎依赖模板Haskell都让人望而却步。这是我的误解吗?透镜能否以避免这些问题的方式解决我的问题?)

(我提到的函数看起来像这样。)

convert_0_1_2 :: State (a, b) x -> State (a, b, c) x
convert_0_1_2 f = do
    (a, b, c) <- get
    let (x, (a', b')) = runState f (a, b)
    put (a', b', c)
    return x

convert_0_2_1_0 :: State (c, b) x -> State (a, b, c, d) x
convert_0_2_1_0 f = do
    (a, b, c, d) <- get
    let (x, (b', c')) = runState f (b, c)
    put (a, b', c', d)
    return x

5
我自己没有使用过,但它听起来非常像lenszoom:http://hackage.haskell.org/package/lens-4.13/docs/Control-Lens-Zoom.html#v:zoom - Joachim Breitner
1
这应该很容易使用依赖类型实现,但不要过于痴迷。 - effectfully
lens + vinyl 可以带来一种半不错的 Haskell 自虐体验。 - András Kovács
2个回答

9
您可以使用来自 lens-family 或者lens包以及tuple-lenses包来实现。其中,zoom的简化类型如下:
zoom :: Lens' s a -> State a x -> State s x

因此,zoom使用较小的状态运行计算。 Lens用于指定较小状态a在较大状态s内的位置。

使用这两个软件包,您可以按以下方式运行ghi

f :: State (A,B,C) x
f = do
  zoom _12 g -- _12 :: Lens' (A,B,C) (A,B)
  zoom _23 h -- _23 :: Lens' (A,B,C) (B,C)
  zoom _13 i -- _13 :: Lens' (A,B,C) (A,C)

谢谢,这正是我想要的 :-) - Joachim Breitner

4
如果您不想麻烦地使用元组,可以使用记录(record)的方式。在lens包中有一些花哨的Template Haskell来支持这种方法,但是您也可以手动实现。这个想法是为每个状态的一部分创建至少一个类。
class HasPoints s where
  points :: Lens' s Int

class ReadsPoints s where
  getPoints :: Getter s Int
  default getPoints :: HasPoints s => Getter s Int
  getPoints = points

class SetsPoints s where
  setPoints :: Setter' s Int
  ...

那么每一个操作状态的函数都会有如下类型签名:

fight :: (HasPoints s, ReadsHealth s) => StateT s Game Player

这个特定签名的操作可以完全访问点数,并只读访问健康状态。


我常用的一个例子是:f :: State (A, A) ()g :: State A ()。在 f 中,我想要能够调用 g 来处理 f 状态中的第一个或第二个部分。g 不应该知道它正在处理哪一个部分。g 甚至不应该知道它是作为更大的应用程序的一部分存在的。但是使用优雅的解决方案,fg 将具有相同的状态类型(一个 Game 值),并使用相同的 getter/setter 来访问该状态的任何给定部分,对吗?虽然这让我可以从一个职责较少的函数中隐藏信息,但似乎会使我的函数无法重用。 - Jordan

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