Первая примечательная вещь в объявлении – это декларация типа. Она говорит, что функция принимает фигуру и возвращает значение типа Float
. Мы не смогли бы записать функцию типа Circle –> Float
, потому что идентификатор Circle
не является типом; типом является идентификатор Shape
. По той же самой причине мы не смогли бы написать функцию с типом True –> Int
. Вторая примечательная вещь – мы можем выполнять сопоставление с образцом по конструкторам. Мы уже записывали подобные сопоставления раньше (притом очень часто), когда сопоставляли со значениями []
, False
, 5
, только эти значения не имели полей. Только что мы записали конструктор и связали его поля с именами. Так как для вычисления площади нам нужен только радиус, мы не заботимся о двух первых полях, которые говорят нам, где располагается круг.
ghci> area $ Circle 10 20 10
314.15927
ghci> area $ Rectangle 0 0 100 100
10000.0
Ура, работает! Но если попытаться напечатать Circle 10 20 5
в командной строке интерпретатора, то мы получим ошибку. Пока Haskell не знает, как отобразить наш тип данных в виде строки. Вспомним, что когда мы пытаемся напечатать значение в командной строке, интерпретатор языка Haskell вызывает функцию show
, для того чтобы получить строковое представление значения, и затем печатает результат в терминале. Чтобы определить для нашего типа Shape
экземпляр класса Show
, модифицируем его таким образом:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float
deriving (Show)
Не будем пока концентрировать внимание на конструкции deriving (Show)
. Просто скажем, что если мы добавим её в конец объявления типа данных, Haskell автоматически определит экземпляр класса 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
Конструкторы значений – это функции, а значит, мы можем их отображать, частично применять и т. д. Если нам нужен список концентрических кругов с различными радиусами, напишем следующий код:
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
. Так легче разобраться, что есть что. То же верно и для прямоугольника. Теперь, после всех изменений, мы должны исправить функцию area
:
area :: Shape –> Float
area (Circle _ r) = pi * r 2
area (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 – x1) * (abs $ y2 – y1)
Единственное, что мы должны поменять, – это образцы. Мы игнорируем точку у образца для круга. В образце для прямоугольника используем вложенные образцы при сопоставлении для того, чтобы получить все поля точек. Если бы нам нужны были точки целиком, мы бы использовали именованные образцы. Проверим улучшенную версию:
ghci> area (Rectangle (Point 0 0) (Point 100 100))
10000.0
ghci> area (Circle (Point 0 0) 24)
1809.5574
Как насчёт функции, которая двигает фигуру? Она принимает фигуру, приращение координаты по оси абсцисс, приращение координаты по оси ординат – и возвращает новую фигуру, которая имеет те же размеры, но располагается в другом месте.
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))