ТОП 10:

Верный способ улучшить фигуру



 
 

Наш тип данных хорош, но может быть и ещё лучше. Давайте со- здадим вспомогательный тип данных, который определяет точку в двумерном пространстве. Затем используем его для того, чтобы сделать наши фигуры более понятными:

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))

Всё довольно очевидно. Мы добавляем смещение к точкам, оп- ределяющим положение фигуры:

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(..)

, area

, nudge

, baseCircle

, baseRect

 
 

) where

Запись Shape(..) обозначает, что мы экспортируем все конструк- торы данных для типа Shape. Тот, кто импортирует наш модуль, смо- жет создавать фигуры, используя конструкторы Rectangle и Circle. Это то же самое, что и Shape (Rectangle, Circle), но короче.

К тому же, если мы позже решим дописать несколько конструк- торов данных, перечень экспортируемых объектов исправлять не придётся. Всё потому, что конструкция .. автоматически экспорти- рует все конструкторы соответствующего типа.


 

Мы могли бы не указывать ни одного конструктора для типа Shape, просто записав Shape в операторе экспорта. В таком случае тот, кто импортирует модуль, сможет создавать фигуры только с по- мощью функций baseCircle и baseRect.

Помните, конструкторы данных – это простые функции, принимающие поля как параметры и возвращающие значение некоторого типа (например, Shape) как результат. Если мы их не экспортируем, то вне модуля они будут недоступны. Отказ от экс- порта конструкторов данных делает наши типы данных более абс- трактными, поскольку мы скрываем их реализацию. К тому же, пользователи нашего модуля не смогут выполнять сопоставление с образцом для этих конструкторов данных. Это полезно, если мы хотим, чтобы программисты, импортирующие наш тип, работали только со вспомогательными функциями, которые мы специаль- но для этого написали. Таким образом, у них нет необходимости знать о деталях реализации модуля, и мы можем изменить эти де- тали, когда захотим – лишь бы экспортируемые функции работа- ли как прежде.

Модуль Data.Map использует такой подход. Вы не можете создать отображение напрямую при помощи соответствующего конструкто- ра данных, потому что такой конструктор не экспортирован. Одна- ко можно создавать отображения, вызвав одну из вспомогательных функций, например Map.fromList. Разработчики, ответственные за Data.Map, в любой момент могут поменять внутреннее представле- ние отображений, и при этом ни одна существующая программа не сломается.

Разумеется, экспорт конструкторов данных для типов попроще вполне допустим.

 

Синтаксис записи

с именованными полями

Есть ещё один способ опреде- лить тип данных. Предполо- жим, что перед нами поставле- на задача создать тип данных для описания человека. Данные, которые мы намереваемся хра-


СИНТАКСИС ЗАПИСИ С ИМЕНОВАННЫМИ ПОЛЯМИ 155

 
 

нить, – имя, фамилия, возраст, рост, телефон и любимый сорт мо- роженого. (Не знаю, как насчёт вас, но это всё, что я хотел бы знать о человеке!) Давайте опишем такой тип:

data Person = Person String String Int Float String String deriving (Show)

 
 

Первое поле – это имя, второе – фамилия, третье – возраст и т. д. И вот наш персонаж:

gghci> let guy = Person "Фредди" "Крюгер" 43 184.2 "526–2928" "Эскимо" ghci> guy

 
 

Person "Фредди" "Крюгер" 43 184.2 "526–2928" "Эскимо"

Ну, в целом приемлемо, хоть и не очень «читабельно». Что если нам нужна функция для получения какого-либо поля? Функция, ко- торая возвращает имя, функция для фамилии и т. д.? Мы можем оп- ределить их таким образом:

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 "Фредди" "Крюгер" 43 184.2 "526–2928" "Эскимо" ghci> firstName guy

"Фредди"

ghci> height guy


 

184.2

 
 

ghci> flavor guy "Эскимо"

