logo search
Языки программирования

16.3. Составные типы

Списки

Список можно создать из элементов любого предварительно определенного типа, в частности из встроенных типов, например целого или булева. Следую­щие списки:

[2,3,5,7,11 ] [true, false, false]

имеют типы int list и bool list, соответственно. Список можно создать также с помощью конструкторов (constructors); конструкторы списка — это [] для пус­того списка, и element::list для непустого списка, создаваемого добавлением элемента (element) к существующему списку (list). Конструкторы могут ис­пользоваться при определении функций путем сопоставления с образцом:

fun member [] e = false

| member [e :: tail] e = true

j member [e1 :: tail] e = member tail e

Тип функции member (член) определяется как:

member: int list x jnt -> boolean

и это можно прочитать следующим образом:

Когда функция member применяется к списку L, а (затем) к элементу е, вычисление основывается на вариантах выбора в зависимости от аргумен­тов: 1) если L пуст, е не является членом L; 2) если е — первый элемент L, то е является членом L; 3) в противном случае, е1, первый элемент списка L, отличен от е, и мы (рекурсивно) проверяем, является ли е членом остав­шейся части списка L.

В языке ML вам не нужно объявлять тип функции; компилятор автомати­чески выводит тип функции из типов аргументов и типа результата. Если ком­пилятор не может вывести тип, вам придется задать нужное количество объ­явлений типа, чтобы ликвидировать неопределенность выражения. Контроль соответствия типов статический, поэтому, когда функция применяется к значению, проверка соответствия типа функции типу параметра делается во вре­мя компиляции.

Обратите внимание, что эта функция рекурсивная. Рекурсия чрезвычайно важна в функциональных языках программирования; при отсутствии «опера­торов» это единственный способ создания циклов при вычислении выраже­ния.

В качестве заключительного примера покажем, как на языке ML написать алгоритм сортировки вставкой (insertion sort). Вы используете этот алгоритм для сортировки при игре в карты: по очереди берете карты из колоды и кла­дете их в нужное место:

fun insertion_sort [] = []

| insertion_sort head:: tail =

insert_element head insertion_sort tail

and

fun insert_element x [] = [x]

| insert_element x head:: tail =

if x < head then x::head:: tail

else head:: (insert_element x tail)

Эти функции имеют типы:

insertion_sort: int list -> int list

insert_element: int -> int list -> int list

Как только вы привыкнете к этой записи, читать такие программы будет про­сто:

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

Элемент х вставляется в пустой список, создавая список из одного элемен­та. Чтобы вставить х в непустой список, нужно сравнить х с первым эле­ментом (head) списка: 1) если х меньше head, сделать х новым первым эле­ментом списка; 2) в противном случае создать новый список, составлен­ный из head, за которым следует остаток списка, в который вставлен х.

Обратите внимание, что -> имеет правую ассоциативность:

insert_element: int -> (int list -> int list)

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

fun insert_4 = insert_element 4

которая вставляет 4 в список целых.

В отличие от процедурной программы для того же самого алгоритма здесь нет никаких индексов и никаких for-циклов. Кроме того, ее можно легко обобщить для сортировки объектов других типов, просто заменяя операцию «<» соответствующей булевой функцией для сравнения двух значений рас­сматриваемого типа. Чтобы создать список, явные указатели не нужны; они заданы неявно в представлении данных. Конечно, сортировка списков в лю­бом языке не так эффективна, как сортировка массива «на своем месте», но для многих приложений, использующих списки, вполне практична.

Определение новых типов

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

datatype int tree =

Empty

| T of (int tree x int x int tree)

Это следует читать так:

int tree является новым типом данных, значения которого: 1) новое кон­стантное значение Empty (пусто) или 2) значение, которое сформировано конструктором Т, примененным к тройке, состоящей из дерева, целого числа и другого дерева.

Определив новый тип, мы можем написать функции, которые обрабатыва­ют дерево.

Например:

fun sumtree Empty = О

| sumtree T(left, value, right) =

(sumtree left) + value + (sumtree right)

суммирует значения, помечающие узлы дерева, и:

fun mintree Empty = maxint

| mintree T(left, value, right) =

min left (min value (mintree right))

вычисляет минимальное из всех значений, помечающих узлы, возвращая наи­большее'целое число maxint для пустого дерева.

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