Mix and match stateful computations within the Sta

2020-06-09 07:25发布

问题:

The state of my program consists of three values, a, b, and c, of types A, B, and C. Different functions need access to different values. I want to write functions using the State monad so that each function can only access the pieces of the state that it needs to access.

I have four functions of the following types:

f :: State (A, B, C) x
g :: y -> State (A, B) x
h :: y -> State (B, C) x
i :: y -> State (A, C) x

Here is how I call g within f:

f = do
    -- some stuff
    -- y is bound to an expression somewhere in here
    -- more stuff
    x <- g' y
    -- even more stuff

    where g' y = do
              (a, b, c) <- get
              let (x, (a', b')) = runState (g y) (a, b)
              put (a', b', c)
              return x

That g' function is an ugly piece of boilerplate that does nothing but bridge the gap between the types (A, B, C) and (A, B). It is basically a version of g that operates on a 3-tuple state, but leaves the 3rd tuple item unchanged. I am looking for a way to write f without that boilerplate. Maybe something like this:

f = do
    -- stuff
    x <- convert (0,1,2) (g y)
    -- more stuff

Where convert (0,1,2) converts a computation of type State (a, b) x to type State (a, b, c) x. Likewise, for all types a, b, c, d:

  • convert (2,0,1) converts State (c,a) x to State (a,b,c) x
  • convert (0,1) converts State b x to State (a,b) x
  • convert (0,2,1,0) converts State (c,b) x to State (a,b,c,d) x

My questions:

  1. Is there a better solution than putting state values in tuples? I thought about using a monad transformer stack. However, I think that only works if, for any two functions f and g, either FG or GF, where F is the set of state values needed by f and G is the set of state values needed by g. Am I wrong about that? (Note that my example does not satisfy this property. For example, G = {a, b} and H = {b, c}. Neither is a subset of the other.)
  2. If there is no better way than tuples, then is there a good way to avoid the boilerplate I mentioned? I am even willing to write a file with a bunch of boilerplate functions (see below) as long as the file can be automatically generated once and then forgotten about. Is there a better way? (I have read about lenses, but their complexity, ugly syntax, enormous set of unnecessary features, and seeming reliance on Template Haskell are off-putting. Is that a misconception of mine? Can lenses solve my problem in a way that avoids those problems?)

(The functions I mentioned would look something like this.)

convert_0_1_2 :: State (a, b) x -> State (a, b, c) x
convert_0_1_2 f = do
    (a, b, c) <- get
    let (x, (a', b')) = runState f (a, b)
    put (a', b', c)
    return x

convert_0_2_1_0 :: State (c, b) x -> State (a, b, c, d) x
convert_0_2_1_0 f = do
    (a, b, c, d) <- get
    let (x, (b', c')) = runState f (b, c)
    put (a, b', c', d)
    return x

回答1:

You can do it by using zoom from lens-family or the lens package with the tuple-lenses package: the simplified type of zoom is:

zoom :: Lens' s a -> State a x -> State s x

So zoom runs a computation using a smaller state. The Lens is used to specify the location of smaller state a inside the larger state s.

With these two packages, you can run g, h and i as follows:

f :: State (A,B,C) x
f = do
  zoom _12 g -- _12 :: Lens' (A,B,C) (A,B)
  zoom _23 h -- _23 :: Lens' (A,B,C) (B,C)
  zoom _13 i -- _13 :: Lens' (A,B,C) (A,C)


回答2:

If you don't want to fuss around with tuples, you can use a "classy" approach with a record. There's some fancy Template Haskell to support this in the lens package, but you can also do it by hand. The idea is to create at least one class for each piece of the state:

class HasPoints s where
  points :: Lens' s Int

class ReadsPoints s where
  getPoints :: Getter s Int
  default getPoints :: HasPoints s => Getter s Int
  getPoints = points

class SetsPoints s where
  setPoints :: Setter' s Int
  ...

Then each function that manipulates the state will have a type signature like

fight :: (HasPoints s, ReadsHealth s) => StateT s Game Player

An action with this particular signature has full access to the points, and read-only access to the health.