Вы скажете – должен быть лучший способ! Ан нет, извиняйте, нету... Шучу, конечно же. Такой метод есть! «Ха-ха» два раза. Созда- тели языка Haskell предусмотрели подобную возможность – предо- ставили ещё один способ для записи типов данных. Вот как мы мо- жем достигнуть той же функциональности с помощью синтаксиса записей с именованными полями:

data Person = Person { firstName :: String

, lastName :: String

, age :: Int

, height :: Float

, phoneNumber :: String

 
 

, flavor :: String } deriving (Show)

Вместо того чтобы просто перечислять типы полей через за- пятую, мы используем фигурные скобки. Вначале пишем имя поля, например firstName, затем ставим два двоеточия :: и, наконец, указываем тип. Результирующий тип данных в точности такой же. Главная выгода – такой синтаксис генерирует функции для извлече- ния полей. Язык Haskell автоматически создаст функции firstName, lastName, age, height, phoneNumber и flavor.

ghci> :t flavor

flavor :: Person –> String ghci> :t firstName

 
 

firstName :: Person –> String

Есть ещё одно преимущество в использовании синтаксиса за- писей. Когда мы автоматически генерируем экземпляр класса Show для типа, он отображает тип не так, как если бы мы использовали синтаксис записей с именованными полями для объявления и ин- станцирования типа. Например, у нас есть тип, представляющий автомобиль. Мы хотим хранить следующую информацию: компа- ния-производитель, название модели и год производства.

 
 

data Car = Car String String Int deriving (Show)

Автомобиль отображается так:


 

 
 

ghci> Car "Форд" "Мустанг" 1967 Car "Форд" "Мустанг" 1967

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

data Car = Car { company :: String

, model :: String

, year :: Int

 
 

} deriving (Show)

Автомобиль теперь создаётся и отображается следующим обра- зом:

 
 

ghci> Car {company="Форд", model="Мустанг", year=1967} Car {company = "Форд", model = "Мустанг", year = 1967}

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

Используйте синтаксис записей с именованными полями, если конструктор имеет несколько полей и не очевидно, какое поле для чего используется. Если, скажем, мы создаём трёхмерный век- тор: data Vector = Vector Int Int Int, то вполне понятно, что поля конструктора данных – это компоненты вектора. Но в типах Person и Car назначение полей совсем не так очевидно, и мы значительно выиграем, используя синтаксис записей с именованными полями.

 

Параметры типа

Конструктор данных может принимать несколько параметров-зна- чений и возвращать новое значение. Например, конструктор Car принимает три значения и возвращает одно – экземпляр типа Car. Таким же образом конструкторы типа могут принимать типы-па- раметры и создавать новые типы. На первый взгляд это несколько абстрактно, но на самом деле не так уж сложно. Если вы знакомы с шаблонами в языке С++, то увидите некоторые параллели. Чтобы получить более ясное представление о том, как работают типы-па- раметры, давайте посмотрим, как реализованы типы, с которыми мы уже встречались.


 

 
 

data Maybe a = Nothing | Just a

В данном примере идентификатор а – тип-параметр (переменная типа, типовая переменная). Так как в выражении присутствует тип-

параметр, мы называем иденти- фикатор Maybe конструктором типов. В зависимости от того, какой тип данных мы хотим со- хранять в типе Maybe, когда он не Nothing, конструктор типа может производить такие типы, как Maybe Int, Maybe Car, Maybe String и т. д. Ни одно значение не мо- жет иметь тип «просто Maybe», потому что это не тип как тако- вой – это конструктор типов. Для того чтобы он стал настоящим типом, значения которого мож- но создать, мы должны указать все типы-параметры в конструк- торе типа.

Итак, если мы передадим тип Char как параметр в тип Maybe, то получим тип Maybe Char. Для примера: значение Just 'a' имеет тип Maybe Char.

