Изучай Haskell во имя добра!

Липовача Миран

1

На старт, внимание, марш!

 

 

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

Первое, что мы сделаем, – запустим компилятор GHC в интерактивном режиме и вызовем несколько функций, чтобы «прочувствовать» язык Haskell – пока ещё в самых общих чертах. Откройте консоль и наберите ghci. Вы увидите примерно такое приветствие:

GHCi, version 7.0.3: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done.

Loading package integer-gmp ... linking ... done.

Loading package base ... linking ... done.

Loading package ffi-1.0 ... linking ... done.

Prelude>

Поздравляю – вы в GHCi!

ПРИМЕЧАНИЕ. Приглашение консоли ввода – Prelude> , но поскольку оно может меняться в процессе работы, мы будем использовать просто ghci> . Если вы захотите, чтобы у вас было такое же приглашение, выполните команду :set prompt "ghci> " .

Немного школьной арифметики:

ghci> 2 + 15 17

ghci> 49 * 100

4900

ghci> 1892 – 1472 420

ghci> 5 / 2

2.5

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

ghci> (50 * 100) – 4999 1

ghci> 50 * 100 – 4999

1

ghci> 50 * (100 – 4999)

–244950

Здорово, правда? Чувствую, вы со мной не согласны, но немного терпения! Небольшая опасность кроется в использовании отрицательных чисел. Если нам захочется использовать отрицательные числа, то всегда лучше заключить их в скобки. Попытка выполнения 5 * –3 приведёт к ошибке, зато 5 * (–3) сработает как надо.

Булева алгебра в Haskell столь же проста. Как и во многих других языках программирования, в Haskell имеется два логических значения True и False, для конъюнкции используется операция && (логическое «И»), для дизъюнкции – операция || (логическое «ИЛИ»), для отрицания – операция not.

ghci> True && False False

ghci> True && True True

ghci> False || True True

ghci> not False

True

ghci> not (True&&True)

False

Можно проверить два значения на равенство и неравенство с помощью операций == и /=, например:

ghci> 5 == 5

True

ghci> 1 == 0

False

ghci> 5 /= 5

False

ghci> 5 /= 4

True

ghci> "привет" == "привет"

True

А что насчёт 5 + лама или 5 == True? Если мы попробуем выполнить первый фрагмент, то получим большое и страшное сообщение об ошибке!

No instance for (Num [Char])

arising from a use of `+' at :1:0–9

Possible fix: add an instance declaration for (Num [Char]) In the expression: 5 + "лама"

In the definition of `it': it = 5 + "лама"

Та-ак! GHCi говорит нам, что лама не является числом, и непонятно, как это прибавить к 5. Даже если вместо лама подставить четыре или 4, Haskell всё равно не будет считать это числом! Операция + ожидает, что аргументы слева и справа будут числовыми. Если же мы попытаемся посчитать True == 5, GHCi опять скажет нам, что типы не совпадают.

Несмотря на то что операция + производится только в отношении элементов, воспринимаемых как число, операция сравнения (==), напротив, применима к любой паре элементов, которые можно сравнить. Фокус заключается в том, что они должны быть одного типа. Вы не сможете сравнивать яблоки и апельсины. В подробностях мы это обсудим чуть позже.

ПРИМЕЧАНИЕ. Запись 5 + 4.0 вполне допустима, потому что 5 может вести себя как целое число или как число с плавающей точкой. 4.0 не может выступать в роли целого числа, поэтому именно число 5 должно «подстроиться».

 

Вызов функций

Возможно, вы этого пока не осознали, но всё это время мы использовали функции. Например, операция * – это функция, которая принимает два числа и перемножает их. Как вы видели, мы вызываем её, вставляя символ * между числами. Это называется «инфиксной записью».

Обычно функции являются префиксными, поэтому в дальнейшем мы не будем явно указывать, что функция имеет префиксную форму – это будет подразумеваться. В большинстве императивных языков функции вызываются указанием имени функции, а затем её аргументов (как правило, разделённых запятыми) в скобках. В языке Haskell функции вызываются указанием имени функции и – через пробел – параметров, также разделённых пробелами. Для начала попробуем вызвать одну из самых скучных функций языка:

