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

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

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

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

Ниже представлены функции свертки при использовании других вариантов обхода деревьев. Функция foldTreeLR обходит дерево в левостороннем порядке:

foldTreeLR :: (a -> b -> b) -> b -> Tree a -> b foldTreeLR _ seed Null = seed

foldTreeLR f seed (Tree a tl tr) = foldTreeLR f (f a (foldTreeLR f seed tl)) tr

Для дерева поиска этот порядок соответствует перебору узлов в возрастающем порядке.

Функция foldTreeUpDown соответствует прохождению узлов «сверху вниз», при котором сначала проходится корень дерева, а после этого обходятся его поддеревья, в нисходящем порядке:

foldTreeUpDown :: (a -> b -> b) -> b -> Tree a ->

b

foldTreeUpDown _ seed Null = seed foldTreeUpDown f seed (Tree a tl tr) =

foldTreeUpDown f (foldTreeUpDown f (f a seed) tl) tr

Принципиальное отличие функции foldTreeUpDown от foldTreeLR и foldTree в порядке применения функции f. Так в функции foldTreeLR происходит рекурсивный спуск по левому поддереву, пока не будет достигнуто дерево Null, и только после этого происходит применение функции f. В функции foldTreeUpDown применение функции f выполняется на первом же шаге (f a seed).

6.2. Множества

Одной из наиболее удобных форм представления множества является его характеристическая функция. Характеристической функцией множества, состоящего из элементов некоторого типа T,

– 61 –

является логическая функция, определенная на значениях типа T и выдающая значение True, если значение аргумента принадлежит этому множеству, и False в противном случае. Хотя такое представление не позволяет в явном виде быстро определять элементы множества, оно одинаково хорошо приспособлено для представления как конечных, так и бесконечных множеств. Например, конечное множество целых чисел, состоящее из всех чисел диапазона 1..10, может быть лямбда-функцией \n-> (n>=1) && (n<=10), а бесконечное множество всех четных чисел с такой же (едва ли не большей) легкостью может быть представлено функцией

\n -> (n `mod` 2 == 0).

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

type Set a = a -> Bool

--Функция объединения множеств union :: Set a -> Set a -> Set a union s1 s2 e = (s1 e) || (s2 e)

--Функция пересечения множеств intersect :: Set a -> Set a -> Set a intersect s1 s2 e = (s1 e) && (s2 e)

--Разность множеств

difference :: Set a -> Set a -> Set a difference s1 s2 e = (s1 e) && not (s2 e)

--Дополнение до универсума complement :: Set a -> Set a complement s1 e = not (s1 e)

--Добавление элемента

addElem :: Eq a => Set a -> a -> Set a

– 62 –

addElem set elem e = (elem == e) || (set e)

-- Удаление элемента

remElem :: Eq a => Set a -> a -> Set a remElem set elem e = (elem /= e) && (set e)

Тип Set a задает представление множеств в виде функции a - > Bool. Может показаться, что при определении этих функций количество аргументов указано неправильно. Например, функция union :: Set a -> Set a -> Set a должна принимать два аргумента типа Set a и возвращать аргумент типа Set a. Но, как уже было сказано, type Set a – это функция типа a -> Bool, и, следовательно, при определении уравнения для этой функций необходимо, чтобы результатом ее выполнения была снова функция. Обычно в таких случаях используются лямбда-функции, а именно:

union s1 s2 = \e -> (s1 e) || (s2 e)

Как видно, в правой части данного уравнения находится функция, принимающая один аргумент и возвращающая тип Bool. В Haskell часто используется следующее правило:

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

Уравнение, которое при этом получилось, можно прочитать так: «Объединение множеств s1 и s2 содержит только такие элементы e, что e содержится в s1 или e содержится в s2».

6.2.1. Решето Эратосфена

Алгоритм – решето Эратосфена используется для построения множества простых чисел от 2 до некоторого заданного целого значения. Идея алгоритма заключается в том, что сначала берется все множество целых чисел от 2 до заданного целого значения, а потом из него исключаются непростые числа (множество «просеивается через решето»). Таким образом, множество простых чисел можно получить, если последовательно исключать из исходного множества элементы, делящиеся на 2, затем на 3 и т.д.

– 63 –

primesTo :: Integer -> Set Integer

primesTo max = erato 2 (\x -> (x >= 2) && (x <= max)) max

erato :: Integer -> Set Integer -> Integer -> Set Integer

erato n set max

| n * n > max = set -- множество уже

отфильтровано

