04. Пользовательские структуры данных

Простые типы

Мы уже рассматривали некоторые «встроенные» типы данных (Int, Char, Bool и пр.). Haskell, естественно, позволяет определять новые типы данных. Одним из способов определения типа данных является использование ключевого слова data:

data Bool = True | False

Ключевое слово data указывает на объявление типа данных, после него идет название типа и затем, после знака =, — список конструкторов значений (в нашем случае конструкторы сами являются значениями). Вертикальная черта | читается как «или». То есть определение типа Bool можно прочитать как «значение типа Bool — это True или False». Имя определяемого типа данных, как и имена конструкторов должны начинаться с заглавной буквы.

Теперь допустим, нам хочется определить тип, значения которого представляют различные фигуры: окружности и прямоугольники. Для определения окружности можно было бы воспользоваться кортежем (x0, y0, R), а для прямоугольника — кортежем (x1, y1, x2, y2). Но если планируется использовать фигуры в одном контексте, лучше определить общий тип данных:

data Figure = Circle Double Double Double | Rectangle Double Double Double Double

Здесь определяется 2 конструктора значений: Circle и Rectangle. При этом конструктор Circle имеет 3 параметра типа Double, а Rectangle — 4. Конструкторы значений по сути являются функциями:

Circle    :: Double -> Double -> Double           -> Figure
Rectangle :: Double -> Double -> Double -> Double -> Figure

Конструкторы могут использоваться при сопоставлении с образцом. Определим функцию вычисления площади фигуры:

surface :: Figure -> Double
surface (Circle _ _ r) = pi * r^2
surface (Rectangle x1 y1 x2 y2) = abs (x2 - x1) * abs (y2 - y1)

Следует отметить, что в типе функции используется тип Figure. Circle и Rectangle не могут быть использованы в качестве типа, т.е. нельзя написать функцию circleSurface :: Circle -> Double, точно так же, как и нельзя написать тип True -> Int.

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

map (Circle 0 0) [1..10]

При определении окружности или прямоугольника, мы задаем координаты (центра или противоположных углов). Можно ввести новый тип данных и изменить наш тип данных Figure:

data Point = Point Double Double
data Figure = Circle Point Double | Rectangle Point Point

Заметим, что для типа Point имя конструктора значений совпадает с именем типа. Это не обязательное правило, но довольно часто встречается на практике. Теперь наш тип Figure более понятен: окружность задается точкой и радиусом и теперь понятно, какой параметр чему соответствует (аналогично с прямоугольником). Перепишем функцию surface в соответствии с новым определением типа:

surface :: Figure -> Double
surface (Circle _ r) = pi * r^2
surface (Rectangle (Point x1 y1) (Point x2 y2)) = abs (x2 - x1) * abs (y2 - y1)

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

Перейдем к более сложным структурам:

data Person = Person String String Int Double String String

По такому определению довольно проблематично понять, что есть что. Можно написать функции доступа:

firstName :: Person -> String
firstName (Person name _ _ _ _ _) = name

age :: Person -> Int
age (Person _ _ age _ _ _) = age

-- и так далее...

Но это, как видно, довольно неудобно и многословно. Поэтому в Haskell разрешен следующий вид определения структур данных типа запись (record в некоторых языках программирования):

data Person = Person {
    firstName   :: String,
    lastName    :: String,
    age         :: Int,
    height      :: Double,
    phoneNumber :: String,
    address     :: String
}

Теперь для каждого поля структуры у нас есть его имя. На самом деле, имя поля — это просто функция:

firstName :: Person -> String
age :: Person -> Int

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

let me = Person {
    firstName   = "Walter",
    lastName    = "White",
    age         = 50,
    height      = 180,
    phoneNumber = "617-234-12",
    address     = "nowhere" }

Конструкторы типов

Конструкторы значений принимают значения в качестве параметров и возвращают новое значение. Подобным образом, конструкторы типов принимают в качестве параметров типы и возвращают новый тип:

data Maybe a = Nothing | Just a

Здесь a — тип-параметр, а Maybe — конструктор типа. Nothing и Just — конструкторы значений. Конструктор типа позволяет вводить какие угодно типы: Maybe Int, Maybe Char, Maybe [a], Maybe (Maybe a) и т.п.

Вывод классов типов

Определение собственных типов данных часто полезно на практике. Но для того, чтобы использовать эти структуры во многих случаях бывает полезно, чтобы эти типы были членами таких классов, как Eq, Ord, Show или других. Для многих базовых классов в Haskell существует возможность автоматического вывода реализации для класса типов:

data Point = Point Double Double deriving (Eq)

Очевидно, что тип Point эквивалентен типу (Double, Double), для которого реализация Eq может быть легко получена.

data Vector a = Vector a a a deriving (Eq)

Здесь членами класса Eq будут не все возможные типы Vector a, а только такие, для которых a принадлежит классу Eq.

Автоматический вывод возможен для классов: Eq, Ord, Show, Read, Bounded, Enum и некоторых других.

Синонимы типов

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

type String = [Char]

Такие переназначения бывают удобны для конкретных предметных областей. Например:

type PhoneNumber = String
type Address = String
type Name = String
type Contact = (Name, PhoneNumber, Address)
type PhoneBook = [Contact]

Для синонимов типов также можно использовать параметры типов:

type AssocList k v = [(k, v)]

Познакомимся с еще одним важным типом данных:

data Either a b = Left a | Right b

Рекурсивные структуры данных

В Haskell возможно рекурсивное определение типов данных:

data List = Empty | Cons a (List a) deriving (Eq, Ord, Show, Read)

data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)