ghci> succ 8 9

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

Функции min и max принимают по два аргумента, которые можно сравнивать (как и числа!), и возвращают большее или меньшее из значений:

ghci> min 9 10

9

ghci> min 3.4 3.2

3.2

ghci> max 100 101 101

Операция применения функции (то есть вызов функции с указанием списка параметров через пробел) имеет наивысший приоритет. Для нас это значит, что следующие два выражения эквивалентны:

ghci> succ 9 + max 5 4 + 1

16

ghci> (succ 9) + (max 5 4) + 1

16

Однако если мы хотим получить значение, следующее за произведением чисел 9 и 10, мы не можем написать succ 9 * 10, потому что это даст значение, следующее за 9 (т. е. 10), умноженное на 10, т. е. 100. Следует написать succ (9 * 10), чтобы получить 91.

Если функция принимает ровно два параметра, мы также можем вызвать её в инфиксной форме, заключив её имя в обратные апострофы. Например, функция div принимает два целых числа и выполняет их целочисленное деление:

ghci> div 92 10

9

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

ghci> 92 `div` 10

9

Многие люди, перешедшие на Haskell с императивных языков, придерживаются мнения, что применение функции должно обозначаться скобками. Например, в языке С используются скобки для вызова функций вроде foo(), bar(1) или baz(3, ха-ха). Однако, как мы уже отмечали, для применения функций в Haskell предусмотрены пробелы. Поэтому вызов соответствующих функций производится следующим образом: foo, bar 1 и baz 3 ха-ха. Так что если вы увидите выражение вроде bar (bar 3), это не значит, что bar вызывается с параметрами bar и 3. Это значит, что мы сначала вызываем функцию bar с параметром 3, чтобы получить некоторое число, а затем опять вызываем bar с этим числом в качестве параметра. В языке С это выглядело бы так: “bar(bar(3))”.

 

Функции: первые шаги

Определяются функции точно так же, как и вызываются. За именем функции следуют параметры, разделённые пробелами. Но при определении функции есть ещё символ =, а за ним – описание того, что функция делает. В качестве примера напишем простую функцию, принимающую число и умножающую его на 2. Откройте свой любимый текстовый редактор и наберите в нём:

doubleMe x = x + x

Сохраните этот файл, например, под именем baby.hs. Затем перейдите в каталог, в котором вы его сохранили, и запустите оттуда GHCi. В GHCi выполните команду :l baby. Теперь наш сценарий загружен, и можно поупражняться c функцией, которую мы определили:

ghci> :l baby

[1 of 1] Compiling Main     ( baby.hs, interpreted )

Ok, modules loaded: Main.

ghci> doubleMe 9

18

ghci> doubleMe 8.3

16.6

Поскольку операция + применима как к целым числам, так и к числам с плавающей точкой (на самом деле – ко всему, что может быть воспринято как число), наша функция одинаково хорошо работает с любыми числами. А теперь давайте напишем функцию, которая принимает два числа, умножает каждое на два и складывает их друг с другом. Допишите следующий код в файл baby.hs:

doubleUs x y = x*2 + y*2

ПРИМЕЧАНИЕ. Функции в языке Haskell могут быть определены в любом порядке. Поэтому совершенно неважно, в какой последовательности приведены функции в файле baby.hs .

Теперь сохраните файл и введите :l baby в GHCi, чтобы загрузить новую функцию. Результаты вполне предсказуемы:

ghci> doubleUs 4 9

26

ghci> doubleUs 2.3 34.2

73.0

ghci> doubleUs 28 88 + doubleMe 123

478

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

doubleUs x y = doubleMe x + doubleMe y

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

Кроме прочего, подобный подход позволяет избежать дублирования кода. Например, представьте себе, что какие-то «математики» решили, будто 2 – это на самом деле 3, и вам нужно изменить свою программу. Тогда вы могли бы просто переопределить doubleMe как x + x + x, и поскольку doubleUs вызывает doubleMe, данная функция автоматически работала бы в странном мире, где 2 – это 3.

