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 中的几种效果,可以帮助您推理效果并更好地理解事物。
您想在不可变数据类型中更新 var1
。这只能通过创建新对象来完成。所以让我们写这样的函数:
plusFour :: Vars -> Vars
plusFour (Vars v1 v2) = Vars (v1 + 4) v2
在 Haskell 中有一些方法可以将这个函数写得更短,但更难理解,但我们现在不关心这些。
现在您想在 State
monad 中使用此函数来更新不可变状态并通过此模拟可变性。仅通过查看它的类型签名:change :: StateT Vars IO a
就可以知道这个函数的什么?我们可以说这个函数有几个作用:它可以访问 Vars
状态并且可以执行任意 IO
操作。此函数 returns 类型 a
的值。嗯,最后一个很奇怪。什么是 a
?这个函数应该return?在命令式编程中,此函数的类型为 void
或 Unit
。它只是 做 事情,而不是 return 一切。仅更新上下文。所以它的结果类型应该是()
。它可以不同。例如我们可能希望 return new Vars
后改变。但这在编程中通常是不好的方法。它使这个功能更加复杂。
在我们理解了函数应该有什么类型之后(尝试总是从定义类型开始)我们就可以实现它了。我们想改变我们的状态。有些功能与我们上下文的有状态部分一起运行。基本上,你对这个感兴趣:
modify :: Monad m => (s -> s) -> StateT s m ()
modify
函数采用更新状态的函数。在你 运行 这个函数之后,你可以观察到状态根据传递的函数进行了修改。现在change
可以这样写:
change :: StateT Vars IO ()
change = modify plusFour
您可以实现 modify
(因此 change
仅使用 put
和 get
函数,这对初学者来说是很好的练习)。
现在让我们从其他函数调用 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 设计模式可以更好地编写这些函数。但是你现在不需要关心那些,只要试着理解这里发生了什么。
我正在努力学习 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 中的几种效果,可以帮助您推理效果并更好地理解事物。
您想在不可变数据类型中更新
var1
。这只能通过创建新对象来完成。所以让我们写这样的函数:plusFour :: Vars -> Vars plusFour (Vars v1 v2) = Vars (v1 + 4) v2
在 Haskell 中有一些方法可以将这个函数写得更短,但更难理解,但我们现在不关心这些。
现在您想在
State
monad 中使用此函数来更新不可变状态并通过此模拟可变性。仅通过查看它的类型签名:change :: StateT Vars IO a
就可以知道这个函数的什么?我们可以说这个函数有几个作用:它可以访问Vars
状态并且可以执行任意IO
操作。此函数 returns 类型a
的值。嗯,最后一个很奇怪。什么是a
?这个函数应该return?在命令式编程中,此函数的类型为void
或Unit
。它只是 做 事情,而不是 return 一切。仅更新上下文。所以它的结果类型应该是()
。它可以不同。例如我们可能希望 return newVars
后改变。但这在编程中通常是不好的方法。它使这个功能更加复杂。在我们理解了函数应该有什么类型之后(尝试总是从定义类型开始)我们就可以实现它了。我们想改变我们的状态。有些功能与我们上下文的有状态部分一起运行。基本上,你对这个感兴趣:
modify :: Monad m => (s -> s) -> StateT s m ()
modify
函数采用更新状态的函数。在你 运行 这个函数之后,你可以观察到状态根据传递的函数进行了修改。现在change
可以这样写:change :: StateT Vars IO () change = modify plusFour
您可以实现
modify
(因此change
仅使用put
和get
函数,这对初学者来说是很好的练习)。现在让我们从其他函数调用
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 设计模式可以更好地编写这些函数。但是你现在不需要关心那些,只要试着理解这里发生了什么。