...

суббота, 4 марта 2017 г.

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.

Каждая строчка в do-конструкции так же имеет тип «m a». Значение «a» в каждой строчке может быть разным.

Символ "<-", как бы, преобразует тип «IO String» в тип «String».

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

return :: a -> m a

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


Функция return заворачивает любой тип «a» в монадический тип «m a».

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


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

Допустим, мы хотим прочитать содержимое файла. Для этого у нас имеется функция 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» можно выводить текст в консоль.

main = do 
        print "Hello"

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

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)


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

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

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

Приведенный выше пример имеет определенные ограничения. Внутри монады State мы не можем выполнять действия, доступные в IO.

Мы оказались «подвешенными в воздухе», потеряли связь с землей.

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

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

Изучите внимательно, как последовательно преобразуется тип в данном примере.

Операция «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.

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

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

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

    Let's block ads! (Why?)

    Комментариев нет:

    Отправить комментарий