Теперь давайте напишем функцию, умножающую число на два, но только при условии, что это число меньше либо равно 100 (поскольку все прочие числа и так слишком большие!):

doubleSmallNumber x = if x > 100

                      then x

                      else x*2

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

Можно было бы написать конструкцию if в одну строку, но я считаю, что это не так «читабельно». Ещё одна особенность условной конструкции в языке Haskell состоит в том, что она является выражением. Выражение – это код, возвращающий значение. 5 – это выражение, потому что возвращает 5; 4 + 8 – выражение, x + y – тоже выражение, потому что оно возвращает сумму x и y.

Поскольку ветвь else обязательна, конструкция if всегда что-нибудь вернёт, ибо является выражением. Если бы мы хотели добавить единицу к любому значению, получившемуся в результате выполнения нашей предыдущей функции, то могли бы написать её тело вот так:

doubleSmallNumber' x = (if x > 100 then x else x*2) + 1

Если опустить скобки, то единица будет добавляться только при условии, что x не больше 100. Обратите внимание на символ апострофа (') в конце имени функции. Он не имеет специального значения в языке Haskell. Это допустимый символ для использования в имени функции.

Обычно мы используем символ прямого апострофа ' для обозначения строгой (не ленивой) версии функции либо слегка модифицированной версии функции или переменной. Поскольку апостроф – допустимый символ в именах функций, мы можем определять такие функции:

conanO'Brien = "Это я, Конан О'Брайен!"

Здесь следует обратить внимание на две важные особенности. Во-первых, в названии функции мы не пишем имя conan с прописной буквы. Дело в том, что наименования функций не могут начинаться с прописной буквы – чуть позже мы разберёмся, почему. Во-вторых, данная функция не принимает никаких пара метров.

Когда функция не принимает аргументов, говорят, что это константная функция. Поскольку мы не можем изменить содержание имён (и функций) после того, как их определили, идентификатор conanO'Brien и строка "Это я, Конан О'Брайен!" могут использоваться взаимозаменяемо.

 

Списки

 

Как и списки покупок в реальном мире, списки в языке Haskell очень полезны. В данном разделе мы рассмотрим основы работы со списками, генераторами списков и строками (которые также являются списками).

Списки в языке Haskell являются гомогенными структурами данных; это означает, что в них можно хранить элементы только одного типа. Можно иметь список целых или список символов, но нельзя получить список с целыми числами и символами одновременно.

Списки заключаются в квадратные скобки, а элементы разделяются запятыми:

ghci> let lostNumbers = [4,8,15,16,23,42]

ghci> lostNumbers

[4,8,15,16,23,42]

ПРИМЕЧАНИЕ. Можно использовать ключевое слово let , чтобы определить имя прямо в GHCi. Например, выполнение let a = 1 из GHCi – эквивалент указания a = 1 в скрипте с последующей загрузкой.

 

Конкатенация

Объединение двух списков – стандартная задача. Она выполняется с помощью оператора ++.

ghci> [1,2,3,4] ++ [9,10,11,12] [1,2,3,4,9,10,11,12]

ghci> "привет" ++ " " ++ "мир"

"привет мир"

ghci> ['в','о'] ++ ['-'] ++ ['о','т']

"во-от"

ПРИМЕЧАНИЕ. Строки в языке Haskell являются просто списками символов. Например, строка привет – это то же самое, что и список ['п','р','и','в','е','т'] . Благодаря этому для работы со строками можно использовать функции обработки символов, что очень удобно.

Будьте осторожны при использовании оператора ++ с длинными строками. Если вы объединяете два списка (даже если в конец первого из них дописывается второй, состоящий из одного элемента, например [1,2,3] ++ [4]), то язык Haskell должен обойти весь список с левой стороны от ++. Это не проблема, когда обрабатываются небольшие списки, но добавление к списку из 50 000 000 элементов займёт много времени. А вот если вы добавите что-нибудь в начало списка с помощью оператора : (также называемого «cons»), долго ждать не придётся.

ghci> 'В':"ОТ КОШКА"

"ВОТ КОШКА"

ghci> 5:[1,2,3,4,5]

[5,1,2,3,4,5]

Обратите внимание, что оператор : принимает число и список чисел или символ и список символов, в то время как ++ принимает два списка. Даже если вы добавляете один элемент в конец списка с помощью оператора ++, следует заключить этот элемент в квадратные скобки, чтобы он стал списком:

ghci> [1,2,3,4] ++ [5]

[1,2,3,4,5]

Написать [1,2,3,4] ++ 5 нельзя, потому что оба параметра оператора ++ должны быть списками, а 5 – это не список, а число.

Интересно, что [1,2,3] – это на самом деле синтаксический вариант 1:2:3:[]. Список [] – пустой, и если мы добавим к его началу 3, получится [3]; если затем добавим в начало 2, получится [2,3] и т. д.

ПРИМЕЧАНИЕ. Списки [] , [[]] и [[],[],[]] совершенно разные. Первый – это пустой список; второй – список, содержащий пустой список; третий – список, содержащий три пустых списка.

 

Обращение к элементам списка

Если вы хотите извлечь элемент из списка по индексу, используйте оператор !!. Индексы начинаются с нуля.

ghci> "Стив Бушеми" !! 5

'Б'

ghci> [9.4,33.2,96.2,11.2,23.25] !! 1

33.2

Но если вы попытаетесь получить шестой элемент списка, состоящего из четырёх элементов, то получите сообщение об ошибке, так что будьте осторожны!

 

Списки списков

Списки могут содержать другие списки. Также они могут содержать списки, которые содержат списки, которые содержат списки…

ghci> let b = [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]

ghci> b

[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]

ghci> b ++ [[1,1,1,1]]

[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3],[1,1,1,1]]

