Изучай Haskell ради Добра! Создание своих собственных типов и классов типов |
- Statistics
- Participants
- Translate into Russian
- Translation result
- Translated in draft, editing and proof-reading required. Completed: 16%.
Создание своих собственных типов и классов типов
В предыдущих главах мы изучили некоторые типы и классы типов в Хаскеле. В этой главе мы узнаем как создать и заставить работать свои собственные!
Введение в алгебраические типы данных
До сих пор мы сталкивались с многими типами данных. Bool, Int, Char, Maybe и т.д. Но как создать свой собственный тип? Один из способов - использовать ключевое слово data. Давайте посмотрим, как в стандартной библиотеке определен тип Bool.
data Bool = False | True
Ключевое слово data объявляет новый тип данных. Часть до знака равенства обозначает тип, в данном случае Bool. Часть после знака равенства - это конструкторы значений. Они определяют, какие значения может принимать тип. Знак | читается как "или". Объявление можно прочесть так: тип Bool может принимать значения True или False. И имя типа и конструкторы значений должны начинаться с заглавной буквы.
Рассуждая таким образом, мы можем думать что тип Int объявлен так:
data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | ... | 2147483647
Первое и последнее значение - минимальное и максимально возможное значение для Int. На самом деле Int объявлен не так, троеточия заменяют огромное количество чисел, так что подобная запись нам нужна только для демонстрации.
Теперь, подумаем как бы мы представили некоторую фигуру в Хаскеле. Один из способов - использовать кортежи. Круг может быть представлен как (43.1, 55.0, 10.4), где первое и второе поле - координаты центра, а третье поле - радиус. Вроде подходит, но такой же кортеж может представлять вектор в трехмерном пространстве или что-нибудь другое. Лучшим решением было бы определить свой собственный тип для фигуры. Скажем, наша фигура может быть кругом или прямоугольником.
data Shape = Circle Float Float Float | Rectangle Float Float Float Float
Ну и что это? Размышляйте следующим образом. Конструктор для значения Circle содержит три поля типа Float. Когда мы записываем конструктор значения типа, опционально мы можем добавлять типы после имени конструктора, эти типы определяют какие значения будет содержать тип с этим конструктором. В нашем случае первые два числа - это координаты центра, третье число - радиус. Конструктор для значения Rectangle имеет четыре поля, которые так же являются числами с плавающей точкой. Первые два числа это координаты верхнего левого угла, вторые два числа - координаты нижнего правого угла.
Когда я говорю "поля", на самом деле я имею ввиду "параметры". Конструкторы значений на самом деле являются функциями, и только эти функции возвращают значения типа данных. Давайте посмотрим на сигнатуры для наших двух конструкторов.
ghci> :t Circle
Circle :: Float -> Float -> Float -> Shape
ghci> :t Rectangle
Rectangle :: Float -> Float -> Float -> Float -> Shape
Круто, конструкторы значений такие же функции как любые другие. Кто бы мог подумать? Давайте напишем функцию, которая принимает фигуру и возвращает площадь ее поверхности.
surface :: Shape -> Float
surface (Circle _ _ r) = pi * r ^ 2
surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)
Первая примечательная вещь в объявлении - это декларация типа. Она говорит, что функция принимает фигуру и возвращает Float. Мы не смогли бы записать функцию Circle -> Float, потому что Circle не является типом, типом является Shape. По той же самой причине мы не смогли бы написать функцию с типом True -> Int. Вторая примечательная вещь - мы можем выполнять сопоставление с образцом по конструкторам. Мы уже записывали подобные сопоставления раньше (и очень часто, на самом деле) когда мы сопоставляли со значениями [], False, 5, только эти значения не имели полей. Мы только что записали конструктор и связали его поля с именами. Так как для вычисления площади нам нужен только радиус, мы не заботимся о двух первых полях, которые говорят нам где располагается круг.
ghci> surface $ Circle 10 20 10
314.15927
ghci> surface $ Rectangle 0 0 100 100
10000.0
Ура, работает! Но если мы попытаемся напечатать Circle 10 20 5 в командной строке интерпретатора, мы получим ошибку. Пока Хаскель не знает как отобразить наш тип данных в виде строки. Вспомним, что когда мы пытаемся напечатать значение в командной строке, Хаскель вызывает функцию show для того чтобы получить строковое представление значения, и затем печатает результат в терминале. Для того чтобы сделать наш тип Shape частью класса типов Show, модифицируем его таким образом:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)
Не будем пока концентрировать внимание на наследовании. Просто скажем, что если мы добавим deriving (Show) в конец объявления типа данных, Хаскель автоматически сделает этот тип частью класса типов Show. Теперь мы можем делать так:
ghci> Circle 10 20 5
Circle 10.0 20.0 5.0
ghci> Rectangle 50 230 60 90
Rectangle 50.0 230.0 60.0 90.0
Конструкторы значений это функции, а значит мы можем их отображать (map), частично применять, и так далее. Если нам нужен список концентрических кругов с различными радиусами, мы можем сделать так:
ghci> map (Circle 10 20) [4,5,6,6]
[Circle 10.0 20.0 4.0,Circle 10.0 20.0 5.0,Circle 10.0 20.0 6.0,Circle 10.0 20.0 6.0]
Наш тип данных хорош, но он может быть лучше. Давайте создадим промежуточный тип данных, который определяет точку в двумерном пространстве. Затем мы используем его для того чтобы сделать наши фигуры более понятными.
data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float | Rectangle Point Point deriving (Show)
Обратите внимание, что при определении точки мы использовали одинаковое имя для типа данных и для конструктора значения. В этом нет никакого специального смысла, но если у типа данных только один конструктор, как правило он носит то же имя что и тип. Итак, теперь у Circle два поля, первое имеет тип Point, второе - Float. Так легче разобраться что есть что. То же верно для прямоугольника. Теперь мы должны исправить функцию surface после наших изменений.
surface :: Shape -> Float
surface (Circle _ r) = pi * r ^ 2
surface (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1)
Единственное что мы должны поменять - это шаблоны. Мы игнорируем точку у шаблона круга. В шаблоне прямоугольника мы используем вложенные шаблоны при сопоставлении с образцом для того чтобы получить все поля точек. Если бы нам нужны были точки целиком, мы бы использовали as-шаблоны.
ghci> surface (Rectangle (Point 0 0) (Point 100 100))
10000.0
ghci> surface (Circle (Point 0 0) 24)
1809.5574
Как насчет функции, которая двигает фигуру? Она принимает фигуру, приращение координаты по оси x, приращение координаты по оси y, и возвращает новую фигуру, которая имеет те же размеры, но располагается в другом месте.
nudge :: Shape -> Float -> Float -> Shape
nudge (Circle (Point x y) r) a b = Circle (Point (x+a) (y+b)) r
nudge (Rectangle (Point x1 y1) (Point x2 y2)) a b = Rectangle (Point (x1+a) (y1+b)) (Point (x2+a) (y2+b))
Все довольно очевидно. Мы добавляем смещение к точкам, определяющим положение фигуры.
ghci> nudge (Circle (Point 34 34) 10) 5 10
Circle (Point 39.0 44.0) 10.0
Если мы не хотим иметь дело напрямую с точками, мы можем сделать вспомогательные функции которые создают фигуры некоторого размера и затем двигают их.
baseCircle :: Float -> Shape
baseCircle r = Circle (Point 0 0) r
baseRect :: Float -> Float -> Shape
baseRect width height = Rectangle (Point 0 0) (Point width height)
ghci> nudge (baseRect 40 100) 60 23
Rectangle (Point 60.0 23.0) (Point 100.0 123.0)
Конечно же, вы можете экспортировать типы данных из модулей. Чтобы сделать это, запишите имена ваших типов вместе с именами экспортируемых функций. В отдельных скобках, через запятую, укажите, какие конструкторы значений вы хотели бы экспортировать. Если вы хотите экспортировать все конструкторы значений, просто напишите "..".
Если бы мы хотели поместить функции и типы определенные выше в модуль, мы могли бы начать как-то так:
module Shapes
( Point(..)
, Shape(..)
, surface
, nudge
, baseCircle
, baseRect
) where
Shape(..) означает, что мы экспортируем все конструкторы для Shape. Тот, кто импортирует наш модуль, сможет создавать фигуры используя конструкторы Rectangle и Circle. Это то же самое что записать Shape (Rectangle, Circle).
Мы могли бы не указывать ни одного конструктора для Shape, просто записав Shape в операторе экспорта. В таком случае тот, кто импортирует модуль, сможет создавать фигуры только с помощью функций baseCircle и baseRect. Data.Map использует этот прием. Вы не можете создать отображение (map), выполнив Map.Map [(1,2),(3,4)], потому что такой конструктор не экспортирован. Но вы можете создавать отображения, вызвав одну из вспомогательных функций, например Map.fromList. Помните, конструкторы значений это простые функции, принимающие поля как параметры и возвращающие значение некоторого типа (например Shape) как результат. Если мы их не экспортируем, то вне модуля они будут недоступны. Но если другие экспортируемые функции возвращают наш тип, мы можем использовать их для создания значений этого типа.
Не-экспортирование конструкторов значений делает типы данных более абстрактными, в том смысле что мы прячем их реализацию. Кроме того, пользователь нашего модуля не сможет делать сопоставление с образцом по конструкторам.
Синтаксис записи
У нас уже была задача создать тип данных для описания человека. Данные, которые мы хотим хранить: имя, фамилия, возраст, рост, телефон и любимый сорт мороженного. Не знаю как на счет вас, но это все что я когда-либо хотел знать о человеке. Давайте опишем такой тип.
data Person = Person String String Int Float String String deriving (Show)
О-кей. Первое поле это имя, второе это фамилия, третье - возраст, и так далее. Создадим человека.
ghci> let guy = Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"
ghci> guy
Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"
Ну, довольно-таки ничего, хоть и не очень читаемо. Что если нам нужна функция для получения какого-либо поля? Функция, которая возвращает имя, функция для фамилии, и так далее. Мы можем определить их таким образом:
firstName :: Person -> String
firstName (Person firstname _ _ _ _ _) = firstname
lastName :: Person -> String
lastName (Person _ lastname _ _ _ _) = lastname
age :: Person -> Int
age (Person _ _ age _ _ _) = age
height :: Person -> Float
height (Person _ _ _ height _ _) = height
phoneNumber :: Person -> String
phoneNumber (Person _ _ _ _ number _) = number
flavor :: Person -> String
flavor (Person _ _ _ _ _ flavor) = flavor
Фух! Не много радости писать такие функции! Этот метод очень громоздкий и скучный, но он работает.
ghci> let guy = Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"
ghci> firstName guy
"Buddy"
ghci> height guy
© Miran Lipovača
Original (English): Learn You a Haskell for Great Good! Making Our Own Types and Typeclasses
Translation: © asinitsyn, malphunction, slayzx, zanuda, neor, Yasir Arsanukaev, savask .
License: creative commons attribution noncommercial blah blah blah ... license
