Смешайте и сопоставьте вычисления с учётом состояния в рамках государственной монады

Состояние моей программы состоит из трех значений: a, b и c, типов a, b и c. Различные функции требуют доступа к различным значениям. Я хочу писать функции с помощью монады State, чтобы каждая функция могла получить доступ только к частям состояния, к которым он должен получить доступ.

У меня есть четыре функции следующих типов:

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

Вот как я вызываю g внутри 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

Эта функция g' является уродливой частью шаблона, которая ничего не делает, кроме как преодоление зазора между типами (A, B, C) и (A, B). Это в основном версия g, которая работает в состоянии с тремя кортежами, но оставляет третий элемент кортежа без изменений. Я ищу способ написать f без этого шаблона. Может быть, что-то вроде этого:

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

Где convert (0,1,2) преобразует вычисление типа State (a, b) x в тип State (a, b, c) x. Аналогично, для всех типов a, b, c, d:

  • convert (2,0,1) преобразует State (c,a) x в State (a,b,c) x
  • convert (0,1) преобразует State b x в State (a,b) x
  • convert (0,2,1,0) преобразует State (c,b) x в State (a,b,c,d) x

Мои вопросы:

  • Есть ли лучшее решение, чем установка значений состояния в кортежах? Я думал об использовании стека трансформатора монады. Тем не менее, я думаю, что работает только если для любых двух функций f и g либо fg, либо gf, где f - множество значений состояния, необходимых для f и g - это набор значений состояния, необходимых для g. Неужели я ошибаюсь? (Обратите внимание, что мой пример не удовлетворяет этому свойству. Например, g= {a, b} и H= {b, c}. Ни одно из подмножеств другого.)
  • Если нет лучшего способа, чем кортежи, то есть ли хороший способ избежать описанного выше шаблона? Я даже готов написать файл с кучей функций шаблонов (см. Ниже), если файл может быть автоматически сгенерирован один раз, а затем забыт. Есть ли способ лучше? (Я читал об объективах, но их сложность, уродливый синтаксис, огромный набор ненужных функций и кажущаяся зависимость от Template Haskell отключаются. Является ли это неправильным пониманием? Могут ли линзы решить мою проблему таким образом, чтобы избежать этих проблем?)

(Функции, о которых я говорил, выглядят примерно так.)

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

Вы можете сделать это, используя увеличение от lens-family или lens с пакетом tuple-lenses: упрощенным типом zoom является:

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

Итак, zoom выполняет вычисление с использованием меньшего состояния. lens используется для указания местоположения меньшего состояния a внутри большего состояния s.

С этими двумя пакетами вы можете запускать g, h и i следующим образом:

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

Если вы не хотите суетиться с кортежами, вы можете использовать "классный" подход с записью. Там какой-то модный шаблон Haskell для поддержки этого в пакете lens, но вы также можете сделать это вручную. Идея состоит в том, чтобы создать по крайней мере один класс для каждой части состояния:

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
  ...

Затем каждая функция, управляющая состоянием, будет иметь сигнатуру типа

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

Действие с этой конкретной сигнатурой имеет полный доступ к точкам и доступ только для чтения к здоровью.