为 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 转换器堆栈顶部的 ReaderTStateT 并不难(事实上,上面的定义完全适用于这种情况,只需更改类型) .将它扩展到堆栈中间的 ReaderTStateT 更难。你基本上需要一个函数

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,同时仍按预期使用 iReadDataiWriteData。您也不必使用 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 = ...

请注意,extractreplace 函数构成了一个 lens,并且有相应的库,甚至可能已经包含这样的函数.