在 Haskell 中的 do 块中使用 let 中的状态

Using the state in a let in a do block in Haskell

我有以下数据结构和函数:

data BTree a = BLeaf | BNode (BTree a) a (BTree a) deriving (Show, Eq)
freshNodesS :: BTree String -> State [String] (BTree String)
freshNodesS BLeaf = return BLeaf
freshNodesS (BNode l m r) = do  l' <- freshNodesS l
                                let m' = getFresh m s
                                let s' = m : s
                                r' <- freshNodesS r s'
                                return (BNode l' m' r')

有一个问题,我实际上想使用 freshNodesS l 的状态,它应该给出 (BTree String, [String]) 的输出,但在 do 块中我不能使用 (l', s) <- freshNodesS l ,我看到的唯一选择是将所有内容都放在 lambda 函数中。 但是有没有办法我仍然可以使用 do 表示法?


在@chi 说我这样做之后:

freshNodesS BLeaf           = return BLeaf
freshNodesS (BNode l m r)   = do    l' <- freshNodesS l
                                    m' <- getFreshS m
                                    r' <- freshNodesS r
                                    return (BNode l' m' r')
                    
getFreshS :: String -> State [String] String
getFreshS x = state $ (\s -> let newx = getFresh x s in (newx, newx: s))

这奏效了。

State monad 的全部意义在于自动传递状态,而无需您明确地这样做。几乎没有函数应该得到 s 作为参数,或者 return 它。

例如,

let m' = getFresh m s

可疑,应该是

m' <- getFresh m

getFresh :: String -> State [String] String.

整个代码应该读作

       do l' <- freshNodesS l
          m' <- getFresh m
          r' <- freshNodesS r
          return (BNode l' m' r')

请注意从未提及 ss'。这应该看起来像命令式代码,其中一个可变状态变量被每个函数调用修改,即使代码没有明确提及。

现在,在 getFresh 中,您将不得不与各州打交道,因为没有办法解决这个问题。如果您的 State monad 是标准的,您可以使用 getput 访问状态。你可能需要像

这样的东西
getFresh :: String -> State [String] String
getFresh m = do
   s <- get          -- read the current state
   let m' = ...      -- compute a fresh name m'
   let s' = m' : s   -- mark m' as used
   put s'            -- write the current state
   return m'

do-notation 只是语法糖重写 <- 绑定与 >>= 绑定和 lambda。如果你可以用一个写它,你可以用另一个写它。所以,如果你认为你可以用 lambda 来写这个,我鼓励你这样做。然后,将其重写为使用 do-notation,您将学到一些东西。但我怀疑你会 运行 陷入同样的​​绊脚石,因为正如我所说,使用 lambdas 而不是 do-notation.

实际上并没有什么特别之处

我很难说出你想写什么,因为你没有为 getFresh 提供类型签名。有点令人费解的是,此函数将状态作为直接参数,而不是像程序的其余部分那样参与状态 monad。我建议重写它以具有签名

getFresh :: String -> State [String] String
getFresh m = do {...}

您当然必须更改实现,为此我建议您查看 getput 操作。但是进行此更改后,您的 freshNodesS 函数将不再需要对状态参数进行任何手动操作 threading-through,因为它将完全由状态机处理,如预期的那样:

freshNodesS (BNode l m r) = do
  l' <- freshNodesS l
  m' <- getFresh m
  r' <- freshNodesS r
  return (BNode l' m' r')

或者,您可以改用应用风格来编写:

freshNodesS (BNode l m r) = 
  BNode <$> freshNodesS l <*> getFresh m <*> freshNodesS r

这样你就清楚了 none 这三个操作除了共享相同的状态外还相互依赖,你不需要为不做太多事情的变量命名。