由于有许多相关功能需要在半成品中对同一条数据进行操作,我经常会遇到使用State
monad非常方便的情况。 - 紧张的方式。
某些函数需要读取State monad中的数据,但永远不需要更改它。像往常一样在这些函数中使用State
monad工作得很好,但我无法帮助,但我觉得我已经放弃了Haskell的固有安全性并复制了一种语言,其中任何函数都可以改变一切。
我是否可以做一些类型级别的事情来确保这些函数只能从State
读取,并且永远不会写入它?
现状:
iWriteData :: Int -> State MyState ()
iWriteData n = do
state <- get
put (doSomething n state)
-- Ideally this type would show that the state can't change.
iReadData :: State MyState Int
iReadData = do
state <- get
return (getPieceOf state)
bigFunction :: State MyState ()
bigFunction = do
iWriteData 5
iWriteData 10
num <- iReadData -- How do we know that the state wasn't modified?
iWRiteData num
理想情况下,iReadData
可能会有Reader MyState Int
类型,但它与State
不能很好地匹配。让iReadData
成为常规函数似乎是最好的选择,但是我必须经历明确提取并在每次使用时将状态传递给它的体操。我有什么选择?
答案 0 :(得分:7)
将Reader
monad注入State
并不难:
read :: Reader s a -> State s a
read a = gets (runReader a)
然后你可以说
iReadData :: Reader MyState Int
iReadData = do
state <- ask
return (getPieceOf state)
并将其命名为
x <- read $ iReadData
这将允许您将Reader
构建为更大的只读子程序,并将它们注入State
,只需要将它们与mutator组合在一起。
将其扩展到monad变换器堆栈顶部的ReaderT
和StateT
并不难(事实上,上面的定义完全适用于这种情况,只需更改类型)。将其扩展到堆栈中间的ReaderT
和StateT
更难。你基本上需要一个功能
lift1 :: (forall a. m0 a -> m1 a) -> t m0 a -> t m1 a
对于t
/ ReaderT
上方的堆栈中的每个monad转换器StateT
,它不是标准库的一部分。
答案 1 :(得分:6)
我建议在State
中整理newtype
monad并为其定义MonadReader
个实例:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
import Control.Applicative
import Control.Monad.State
import Control.Monad.Reader
data MyState = MyState Int deriving Show
newtype App a = App
{ runApp' :: State MyState a
} deriving
( Functor
, Applicative
, Monad
, MonadState MyState
)
runApp :: App a -> MyState -> (a, MyState)
runApp app = runState $ runApp' app
instance MonadReader MyState App where
ask = get
local f m = App $ fmap (fst . runApp m . f) $ get
iWriteData :: MonadState MyState m => Int -> m ()
iWriteData n = do
MyState s <- get
put $ MyState $ s + n
iReadData :: MonadReader MyState m => m Int
iReadData = do
MyState s <- ask
return $ s * 2
bigFunction :: App ()
bigFunction = do
iWriteData 5
iWriteData 10
num <- iReadData
iWriteData num
这肯定是@ jcast解决方案的更多代码,但它遵循将变换器堆栈实现为新类型包装器的传统,并且通过坚持使用约束而不是固化类型,您可以对使用提供强有力的保证您的代码,同时为重复使用提供最大的灵活性。使用您的代码的任何人都可以使用自己的变换器扩展您的App
,同时仍按预期使用iReadData
和iWriteData
。您也不必使用Reader
函数将每次调用打包到read
monad,MonadReader MyState
函数与App
monad中的函数无缝集成。
答案 2 :(得分:3)
jcast和bhelkir的精彩答案,完全是我想到的第一个想法 - 在Reader
内嵌入State
。
我认为有必要解决你问题的半边点:
在这些函数中像往常一样使用
State
monad工作正常,但我不禁觉得我放弃了Haskell的固有安全性并复制了一种语言,任何函数都可以改变任何东西。
确实,这是一个潜在的红旗。我总是发现State
最适合具有“小”状态的代码,这些状态可以包含在runState
的单个简短应用程序的生命周期内。我的首要例子是对Traversable
数据结构的元素进行编号:
import Control.Monad.State
import Data.Traversable (Traversable, traverse)
tag :: (Traversable t, Enum s) => s -> t a -> t (s, a)
tag i ta = evalState (traverse step ta) init
where step a = do s <- postIncrement
return (s, a)
postIncrement :: Enum s => State s s
postIncrement = do result <- get
put (succ result)
return result
你没有直接这么说,但是你可以说它可能具有很大的状态值,在长期runState
调用中有许多不同的字段以多种不同的方式使用。也许它确实需要在这一点上为你的程序。但是,应对此问题的一种方法可能是编写较小的State
操作,以便它们只使用比“大”类型更窄的状态类型,然后将它们嵌入到更大的State
类型中,其函数类似于这样:
-- | Extract a piece of the current state and run an action that reads
-- and modifies only that piece.
substate :: (s -> s') -> (s' -> s -> s) -> State s' a -> State s a
substate extract replace action =
do s <- get
let (s', a) = runState action (extract s)
put (replace s' s)
return a
示意图
example :: State (A, B) Whatever
example = do foo <- substate fst (,b) action1
bar <- substate snd (a,) action2
return $ makeWhatever foo bar
-- Can only touch the `A` component of the state
action1 :: State A Foo
action1 = ...
-- Can only touch the `B` component of the state
action2 :: State B Bar
action2 = ...
请注意,extract
和replace
函数构成镜头,并且有可用的库,甚至可能已包含这样的函数。