Классы типов часто путают с классами в языках вроде Java, Python, C++ и им подобных, что сбивает с толку множество людей. В вышеперечисленных языках классы – это нечто вроде чертежей, по которым потом создаются объекты, хранящие некое состояние и способные производить некие действия. Мы не создаём типы из классов типов – вместо этого мы сначала создаём свои типы данных, а затем думаем о том, как они могут себя вести. Если то, что мы создали, можно проверить на равенство, – определяем для него экземпляр класса Eq
. Если наш тип может вести себя как нечто, что можно упорядочить, – создаём для него экземпляр класса Ord
.
Давайте посмотрим, как язык Haskell умеет автоматически делать наши типы экземплярами таких классов типов, как Eq
, Ord
, Enum
, Bounded
, Show
и Read
. Haskell умеет порождать поведение для наших типов в этих контекстах, если мы используем ключевое слово deriving
при создании типа данных.
Сравнение людей на равенство
Рассмотрим такой тип данных:
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
}
Тип описывает человека. Предположим, что среди людей не встречаются тёзки одного возраста. Если у нас есть два описания, можем ли мы выяснить, относятся ли они к одному и тому же человеку? Есть ли в такой операции смысл? Конечно, есть. Мы можем сравнить записи и проверить, равны они или нет. Вот почему имело бы смысл определить для нашего типа экземпляр класса Eq
. Порождаем экземпляр:
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
} deriving (Eq)
Когда мы определяем экземпляр класса Eq
для типа и пытаемся сравнить два значения с помощью операторов ==
или /=
, язык Haskell проверяет, совпадают ли конструкторы значений (хотя в нашем типе только один конструктор), а затем проверяет все данные внутри конструктора на равенство, сравнивая каждую пару полей с помощью оператора ==
. Таким образом, типы всех полей также должны иметь определённый экземпляр класса Eq
. Так как типы полей нашего типа, String
и Int
, имеют экземпляры класса Eq
, всё в порядке.
Запишем в файл несколько людей:
mikeD = Person {firstName = "Майкл", lastName = "Даймонд", age = 45}
adRock = Person {firstName = "Адам", lastName = "Горовиц", age = 45}
mca = Person {firstName = "Адам", lastName = "Яух", age = 47}
И проверим экземпляр класса Eq
:
ghci> mca == adRock
False
ghci> mikeD == adRock
False
ghci> mikeD == mikeD
True
ghci> mca == Person {firstName = "Адам", lastName = "Яух", age = 47}
True
Конечно же, так как теперь тип Person
имеет экземпляр класса Eq
, мы можем передавать его любым функциям, которые содержат ограничение на класс типа Eq
в декларации, например функции elem
.
ghci> let beastieBoys = [mca, adRock, mikeD]
ghci> mikeD `elem` beastieBoys
True
Покажи мне, как читать
Классы типов Show
и Read
предназначены для сущностей, которые могут быть преобразованы в строки и из строк соответственно. Как и для класса Eq
, все типы в конструкторе типов также должны иметь экземпляры для классов Show
и/или Read
, если мы хотим получить такое поведение. Давайте сделаем наш тип данных Person
частью классов Show
и Read
:
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
} deriving (Eq, Show, Read)
Теперь мы можем распечатать запись на экране:
ghci> mikeD
Person {firstName = "Michael", lastName = "Diamond", age = 43}
ghci> "mikeD is: " ++ show mikeD
"mikeD is: Person {firstName = \"Michael\", lastName = \"Diamond\", age = 43}"
Если бы мы попытались распечатать запись до того, как предусмотрели для типа Person
экземпляры класса Show
, язык Haskell пожаловался бы на то, что он не знает, как представить запись в виде строки. Но после того как мы определили экземпляр класса Show
, всё проясняется.