Пример использования State и StateT

от автора

Вступление

Мне встречалась фраза: «для многих знакомство с Haskell заканчивается на монадах». Монады действительно сложны для понимания, а самая непонятная, лично для меня, была монада State.

На простом примере, я хочу показать всю полезность монады State и еще большую полезность трансформера StateT.

Идея

  • Есть игровое поле

  • Пустые ячейки поля обозначены символом ‘O’

  • Символом ‘X’ будет обозначен «герой», который сможет перемещаться по игровому полю вверх, вниз, влево, вправо.

Игровое поле

Начнем с определения игрового поля. Оно будет квадратным. В нем хранится информация о размере этого самого поля и позиция героя:

data GameField = GameField Int (Int, Int)

Для приведения игрового поля к строке, воспользуемся модулем Data.Array понадобится три функции:

listArray :: Ix i => (i, i) -> [e] -> Array i e (//)      :: Ix i => Array i e -> [(i, e)] -> Array i e elems     :: Array i e -> [e]

listArray принимает нижнюю и верхнюю границы индексов массива, список значений, а возвращает массив. Список значений может быть любой длины, лишь бы только элементов списка хватило для построения возвращаемого массива. Бесконечного списка точно хватит, repeat как раз и создает бесконечный список. И да вместо чисел можно использовать, например пары, они будут играть роль координат.

listArray ((1,1), (3,3)) (repeat 'O') array ((1,1),(3,3)) [((1,1),'O'),((1,2),'O'),((1,3),'O')                     ,((2,1),'O'),((2,2),'O'),((2,3),'O')                     ,((3,1),'O'),((3,2),'O'),((3,3),'O')]

Оператор (//) принимает массив, список пар (индекс, значение). С помощью (//) можно будет помещать героя в заданные координаты

arr // [((2,2), 'X')] array ((1,1),(3,3)) [((1,1),'O'),((1,2),'O'),((1,3),'O')                     ,((2,1),'O'),((2,2),'X'),((2,3),'O')                     ,((3,1),'O'),((3,2),'O'),((3,3),'O')]

elems возвращает список элементов массива (строка в нашем случае). Далее список разделим на список списков, который соберется функцией unlines в строку в задуманном виде. Сейчас файл GameField.hs выглядит так:

module GameField where  import           Data.Array  data GameField = GameField Int (Int, Int)  instance Show GameField where   show (GameField s h) =     unlines $ splitString $ elems $ (// [(h, 'X')]) $ listArray       ((1, 1), (s, s))       (repeat 'O')    where     splitString :: String -> [String]     splitString ""  = []     splitString str = let (l, rest) = splitAt s str in l : splitString rest

Осталось реализовать функцию для передвижения

move :: Char -> GameField -> GameField move 'W' (GameField s (yH, xH)) = GameField s ((yH - 1) `max` 1, xH) move 'A' (GameField s (yH, xH)) = GameField s (yH, (xH - 1) `max` 1) move 'S' (GameField s (yH, xH)) = GameField s ((yH + 1) `min` s, xH) move 'D' (GameField s (yH, xH)) = GameField s (yH, (xH + 1) `min` s) move _   gf                     = gf

Записи типа

(yH - 1) max 1  (yH + 1) min s

будут контролировать, чтобы герой не вышел за пределы поля

Игровое поле, как изменяемое состояние

Монады в Haskell это вычисления с побочным эффектом. Побочный эффект монады State — изменяемое состояние:

State s a, где s - какое-либо состояние,                 a - значение, получаемое каким то образом из состояния

Определим функцию

heroMove :: Char -> State GameField () heroMove ch = modify (move ch)

она принимает символ, а возвращает монаду State, состоянием которой будет GameField (игровое поле), а возвращаемое значение (). Реализация проста: функция modify принимает функцию (s -> s), (GameField -> GameField) в нашем случае.

И наконец, определим функцию

pathMove :: String -> State GameField () pathMove = mapM_ heroMove

Эта функция будет обрабатывать строку, которая по сути будет путем, по которому пройдет наш герой. Реализована эта функция через

mapM_ :: (Foldable t, Monad m) => (a -> m b) -> t a -> m () -- а если без полиморфизма mapM_ :: (Char -> State GameField ()) -> String -> State GameField ()

Все готово, чтобы написать простую интерактивную программу. Пользователь введет путь и получит результат. Полный листинг файла GameFieldState.hs:

module GameFieldState where  import           Control.Monad.State import           GameField  heroMove :: Char -> State GameField () heroMove ch = modify (move ch)  pathMove :: String -> State GameField () pathMove = mapM_ heroMove  main :: IO () main = do   let gf = GameField 3 (2, 2) -- создание игрового поля   print gf                    -- вывод первоначального игрового поля   path <- getLine             -- получение пользовательского ввода   print $ execState (pathMove path) gf -- изменение и вывод игрового поля

Попробуем в ghci:

:l GameFieldState main OOO OXO OOO  WA XOO OOO OOO

Введя строку WA, мы «загнали» героя в верхний левый угол.

StateT — еще больше интерактивности

Но что если хочется вводить не весь путь сразу, а управлять каждым шагом героя и видеть результат. Благодаря трансформерам монад это возможно.

Тип функции передвижения героя будет уже такой:

heroMove :: StateT GameField IO ()

Уже нет символа(Char) только

StateT s m a, s - также состояние               a - также возвращаемое значение               m - внутренняя монада

IO будет внутренней монадой. Файл GameFieldStateT.hs выглядит так

module GameFieldStateT where  import           Control.Monad.Trans import           Control.Monad.Trans.State import           GameField  heroMove :: StateT GameField IO () heroMove = do   gf <- get           -- получение игрового поля   lift $ print gf     -- вывод, lift позволяет производить вычисления в IO    ch <- lift getChar  -- получить символ от пользователя   lift $ putStrLn ""  -- новая строка в терминале   modify $ move ch    -- уже знакомая modify и move ch   heroMove            -- рекурсивный вызов, нет покоя герою  main :: IO () main = evalStateT heroMove (GameField 4 (2, 2)) -- вычисления в StateT

Испытаем в ghci:

:l GameFieldStateT main OOOO OXOO OOOO OOOO  d OOOO OXOO OOOO OOOO  D OOOO OOXO OOOO OOOO  S OOOO OOOO OOXO OOOO

Заключение

В свое время, лично меня очень пугала монада State. Тогда мне очень пригодился бы пример ее использования. Ведь когда видишь, как что-то непонятное применяется на практике, становится проще понять это самое непонятное. Надеюсь данная статья кому-то поможет.


ссылка на оригинал статьи https://habr.com/ru/post/694944/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *