Списки играют важную роль, выражая фундаментальную идею, связанную с рекурсивным типом данных. Списки являются самым базовым контейнерным типом.
- Создать пустой список (это констрктор-константа)
[] :: [a]
- Взять существующий список и добавить в голову элемент (бинарный оператор
:
)
infixr 5 : -- оператор добавления в список правоассоциативный
(:) :: a -> [a] -> [a]
let lst = 3 : 5 : [] --добавление двух элементов в голову по порядку
Конструктор :
представляет собой оператор. В Haskell конструкторы-операторы допустимы, они называются инфиксными конструкторами данных. Однако на их имена
накладывается специальное ограничение: они должны начинаться с символа двоеточия.
Конструктор списка удовлетворяет этому требованию.
Синтаксис для списка с квадратными скобками это синтаксический сахар для применения конструкторов.
Также можно строить сечения оператора :
let const42 = (42 :) -- добавляет в голову списка элемент 42
head
- возвращает первый элемент спискаtail
- возвращает хвост списка (весь список, кроме первого элемента)
head :: [a] -> a
head (x:xs) = x
head [] = error "Prelude.head: empty list"
tail :: [a] -> [a]
tail (x:xs) = xs
tail [] = error "Prelude.tail: empty list"
Многие функции над списками используют сопоставление с двумя образцами: один для пустого списка [], другой для непустого (x:xs). При этом переменная x связывается с головой (первым элементом) непустого списка, а переменная xs — с его хвостом (остатком после отделения первого элемента). Две приведенные выше функции просто возвращают результаты этих связываний. Функции head и tail являются частичными (то есть определены не на всех возможных аргументах).
second :: [a] -> a
second xs = head (tail xs)
А вот так в бесточечном стиле:
second :: [a] -> a
second = head . tail
Гораздо более мощным способом деконструкции списка является сопоставление с образцом (в качестве образцов используются конструкторы):
second (_ : x : _) = x -- получение второго элемента списка
Можно работать с рекурсией: отрывая голову списка, длина хвоста становится на единицу меньше, такми образом, выполняя это действия, осущесвтляя рекурсивный вызов над хвостом списка, длина списка становится на единицу меньше с каждым вызовом. Конструктор пустого списка - терминирующее условие. Такая функция будет определена всегда на любом списке конечной длины. Ниже приведена реализация стандартной функции, которая возвращает длину списка.
length :: [a] -> Int -- длина списка
length [] = 0
length (_:xs) = 1 + length xs
Еще одним важнейшим оператором для списков служит оператор бинарной конкатенации: он делает из двух списков один, присоединяя первый к началу второго.
infixr 5 ++
(++) :: [a] -> [a] -> [a]
[] ++ ys = ys
(x:xs) ++ ys = x : xs ++ ys
Функция, обеспечивающая конкатенацию списка списков в список:
concat :: [[a]] -> [a]
concat [] = []
concat (xs:xss) = xs ++ concat xss
Функция, проверяющая, является ли список пустым:
null :: [a] -> Bool
null [] = True
null _ = False
Функция, проверяющая наличие элемента в списке:
infix 4 `elem`
elem :: (Eq a) => a -> [a] -> Bool
elem _ [] = False
elem x (y:ys) = x == y || elem x ys
Часто использутся инфиксно
GHCi> 'e' `elem` "Hello"
True
GHCi> 'i' `elem` "Hello"
False
Функция, осуществляющая поиск значения с заданным ключом в ассоциативном списке (то есть списке пар ключ-значение):
lookup :: Eq a => a -> [(a,b)] -> Maybe b
lookup _ [] = Nothing
lookup key ((k,v):kvs)
| key == k = Just v
| otherwise = lookup key kvs
Возвращаемым значением служит тип Maybe
, поскольку поиск может завершится неудачей
GHCi> lookup 2 [(1,"Hello"),(3,"world")]
Nothing
GHCi> lookup 3 [(1,"Hello"),(3,"world")]
Just "world"
Оператор, возвращающий элемент списка с заданным индексом:
infixl 9 !!
(!!) :: [a] -> Int -> a
xs !! n | n < 0 = error "Prelude.!!: negative index"
[] !! _ = error "Prelude.!!: index too large"
(x:_) !! 0 = x
(_:xs) !! n = xs !! (n-1)
Левая ассоциативность позволяет удобно обслуживать вложенные списки:
GHCi> ["Hello","world"] !! 0 !! 1
'e'
GHCi> ["Hello","world"] !! 1 !! 2
'r'
Функция, возвращающая последний элемент списка:
last :: [a] -> a
last (x:[]) = x
last (_:xs) = last xs
Функция, которая переворачивает список:
reverse :: [a] -> [a]
reverse l = rev l [] where
rev [] a = a
rev (x:xs) a = rev xs (x:a)
Функция take
получает целое число n и список и возвращает первые n элементов списка. Если элементов меньше, чем n, возвращается сколько есть. Если n не положительно,
возвращается пустой список.
take :: Int -> [a] -> [a]
take n _ | n <= 0 = []
take _ [] = []
take n (x:xs) = x : take (n-1) xs
GHCi> take 3 "ABCDE"
"ABC"
GHCi> take 10 "ABCDE"
"ABCDE"
Функция drop
«дуальна» к take
: она отбрасывает первые n элементов, возвращая то,
что осталось.
drop :: Int -> [a] -> [a]
drop n xs | n <= 0 = xs
drop _ [] = []
drop n (_:xs) = drop (n-1) xs
Имеется набор стандартных функции, позволяющих соединить несколько списков в один. Мы уже обсуждали конкатенацию, когда один из списков присоединялся в голову другого. Другим способом соединения служит «зиппирование», от английского zipper (застежка-молния). Зубцы с двух сторон такой застежки сцепляются последовательно: первый с первым, второй со вторым и т.д. Именно так обрабатываются и элементы двух списков в функции zip: из элементов с одинаковыми индексами образуется пара, помещаемая в результирующий список с тем же индексом:
zip :: [a] -> [b] -> [(a,b)]
zip [] _ = []
zip _ [] = []
zip (a:as) (b:bs) = (a,b) : zip as bs
На самом деле имеется целое семейство функций, подобных zip: сама zip, zip3, zip4 и т.д. Результирующий список образуется конструированием кортежа подходящего размера из элементов списков-аргументов:
zip3 :: [a] -> [b] -> [c] -> [(a,b,c)]
zip4 :: [a] -> [b] -> [c] -> [d] -> [(a,b,c,d)]
...
Функция zip
задает стандартный для Haskell способ явного индексирования:
GHCi> zip [1,2,3,4] "ABC"
[(1,'A'),(2,'B'),(3,'C')]
Отметим, что длина самого короткого из списков определяет длину результата. Это означает, что при «зиппировании» мы можем потерять часть информации — в приведенном примере это число 4. Имеется семейство unzip, «обратное» zip:
unzip :: [(a,b)] -> ([a],[b])
unzip3 :: [(a,b,c)] -> ([a],[b],[c])
...
Более общим способом соединения двух списков служит замена в функциях семейства zip конструктора данных пары (тройки, четверки и т.п.) на произвольную функцию нужного числа аргументов, предоставляеную пользователем. Это порождает семейство функции высших порядков zipWith
:
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith _ [] _ = []
zipWith _ _ [] = []
zipWith f (a:as) (b:bs) = f a b : zipWith f as bs
zipWith3 :: (a -> b -> c -> d) -> [a] -> [b] -> [c] -> [d]
...
Функции высших порядков - это функции, которые принимают в качестве аргументов другие функции. В библиотеке Data.List
много функций высших порядков. У следующих двух функций
первый аргумент — функция типа a -> Bool
, то есть унарный предикат.
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter p (x:xs)
| p x = x : filter p xs
| otherwise = filter p xs
takeWhile :: (a -> Bool) -> [a] -> [a]
takeWhile _ [] = []
takeWhile p (x:xs)
| p x = x : t
Функция filter
возвращает (сохраняя порядок) все те элементы списка, которые удовлетворяют предикату. Функция takeWhile возвращает наибольший префикс списка, все элементы которого удовлетворяют предикату.
GHCi> filter (<50) [2,12,85,0,6]
[2,12,0,6]
GHCi> takeWhile (<50) [2,12,85,0,6]
[2,12]
GHCi> dropWhile (<50) [2,12,85,0,6]
[85,0,6]
У функции высшего порядка map
функциональный аргумент — произвольная функция:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
Функция map
обрабатывает каждый элемент списка переданной функцией-обработчиком,
формируя список результатов той же длины, но, возможно, другого типа.
GHCi> map (+1) [1,2,3]
[2,3,4]
GHCi> map length ["Good","bye","world"]
[4,3,5]
GHCi> map (^2) . map length $ ["Good","bye","world"]
[16,9,25]
Функция all
принимает в качестве аргумента унарный предикат и применяет его к элементам списка:
and, or :: [a] -> Bool
and [] = True
and (x:xs) = x && and xs
or [] = False
or (x:xs) = x || or xs
all :: (a -> Bool) -> [a] -> Bool
all p = and . map p
Пример вызова all
для определения, являются все элементы списка нечетными:
GHCi> all odd [1, 3, 5]
True
GHCi> all odd [1, 2, 3]
False
Функция any
проверяет, присутствует ли в списке хотя бы один элемент, удовлетворяющий условиям:
any :: (a -> Bool) -> [a] -> Bool
any p = or . map p
Если определить рекурсивную функцию без терминирующего условия, то такая рекурсия во время исполнения приведет к расходимости.
GHCi> ones = 1 : ones
GHCi> :type ones
ones :: Num a => [a]
GHCi> ones
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,Interrupted.
GHCi> numsFrom n = n : numsFrom (n+1)
GHCi> :t numsFrom
numsFrom :: Num t => t -> [t]
GHCi> numsFrom 3
[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,Interrupted.
Однако, если, как в приведенных выше примерах, в процессе рекурсии мы конструируем некоторую структуру данных рекурсивного типа, то возникающая расходимость является продуктивной. Это значит, что мы можем терминировать ее внешними средствами, пользуясь тем, что сопоставление с образцом форсирует вычисления до слабой головной нормальной формы, то есть до конструктора на верхнем уровне.
GHCi> take 2 ones
[1,1]
GHCi> take 4 (numsFrom 3)
[3,4,5,6]
Каков механизм этой «внешней терминации»? Вспомнив реализацию
take :: Int -> [a] -> [a]
take n _ | n <= 0 = []
take _ [] = []
take n (x:xs) = x : take (n-1) xs
приведем шаги ленивого вычислителя:
take 2 ones → -- (1)
take 2 (1 : ones) → -- (2)
1 : take (2-1) ones) → -- (3)
1 : take 1 (1 : ones) → -- (4)
1 : 1 : take (1-1) ones) → -- (5)
1 : 1 : take 0 ones) → -- (6)
1 : 1 : []
- сопоставление с образцом форсирует вычисление ones до WHNF;
- подходит последнее уравнение в определении take, используем его;
- сопоставление с образцом форсирует вычисление до WHNF обоих аргументов take: первого, чтобы отвергнуть первое из уравнений в определении take, второго — чтобы отвергнуть второе;
- используем последнее уравнение в определении take;
- сопоставление с образцом форсирует вычисление до WHNF первого аргумента take;
- подходит первое уравнение в определении take, используем его
Таким образом мы можем эффективно работать с «бесконечными» списками. Более того, мы можем декларативно описывать преобразования бесконечных списков, порождая новые (бесконечные) списки. Функции работы со списками из Data.List реализованы так, чтобы способ обработки бесконечных списков ничем не отличается от конечных:
GHCi> squares = map (^2) (numsFrom 0)
GHCi> takeWhile (<=100) squares
[0,1,4,9,16,25,36,49,64,81,100]
Каноническим примером рекурсивного построения бесконечного списка служит бесконечный список чисел Фибоначчи
GHCi> fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)
GHCi> take 10 fibs
[0,1,1,2,3,5,8,13,21,34]
Имеется библиотечный аналог нашей пользовательской функции numsFrom. Он носит название enumFrom и имеет более общий тип, позволяющий работать не только с числами, но и с любыми перечислениями, то есть любыми представителями класса типов Enum.
GHCi> :t enumFrom
enumFrom :: Enum a => a -> [a]
GHCi> enumFrom False
[False,True]
GHCi> enumFrom ((maxBound::Int) - 2)
[9223372036854775805,9223372036854775806,9223372036854775807]
Заметим, что когда тип перечисления является ограниченным (то есть представителем класса типов Bounded), то список аккуратно терминируется. Для неограниченного типа Integer функция enumFrom порождает бесконечный список. Для функции enumFrom имеется удобный синтаксический сахар, задаваемый следующим правилом трансляции
[e1..] ≡ enumFrom e1
То есть предыдущие примеры могут быть записаны так
GHCi> [False ..]
[False,True]
GHCi> [(maxBound::Int) - 2 ..]
[9223372036854775805,9223372036854775806,9223372036854775807]
Класс типов Enum задает еще ряд полезных арифметических последовательностей, которые транслируются в функции, подобные enumFrom:
GHCi> [1..10]
[1,2,3,4,5,6,7,8,9,10]
GHCi> ['A'..'z']
"ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz"
GHCi> [1,3..17]
[1,3,5,7,9,11,13,15,17]
GHCi> [1,3..]
[1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,Interrupted.
Для формирования «нелинейных» последовательностей имеется другая техника, носящая название выделение списка (list comprehension)
GHCi> digits = [0..9]
GHCi> [ x^2 | x <- digits ]
[0,1,4,9,16,25,36,49,64,81]
Часть справа от вертикальной черты носит название генератора: элементы, связываемые с переменной x пробегают по всему списку digits. Слева от вертикальной черты находится выражение, в котором можно использовать x^30. При наличии нескольких генераторах чаще обновляется тот, что правее:
GHCi> [ [x,y] | x <- "ABC", y <- "de" ]
["Ad","Ae","Bd","Be","Cd","Ce"]
Генераторы могут ссылаться на значения из предыдущих генераторов; кроме того можно использовать предикаты над этими значениями для фильтрации результатов. Вот например, выделение пифагоровых троек (сторон прямоугольных треугольников с целыми длинами):
GHCi> ls = [1..19]
GHCi> [ (x,y,z) | x <- ls, y <- [1..x], z <- ls, x^2 + y^2 == z^2 ]
[(4,3,5),(8,6,10),(12,5,13),(12,9,15),(15,8,17)]
Для фильтрации можно использовать сопоставление с образцом в генераторе:
GHCi> tst = [Just 2,Nothing,Just 3]
GHCi> [ x | Just x <- tst ]
[2,3]
Хотя допустим всего один образец, неудачное сопоставление с ним не влечет расходимости: элемент просто не включается в результирующий список.