Обычно нам не приходится явно передавать параметры конс- трукторам типов, поскольку в языке Haskell есть вывод типов. Поэ- тому когда мы создаём значение Just 'a', Haskell тут же определяет его тип – Maybe Char.

 
 

Если мы всё же хотим явно указать тип как параметр, это нужно делать в типовой части выражений, то есть после символа ::. Яв- ное указание типа может понадобиться, если мы, к примеру, хотим, чтобы значение Just 3 имело тип Maybe Int. По умолчанию Haskell выведет тип (Num a) => Maybe a. Воспользуемся явным аннотирова- нием типа:

ghci> Just 3 :: Maybe Int Just 3

Может, вы и не знали, но мы использовали тип, у которого были типы-параметры ещё до типа Maybe. Этот тип – список. Несмотря на


 

то что дело несколько скрывается синтаксическим сахаром, конс- труктор списка принимает параметр для того, чтобы создать конк- ретный тип. Значения могут иметь тип [Int], [Char], [[String]], но вы не можете создать значение с типом [].

ПРИМЕЧАНИЕ.Мы называем тип конкретным, если он вообще не принимает никаких параметров (например, Int или Bool) либо если параметры в типе заполнены (например, Maybe Char). Если у вас есть какое-то значение, у него всегда конкретный тип.

 
 

Давайте поиграем с типом Maybe:

ghci> Just "Ха-ха" Just "Ха-ха"

ghci> Just 84

Just 84

ghci> :t Just "Ха-ха"

Just "Ха-ха" :: Maybe [Char] ghci> :t Just 84

Just 84 :: (Num t) => Maybe t ghci> :t Nothing

Nothing :: Maybe a

 
 

ghci> Just 10 :: Maybe Double Just 10.0

Типы-параметры полезны потому, что мы можем с их помощью создавать различные типы, в зависимости от того, какой тип нам надо хранить в нашем типе данных. К примеру, можно объявить отдельные Maybe-подобные типы данных для любых типов:

data IntMaybe = INothing | IJust Int

 

data StringMaybe = SNothing | SJust String data ShapeMaybe = ShNothing | ShJust Shape

Более того, мы можем использовать типы-параметры для опре-

деления самого обобщённого Maybe, который может содержать дан- ные вообще любых типов!

Обратите внимание: тип значения Nothing – Maybe a. Это поли- морфный тип: в его имени присутствует типовая переменная – кон- кретнее, переменная a в типе Maybe a. Если некоторая функция при- нимает параметр типа Maybe Int, мы можем передать ей значение


 

Nothing, так как оно не содержит значения, которое могло бы этому препятствовать. Тип Maybe a может вести себя как Maybe Int, точно так же как значение 5 может рассматриваться как значение типа Int или Double. Аналогичным образом тип пустого списка – это [a]. Пустой список может вести себя как список чего угодно. Вот почему можно производить такие операции, как [1,2,3] ++ [] и ["ха","ха","ха"] ++ [].

 

Параметризовать ли машины?

 
 

Когда имеет смысл применять типовые параметры? Обычно мы ис- пользуем их, когда наш тип данных должен уметь сохранять внутри себя любой другой тип, как это делает Maybe a. Если ваш тип – это не- которая «обёртка», использование типов-параметров оправданно. Мы могли бы изменить наш тип данных Car с такого:

data Car = Car { company :: String

, model :: String

, year :: Int

 
 

} deriving (Show)

на такой:

data Car a b c = Car { company :: a

, model :: b

, year :: c

 
 

} deriving (Show)

Но выиграем ли мы в чём-нибудь? Ответ – вероятно, нет, потому что впоследствии мы всё равно определим функции, которые рабо- тают с типом Car String String Int. Например, используя первое оп- ределение Car, мы могли бы создать функцию, которая отображает свойства автомобиля в виде понятного текста:

tellCar :: Car –> String

 
 

