Функциональное программирование. Конспект лекций
.pdf
| otherwise = False
Реализация других функций оказывается немного сложнее. Функция length (длина списка) чем-то похожа на функцию sumList:
length :: [a] -> Int length [] = 0
length (_:s) = 1 + length s
Однако такая реализация не оптимальна, так как рекурсивный вызов функции length сначала будет строить арифметическое выражение вида 1+1+…+1, а потом будет происходить свертка этого выражения. При списках большой длины это приведет к существенным затратам. Гораздо лучше использовать переменную «аккумулятор»:
length :: [a] -> Int length s = length’ s 0
length’ :: [a] -> Int -> Int length’ [] len = len
length’ (_:s) len = length’ s (len+1)
Здесь вычисление длины списка выполняется во вспомогательной функции length’. Основная функция вызывает вспомогательную функцию с заданными параметрами.
Для реализации других функций удобно воспользоваться функцией реверсирования списка, т.е. расположения элементов списка в обратном порядке. Заметим, что функция reverse :: [a] -> [a] определена в модуле GHC.List, но здесь она будет определена как самостоятельная функция:
reverse::[a]->[a] reverse s = reverse' [] s
reverse'::[a]->[a]->[a] reverse' s [] = s
reverse' s1 (x:s2) = reverse' (x:s1) s2
– 41 –
Тогда функцию last можно определить как выбор первого элемента у реверсивного списка:
last :: [a] -> a
last s = head (reverse s)
Аналогично, функцию init можно определить как выбор «хвоста» у реверсивного списка (в этом случае не учитывается последний элемент), и далее надо опять применить функцию reverse для правильного вывода списка:
init::[a]->[a]
init s = reverse (tail (reverse s))
Более сложная реализация для функций !! и ++. Для реализации функции !! требуется перемещаться по элементам списка до заданного значения и после этого выдать требуемый элемент списка:
!!:: [a]-> Int -> a
!!s n = f’ s n 0
f’ :: [a]-> Int -> Int -> a f’ (x:s) n k | n==k = x
| otherwise = f’ s n (k+1)
Функция f’ является вспомогательной, но именно в ней реализован весь функционал.
Для реализации функции конкатенации списков ++ следует брать по одному элементу из первого списка и помещать во второй:
++:: [a]->[a]->[a]
++s1 s2 = f' (reverse s1) s2
f' :: [a]->[a]->[a] f' [] s = s
f' (x:s1) s2 = f' s1 (x:s2)
Как и в предыдущих примерах, функция f' является вспомогательной, но именно в ней реализован весь функционал. Бинарная
– 42 –
функция ++ вызывает f', передавая ей в качестве аргументов реверсированный первый список и неизменный второй. Реверсирование необходимо, чтобы элементы первого списка добавлялись ко второму в правильной последовательности.
4.4. Функция обработки строк
Для работы с символьными данными существует специальный модуль Data.Char. Рассмотрим функцию, которая определяет количество строк в списке, содержащих хотя бы одну букву:
module TestList where
import Data.Char
hasLetters::[String]->Int hasLetters [] = 0
hasLetters (s:ls) = (if hasLetter s then 1 else 0) + hasLetters s
hasLetter :: String -> Bool hasLetter [] = False
hasLetter (x:s) | isAlpha x = True
| otherwise = hasLetter s
Функция hasLetters просматривает строку за строкой, причем для проверки наличия в строке хотя бы одной буквы используется функция hasLetter. Буквой считается символ, для которого функция isAlpha :: Char -> Bool выдает значение True. Таким образом, функция hasLetter анализирует символ за символом, пока не встретит букву. В этом случае возвращается истина (True) и функция завершает свою работу.
Если hasLetter возвращает True, то выражение if hasLetter s then 1 else 0 заменяется на 1 и таким образом формируется выражение: 1+1+…+0, которое потом вычисляется.
И в заключение рассмотрим функцию поиска подстроки в строке. В данном случае функция будет искать вхождение одного короткого списка в другой, более длинный список. Для решения данной задачи введены несколько вспомогательных функций:
– 43 –
subList1::[a]-> Int -> Int -> [a] subList1 l@(x:xs) n k | n==k = l
| otherwise = subList1 xs n
(k+1)
subList2::[a]-> Int -> Int -> [a] -> [a] subList2 l@(x:xs) m k s | m==k = my_reverse (x:s)
| otherwise = subList2 xs
m (k+1) (x:s)
subList::[a]-> Int -> Int -> [a]
subList s n m = subList2 (subList1 s n 0) m 0 []
inList::Eq a => [a]-> [a] -> Int -> Bool inList s1 s2 n | sl1 == s2 = True
| n < l = inList s1 s2 (n+1) | otherwise = False
where sl1 = subList s1 n l2 l1 = length s1
l2 = length s2 - 1 l = l1 - l2 – 1
Функция subList1 исключает из списка первые n элементов,
т.е. если имеется список [1,2,3,4,5,6,7,8,9], то subList1
[1,2,3,4,5,6,7,8,9] 3 0 вернет список [4,5,6,7,8,9].
Функция subList2 возвращает из списка первые m+1 элементов,
т.е. если имеется список [1,2,3,4,5,6,7,8,9], то subList2 [1,2,3,4,5,6,7,8,9] 3 0 [] вернет [1,2,3,4]. В этих функци-
ях используется синтаксис l@(x:xs), который позволяет разбивать список на «голову» и «хвост» и вместе с тем дает синоним для обращения ко всему списку через имя l.
Функция subList s n m возвращает из списка s m+1 элемент, начиная с позиции n. Нетрудно увидеть, что реализация функции subList основана на комбинации функций subList1 и subList2. Данная функция аналогична функции substr в языке программирования «C».
Проверка вхождения списка s2 в список s1 выполняется функцией inList. Для этого из s1, начиная с нулевой, начальной пози-
– 44 –
ции, выделяется с помощью функции subList часть списка длиной, равной s2, и производится их сравнение. Если списки не совпадают, то позиция выделения подсписка сдвигается на единицу и происходит рекурсивный вызов функции inList s1 s2 (n+1). Процесс выделения части списка продолжается до тех пор, пока можно выделить список длиной, совпадающей с s2.
4.5. Сортировка списка
Существует довольно много алгоритмов сортировки списков (массивов). Ниже представлен алгоритм сортировки списка методом «простых вставок»:
simpleSort :: (Ord a) => [a] -> [a] simpleSort [] = []
simpleSort (x:s) = insert x (simpleSort s)
insert :: (Ord a) => a -> [a] -> [a] insert elem [] = [elem]
insert elem list@(x:s) | elem < x = elem:list
| otherwise = x:(insert el-
em s)
Функция simpleSort simpleSort (x:s) использует вспомогательную функцию insert для вставки (перестановки) очередного элемента x внутрь списка. Для этого сначала выбирается первый элемент и вызывается функция insert, которая вставляет данный элемент в отсортированную часть «хвоста» списка таким образом, чтобы не нарушить сортировку. Если вставляемый элемент меньше головного, то он добавляется в начало списка, если нет, то рекурсивно вызывается функция insert elem s, которая пытается вставить элемент в «хвост» списка. В реализации данного алгоритма наглядно продемонстрирована мощь и красота функционального стиля программирования. Разработка программы основана на декларации требуемого результата, а не на деталях реализации.
– 45 –
Лекция 5. Функции высших порядков
Рассматриваются функции высших порядков для работы со списками. Приводятся примеры работы со списками, преобразования списков, свертки списков.
Ключевые слова: список; функция map; функции foldl, foldr; функция zip.
Введение. Рассмотрим функции, аргументами или результатами которых могут быть тоже функции. Такие функции принято называть функциями высших порядков. Так как в Haskell функция может иметь много аргументов, то применение неполного комплекта аргументов будет возвращать функцию. Поэтому можно считать, что любая функция в Haskell – функция высшего порядка. Тем не менее к функциям высшего порядка обычно относят функции, принимающие в качестве аргументов другие функции. В императивных языках программирования такие функции часто бывает невозможно описать. В функциональном программировании, однако, функции являются базовыми понятиями, так что подобные описания не только возможны, но и достаточно часто встречаются. В частности, функция, определяющая суперпозицию двух других функций, может быть легко описана средствами языка Haskell. Суперпозиция функций будет подробно рассмотрена в лекции, посвященной монадам и функторам.
5.1. Функция map
Функция map – это стандартная функция, определенная в ядре языка имеющая тип:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map func (x:s) = (func x):(map func s)
Аргументами map являются функция преобразования и список элементов. Результат – список из преобразованных элементов, по-
– 46 –
лученных применением функционального параметра последовательно к каждому элементу списка. Вызов
map (\x->2*x) [1, 2, 5, -2].
приведет к удвоению каждого элемента списка, т.е. на выходе будет получен список [2,4,10,-4]. Здесь в качестве функции преобразования использована так называемая лямбда-функция, хотя можно было бы использовать любую функцию типа Integer -> Integer. Лямбда-функция представляет собой функциональное значение, изображающее безымянную функцию.
Если, например, функция sqr возводит в квадрат целое число, то с помощью функции map можно получить список квадратов элементов списка:
map sqr [1, 2, 5, -2] [1, 4, 25, 4].
Первое уравнение в определении map _ [] = [] определяет результат работы отображения в случае пустого списка. В этом случае первый аргумент функции не используется, и результатом будет пустой список независимо от того, какая функция применяется для отображения его элементов.
Второе уравнение map func (x:s) = (func x):(map func s)
использует рекурсивное обращение к функции map для того, чтобы построить отображение «хвоста» списка, а потом присоединяет к результату головной элемент, который получается с помощью применения к первому элементу исходного списка функции func.
Функция map, которая в качестве аргумента получает другую функцию или выдает функцию в качестве результата, называется
функцией высшего порядка или функционалом.
Функция может иметь и более сложный вид. Так, например, для того чтобы получить список из знаков заданного списка целых чисел с помощью функции map (знак числа – это функция, выдающая нуль, единицу или минус единицу в зависимости от того, является ли число нулевым, положительным или отрицательным), можно определить функцию для вычисления знака числа:
sign |
0 |
= |
0 |
sign |
n | |
n < 0 = -1 |
|
|
|
| |
otherwise = 1 |
– 47 –
Нахождение списка знаков для заданного списка чисел имеет вид:
map sign [2,5,0,-3,2,-2]
(в результате получится список [1, 1, 0, -1, 1, -1]).
Задание функции преобразования возможно в блоке where, который может содержать несколько определений функций, а каждая из этих функций может иметь и несколько уравнений и даже определение типа, например:
map fact [1..10] where
fact :: Integer -> Integer fact 0 = 1
fact n | n > 0 = n * fact (n-1)
5.2. Функции foldl и foldr
Рассмотрим функцию высшего порядка, которая в качестве аргумента будет получать функцию двух аргументов (бинарную функцию). Эта функция последовательно применяется к элементам списка так, чтобы в результате обработки всего списка получилось бы одно значение, т.е. выполняется свертка списка. Например, если бинарную операцию сложения применять последовательно ко всем элементам списка (взяв в качестве начального значения нуль), то в результате получится сумма всех элементов списка. Если же в качестве бинарной операции взять операцию умножения, то в результате перемножения всех элементов списка получится произведение этих элементов (теперь взяв в качестве начального значения единицу).
В стандартной библиотеке языка Haskell имеются две функции (функции свертки списков), одна из которых (foldl) применяет заданную бинарную операцию к элементам списка от начала списка к его концу, а вторая (foldr ) начинает обработку элементов с конца списка. Данные функции определены следующим образом:
foldl :: (b -> a -> b) -> b -> [a] -> b foldl _ seed [] = seed
– 48 –
foldl f seed (x:s) = foldl f (f seed x) s
foldr :: (a -> b -> b) -> b -> [a] -> b foldr _ seed [] = seed
foldr f seed (x:s) = f x (foldr f seed s)
Здесь f – применяемая функция с двумя аргументами (бинарная функция), seed – начальное значение (оно будет результатом работы функции свертки, если обрабатываемый список не содержит элементов). Первое уравнение в определении foldl и foldr определяет поведение функции свертки для случая, когда исходный список пуст. В этом случае результатом будет начальное значение – seed. Если же исходный список не пуст, то для foldr построим свертку «хвоста» списка с помощью рекурсивного обращения к функции foldr, а затем применим бинарную операцию f к первому элементу списка и результату свертки «хвоста».
Для функции foldl выполняется рекурсивный вызов этой функции с измененными значениями аргументов: foldl f (f seed x) s. Здесь функция f применяется к seed и x. Результат применения становится новым начальным значением, а список уменьшается на один элемент, так как из него убирается «голова» списка. Для получения суммы элементов списка достаточно вызвать функцию
foldl (+) 0 [1,2,3,4,5] --или
foldr (+) 0 [1,2,3,4,5]
В качестве начального значения используется 0, а в качестве бинарной операции – операция сложения (+). Следует обратить внимание, что бинарную операцию следует заключить в круглые скобки.
Функции высших порядков часто позволяют писать короткие и выразительные программы, не содержащие в явном виде рекурсии. Например, можно очень элегантно и компактно определить функцию реверсирования списка:
addElem :: [a] -> a -> [a] addElem list elem = elem:list
– 49 –
reverse :: [a] -> [a]
reverse list = foldl addElem [] list
Функция addElem добавляет элемент elem к началу списка list. Эта функция отличается от стандартного конструктора списков (:) только порядком аргументов.
Функция reverse просто вызывает foldl с аргументами addElem [] list. В качестве начального значения seed для foldl выбран пустой список. Таким образом функция foldl выбирает по одному элементу из списка list и передает их функции addElem, которая добавляет их к списку (на начальном этапе пустому). В приведенном примере функции реверсирования списка нет явной рекурсии. Рекурсивные вызовы «спрятаны» внутри функции foldl.
В следующем примере дана функции вычисления факториала, в которой для вычисления сначала строится список целых чисел от единицы до заданного значения аргумента, а потом все элементы полученного списка перемножаются:
factorial n = foldr (*) 1 [1..n]
5.3. Функции filter, zip и zipWith
Важной стандартной функцией высших порядков является также функция фильтрации списка filter, которая используется для того, чтобы по заданному списку элементов получить список из тех его элементов, которые удовлетворяют заданному условию. Тип функции фильтрации записывается в виде
filter :: (a -> Bool) -> [a] -> [a] filter f [] = []
filter f (x:s) | f x = x : (filter f s) | otherwise = filter f s
Функция filter часто также используется в виде специальной синтаксической конструкции:
[ x | x <- s, f x ]
– 50 –
