Вступление
Мне встречалась фраза: «для многих знакомство с 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/
Добавить комментарий