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