Смешайте и сопоставьте вычисления с учётом состояния в рамках государственной монады
Состояние моей программы состоит из трех значений: 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
либо f
⊆ g
, либо g
⊆ f
, где 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
Действие с этой конкретной сигнатурой имеет полный доступ к точкам и доступ только для чтения к здоровью.