Haskell. Монады. Монадные трансформеры. Игра в типы

Еще одно введение в монады для совсем совсем начинающих.

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

Написание кода на Haskell похоже на игру, в которой вы должны преобразовать объекты к нужному типу. Поэтому вам в первую очередь нужно понять правила этой игры. При написании кода вы должны четко понимать, какой тип имеет каждый конкретный кусок кода.

С обычными функциями все понятно. Если имеется функция типа «a→b», то подставив в неё аргумент типа «a», вы получите результат типа «b».

С монадами все не так очевидно. Под катом подробно расписано, как работать с do-конструкцией, как последовательно преобразуются типы, и зачем нужны монадные трансформеры.


1. Do-конструкция
Начнем с простого примера.
main = do 
	putStr "Enter your name\n"
	name <- getLine	
	putStr $ "Hello " ++ name

Каждая do-конструкция имеет тип «m a», где «m» — это монада. В нашем случае это монада IO.
d6364706da4c4ec2a3a27e07b6b2efde.png

Каждая строчка в do-конструкции так же имеет тип «m a». Значение «a» в каждой строчке может быть разным.
091e9fdf4b8d44f8b8d21a278c115252.png
Символ »<-", как бы, преобразует тип «IO String» в тип «String».

Если нам необходимо произвести в монаде некоторые вычисления, не связанные с данной монадой, то мы можем воспользоваться функцией return.

return :: a -> m a

main = do 
	text <- getLine 
	doubleText <- return $ text ++ text
 	putStr doubleText 

Функция return заворачивает любой тип «a» в монадический тип «m a».
828ac316d0c74872816145395c886a4f.png
В данном примере, с помощью return выражение типа «String» преобразуется к типу «IO String», которое потом обратно разворачивается в «String». Как вариант, внутри do-конструкции можно использовать ключевое слово let.
main = do 
	text <- getLine 
	let doubleText = text ++ text
 	putStr doubleText

Вся do-конструкция принимает тип последней строчки.
0873265ca66e4812b815a26329a872d0.png

Допустим, мы хотим прочитать содержимое файла. Для этого у нас имеется функция readFile.

readFile :: FilePath -> IO String

Как видим, функция возвращает «IO String». Но нам нужно содержимое файла в виде «String». Это значит, что мы должны выполнить нашу функцию внутри do-конструкции.
printFileContent = do
	fileContent <- readFile "someFile.txt" 
	putStr fileContent

Здесь переменная fileContent имеет тип «String», и мы можем работать с ней, как с обычной строкой (например, вывести на экран). Обратите внимание, что получившаяся функция printFileContent имеет тип «IO ()»
printFileContent :: IO ()
2. Монады и монадные трансформеры
Я приведу следующую простую аналогию. Представьте, что монада — это пространство, внутри которого можно производить некоторые, специфичные для данного пространства, действия.
Например, в монаде «IO» можно выводить текст в консоль.
dfe5fd7e9e8d4f56a5437b0b3987ddc2.gif
main = do 
	print "Hello"

В монаде «State» есть некоторое внешнее состояние, которое мы можем модифицировать.
bff1462ddc3c4d7cb38b1cf4a3d622c5.gif

main = do 
	let r = runState (do 
		modify (+1)
		modify (*2)
		modify (+3)
		) 5
	print r

-- OUTPUT: 
--      ((), 15)

В этом примере мы взяли число 5, прибавили к нему 1, умножили результат на 2, затем прибавили еще 3. В результате получили число 15.

С помощью функции runState

runState :: State s a -> s -> (a, s)

мы «запускаем» нашу монаду.
fd76513311774688a5df3432f4da16f6.png

На монаду можно посмотреть с двух сторон: изнутри и снаружи. Изнутри мы можем выполнить некоторые, специфичные для данной монады, действия. А снаружи — мы можем её «запустить», «распечатать», преобразовать к некоторому немонадическому типу.

Это позволяет нам вкладывать одну do-конструкцию в другую, как в приведенном выше примере. Монада IO — это единственная монада, на которую нельзя посмотреть «снаружи». Все в конечном итоге оказывается вложенным в IO. Монада IO — это наш фундамент.

Приведенный выше пример имеет определенные ограничения. Внутри монады State мы не можем выполнять действия, доступные в IO.
7a654cf067a54bc787a5a20d9f8bcb68.png
Мы оказались «подвешенными в воздухе», потеряли связь с землей.

Для решения этой проблемы существуют монадные трансформеры.

main = do 
	r <- runStateT (do 
		modify (+1)
		modify (*2)
		s <- get
		lift $ print s 
		modify (+3)
		) 5
	print r

-- OUTPUT:
--    12 
--    ((), 15)

Данная программа делает то же самое, что и предыдущая. Мы заменили State на StateT и добавили две строчки,
s <- get
lift $ print s 

с помощью которых выводим промежуточный результат в консоль. Обратите внимание, операция ввода/вывода выполняется внутри «вложенной» монады StateT.

Здесь runStateT запускает монаду StateT, а функция lift «поднимает» операцию, доступную в IO, до монады StateT.

runStateT :: StateT s m a -> s -> m (a, s)
lift :: IO a -> StateT s IO a

Изучите внимательно, как последовательно преобразуется тип в данном примере.
8507427fbe18479290cd34947c645110.png
Операция «print s» имеет тип «IO ()».
С помощью lift мы «поднимаем» его до типа «StateT Int IO ()».
Внутренняя do-конструкция теперь имеет тип «StateT Int IO ()».
Мы «запускаем» её и получаем тип «Int → IO ((), Int)».
Затем мы подставляем значение »5» и получаем тип «IO ((), Int)».
Поскольку, мы получили тип «IO», то мы можем использовать его во внешней do-конструкции.
Стрелочка »<-" снимает монадический тип и возвращает "((), Int)".
В консоль выводится результат »((), 15)».

Внутри StateT мы можем менять внешнее состояние и выполнять операции ввода/вывода. Т.е. монада StateT не «болтается в воздухе», как State, а осталась связанной с внешней монадой IO.
6b329aadcf02450a9de74bf8f3d79cf9.png

Таким образом, в программе может быть куча монад, вложенных друг в друга. Некоторые из этих монад будут сцеплены друг с другом, некоторые — нет.

Надеюсь, моя аналогия помогла Вам взглянуть на вещи с новой точки зрения, и Вы сможете в будущем стать настоящим повелителем монад.
aa2758a2776d4e44a3f12e6056f54a9f.png

Комментарии (0)

© Habrahabr.ru