Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Функциональное программирование. Конспект лекций

.pdf
Скачиваний:
0
Добавлен:
15.08.2026
Размер:
1 Мб
Скачать

Для ввода/вывода в модуле 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 –

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]