Вступление
Мне встречалась фраза: "для многих знакомство с 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. Тогда мне очень пригодился бы пример ее использования. Ведь когда видишь, как что-то непонятное применяется на практике, становится проще понять это самое непонятное. Надеюсь данная статья кому-то поможет.