| set n = addElem (erato (n+1) (filt

set n) max) n

| otherwise = erato (n+1) set max

filt :: Set Integer -> Integer -> Set Integer

filt set number elem = (set elem) && (elem `mod` number /= 0)

Важно отметить, что функция primesTo не строит список простых чисел из заданного диапазона, а отвечает на вопрос, принадлежит ли то или иное число множеству простых чисел из заданного диапазона. Например, при вызове (primesTo 100) 13 будет выдано значение True, так как число 13 содержится во множестве простых чисел от 2 до 100. Сложность анализа данной программы состоит в том, что результатом ее работы является не конкретное значение, а характеристическая функция.

Анализ начнем с функции filt. Ее аргументы – характеристическая функция set (описывающая исходное множество) и некоторое число number. Функция возвращает характеристическую функцию, описывающую «прореженное» множество set, из которого удалены числа, делящиеся без остатка на number. Если

={2,3,4,5,6,7,…} – исходное множество, то ={3,5,7,…} – мно-

жество, в котором нет четных чисел. Если теперь к множеству применить функцию filt set 3, то получим множество={5,7,…}, из которого удалены числа, делящиеся без остатка на 3. Вызывая функцию filt для других значений number, будем и дальше «прореживать» исходное множество.

Анализируя функцию addElem, можно заметить, что она как бы расширяет область применения исходной характеристической

– 64 –

функции, добавляя в нее условие ИЛИ, т.е. (elem == e) ||

(set e).

Функция primesTo просто вызывает основную функцию erato, параметрами которой являются: n – вспомогательная перемен-

ная, изменяющаяся от 2 до max; set – характеристическая функция множества; max – максимальное значение элемента исходного множества.

Рассмотрим шаги выполнения функции erato.

Шаг 1.

erato 2 (\n -> (n >= 2) && (n <= 100)) 100 n=2, set = N = (\x -> (x >= 2) && (x <= 100)), max=100.

Шаг 2.

set 2 => True, и, следовательно, выполняется операция addElem

(erato 3 (filt N 2) 100) 2.

Функция addElem вернет характеристическую функцию, заданную выражением (erato 3 (filt N 2) 100), и добавит к этой

функции элемент 2.

 

(filt N 2) вернет

характеристическую функцию для множества

, и будет вызвана

функция erato 3 100.

Шаг 3.

set 3 => True, и, следовательно, выполняется операция addElem

(erato 4 (filt 3) 100) 3.

Функция addElem вернет характеристическую функцию, заданную выражением (erato 4 (filt 3) 100), и добавит к этой функции элемент 3.

(filt 3) вернет характеристическую функцию для множества , и будет вызвана функция erato 4 , 100.

Шаг 4.

set 4 => False, так как множество N23 не содержит четных чисел,

и, следовательно, выполняется операция erato 5

 

max.

К результирующей характеристической функции

элемент 4 добав-

 

 

ляться не будет.

– 65 –

Шаг 5.

 

 

 

 

set 5 => True, и,

следовательно, выполняется операция addElem

(erato 6 (filt

 

5) 100) 5.

 

Функция

addElem

вернет характеристическую функцию, заданную

 

 

 

 

 

выражением (erato 6

(filt

5) 100), и добавит к этой

функции элемент 5.

 

 

(filt N23 5) вернет характеристическую функцию для множества N235 , и будет вызвана функция erato 6 N235 100.

Данные шаги будут повторяться до тех пор, пока не выполнится условие n * n > max. При выполнении условия будет возвращена построенная рекурсивно характеристическая функция вида

\x -> x==2 || x==3 || x==5 … || x==m .

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

– 66 –

Лекция 7. Ввод/вывод в Hаskell

Описываются механизмы ввода/вывода в Haskell. Чтение и запись в файл. Рассматриваются функции применения из класса

Monad.

Ключевые слова: ввод/вывод; монада IO; работа с файлами.

Введение. При разработке программы, которой можно было бы пользоваться вне интерпретатора, требуется решить ряд проблем, связанных с так называемой чистотой функциональных программ. Программа взаимодействует с пользователем (ожидает ввода с клавиатуры) и изменяет состояние компьютера (выводит сообщения на экран, записывает данные в файлы). Все это может привести к невоспроизводимости функций. В Haskell, когда определяются новые функции или константы, это сводится к введению новых имен для комбинациям значений. Можно сказать, что в Haskell ничего не изменяется. Мир Haskell статичен [1]. Это называется функцио-

нальной чистотой (referential transparency). Можно свободно заме-

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

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

