Изучай Haskell во имя добра! - Миран Липовача
Шрифт:
Интервал:
Закладка:
Используйте кортежи, когда вы знаете заранее, из скольких элементов будет состоять некоторая часть данных. Кортежи гораздо менее гибки, поскольку количество и тип элементов образуют тип кортежа, так что вы не можете написать общую функцию, чтобы добавить элемент в кортеж – понадобится написать функцию, чтобы добавить его к паре, функцию, чтобы добавить его к тройке, функцию, чтобы добавить его к четвёрке, и т. д.
Как и списки, кортежи можно сравнить друг с другом, если можно сравнивать их компоненты. Однако вам не удастся сравнить кортежи разных размеров (хотя списки разных размеров сравниваются, если можно сравнивать их элементы).
Несмотря на то что есть списки с одним элементом, не бывает кортежей с одним компонентом. Если вдуматься, это неудивительно. Кортеж с единственным элементом был бы просто значением, которое он содержит, и, таким образом, не давал бы нам никаких дополнительных возможностей[6].
Использование пар
Вот две полезные функции для работы с парами:
• fst – принимает пару и возвращает её первый компонент.
ghci> fst (8,11)
8
ghci> fst ("Вау", False)
"Вау"
• snd – принимает пару и возвращает её второй компонент. Неожиданно!
ghci> snd (8,11)
11
ghci> snd ("Вау", False)
False
ПРИМЕЧАНИЕ. Эти функции работают только с парами. Они не будут работать с тройками, четвёрками, пятёрками и т. д. Выделение данных из кортежей мы рассмотрим чуть позже.
Замечательная функция, производящая список пар, – zip. Она принимает два списка и сводит их в один, группируя соответствующие элементы в пары. Это очень простая, но крайне полезная функция. Особенно она полезна, когда вы хотите объединить два списка или обойти два списка одновременно. Продемонстрируем работу zip:
ghci> zip [1,2,3,4,5] [5,5,5,5,5]
[(1,5),(2,5),(3,5),(4,5),(5,5)]
ghci> zip [1 .. 5] ["один", "два", "три", "четыре", "пять"]
[(1,"один"),(2,"два"),(3,"три"),(4,"четыре"),(5,"пять")]
Функция «спаривает» элементы и производит новый список. Первый элемент идёт с первым, второй – со вторым и т. д. Обратите на это внимание: поскольку пары могут содержать разные типы, функция zip может принять два списка, содержащих разные типы, и объединить их. А что произойдёт, если длина списков не совпадает?
ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["я","не","черепаха"]
[(5,"я"),(3,"не"),(2,"черепаха")]
Более длинный список просто обрезается до длины более короткого! Поскольку язык Haskell ленив, мы можем объединить бесконечный список с конечным:
ghci> zip [1..] ["яблоко", "апельсин", "вишня", "манго"]
[(1,"яблоко"),(2,"апельсин"),(3,"вишня"),(4,"манго")]
В поисках прямоугольного треугольника
Давайте закончим главу задачей, в решении которой пригодятся и генераторы списков, и кортежи. Предположим, что требуется найти прямоугольный треугольник, удовлетворяющий всем следующим условиям:
• длины сторон являются целыми числами;
• длина каждой стороны меньше либо равна 10;
• периметр треугольника (то есть сумма длин сторон) равен 24.
Треугольник называется прямоугольным, если один из его углов является прямым (равен 90 градусам). Прямоугольные треугольники обладают полезным свойством: если возвести в квадрат длины сторон, образующих прямой угол, то сумма этих квадратов окажется равной квадрату стороны, противоположной прямому углу. На рисунке стороны, образующие прямой угол, помечены буквами a и b; сторона, противоположная прямому углу, помечена буквой c. Эта сторона называется гипотенузой.
Первым делом построим все тройки, элементы которых меньше либо равны 10:
ghci> let triples = [(a,b,c) | c <– [1..10], b <– [1..10], a <– [1..10]]
Мы просто собираем вместе три списка, и наша производящая функция объединяет их в тройки. Если вы вызовете функцию triples в GHCi, то получите список из тысячи троек. Теперь добавим условие, позволяющее отфильтровать только те тройки, которые соответствуют длинам сторон прямоугольных треугольников. Мы также модифицируем эту функцию, приняв во внимание, что сторона b не больше гипотенузы, и сторона a не больше стороны b.
ghci> let rightTriangles = [ (a,b,c) | c <– [1..10], b <– [1..c], a <– [1..b], a 2 + b 2 == c 2]
ПРИМЕЧАНИЕ. В консоли интерпретатора GHCi невозможно определять программные сущности в нескольких строках. Но в данной книге нам иногда приходится разбивать определения на несколько строк, чтобы код помещался на странице. В противном случае книга оказалась бы такой широкоформатной, что для неё вам пришлось бы купить гигантский книжный шкаф!
Почти закончили. Теперь давайте модифицируем функцию, чтобы получить треугольники, периметр которых равен 24.
ghci> let rightTriangles' = [ (a,b,c) | c <– [1..10], b <– [1..c], a <– [1..b], a 2 + b 2 == c 2, a+b+c == 24]
ghci> rightTriangles'
[(6,8,10)]
Вот и ответ! Это общий шаблон в функциональном программировании. Вы берёте начальный набор решений и затем применяете преобразования и фильтруете их, пока не получите результат.
2
Типы и классы типов
Поверь в типы
Мы уже говорили о том, что Haskell является статически типизированным языком. Тип каждого выражения известен во время компиляции – это залог безопасного кода. Если вы напишете программу, которая попытается поделить булевский тип на число, то она даже не скомпилируется.
И хорошо, потому что уж лучше ловить такие ошибки на этапе компиляции, чем наблюдать, как ваша программа аварийно закрывается во время работы! Всему в языке Haskell назначен свой тип, так что компилятор может сделать довольно много выводов о программе перед её компиляцией.
В отличие от языков Java или Pascal, у Haskell есть механизм вывода типов. Если мы напишем число, то нет необходимости указывать, что это число. Язык Haskell может вывести это сам, так что нам не приходится явно обозначать типы функций и выражений.
Мы изучили некоторые основы языка, лишь вскользь упомянув о типах. Тем не менее понимание системы типов – очень важная часть обучения языку Haskell.
Тип – это нечто вроде ярлыка, который есть у каждого выражения. Он говорит нам, к какой категории относится данное выражение. Выражение True – булево, "привет" – это строка, и т. д.
Явное определение типов
А сейчас воспользуемся интерпретатором GHCi для определения типов нескольких выражений. Мы сделаем это с помощью команды :t, которая, если за ней следует любое правильное выражение, выдаст нам тип последнего. Итак…
ghci> :t 'a'
'a' :: Char
ghci> :t True
True :: Bool
ghci> :t "ПРИВЕТ!"
"ПРИВЕТ!" :: [Char]
ghci> :t (True, 'a')
(True, 'a') :: (Bool, Char)
ghci> :t 4 == 5
4 == 5 :: Bool
Мы видим, что :t печатает выражения, за которыми следуют :: и их тип. Символы :: означают: «имеет тип». У явно указанных типов первый символ всегда в верхнем регистре. Символ 'a', как вы заметили, имеет тип Char. Несложно сообразить, что это сокращение от «character» – символ. Константа True имеет тип Bool. Выглядит логично… Идём дальше.
Исследуя тип "ПРИВЕТ!", получим [Char]. Квадратные скобки указывают на список – следовательно, перед нами «список символов». В отличие от списков, каждый кортеж любой длины имеет свой тип. Так выражение (True, 'a') имеет тип (Bool, Char), тогда как выражение ('a','b','c') будет иметь тип (Char, Char, Char). Выражение 4==5 всегда вернёт False, поэтому его тип – Bool.
У функций тоже есть типы. Когда мы пишем свои собственные функции, то можем указывать их тип явно. Обычно это считается нормой, исключая случаи написания очень коротких функций. Здесь и далее мы будем декларировать типы для всех создаваемых нами функций.
Помните генератор списка, который мы использовали ранее: он фильтровал строку так, что оставались только прописные буквы? Вот как это выглядит с объявлением типа:
removeNonUppercase :: [Char] –> [Char]
removeNonUppercase st = [ c | c <– st, c `elem` ['А'..'Я']]
Функция removeNonUppercase имеет тип [Char] –> [Char]. Эта запись означает, что функция принимает одну строку в качестве параметра и возвращает другую в качестве результата.
А как записать тип функции, которая принимает несколько параметров? Вот, например, простая функция, принимающая три целых числа и складывающая их:
addThree :: Int –> Int –> Int –> Int