tellCar (Car {company = c, model = m, year = y}) = "Автомобиль " ++ c ++ " " ++ m ++ ", год: " ++ show y

 

ghci> let stang = Car {company="Форд", model="Мустанг", year=1967} ghci> tellCar stang

 
 

"Автомобиль Форд Мустанг, год: 1967"

Приятная маленькая функция. Декларация типа функции кра- сива и понятна. А что если Car – это Car a b c?


 

tellCar :: (Show a) => Car String String a –> String tellCar (Car {company = c, model = m, year = y}) =

 
 

"Автомобиль " ++ c ++ " " ++ m ++ ", год: " ++ show y

Мы вынуждены заставить функцию принимать параметр Car типа (Show a) => Car String String a. Как видите, декларация типа функции более сложна; единственное преимущество, которое здесь имеется,

– мы можем использовать любой тип, имеющий экземпляр класса

 
 

Show, как тип для типовой переменной c.

ghci> tellCar (Car "Форд" "Мустанг" 1967) "Автомобиль Форд Мустанг, год: 1967"

ghci> tellCar (Car "Форд" "Мустанг" "тысяча девятьсот шестьдесят седьмой") "Автомобиль Форд Мустанг, год: \"тысяча девятьсот шестьдесят седьмой\"" ghci> :t Car "Форд" "Мустанг" 1967

Car "Форд" "Мустанг" 1967 :: (Num t) => Car [Char] [Char] t

ghci> :t Car "Форд" "Мустанг" "тысяча девятьсот шестьдесят седьмой" Car "Форд" "Мустанг" "тысяча девятьсот шестьдесят седьмой"

 
 

:: Car [Char] [Char] [Char]

На практике мы всё равно в большинстве случаев использовали бы Car String String Int, так что в параметризации типа Car большого смысла нет. Обычно мы параметризируем типы, когда для работы нашего типа неважно, что в нём хранится. Список элементов – это просто список элементов, и неважно, какого они типа: список рабо- тает вне зависимости от этого. Если мы хотим суммировать список чисел, то в суммирующей функции можем уточнить, что нам нужен именно список чисел. То же самое верно и для типа Maybe. Он предо- ставляет возможность не иметь никакого значения или иметь какое- то одно значение. Тип хранимого значения не важен.

 
 

Ещё один известный нам пример параметризованного типа – отображения Map k v из модуля Data.Map. Параметр k – это тип ключей в отображении, параметр v – тип значений. Это отличный пример правильного использования параметризации типов. Параметриза- ция отображений позволяет нам использовать любые типы, требуя лишь, чтобы тип ключа имел экземпляр класса Ord. Если бы мы оп- ределяли тип для отображений, то могли бы добавить ограничение на класс типа в объявлении:

data (Ord k) => Map k v = ...


 

Тем не менее в языке Haskell принято соглашение никогда не использовать ограничения класса типов при объявлении типов данных. Почему? Потому что серьёзных преимуществ мы не по- лучим, но в конце концов будем использовать всё больше ограни- чений, даже если они не нужны. Поместим ли мы ограничение (Ord k) в декларацию типа или не поместим – всё равно придётся указывать его при объявлении функций, предполагающих, что ключ может быть упорядочен. Но если мы не поместим ограни- чение в объявлении типа, нам не придётся писать его в тех фун- кциях, которым неважно, может ключ быть упорядочен или нет. Пример такой функции – toList :: Map k a –> [(k, a)]. Если бы Map k a имел ограничение типа в объявлении, тип для функции toList был бы таким: toList :: (Ord k) => Map k a –> [(k, a)], даже несмотря на то что функция не сравнивает элементы друг с другом.

Так что не помещайте ограничения типов в декларации типов данных, даже если это имело бы смысл, потому что вам всё равно придётся помещать ограничения в декларации типов функций.

 

Векторы судьбы

 
 

