Haskell: Monad 转换器和全局状态

Haskell: Monad transformers and global state

我正在努力学习 Haskell。我正在尝试编写一个包含 "global state": Vars 的程序。每次调用函数时,我都想更改状态的一个组件(例如 var1)。更改可以是组件上的一个简单函数(例如 +4)。此外,它打印出更改的组件。这是我到目前为止所做的(但我被卡住了)。编辑:在 运行 代码之后,我想查看全局状态的最新版本。

import Control.Monad.State
import Control.Monad.IO.Class (liftIO)

data Vars = Vars {
 var1 :: Int,
 var2 :: Float
} deriving (Show)

sample :: StateT Vars IO a
sample = do 
        a <- change
        liftIO $ print a
        -- I want to call change again and apply more change to the state


change  :: StateT Vars IO a
change  = do
        dd <- get
         -- I don't know what to do next!

main = do 
  runStateT sample (Vars 20 3)
  evalStateT sample (Vars 20 3)

让我们尝试从更简单的小部分开始逐步解决您的问题。这是编程中的重要技能,FP 以很好的方式教给您该技能。此外,使用 State monad,尤其是 monad-transformers 中的几种效果,可以帮助您推理效果并更好地理解事物。

  1. 您想在不可变数据类型中更新 var1。这只能通过创建新对象来完成。所以让我们写这样的函数:

    plusFour :: Vars -> Vars
    plusFour (Vars v1 v2) = Vars (v1 + 4) v2
    

    在 Haskell 中有一些方法可以将这个函数写得更短,但更难理解,但我们现在不关心这些。

  2. 现在您想在 State monad 中使用此函数来更新不可变状态并通过此模拟可变性。仅通过查看它的类型签名:change :: StateT Vars IO a 就可以知道这个函数的什么?我们可以说这个函数有几个作用:它可以访问 Vars 状态并且可以执行任意 IO 操作。此函数 returns 类型 a 的值。嗯,最后一个很奇怪。什么是 a?这个函数应该return?在命令式编程中,此函数的类型为 voidUnit。它只是 事情,而不是 return 一切。仅更新上下文。所以它的结果类型应该是()。它可以不同。例如我们可能希望 return new Vars 后改变。但这在编程中通常是不好的方法。它使这个功能更加复杂。

  3. 在我们理解了函数应该有什么类型之后(尝试总是从定义类型开始)我们就可以实现它了。我们想改变我们的状态。有些功能与我们上下文的有状态部分一起运行。基本上,你对这个感兴趣:

    modify :: Monad m => (s -> s) -> StateT s m ()

    modify 函数采用更新状态的函数。在你 运行 这个函数之后,你可以观察到状态根据传递的函数进行了修改。现在change可以这样写:

    change :: StateT Vars IO ()
    change = modify plusFour
    

    您可以实现 modify(因此 change 仅使用 putget 函数,这对初学者来说是很好的练习)。

  4. 现在让我们从其他函数调用 change 函数。在这种情况下调用是什么意思?这意味着你执行 monadic 动作 change。此操作会更改您的上下文,您不关心它的结果,因为它是 ()。但是如果你在 change 之后 运行 get 函数(将整个状态绑定到变量),你可以观察到新的变化。如果你只想打印改变的组件,比如 var1 你可以使用 gets 函数。而且,sample 应该有什么类型? return 应该是什么?如果在调用方你只对结果状态感兴趣,那么,同样,它应该是 () 像这样:

    sample :: StateT Vars IO ()
    sample = do
        change
        v1 <- gets var1
        liftIO $ print v1
        change
        v1' <- gets var1 
        liftIO $ print v1'  -- this should be v1 + 4
    

这应该会让您对正在发生的事情有所了解。 Monad 转换器需要一些时间来适应它们,尽管它是一个强大的工具(不完美但非常有用)。

作为旁注,我想补充一点,使用常见的 Haskell 设计模式可以更好地编写这些函数。但是你现在不需要关心那些,只要试着理解这里发生了什么。