Функциональное программирование. Конспект лекций
.pdf
лять факториал. Для этого нужно ввести дополнительную «аккумулирующую» переменную, в которой и будет накапливаться значение факториала по мере рекурсии. Как только рекурсия завершится, «аккумулирующая» переменная будет содержать искомую величину. Ниже представлен пример «хвостовой» рекурсии:
fact' :: Integer -> Integer -> Integer fact' 1 acc = acc
fact' n acc = fact' (n-1) (n* acc)
fact3::Integer -> Integer fact3 n = fact' n 1
Рекурсивное вычисление факториала осуществляет вспомогательная функция fact', вторым аргументом которой является «аккумулятор». При первом вызове функции fact' значение acc=1. При дальнейших рекурсивных вызовах значение n уменьшается на 1, а «аккумулятор» изменяется как acc= acc* n. Когда n достигнет 1, рекурсия завершается и «аккумулятор» будет содержать значение n! Функция fact3 хотя и считается главной, но ее назначение – только вызов fact' n 1 с аргументами n и 1.
3.2. Рекурсивное вычисление чисел Фибоначчи
Рассмотрим пример программы для вычисления чисел Фибоначчи, а именно: по заданному номеру n найти соответствующее число в последовательности чисел Фибоначчи. Для чисел Фибоначчи справедливы соотношения:
= 1; = 1;
= |
+ , |
= 3,4,…, |
из которых нетрудно получить последовательность: 1,1,2,3,5,8,,,. Исходя из представленных формул, программа для вычисления заданного числа Фибоначчи может быть записана как
fib :: Integer -> Integer fib 1 = 1
fib 2 = 1
fib n = fib (n-1) + fib (n-2)
– 31 –
В этом решении производятся два рекурсивных обращения к функции fib. Это приводит к тому, что число обращений к функции резко возрастает при увеличении значения n. С увеличением аргумента время, требующееся для решения задачи, растет экспоненциально.
Для получения эффективного решения надо добавить в функцию дополнительные аргументы, такие, чтобы ей не приходилось при каждом вызове находить уже найденные ранее значения чисел Фибоначчи. Например, можно определить функцию, которая вычисляет n-е число Фибоначчи, зная значения двух последних вычисленных чисел.
-- fib' - вспомогательная функция.
fib' :: Integer -> Integer -> Integer -> Integer - > Integer
fib' n k fk fk1 | k == n = fk
| k < n = fib' n (k+1) (fk+fk1) fk
-- fib - основная функция вычисления n-го числа Фибоначчи.
fib :: Integer -> Integer fib 1 = 1
fib n = fib' n 2 1 1
В приведенном примере все вычисления выполняет вспомогательная функция fib', которая имеет четыре аргумента: n – номер числа Фибоначчи, которое надо вычислить, k – номер последнего вычисленного числа Фибоначчи, fk – значение k-го числа Фибоначчи и fk1 – значение предыдущего числа Фибоначчи. Реализация функции основана на использовании «охранных» выражений с помощью символа «|». Главная функция fib вызывает fib'с аргументами n 2 1 1. Рекурсивный вызов будет продолжаться до тех пор, пока значение второго аргумента не станет равным перво-
му. Такие аргументы называют накапливающими параметрами функции. При рекурсивном вызове fib' n (k+1) (fk+fk1) fk значение второго аргумента увеличивается на 1, вычисляется очередное число Фибоначчи (fk+fk1), а четвертый аргумент принимает значение предыдущего числа Фибоначчи.
– 32 –
3.3. Рекурсивное вычисление числа Эйлера
Рассмотрим программу вычисления числа Эйлера e=2.71828…. с заданной точностью. Для вычисления числа Эйлера используем разложение функции в ряд Тейлора в точке 0. Как известно, данное разложение имеет вид
ex =1+ x + |
x2 |
+ |
x3 |
+... |
xn |
+...= ∞ |
xk |
. |
|
|
n! |
|
|||||
2! |
3! |
|
k =0 k ! |
|||||
Вместо того, чтобы вычислять каждое слагаемое в соответствии с приведенной формулой, найдем отношение членов ряда под номером k и k – 1:
xk |
|
xk −1 |
|
= |
x |
. |
k ! |
|
(k − 1)! |
k |
|||
|
|
|
||||
Таким образом, если известен (k – 1)-й член ряда, то для нахождения k члена ряда надо значение (k – 1)-го члена умножить на kx .
Для вычисления e полагаем x = 1, и тогда e = ∞k =0 k1! и отно-
шение между k-м и (k – 1)-м и членами ряда равно 1k .
Данный подход реализован в приведенной ниже программе:
e :: Double -> Double e eps = e' eps 1 1 0
e' :: Double -> Double -> Double -> Double -> Double
e' eps k mem sum | mem < eps = sum
| otherwise = e' eps (k+1)
(mem/k) (sum+mem)
Основные вычисления реализованы во вспомогательной функции e', которая имеет четыре аргумента. Первый аргумент eps – точность вычислений, k – номер последнего вычисленного члена ряда, mem – значение последнего вычисленного члена ряда, sum –
– 33 –
накопленное значение суммы ряда. Реализация функции основана на использовании «охранных» выражений с помощью символа «|». Рекурсивный вызов будет продолжаться до тех пор, пока значение последнего вычисленного члена ряда не станет меньше eps. Четвертый аргумент является накапливающим параметром функции, так как в нем накапливается сумма ряда. При рекурсивном вызове
e' eps (k+1) (mem/k) (sum+mem)
значение k увеличивается на 1, вычисляется очередной член ряда и накапливается сумма. Главная функция e вызывает e' с аргумен-
тами eps 1 1 0.
3.4. Проверка простоты натурального числа
Рассмотрим программу проверки того, является ли заданное натуральное число простым. Простым является число, которое делится без остатка только на единицу и само на себя. Алгоритм проверки заключается в том, что заданное число последовательно проверяется на его делимость на все возрастающие натуральные числа до тех пор, пока не оказывается, что либо делитель найден, либо этот делитель уже превосходит квадратный корень из аргумента. Если делитель найден, то число составное, если нет, то простое. Как и в предыдущем примере, решение будет содержать основную и вспомогательную функции, причем основную работу будет выполнять вспомогательная функция, а основная служит для вызова вспомогательной с правильными начальными аргументами.
prime :: Integer -> Bool
prime p | p <= 0 = error "prime: Non-positive argument"
| otherwise = prime' 2 p
prime' :: Integer -> Integer -> Bool prime' d p | d * d > p = True
| p `mod` d == 0 = False
| otherwise = prime' (d+1) p
– 34 –
В основной функции prime проводится проверка аргумента на принадлежность к натуральным числам, т.е. проверяется, что p>0. Если это не так, то выдается сообщение об ошибке путем вызова функции error. В противном случае вызывается вспомогательная функция prime' 2 p, которой передаются аргументы: начальный делитель d=2 и число p. Функция error является функцией с побочным эффектом. Это так называемая «нечистая» функция. Она осуществляет вывод на консоль, а это может привести к невоспроизводимости выполнения функции (например, консоль может быть не инициализирована или по разному отображать символы в зависимости от платформы). Язык Haskell содержит помимо чисто функциональных средств также набор средств, выходящих за рамки собственно функционального программирования. Функция error – один из примеров применения нефункциональных средств в функциональном языке программирования. Рассмотрению функций с побочными эффектами будут посвящены лекции о монадах и функторах и средствах ввода/вывода.
Вспомогательная функция prime' принимает два аргумента: делитель d и само число p. Реализация функции основана на использовании «охранных» выражений с помощью символа «|». Рекурсивный вызов будет продолжаться до тех пор, пока квадрат делителя меньше p, причем значение делителя увеличивается на единицу при каждом новом рекурсивном вызове prime' (d+1) p. Если окажется, что остаток от целочисленного деления равен нулю (p `mod` d == 0), то рекурсия прекращается и возвращается значение False, т.е. число составное.
Приведенные примеры наглядно иллюстрируют всю мощь и красоту использования рекурсии. Важно следить за выполнением условия окончания рекурсии, иначе рекурсия может продолжаться бесконечно и завершится только после того, как будет исчерпана оперативная память компьютера. Компилятор Haskell отслеживает ситуацию отсутствия условия на окончание рекурсии и выдает соответствующее сообщение об ошибке. Еще раз подчеркнем важность порядка расположения функций при наличии рекурсии. Так, например, если программа вычисления факториала будет записана следующим образом:
– 35 –
fact2 :: Integer -> Integer fact2 n = n * fact2 (n-1) fact2 0 = 1
fact2 1 = 1
то рекурсия никогда не завершится, так как поиск синонимовфункций=выражений будет осуществляться сверху вниз, и первая строка fact2 n = n * fact2 (n-1) будет соответствовать образцу, и рекурсия будет выполняться для отрицательных значений n.
– 36 –
Лекция 4. Работа со списками
Рассматриваются сложные структуры данных – списки. Рассматриваются базовые функции для работы со списками. Приводятся примеры работы со списками, поиск и выделение элементов списка, сравнения списков.
Ключевые слова: список; функции head, tail, last, init; конкате-
нация списков.
Введение. В императивных языках программирования для построения сложных объектов используют такие структуры данных, как записи, массивы, списковые структуры, классы и др. Аналогом записи в языке Haskell является кортеж. Способов построения массивов в языке Haskell нет (хотя при использовании типа Monad такая возможность имеется). Причина этого состоит в том, что для работы с массивом требуется, чтобы отдельные компоненты массива могли быть изменяемыми, но это не соответствует функциональному стилю программирования. В Haskell способ построения структур данных произвольной степени сложности из более простых объектов основан на списках. Списки в Haskell напоминают обычные массивы императивных языков программирования. Список – последовательность объектов одного и того же типа, состоящая из произвольного числа объектов. Список, не содержащий ни одного объекта, называется пустым.
4.1. Конструкторы списков
Тип списка формально определятся как data [a] = a : [a] | [].
Он обозначается в квадратных скобках [a], где a – тип элементов списка. Список может быть пустым [] или создается с помощью конструктора (:), который является бинарной функцией с аргументами типа a и списком [a]. С помощью конструктора списки могут создаваться в программе путем последовательного присоединения элементов к началу списка.
– 37 –
Таким образом, [Int] будет обозначать тип списка, содержащего в качестве компонентов целые числа, а [([Char], Double)] – тип списка, компонентами которого являются кортежи из двух полей, причем первым полем в каждом кортеже будет список символов, а вторым полем – вещественное число.
В простейшем случае списки задаются перечислением своих компонентов в квадратных скобках через запятую, например [1, 3, 4, 8] – четырехкомпонентный список целых чисел (типом этого списка будет [Int]).
Запись, в которой компонентами списка будут объекты разных типов, синтаксически неверна. Запись [3, 4, '*'] не может быть списком, поскольку из трех элементов первые два – целые числа, а третий элемент – символ.
Список, состоящий из символов, можно записать в виде строки, заключив составляющие его символы в двойные кавычки. Так, например, строка "Haskell" (тип String) обозначает в программе список из семи символьных значений, и такая запись полностью эквивалентна расширенной записи: ['H', 'a', 's', 'k', 'e', 'l', 'l'].
Существует удобное средство построения списков целых чисел, образующих арифметическую прогрессию. Если шаг прогрессии равен единице, то можно указать только первый и последний элементы списка, разделив их символом из двух точек. Так [1..10] обозначает список из десяти последовательных натуральных чисел от 1 до 10. Если шаг прогрессии не равен единице, то указывают два первых элемента списка и последний элемент. Обозначение [3, 5..11] задает список из пяти целых чисел 3, 5, 7, 9 и 11. Подобная запись может использоваться и для задания списков с числом элементов, неизвестным в момент написания программы. Например, запись [1..n] может использоваться для создания списка первых натуральных чисел от единицы до текущего значения n, которое в разных ситуациях при исполнении программы может быть различным. Комбинировать в изображении списка прогрессию с отдельными элементами, не входящими в прогрессию, нельзя. Так, например, запись [3, 5..13, 14] будет некорректной.
Изображение списка в виде последовательности элементов всегда можно записать с помощью конструктора списков. Например, список [2, 3, 4] может быть записан в виде 2:(3:(4:[])).
– 38 –
4.2. Функции работы со списками
Для обработки списков необходимо извлекать отдельные элементы списка. Существует большое количество библиотечных функций, например:
head – возвращает первый элемент списка, применима только к непустому списку;
last – возвращает последний элемент списка, применима только к непустому списку;
tail – возвращает остаток списка, полученный отбрасыванием первого элемента, применима только к непустому списку;
init – возвращает список без последнего элемента, применима только к непустому списку;
!! – по списку и заданному номеру элемента выдает соответствующий элемент списка. Список должен быть непустым, а номер должен лежать в пределах от нуля (первый элемент списка имеет номер нуль) до количества элементов списка без единицы. Например, вычисление [1, 2, 3, 4] !! 2 приведет к выдаче в качестве результата числа 3 – элемента списка, имеющего номер 2;
null – функция проверки пустоты списка. Если в качестве аргумента этой операции будет задан пустой список, то функция выдаст значение True, в противном случае – False;
length – функция, вычисляющая количество элементов в списке (длину списка). Если аргумент – пустой список, то результатом функции будет нуль;
++ – операция соединения (конкатенации) двух списков. В результате применения этой операции к двум спискам из элементов одного и того же типа получается новый список из элементов того же типа, составленный из элементов первого списка, за которыми следуют элементы второго списка.
При использовании списков в качестве аргументов функции можно делать декомпозицию списка в левой части определения функции. Поскольку любой список «собирается» с помощью конструктора списков, то он «разбирается» на первый элемент и остаток списка. В уравнении для определения функции записывается «образец» – формальное выражение с конструктором, в котором отдельные части списка – его первый элемент и остаток – обозначены отдельными идентификаторами. Например, функция для вычисления суммы элементов списка может быть записана как:
– 39 –
sumList :: [Integer] -> Integer sumList [] = 0
sumList (x:s) = x + sumList s
Если в качестве аргумента функции будет передан непустой список, то при попытке использовать второе уравнение произойдет сопоставление аргумента с образцом (x:s). Поскольку непустой список всегда имеет форму, образованную с помощью конструктора списка (явно или неявно), то сопоставление пройдет успешно, причем в результате сопоставления произойдет «разборка» списка на первый элемент (x) и хвост (s) списка. В правой части уравнения идентификаторы x и s используются при рекурсивном вызове функции и в операции сложения.
Сопоставление с образцом по первому уравнению в этом случае будет успешным, только если в качестве аргумента функции задан пустой список.
4.3. Реализация функций работы со списками
Хотя для работы со списками существует большое количество библиотечных функций, тем не менее рассмотрим собственную реализацию некоторых функций работы со списками. Функции head, tail, init и null очень просты в реализации:
head :: [a] -> |
a |
head [] => *** |
Exception: empty list |
head (x:s) = x |
|
tail :: [a] -> |
[a] |
tail [] => *** |
Exception: empty list |
tail (x:s) = s |
|
init :: [a] -> |
[a] |
init [x] = []
init (x:xs) = x : init xs
null :: [a] -> Bool null s | s == [] = True
– 40 –