ghci> [6,6,6]:b

[[6,6,6],[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]

ghci> b !! 2

[1,2,2,3,4]

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

 

Сравнение списков

Списки можно сравнивать, только если они содержат сравнимые элементы. При использовании операторов <, <=, >= и > сравнение происходит в лексикографическом порядке. Сначала сравниваются «головы» списков; если они равны, то сравниваются вторые элементы. Если равны и вторые элементы, то сравниваются третьи – и т. д., пока не будут найдены различающиеся элементы. Результат сравнения списков определяется по результату сравнения первой пары различающихся элементов.

Сравним для примера [3,4,2]<[3,4,3]. Haskell видит, что 3 и 3 равны, поэтому переходит к сравнению 4 и 4, но так как они тоже равны, сравнивает 2 и 3. Число 2 меньше 3, поэтому первый список меньше второго. Аналогично выполняется сравнение на <=, >= и >:

ghci> [3,2,1] > [2,1,0]

True

ghci> [3,2,1] > [2,10,100]

True

ghci> [3,4,2] < [3,4,3]

True

ghci> [3,4,2] > [2,4]

True

ghci> [3,4,2] == [3,4,2]

True

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

 

Другие операции над списками

Что ещё можно делать со списками? Вот несколько основных функций работы с ними.

Функция head принимает список и возвращает его головной элемент. Головной элемент списка – это, собственно, его первый элемент.

ghci> head [5,4,3,2,1]

5

Функция tail принимает список и возвращает его «хвост». Иными словами, эта функция отрезает «голову» списка и возвращает остаток.

ghci> tail [5,4,3,2,1]

[4,3,2,1]

Функция last принимает список и возвращает его последний элемент.

ghci> last [5,4,3,2,1]

1

Функция init принимает список и возвращает всё, кроме его последнего элемента.

ghci> init [5,4,3,2,1]

[5,4,3,2]

Если представить список в виде сороконожки, то с функциями получится примерно такая картина:

Но что будет, если мы попытаемся получить головной элемент пустого списка?

ghci> head []

*** Exception: Prelude.head: empty list

Ну и ну! Всё сломалось!.. Если нет сороконожки, нет и «головы». При использовании функций head, tail, last и init будьте осторожны – не применяйте их в отношении пустых списков. Эту ошибку нельзя отловить на этапе компиляции, так что всегда полезно предотвратить случайные попытки попросить язык Haskell выдать несколько элементов из пустого списка.

Функция length, очевидно, принимает список и возвращает его длину:

ghci> length [5,4,3,2,1]

5

Функция null проверяет, не пуст ли список. Если пуст, функция возвращает True, в противном случае – False. Используйте эту функцию вместо xs == [] (если у вас есть список с именем xs).

ghci> null [1,2,3]

False

ghci> null []

True

Функция reverse обращает список (расставляет его элементы в обратном порядке).

ghci> reverse [5,4,3,2,1]

[1,2,3,4,5]

Функция take принимает число и список. Она извлекает соответствующее числовому параметру количество элементов из начала списка:

ghci> take 3 [5,4,3,2,1]

[5,4,3]

ghci> take 1 [3,9,3]

[3]

ghci> take 5 [1,2]

[1,2]

ghci> take 0 [6,6,6]

[]

Обратите внимание, что если попытаться получить больше элементов, чем есть в списке, функция возвращает весь список. Если мы пытаемся получить 0 элементов, функция возвращает пустой список.

Функция drop работает сходным образом, но отрезает указанное количество элементов с начала списка:

ghci> drop 3 [8,4,2,1,5,6]

[1,5,6]

ghci> drop 0 [1,2,3,4]

[1,2,3,4]

ghci> drop 100 [1,2,3,4]

[]

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

Функция minimum возвращает наименьший элемент.

ghci> minimum [8,4,2,1,5,6]

1

ghci> maximum [1,9,2,3,4]

9

Функция sum принимает список чисел и возвращает их сумму.

Функция product принимает список чисел и возвращает их произведение.

ghci> sum [5,2,1,6,3,2,5,7]

31

ghci> product [6,2,1,2]

24

ghci> product [1,2,5,6,7,9,2,0]

0

Функция elem принимает элемент и список элементов и проверяет, входит ли элемент в список. Обычно эта функция вызывается как инфиксная, поскольку так её проще читать:

ghci> 4 `elem` [3,4,5,6]

True

ghci> 10 `elem` [3,4,5,6]

False

 

Интервалы

А что если нам нужен список всех чисел от 1 до 20? Конечно, мы могли бы просто набрать их подряд, но, очевидно, это не решение для джентльмена, требующего совершенства от языка программирования. Вместо этого мы будем использовать интервалы. Интервалы – это способ создания списков, являющихся арифметическими последовательностями элементов, которые можно перечислить по порядку: один, два, три, четыре и т. п. Символы тоже могут быть перечислены: например, алфавит – это перечень символов от A до Z. А вот имена перечислить нельзя. (Какое, например, имя будет идти после «Иван»? Лично я понятия не имею!)

Чтобы создать список, содержащий все натуральные числа от 1 до 20, достаточно написать [1..20]. Это эквивалентно полной записи [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], и единственная разница в том, что записывать каждый элемент списка, как показано во втором варианте, довольно глупо.

ghci> [1..20]

[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]

ghci> ['a'..'z']

"abcdefghijklmnopqrstuvwxyz"

ghci> ['K'..'Z']

"KLMNOPQRSTUVWXYZ"

Интервалы замечательны ещё и тем, что они позволяют указать шаг. Что если мы хотим внести в список все чётные числа от 1 до 20? Или каждое третье число от 1 до 20?

ghci> [2,4..20]

[2,4,6,8,10,12,14,16,18,20]

ghci> [3,6..20]

[3,6,9,12,15,18]

Нужно всего лишь поставить запятую между первыми двумя элементами последовательности и указать верхний предел диапазона. Но, хотя интервалы достаточно «умны», на их сообразительность не всегда следует полагаться. Вы не можете написать [1,2,4,8,16..100] и после этого ожидать, что получите все степени двойки. Во-первых, потому, что при определении интервала можно указать только один шаг. А во-вторых, потому что некоторые последовательности, не являющиеся арифметическими, неоднозначны, если представлены только несколькими первыми элементами.

ПРИМЕЧАНИЕ. Чтобы создать список со всеми числами от 20 до 1 по убыванию, вы не можете просто написать [20..1] , а должны написать [20,19..1] . При попытке записать такой интервал без шага (т. е. [20..1] ) Haskell начнёт с пустого списка, а затем будет увеличивать начальный элемент на единицу, пока не достигнет или не превзойдёт элемент в конце интервала. Поскольку 20 уже превосходит 1, результат окажется просто пустым списком.

Будьте осторожны при использовании чисел с плавающей точкой в интервалах! Из-за того что они не совсем точны (по определению), их использование в диапазонах может привести к весьма забавным результатам.

ghci> [0.1, 0.3 .. 1]

[0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]

Мой совет: не используйте такие числа в интервалах!

Интервалы, кроме прочего, можно использовать для создания бесконечных списков, просто не указывая верхний предел. Позже мы рассмотрим этот вариант в подробностях. А сейчас давайте посмотрим, как можно получить список первых 24 чисел, кратных 13. Конечно, вы могли бы написать [13,26..24*13]. Но есть способ получше: take 24 [13,26..]. Поскольку язык Haskell ленив, он не будет пытаться немедленно вычислить бесконечный список, потому что процесс никогда не завершится. Он подождёт, пока вы не захотите получить что-либо из такого списка. Тут-то обнаружится, что вы хотите получить только первые 24 элемента, что и будет исполнено.

Немного функций, производящих бесконечные списки:

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

ghci> take 10 (cycle [1,2,3])

[1,2,3,1,2,3,1,2,3,1]

ghci> take 12 (cycle "LOL ")

"LOL LOL LOL "

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

ghci> take 10 (repeat 5)

[5,5,5,5,5,5,5,5,5,5]

Однако проще использовать функцию replicate, если вам нужен список из некоторого количества одинаковых элементов. replicate 3 10 вернёт [10,10,10].

 

Генераторы списков

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

Вот пример простого описания множества. Множество, состоящее из первых десяти чётных чисел, это S = {2 · x | x ∈ N, x ≤ 10}, где выражение перед символом | называется производящей функцией (output function), x – переменная, N – входной набор, а x ≤ 10 – условие выборки. Это означает, что множество содержит удвоенные натуральные числа, которые удовлетворяют условию выборки.

Если бы нам потребовалось написать то же самое на языке Haskell, можно было бы изобрести что-то вроде: take 10 [2,4..]. Но что если мы хотим не просто получить первые десять удвоенных натуральных чисел, а применить к ним некую более сложную функцию? Для этого можно использовать генератор списков. Он очень похож на описание множеств:

ghci> [x*2 | x <– [1..10]]

[2,4,6,8,10,12,14,16,18,20]

В выражении [x*2 | x <– [1..10]] мы извлекаем элементы из списка [1..10], т. е. x последовательно принимает все значения элементов списка. Иногда говорят, что x связывается с каждым элементом списка. Часть генератора, находящаяся левее вертикальной черты |, определяет значения элементов результирующего списка. В нашем примере значения x, извлечённые из списка [1..10], умножаются на два.

Теперь давайте добавим к этому генератору условие выборки (предикат). Условия идут после задания источника данных и отделяются от него запятой. Предположим, что нам нужны только те элементы, которые, будучи удвоенными, больше либо равны 12.

ghci> [x*2 | x <– [1..10], x*2 >= 12]

[12,14,16,18,20]

Это работает. Замечательно! А как насчёт ситуации, когда требуется получить все числа от 50 до 100, остаток от деления на 7 которых равен 3? Легко!

ghci> [ x | x <– [50..100], x `mod` 7 == 3]

[52,59,66,73,80,87,94]

И снова получилось!

ПРИМЕЧАНИЕ. Заметим, что прореживание списков с помощью условий выборки также называется фильтрацией.

Мы взяли список чисел и отфильтровали их условиями. Теперь другой пример. Давайте предположим, что нам нужно выражение, которое заменяет каждое нечётное число больше 10 на БАХ!", а каждое нечётное число меньше 10 – на БУМ!". Если число чётное, мы выбрасываем его из нашего списка. Для удобства поместим выражение в функцию, чтобы потом легко использовать его повторно.

boomBangs xs = [if x < 10 then "БУМ!" else "БАХ!" | x <– xs, odd x]

ПРИМЕЧАНИЕ. Помните, что если вы пытаетесь определить эту функцию в GHCi, то перед её именем нужно написать let . Если же вы описываете её в отдельном файле, а потом загружаете его в GHCi, то никакого let не требуется.

Последняя часть описания – условие выборки. Функция odd возвращает значение True для нечётных чисел и False – для чётных. Элемент включается в список, только если все условия выборки возвращают значение True.

ghci> boomBangs [7..13]

["БУМ!","БУМ!","БАХ!","БАХ!"]

Мы можем использовать несколько условий выборки. Если бы по требовалось получить все числа от 10 до 20, кроме 13, 15 и 19, то мы бы написали:

ghci> [x | x <– [10..20], x /= 13, x /= 15, x /= 19]

[10,11,12,14,16,17,18,20]

Можно не только написать несколько условий выборки в генераторах списков (элемент должен удовлетворять всем условиям, чтобы быть включённым в результирующий список), но и выбирать элементы из нескольких списков. В таком случае выражения перебирают все комбинации из данных списков и затем объединяют их по производящей функции, которую мы указали:

ghci> [x+y | x <- [1,2,3], y <- [10,100,1000]]

[11,101,1001,12,102,1002,13,103,1003]

Здесь x берётся из списка [1,2,3], а y – из списка [10,100,1000]. Эти два списка комбинируются следующим образом. Во-первых, x становится равным 1, а y последовательно принимает все значения из списка [10,100,1000]. Поскольку значения x и y складываются, в начало результирующего списка помещаются числа 11, 101 и 1001 (1 прибавляется к 10, 100, 1000). После этого x становится равным 2 и всё повторяется, к списку добавляются числа 12, 102 и 1002. То же самое происходит для x равного 3.

Таким образом, каждый элемент x из списка [1,2,3] всеми возможными способами комбинируется с каждым элементом y из списка [10,100,1000], а x+y используется для построения из этих комбинаций результирующего списка.

Вот другой пример: если у нас есть два списка [2,5,10] и [8,10,11], и мы хотим получить произведения всех возможных комбинаций из элементов этих списков, то можно использовать следующее выражение:

ghci> [x*y | x <– [2,5,10], y <– [8,10,11]]

[16,20,22,40,50,55,80,100,110]

Как и ожидалось, длина нового списка равна 9.

Допустим, нам потребовались все возможные произведения, которые больше 50:

ghci> [x*y | x <– [2,5,10], y <– [8,10,11], x*y > 50]

[55,80,100,110]

А как насчёт списка, объединяющего элементы списка прилагательных с элементами списка существительных… с довольно забавным результатом?

ghci> let nouns = ["бродяга","лягушатник","поп"]

ghci> let adjs = ["ленивый","ворчливый","хитрый"]

ghci> [adj ++ " " ++ noun | adj <– adjs, noun <– nouns]

["ленивый бродяга","ленивый лягушатник","ленивый поп",

"ворчливый бродяга","ворчливый лягушатник", "ворчливый поп",

"хитрый бродяга","хитрый лягушатник","хитрый поп"]

Генераторы списков можно применить даже для написания своей собственной функции length! Назовём её length': эта функция будет заменять каждый элемент списка на 1, а затем мы все эти единицы просуммируем функцией sum, получив длину списка:

length' xs = sum [1 | _ <– xs]

Символ _ означает, что нам неважно, что будет получено из списка, поэтому вместо того, чтобы писать имя образца, которое мы никогда не будем использовать, мы просто пишем _. Поскольку строки – это списки, генератор списков можно использовать для обработки и создания строк. Вот функция, которая принимает строку и удаляет из неё всё, кроме букв в верхнем регистре:

removeNonUppercase st = [c | c <– st, c `elem` ['А'..'Я']]

Всю работу здесь выполняет предикат: символ будет добавляться в новый список, только если он является элементом списка ['А'..'Я']. Загрузим функцию в GHCi и проверим:

ghci> removeNonUppercase "Ха-ха-ха! А-ха-ха-ха!"

"ХА"

ghci> removeNonUppercase "ЯнеЕМЛЯГУШЕК"

"ЯЕМЛЯГУШЕК"

Вложенные генераторы списков также возможны, если вы работаете со списками, содержащими вложенные списки. Допустим, список содержит несколько списков чисел. Попробуем удалить все нечётные числа, не разворачивая список:

ghci> let xxs = [[1,3,5,2,3,1,2],[1,2,3,4,5,6,7],[1,2,4,2,1,6,3,1,3,2]]

ghci> [[x | x <– xs, even x ] | xs <– xxs]

[[2,2],[2,4,6],[2,4,2,6,2]]

ПРИМЕЧАНИЕ. Вы можете писать генераторы списков в несколько строк. Поэтому, если вы не в GHCi, лучше разбить длинные генераторы списков, особенно вложенные, на несколько строк.

 

Кортежи

 

Кортежи позволяют хранить несколько элементов разных типов как единое целое.

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

Кортежи обозначаются круглыми скобками, а их компоненты отделяются запятыми:

ghci> (1, 3)

(1,3)

ghci> (3, 'a', "привет")

(3,'a',"привет")

ghci> (50, 50.4, "привет", 'b')

(50,50.4,"привет",'b')

 

Использование кортежей

Подумайте о том, как бы мы представили двумерный вектор в языке Haskell. Один вариант – использовать список. Это могло бы сработать – ну а если нам нужно поместить несколько векторов в список для представления точек фигуры на двумерной плоскости?.. Мы могли бы, например, написать: [[1,2],[8,11],[4,5]].

Проблема подобного подхода в том, что язык Haskell не запретит задать таким образом нечто вроде [[1,2],[8,11,5],[4,5]] – ведь это по-прежнему будет список списков с числами. Но по сути данная запись не имеет смысла. В то же время кортеж с двумя элементами (также называемый «парой») имеет свой собственный тип; это значит, что список не может содержать несколько пар, а потом «тройку» (кортеж размера 3). Давайте воспользуемся этим вариантом. Вместо того чтобы заключать векторы в квадратные скобки, применим круглые: [(1,2),(8,11),(4,5)]. А что произошло бы, если б мы попытались создать такую комбинацию: [(1,2),(8,11,5),(4,5)]? Получили бы ошибку:

Couldn't match expected type `(t, t1)'

against inferred type `(t2, t3, t4)'

In the expression: (8, 11, 5)

In the expression: [(1, 2), (8, 11, 5), (4, 5)]

In the definition of `it': it = [(1, 2), (8, 11, 5), (4, 5)]

Мы попытались использовать пару и тройку в одном списке, и нас предупреждают: такого не должно быть. Нельзя создать и список вроде [(1,2),("Один",2)], потому что первый элемент списка – это пара чисел, а второй – пара, состоящая из строки и числа.

Кортежи также можно использовать для представления широкого диапазона данных. Например, если бы мы хотели представить чьё-либо полное имя и возраст в языке Haskell, то могли бы воспользоваться тройкой: ("Кристофер", "Уокен", 69). Как видно из этого примера, кортежи также могут содержать списки.

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

Как и списки, кортежи можно сравнить друг с другом, если можно сравнивать их компоненты. Однако вам не удастся сравнить кортежи разных размеров (хотя списки разных размеров сравниваются, если можно сравнивать их элементы).

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

 

Использование пар

Вот две полезные функции для работы с парами:

• 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)]

Вот и ответ! Это общий шаблон в функциональном программировании. Вы берёте начальный набор решений и затем применяете преобразования и фильтруете их, пока не получите результат.