В Haskell нет таинственных глобальных переменных, в которые можно записывать данные из одной функции и читать их с помощью другой. Запрещено что-либо записывать и что-либо читать. Следует помнить, что в функциональных программах отсутствует такое понятие как порядок выполнения операторов. Нельзя изменять состояния, можно лишь давать новые имена или строить новые выражения из уже существующих выражений.

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

– 67 –

Пусть имеется довольно простая программа:

let c = getChar

in print $ c : c : []

Здесь getChar – функция, которая читает один символ с клавиатуры, а функция print выводит строку на экран. Оператор (:) – конструктор списка.

Возможен и другой вариант интерпретации этой функции:

print $ getChar : getChar : []

Здесь выполнена замена символа «c» на синоним getChar. Смысл второго выражения отличается от первого: читать с клавиатуры необходимо дважды.

Можно сказать, что взаимодействие с пользователем нарушает чистоту функций. «Нечистыми» функциями называются функции с побочными эффектами. В связи с этим необходим механизм введения порядка выполнения в функциональные языки программирования с сохранением при этом преимущества функциональной чистоты.

7.1. Монада IO

Монадам и функторам будет посвящена следующая лекция. Сейчас же рассмотрим использование монады IO для выполнения операций ввода/вывода.

Для обозначения внешнего мира в Haskell определяется специальный тип RealWorld, определенный в модуле Con-

trol.Monad.ST:

module IO (IO) where

data RealWorld = RealWorld newtype IO a = IO (ST RealWorld a) instance Functor IO where ...

– 68 –

instance Applicative IO where ...

instance Monad IO where ...

Тип IO (Input-Output) обозначает взаимодействие с внешним миром. Значение типа IO a является вычислением, которое, когда выполняется, выполняет некоторый ввод-вывод перед возвратом значения типа a. На самом деле есть только один способ выполнить действие ввода/вывода: привязать его к функции main в разрабатываемой программе. Когда программа запущена, ввод/вывод будет выполнен. Невозможно выполнить ввод/вывод из произвольной функции, если только эта функция сама не находится в монаде ввода/вывода и не вызывается в какой-то момент прямо или косвенно из main.

IO – это монада, поэтому действия IO можно комбинировать, используя либо do-нотацию, либо операции >> и >> = из класса

Monad.

Операция связывания ma >>= mf из класса Monad типа IO говорит о том, что сначала будет выполнено выражение ma и результат будет подставлен в выражение mf, и только затем будет выполнено mf. Оператор связывания для специальных функций вида:

a -> IO b

разделяет статический мир Haskell на «до ввода/вывода» и «после ввода/вывода». Однажды попав «внутрь» монады IO, из нее нельзя выбраться. Тип IO разбивает статический мир на кадры. Можно создавать чистые функции и поднимать их в мир монады IO лишь там, где это действительно нужно.

Пусть требуется выполнить следующий порядок (довольно типичный) действий в программе:

1.Чтение с клавиатуры начального значения.

2.Загрузка файла настроек.

3.Запуск сложной функции.

4.Вывод результата на экран.

Схематично это можно записать так:

program = liftA2 algorithm readInit (readConfig ”file”) >>= print

– 69 –

где readInit :: IO Int

readConfig :: String -> IO Config print :: Show a => a -> IO ()

функции с побочными эффектами,

algorithm :: Int -> Config -> Result

большая, сложная, чистая функция.

Функция readInit читает начальное значение, функция readConfig читает из файла настройки, функция print выводит значение на экран. Функция liftA2 «поднимает» чистые функции в мир монад. Функция algorithm вычисляет какие-то данные. Фактически программа и есть функция algorithm. Взаимодействие с пользователем присутствует лишь в одном месте, вся функция algorithm построена по правилам функционального программирования. Таким образом, вносится порядок выполнения в программу, сохраняя возможность определения чистых функций.

Если потребуется еще один «кадр», т.е. еще одно действие ввода/вывода, тогда программа примет вид:

program =

liftA2 algorithm2 readInit

(liftA2 algorithm1 readInit (readConfig

”file”))

>>= print

где readInit :: IO Int

readConfig :: String -> IO Config print :: Show a => a -> IO ()

функции с побочными эффектами,

algorithm1 :: Int -> Config -> Result1 algorithm2 :: Int -> Config -> Result2

большие, сложные, чистые функции.

Теперь имеется два кадра, т.е. программа выполняется в два этапа. Каждый из них разделен участками взаимодействия с пользователем.

– 70 –

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