为 Haskell 中的州创建只读函数
Making Read-Only functions for a State in Haskell
我经常遇到使用 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
成为一个常规函数似乎是最好的选择,但是我必须经历每次使用时显式提取和传递状态的体操。我有哪些选择?
将 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
中,仅当您需要将它们与修改器组合时。
将它扩展到 monad 转换器堆栈顶部的 ReaderT
和 StateT
并不难(事实上,上面的定义完全适用于这种情况,只需更改类型) .将它扩展到堆栈中间的 ReaderT
和 StateT
更难。你基本上需要一个函数
lift1 :: (forall a. m0 a -> m1 a) -> t m0 a -> t m1 a
对于 ReaderT
/StateT
上方堆栈中的每个 monad 转换器 t
,这不是标准库的一部分。
我建议将 State
monad 包装在 newtype
中并为其定义一个 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
。您也不必使用 read
函数包装对 Reader
monad 的每个调用,MonadReader MyState
函数与 App
monad 中的函数无缝集成。
jcast 和 bhelkir 的出色回答,正是我想到的第一个想法——将 Reader
嵌入 State
。
我认为有必要解决你问题的这个半边问题:
Using the State
monad as usual in these functions works just fine, but I can't help but feel that I've given up Haskell's inherent safety and replicated a language where any function can mutate anything.
这确实是一个潜在的危险信号。我一直发现 State
最适合具有 "small" 状态的代码,这些状态可以包含在 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
操作,以便它们只使用比 "big" 更窄的状态类型,然后将它们嵌入到更大的 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
函数构成了一个 lens,并且有相应的库,甚至可能已经包含这样的函数.
我经常遇到使用 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
成为一个常规函数似乎是最好的选择,但是我必须经历每次使用时显式提取和传递状态的体操。我有哪些选择?
将 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
中,仅当您需要将它们与修改器组合时。
将它扩展到 monad 转换器堆栈顶部的 ReaderT
和 StateT
并不难(事实上,上面的定义完全适用于这种情况,只需更改类型) .将它扩展到堆栈中间的 ReaderT
和 StateT
更难。你基本上需要一个函数
lift1 :: (forall a. m0 a -> m1 a) -> t m0 a -> t m1 a
对于 ReaderT
/StateT
上方堆栈中的每个 monad 转换器 t
,这不是标准库的一部分。
我建议将 State
monad 包装在 newtype
中并为其定义一个 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
。您也不必使用 read
函数包装对 Reader
monad 的每个调用,MonadReader MyState
函数与 App
monad 中的函数无缝集成。
jcast 和 bhelkir 的出色回答,正是我想到的第一个想法——将 Reader
嵌入 State
。
我认为有必要解决你问题的这个半边问题:
Using the
State
monad as usual in these functions works just fine, but I can't help but feel that I've given up Haskell's inherent safety and replicated a language where any function can mutate anything.
这确实是一个潜在的危险信号。我一直发现 State
最适合具有 "small" 状态的代码,这些状态可以包含在 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
操作,以便它们只使用比 "big" 更窄的状态类型,然后将它们嵌入到更大的 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
函数构成了一个 lens,并且有相应的库,甚至可能已经包含这样的函数.