Функциональное программирование. Конспект лекций
.pdf
Для ввода/вывода в модуле IO существует несколько стандартных функций. Функция
getChar :: IO Char
читает символ с клавиатуры, а функция
print :: IO ()
возвращает значение единичного типа, завернутое в тип IO. Если непосредственно в интерпретаторе выполнить команды
Prelude> :m Control.Applicative
Prelude Control.Applicative> let res = (\c -> c:c:[]) <$> getChar >>= print
и вывести результат, то будет получено:
Prelude Control.Applicative> res q”qq”
Функция getChar читает символ q, лямбда-функция (\c -> c:c:[]) создает список-строку “qq”, а функция print выводит результат на экран.
7.2. do-нотация
Монады используются столь часто, что для них имеется специальный синтаксис, который облегчает подстановку специальных значений в функции нескольких переменных. Монады позволяют комбинировать специальные функции вида
a -> m b.
Если бы эти функции выглядели как обычные функции a -> b,
их можно было бы свободно комбинировать с другими функциями. Для упрощения синтаксиса работы с монадными функциями предложено специальное окружение do, в котором специальные функции комбинируются так, словно они являются обычными. Для этого используется обратная стрелка (<–). Например, функция, ко-
– 71 –
торая дважды читает строку со стандартного ввода и возвращает объединение двух строк, может быть записана как
getLine2 :: IO String getLine2 = do
a<- getLine
b<- getLine return (a ++ b)
Важно понимать, что функция getLine возвращает монадный тип IO String (getLine::IO String). Для того чтобы получить доступ к введенному значению типа String, необходимо «развернуть» монаду IO. do-нотация и использование обратной стрелки позволяют легко это записать (такие конструкции получили в Haskell название «синтаксический сахар»). Таким образом, запись a <- getLine позволяет легко «извлечь» введенное значение.
Функция return::String -> IO String «оборачивает» тип String в монаду IO.
Ниже представлен пример интерактивного взаимодействия с пользователем на основе do-нотации.
main :: IO () main = do
putStr "Введите ваше имя: " name <- getLine
putStr "Введите E-mail: " email <- getLine
putStr "Здравствуйте, " ++ name ++ "Ваш E-mail: " ++ email
bye <- getline return ()
Для вывода строк на экран используется функция putStr:: String -> IO (), которая имеет входной аргумент типа String, а возвращает единичный тип, «завернутый» в монаду IO. После вывода строки на экран курсор остается на этой же строке. Для автоматического перехода на другую строку имеется функция putStrLn вида
– 72 –
putStrLn:: String -> IO () putStrLn s = do
putStr s putChar ‘\n’
Функция putChar::Char -> IO () предназначена для вывода на стандартное устройство вывода (консоль) одного символа.
Функция getChar:: IO Char считывает один символ со стандартного устройства ввода (клавиатуры). Ранее использовавшаяся функция getLine::IO String может быть определена через getChar и рекурсивный вызов:
getLine::IO String
getLine = do
c <- getChar
if c ==’\n’ then return “”
else do cs <- getLine return (c:cs)
Следует отметить, что последним выражением в do-нотации должно быть выражение, возвращающее тип функции, в которой do-нотация используется. В данном случае это функция return, которая «оборачивает» тип String в монаду IO.
7.3. Работа с файлами
Для чтения и записи файлов есть три простые функции:
type FilePath = String
readFile :: FilePath -> IO String writeFile :: FilePath -> String -> IO () appendFile :: FilePath -> String -> IO ()
– 73 –
Функция readFile предназначена для чтения из файла, writeFile – для записи в файл строки, заданной вторым аргументом, appendFile добавляет строку в конец файла. Первым аргументом у всех функций указывается имя файла (полный путь к файлу).
В следующем примере (используя do-нотацию) запрашиваются имена входного и выходного файлов, и затем копируется текст из первого файла во второй.
main :: IO () main = do
putStr "Введите имя входного файла: " in_file <- getLine
putStr "Введите имя выходного файла: " out_file <- getLine
s <- readFile in_file
writeFile out_file s putStr "Файл скопирован!"
При использовании монадических операторов применения программа добавления новой информации к содержимому текстового файла в другой может быть записана без использования do-нота- ции. Однако это подход имеет определенную специфику и не так часто используется.
Рассмотрим пример:
main::IO ()
main = msg1 >> getLine >>= read >>= append
where read file = readFile file >>= putStrLn >> return file
append file = msg2 >> getLine >>= ap-
pendFile file
msg1 = putStr ”input file: ” msg2 = putStr ”input text: ”
– 74 –
Вэтой программе сначала запрашивается путь к файлу (msg1
>>getLine). С помощью getLine читается имя файла, которое затем используется в локальной функции read. Затем извлекается
его содержимое (read file = readFile file >>= putStrLn >> return file) и выводится на экран с помощью функции putStrLn. Затем вводится новый текст (msg2 >> getLine), который добавляется в конец файла функцией (appendFile file).
Особенность данного стиля программирования находится в использовании операторов применения из класса Monad:
class Monad IO where return :: a -> IO a
(>>=) :: IO a -> (a -> IO b) -> IO b (>>) :: IO a -> IO b -> IO b
Выражение msg1 >> getLine возвращает тип IO String, т.е. тип строки, «завернутый» в монаду IO. Это значение с помощью оператора применения >>= передается функции read file, имеющей тип String –> IO String. «Заворачивание» в монаду IO необходимо, так как функция применения >>= имеет тип IO String -> (String -> IO String) -> IO String. Данная функция читает содержимое файла, выводит его значение на экран и возвращает имя файла, «завернутое» в монаду IO. «Заворачивание» в монаду IO опять же необходимо из-за дальнейшего использования функции применения >>=.
Рассмотрим пример копирования содержимого одного файла в другой:
main = inFile >>= read >>= append where read file = readFile file
append str = do
outFile <- putStr "out-
put file: " >> getLine
writeFile outFile str inFile = putStr "input file: " >>
getLine
– 75 –
Здесь используются как монадические операторы применения, так и do-нотация, с помощью которой извлекается имя выходного файла, которое затем используется в функции writeFile. Заметим, что функция readFile будет возвращать тип IO String, который будет передаваться функции append, с помощью оператора применения >>=.
Таким образом, операции ввода/вывода в Haskell построены на основе монады IO, которая гарантирует заданную последовательность в выполнении операторов и чистоту функционального стиля. Следует помнить, что, погрузившись в монаду IO, вернуться обратно нельзя, т.е. до момента завершения все дальнейшие вычисления будут проходить внутри монады.
– 76 –
Лекция 8. Монады и функторы
Рассматриваются понятия «монада», «функтор», «аппликативный функтор». Вводится понятие применения и композиции специальных функций. Рассматриваются примеры классов типа
Monad.
Ключевые слова: монада, функтор, аппликативный функтор, класс Клейсли.
Введение. Использование монад и функторов в Haskell является нетривиальной задачей и требует особенно внимательного рассмотрения. Главная идея функционального программирования – использование чистых функций. Чистая функция принимает один или несколько аргументов, что-то вычисляет и возвращает результат. В ней отсутствуют побочные эффекты, т.е. отсутствует чтениезапись в файлы, печать на консоль, изменение и использование глобальных переменных, обработка исключений и т.д. Поведение чистой функции строго определено: для одних и тех же аргументов всегда возвращается одно и то же значение. Чистая функция более предсказуема, проще тестируется и менее подвержена ошибкам. Функция, у которой есть побочные эффекты, не обязательно вычислит один и тот же результат при нескольких одинаковых вызовах. Например, результат может стать другим, если у глобальной переменной изменится значение, или если в считываемом файле окажется разное содержание. Нечистые функции труднее тестируются, подвержены многим ошибкам, и есть множество ситуаций, когда функции завершаются сбоем.
Однако программы в чистых функциях слишком ограничены, причем в некоторых случаях без побочных эффектов обойтись вообще нельзя. Например, программа, которая копирует файл из одной папки в другую, взаимодействует с файловой системой и изменяет ее.
С помощью монад можно использовать вычисления с побочными эффектами без нарушения чистоты языка. Монады и система типов позволяют отделить вычисления с побочными эффектами от других вычислений, при этом можно выполнять побочные эффекты по необходимости.
– 77 –
Монады – это обобщение над функциями, над применением функций и над композицией функции, с помощью которого абстрагируется само понятие вычислений в сравнении со стандартными функциями. Для понимания теории монад и функторов рассмотрим понятие композиции функций [1].
8.1. Композиция функций
Функция композиции записывается как
(.) :: (b -> c) -> (a -> b) -> (a -> c) f . g = \x -> f (g x)
или
(>>) :: (a -> b) -> (b -> c) -> (a -> c) f >> g = \x -> g (f x).
Рассмотрим теперь специальные функции типа mf::a -> m b,
где m – некоторый специальный («монадический») тип, например, тип список []. Если обычная функция возвращает тип b, то функция mf возвращает «обернутый» тип mb. Для каждого m необходимо построить замкнутый мир специальных функций.
Введем специальный класс типов (категория Клейсли): class Kleisli m where
idK :: a -> m a
(*>) :: (a -> m b) -> (b -> m c) -> (a -> m c).
Для «монадических» функций, принадлежащих типу m, должны выполняться правила:
f *> idK == f |
– композиция с id-функцией |
idK *> f == f |
|
f *> (g *> h) == (f *> g) *> h |
– ассоциативность. |
Операция композиции (*>) оперирует с «монадическими» функциями. Для композиции с обычными функциями требуется определить дополнительные операции. Вводится операция композиции вида
– 78 –
(+>) :: Kleisli m => (a -> m b) -> (b -> c) -> (a -> m c) f +> g = f *> (g >> idK).
Проверим выполнение типов в данной операции: g::b -> c; idK::c -> mc
(g >> idK)::b -> mc.
и, следовательно, возможно применение операции композиции f *> (g >> idK).
Таким образом, реализовав «монадические» функции idK и *>, можно построить функцию композиции для «монадической» и обычной функций.
Функция idK :: a -> m a «заворачивает» аргумент в оболочку m.
Функция (*>) :: (a -> m b) -> (b -> m c) -> (a -> m c) выполняет последовательное применение «особых» функций, которые принимают обычный аргумент, но возвращают «обернутый».
Для последовательного применения необходимо после применения первой функции (a -> m b) «развернуть» результат и передать его следующей функции (b -> m c).
Всвою очередь функция (+>) :: (a -> m b) -> (b -> c) -> (a -> m c)
последовательно применяет «особую» функцию (a -> m b), а потом обычную (b -> c), и результат «оборачивается» в тип m.
Вкачестве примера специальных функций рассмотрим тип
Maybe:
data Maybe a = Nothing | Just a deriving (Show, Eq, Ord)
и частично определенные функции типа a -> Maybe b.
Подобные функции удобны для описания вычислений, в которых возможны ошибки. Если значение было вычислено, то возвращается (Just a), а в случае ошибки будет возвращено значение
Nothing.
Реализация класса Клейсли для типа Maybe: instance Kleisli Maybe where
idK = Just
f *> g = \a -> case f a of Nothing -> Nothing
– 79 –
Just b -> g b.
Если функция f вернула значение, то оно подставляется в следующую частично определенную функцию. Если же первая функция не смогла вычислить результат и вернула Nothing, то вся функ-
ция (f*>g) возвращает Nothing.
8.2. Применение функций
Обычная функция применения имеет тип
($) :: (a -> b) -> a -> b.
Данную функцию можно определить как f $ x = (const x >> f) (),
где const:: a -> b -> a; const x _ = x – константная функция. Анали-
зируя данное выражение, можно убедиться в его корректности:
const x :: b -> a; f::(a -> b); (const x >> f):: b -> b; (const x >> f) () :: b.
g >> f = \y -> f (g y) = \y -> f (const x y) = (\y -> f (const x y)) () = f x.
Здесь () – значение единичного типа (unit type) или кортежа с нулем элементов.
Специальные функции применения
(*$) :: (a -> m b) -> m a -> m b (+$) :: (a -> b) -> m a -> m b
и их реализация, полученная аналогично реализации ($): f *$ a = (const a *> f) ()
f +$ a = (const a +> f) ().
Проверим выполнение типов:
const a :: b -> ma; f:: (a -> m b); (*>)::(b -> ma)-> (a -> m b)-> (b->mb);
(const a *> f) () :: mb.
Аналогично
const a :: b -> ma; f:: (a -> b); (+>)::(b -> ma)-> (a -> b)-> (b->mb);
– 80 –