Давайте реализуем трёхмерный вектор и несколько операций для него. Мы будем использовать параметризованный тип, потому что хоть вектор и содержит только числовые параметры, он должен поддерживать разные типы чисел.

data Vector a = Vector a a a deriving (Show)

 

vplus :: (Num a) => Vector a –> Vector a –> Vector a

(Vector i j k) `vplus` (Vector l m n) = Vector (i+l) (j+m) (k+n)

 

scalarProd :: (Num a) => Vector a –> Vector a –> a

(Vector i j k) `scalarProd` (Vector l m n) = i*l + j*m + k*n

 

 
 

vmult :: (Num a) => Vector a –> a –> Vector a (Vector i j k) `vmult` m = Vector (i*m) (j*m) (k*m)

Функция vplus складывает два вектора путём сложения соот- ветствующих координат. Функция scalarProd используется для вы- числения скалярного произведения двух векторов, функция vmult

– для умножения вектора на константу.

Эти функции могут работать с типами Vector Int, Vector Integer, Vector Float и другими, до тех пор пока тип-параметр a из опреде-


 

ления Vector a принадлежит классу типов Num. По типам функций можно заметить, что они работают только с векторами одного типа, и все координаты вектора также должны иметь одинаковый тип. Обратите внимание на то, что мы не поместили ограничение класса Num в декларацию типа данных, так как нам всё равно бы при- шлось повторять его в функциях.

 
 

Ещё раз повторю: очень важно понимать разницу между конс- трукторами типов и данных. При декларации типа данных часть объявления до знака = представляет собой конструктор типа, а часть объявления после этого знака – конструктор данных (возмож- ны несколько конструкторов, разделенных символом |). Попытка дать функции тип Vector a a a -> Vector a a a -> a будет неудачной, потому что мы должны помещать типы в декларацию типа, и конс- труктор типа для вектора принимает только один параметр, в то время как конструктор данных принимает три. Давайте поупраж- няемся с нашими векторами:

ghci> Vector 3 5 8 `vplus` Vector 9 2 8

Vector 12 7 16

ghci> Vector 3 5 8 `vplus` Vector 9 2 8 `vplus` Vector 0 2 3

Vector 12 9 19

ghci> Vector 3 9 7 `vmult` 10

Vector 30 90 70

ghci> Vector 4 9 5 `scalarProd` Vector 9.0 2.0 4.0

74.0

ghci> Vector 2 9 3 `vmult` (Vector 4 9 5 `scalarProd` Vector 9 2 4)

 
 

Vector 148 666 222


 

тип Int имеет экземпляр для класса Eq. Реальная польза от этого видна при использовании функций, которые служат интерфейсом класса Eq, – операторов == и /=. Если тип имеет определённый эк- земпляр класса Eq, мы можем применять оператор == к значениям этого типа. Вот почему выражения 4 == 4 и "раз" /= "два" проходят проверку типов.

Классы типов часто путают с классами в языках вроде Java, Python, C++ и им подобных, что сбивает с толку множество людей. В вышеперечисленных языках классы – это нечто вроде чертежей, по которым потом создаются объекты, хранящие некое состояние и способные производить некие действия. Мы не создаём типы из классов типов – вместо этого мы сначала создаём свои типы дан- ных, а затем думаем о том, как они могут себя вести. Если то, что мы создали, можно проверить на равенство, – определяем для него экземпляр класса Eq. Если наш тип может вести себя как нечто, что можно упорядочить, – создаём для него экземпляр класса Ord.

Давайте посмотрим, как язык Haskell умеет автоматически де- лать наши типы экземплярами таких классов типов, как Eq, Ord, Enum, Bounded, Show и Read. Haskell умеет порождать поведение для наших типов в этих контекстах, если мы используем ключевое сло- во deriving при создании типа данных.

 







Последнее изменение этой страницы: 2017-02-17; Нарушение авторского права страницы

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.80.144.129 (0.031 с.)