Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ВВЕДЕНИЕ.docx
Скачиваний:
0
Добавлен:
01.03.2025
Размер:
898.6 Кб
Скачать

Сопоставление с образцом

Представленные выше функции numerator и denominator показывают один важнейший механизм, реализованный в языке Haskell — сопоставление с образцами. Этот механизм используется в языке в нескольких аспектах, одним из главных является сопоставление с образцами при определении функций. Этот аспект необходимо рассмотреть более внимательно.

«Образцом» называется выражение, в котором некоторые или все поля декартова произведения (если декартово произведение не пусто, то есть конструктор не представляет собой простую метку размеченного объединения) заменены свободными переменными15 для подстановки конкретных значений таким образом, что при сопоставлении происходит означивание параметров образца — однозначное приписывание переменным образца конкретных значений. Для АТД образцом является указание одного из конструкторов с перечислением переменных или конкретных значений в качестве его полей.

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

Этот процесс можно пояснить на примере. Пусть есть определение функции:

head :: [α] -> α

head [] = error "Empty list has no first element."

head (x:xs) = x

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

Первый клоз определяет значение функции в случае, если на вход функции подан пустой список. Как видно, функция предназначена для возвращения первого элемента («головы») списка, а потому для пустого списка её применение ошибочно. Используется системная функция error. Второй клоз применим для случаев, когда список не является пустым. Непустой список, как уже говорилось в теоретической части — это пара, первым элементом которой является некоторое значение, а вторым — список оставшихся элементов, «хвост» списка. В языке Haskell эти элементы декартова произведения соединяются при помощи конструктора (:), что и представлено в образце. Две свободные переменные — x и xs — это параметры образца, которые получают конкретные значения в случае корректного применения функции. Например:

> head [1, 2, 3]

сопоставит с переменной x конкретное значение 1, а с переменной xs — значение [2, 3]. Соответственно, результатом выполнения функции будет значение переменной x, то есть 1.

Приведённый пример можно понимать и по-другому. Раз список есть пара, созданная при помощи конструктора (:), то список [1, 2, 3] может быть представлен как (1:[2, 3]), а ещё вернее как (1:(2:(3:[]))). В данном случае вызов head [1, 2, 3] приведёт к следующей последовательности вычислений:

> let (x:xs) = (1:[2, 3])

in x

или, что то же самое:

> let x = 1

xs = [2, 3]

in x

В конечном итоге, поскольку свободная переменная xs не участвует в вычислении результата, оптимизирующий транслятор языка, основанного на ленивых вычислениях, проведёт такое преобразование17:

> let x = 1

in x

Итогом вычислений будет значение переменной x, то есть 1.

Теперь можно рассмотреть более сложный пример. Пусть определён АТД для представления бинарного дерева (такой же, как в теоретической части):

data Tree α = Empty

| Node α (Tree α) (Tree α)

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

depth :: Tree α -> Int

depth Empty = 0

depth (Node _ l r) = 1 + max (depth l) (depth r)

Первый клоз функции определяет её значение для первого конструктора АТД Tree, то есть для пустого дерева. Второй клоз определяет значение функции уже для непустого дерева. У непустого дерева есть хранимый в узле элемент и два поддерева — левое и правое. Функции depth хранимый в узле элемент неинтересен, а потому в образце применена «маска подстановки» для неиспользуемых элементов АТД — (_). Этот символ при сопоставлении с образцами означает, что на его место может быть подставлено всё, что угодно. Кроме того, это единственный символ, который можно использовать несколько раз в одном образце. Два других компонента конструктора Node, l и r, сопоставляются с левым и правым поддеревьями соответственно.

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

isElement x [] = False

isElement x (x:xs) = True

isElement x (y:xs) = isElement x xs

Здесь во втором клозе в образцах переменная x используется два раза, что недопустимо в образцах.

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