在Haskell中为状态创建只读函数

时间:2015-02-18 15:21:46

标签: haskell monads

由于有许多相关功能需要在半成品中对同一条数据进行操作,我经常会遇到使用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成为常规函数似乎是最好的选择,但是我必须经历明确提取并在每次使用时将状态传递给它的体操。我有什么选择?

3 个答案:

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

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

请注意,extractreplace函数构成镜头,并且有可用的库,甚至可能已包含这样的函数。