ТОП 10:

Сравнение людей на равенство



 
 

Рассмотрим такой тип данных:

data Person = Person { firstName :: String

, lastName :: String

, age :: Int

 
 

}

Тип описывает человека. Предположим, что среди людей не встречаются тёзки одного возраста. Если у нас есть два описания, можем ли мы выяснить, относятся ли они к одному и тому же че- ловеку? Есть ли в такой операции смысл? Конечно, есть. Мы мо- жем сравнить записи и проверить, равны они или нет. Вот почему имело бы смысл определить для нашего типа экземпляр класса Eq. Порождаем экземпляр:

data Person = Person { firstName :: String

, lastName :: String


 

, age :: Int

 
 

} deriving (Eq)

Когда мы определяем экземпляр класса Eq для типа и пытаемся сравнить два значения с помощью операторов == или /=, язык Has- kell проверяет, совпадают ли конструкторы значений (хотя в на- шем типе только один конструктор), а затем проверяет все данные внутри конструктора на равенство, сравнивая каждую пару полей с помощью оператора ==. Таким образом, типы всех полей также должны иметь определённый экземпляр класса 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, всё проясняется.

 
 

Класс Read в чём-то является обратным классом типов для клас- са Show. Класс Show служит для преобразования значений нашего типа в строку, класс Read нужен для преобразования строк в значе- ния типа. Запомните, что при использовании функции чтения мы должны явно аннотировать тип возвращаемого значения. Если не указать тип результата явно, язык Haskell не сможет угадать, какой тип мы желали бы получить. Чтобы это проиллюстрировать, помес- тим в файл строку, представляющую некоторого человека, а затем загрузим файл в GHCi:

mysteryDude = "Person { firstName =\"Майкл\"" ++

 
 

", lastName =\"Даймонд\"" ++ ", age = 45}"

Для большей «читабельности» мы разбили строку на несколь- ко фрагментов. Если теперь необходимо вызвать с этой строкой фунцию read, то потребуется указать тип, который мы ожидаем по- лучить:

ghci> read mysteryDude :: Person


 

 
 

Person {firstName = "Майкл", lastName = "Даймонд", age = 45}

Если далее в программе мы используем результат чтения таким образом, что язык Haskell сможет вывести его тип, мы не обязаны использовать аннотацию типа.

 
 

ghci> read mysteryDude == mikeD True

Так же можно считывать и параметризованные типы, но при этом следует явно указывать все типы-параметры.

 
 

Если мы попробуем сделать так:

ghci> read "Just 3" :: Maybe a

 
 

то получим сообщение об ошибке: Haskell не в состоянии опреде- лить конкретный тип, который следует подставить на место типо- вой переменной a. Если же мы точно укажем, что хотим получить Int, то всё будет прекрасно:

ghci> read "Just 3" :: Maybe Int Just 3

 

Порядок в суде!

 
 

Класс типов Ord, предназначенный для типов, значения которых могут быть упорядочены, также допускает автоматическое порож- дение экземпляров. Если сравниваются два значения одного типа, сконструированные с помощью различных конструкторов данных, то меньшим считается значение, конструктор которого определён раньше. Рассмотрим, к примеру, тип Bool, значениями которого могут быть False или True. Для наших целей удобно предположить, что он определён следующим образом:

data Bool = False | True deriving (Ord)

Поскольку конструктор False указан первым, а конструктор

 
 

True – после него, мы можем считать, что True больше, чем False.

ghci> True `compare` False GT

ghci> True > False


 

True

 
 

ghci> True < False False

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

 
 

В типе данных Maybe a конструктор значений Nothing указан раньше Just – это значит, что значение Nothing всегда меньше, чем Just <нечто>, даже если это «нечто» равно минус одному миллиону триллионов. Но если мы сравниваем два значения Just, после срав- нения конструкторов начинают сравниваться поля внутри них.

ghci> Nothing < Just 100 True

ghci> Nothing > Just (–49999)

False

ghci> Just 3 `compare` Just 2 GT

 
 

ghci>Just 100 > Just 50 True

Но сделать что-нибудь вроде Just (*3) > Just (*2) не получится, потому что (*3) и (*2) – это функции, а они не имеют экземпляров для класса Ord.

 

Любой день недели

 
 

Мы легко можем использовать алгебраические типы данных для того, чтобы создавать перечисления, и классы типов Enum и Bounded помогают нам в этом. Рассмотрим следующий тип:

data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday

Так как все конструкторы значений нульарные (не принимают параметров, то есть не имеют полей), допустимо сделать для наше- го типа экземпляр класса Enum. Класс типов Enum предназначен для типов, для значений которых можно определить предшествующие и последующие элементы. Также мы можем определить для него экземпляр класса Bounded – он предназначен для типов, у которых есть минимальное и максимальное значения. Ну и уж заодно давай-


 

 
 

те сделаем для него экземпляры всех остальных классов типов, ко- торые можно сгенерировать автоматически, и посмотрим, что это даст.

data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Eq, Ord, Show, Read, Bounded, Enum)

 
 

Так как для нашего типа автоматически сгенерированы экзем- пляры классов Show и Read, можно конвертировать значения типа в строки и из строк:

ghci> Wednesday Wednesday

ghci> show Wednesday "Wednesday"

 
 

ghci> read "Saturday" :: Day Saturday

Поскольку он имеет экземпляры классов Eq и Ord, допускаются сравнение и проверка на равенство:

ghci> Saturday == Sunday False

ghci> Saturday == Saturday True

ghci> Saturday > Friday True

 
 

ghci> Monday `compare` Wednesday LT

Наш тип также имеет экземпляр класса Bounded, так что мы мо- жем найти минимальный и максимальный день.

ghci> minBound :: Day Monday

 
 

ghci> maxBound :: Day Sunday

Благодаря тому что тип имеет экземпляр класса Enum, можно получать предшествующие и следующие дни, а также задавать диа- пазоны дней.

ghci> succ Monday Tuesday

ghci> pred Saturday


 

Friday

ghci> [Thursday .. Sunday] [Thursday,Friday,Saturday,Sunday] ghci> [minBound .. maxBound] :: [Day]

 
 

[Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday]

Замечательно!

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

 
 

Ранее мы упоминали, что типы [Char] и String являются эквивалентами и могут взаимно заменяться. Это осуществляется с помощью синонимов типов. Синоним типа сам по себе ничего не делает – он просто даёт другое имя существующему типу, облегчая понимание нашего кода и документации. Вот так стандартная библи- отека определяет тип String как синоним для [Char]:

type String = [Char]

Ключевое слово type может ввести в заблуждение, потому что на самом деле мы не создаём ничего нового (создаём мы с помощью ключевого слова data), а просто определяем синоним для уже сущес- твующего типа.

Если мы создадим функцию, которая преобразует строку в верх- ний регистр, и назовём её toUpperString, то можем дать ей сигнатуру типа toUpperString :: [Char] –> [Char] или toUpperString :: String –> String. Обе сигнатуры обозначают одно и то же, но вторая легче читается.

 

Улучшенная телефонная книга

 
 

Когда мы работали с модулем Data.Map, то вначале представляли за- писную книжку в виде ассоциативного списка, а потом преобразо- вывали его в отображение. Как мы уже знаем, ассоциативный спи- сок – это список пар «ключ–значение». Давайте взглянем на этот вариант записной книжки:

phoneBook :: [(String,String)]


 

phoneBook = [("оля","555–29-38")

,("женя","452–29-28")

,("катя","493–29-28")

,("маша","205–29-28")

,("надя","939–82-82")

,("юля","853–24-92")

 
 

]

Мы видим, что функция phoneBook имеет тип [(String,String)]. Это говорит о том, что перед нами ассоциативный список, который отображает строки в строки, – но не более. Давайте зададим сино- ним типа, и мы сможем узнать немного больше по декларации типа:

 
 

type PhoneBook = [(String,String)]

Теперь декларация типа для нашей записной книжки может быть такой: phoneBook :: PhoneBook. Зададим также синоним для String.

type PhoneNumber = String type Name = String

 
 

type PhoneBook = [(Name,PhoneNumber)]

Те, кто программирует на языке Haskell, дают синонимы типу String, если хотят сделать объявления более «говорящими» – по- яснить, чем являются строки и как они должны использоваться. Итак, реализуя функцию, которая принимает имя и номер телефона и проверяет, есть ли такая комбинация в нашей запис- ной книжке, мы можем дать ей красивую и понятную декларацию

 
 

типа:

inPhoneBook :: Name –> PhoneNumber –> PhoneBook –> Bool inPhoneBook name pnumber pbook = (name,pnumber) `elem` pbook

Если бы мы не использовали синонимы типов, тип нашей фун- кции был бы String –> String –> [(String,String)] –> Bool. В этом случае декларацию функции легче понять при помощи синонимов типов. Однако не надо перегибать палку. Мы применяем синони- мы типов для того, чтобы описать, как используются существую- щие типы в наших функциях (таким образом декларации типов лучше документированы), или когда мы имеем дело с длинной декларацией типа, которую приходится часто повторять (вроде


 

[(String,String)]), причем эта декларация обозначает что-то более специфичное в контексте наших функций.

 

Параметризация синонимов

 
 

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

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

Функция, которая получает значение по ключу в ассоциативном списке, может иметь тип (Eq k) => k –> AssocList k v –> Maybe v. Тип AssocList – это конструктор типов, который принимает два типа и производит конкретный тип, например AssocList Int String.

 
 

Мы можем частично применять функции, чтобы получить но- вые функции; аналогичным образом можно частично применять типы-параметры и получать новые конструкторы типов. Так же, как мы вызываем функцию, не передавая всех параметров для того, чтобы получить новую функцию, мы будем вызывать и конс- труктор типа, не указывая всех параметров, и получать частично применённый конструктор типа. Если мы хотим получить тип для отображений (из модуля Data.Map) с целочисленными ключами, можно сделать так:

type IntMap v = Map Int v

 
 

или так:

type IntMap = Map Int

В любом случае конструктор типов IntMap принимает один па- раметр – это и будет типом, в который мы будем отображать Int.

И вот ещё что. Если вы попытаетесь реализовать этот пример, вам потребуется произвести квалифицированный импорт модуля Data.Map. При квалифицированном импорте перед конструктора- ми типов также надо ставить имя модуля. Таким образом, мы бы записали: IntMap = Map.Map Int.

Убедитесь, что вы понимаете различие между конструкторами типов и конструкторами данных. Если мы создали синоним типа IntMap или AssocList, это ещё не означает, что можно делать такие


 

вещи, как AssocList [(1,2),(4,5),(7,9)]. Это означает только то, что мы можем ссылаться на тип, используя другое имя. Можно на- писать: [(1,2),(3,5),(8,9)] :: AssocList Int Int, в результате чего числа в списке будут трактоваться как целые – но мы также смо- жем работать с этим списком как с обычным списком пар целых чисел. Синонимы типов (и вообще типы) могут использоваться в языке Haskell только при объявлении типов. Часть языка, от- носящаяся к объявлению типов, – собственно объявление типов (то есть при определении данных и типов) или часть объявления после символа :: (два двоеточия). Символ :: используется при де- кларировании или аннотировании типов.

 

Иди налево, потом направо

 
 

Ещё один чудесный тип, принимающий два других в качестве пара- метров, – это тип Either. Он определён приблизительно так:

data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)

 
 

У него два конструктора данных. Если используется конструк- тор Left, его содержимое имеет тип а; если Right – содержимое име- ет тип b. Таким образом, мы можем использовать данный тип для инкапсуляции значения одного из двух типов. Когда мы работаем с типом Either a b, то обычно используем сопоставление с образцом по Left и Right и выполняем действия в зависимости от того, какой вариант совпал.

ghci> Right 20

Right 20

ghci> Left "в00т" Left "в00т"

ghci> :t Right 'a'

Right 'a' :: Either a Char ghci> :t Left True

 
 

Left True :: Either Bool b

Из приведённого примера следует, что типом значения Left True является Either Bool b. Первый параметр типа Bool, посколь- ку значение создано конструктором Left; второй же параметр ос- тался полиморфным. Ситуация подобна тому как значение Nothing имеет тип Maybe a.


 

Мы видели, что тип Maybe главным образом используется для того, чтобы представить результат вычисления, которое может за- вершиться неудачей. Но иногда тип Maybe не так удобен, поскольку значение Nothing не несёт никакой информации, кроме того что что-то пошло не так. Это нормально для функций, которые могут выдавать ошибку только в одном случае – или если нам просто не интересно, как и почему функция «упала». Поиск в отображении типа Data.Map может завершиться неудачей, только если искомый ключ не найден, так что мы знаем, что случилось. Но если нам нужно знать, почему не сработала некоторая функция, обычно мы возвращаем результат типа Either a b, где а – это некоторый тип, который может нам что-нибудь рассказать о причине ошибки, и b – результат удачного вычисления. Следовательно, ошибки использу- ют конструктор данных Left, правильные результаты используют конструктор Right.

 
 

Например, в школе есть шкафчики для того, чтобы ученикам было куда клеить постеры Guns’n’Roses. Каждый шкафчик откры- вается кодовой комбинацией. Если школьнику понадобился шкаф- чик, он говорит администратору, шкафчик под каким номером ему нравится, и администратор выдаёт ему код. Если этот шкафчик уже кем-либо используется, администратор не сообщает код – они вместе с учеником должны будут выбрать другой вариант. Будем ис- пользовать модуль Data.Map для того, чтобы хранить информацию о шкафчиках. Это будет отображение из номера шкафчика в пару, где первый компонент указывает, используется шкафчик или нет, а второй компонент – код шкафчика.

import qualified Data.Map as Map

data LockerState = Taken | Free deriving (Show, Eq) type Code = String

 
 

type LockerMap = Map.Map Int (LockerState, Code)

Довольно просто. Мы объявляем новый тип данных для хране- ния информации о том, был шкафчик занят или нет. Также мы со- здаём синоним для кода шкафчика и для типа, который отображает целые числа в пары из статуса шкафчика и кода. Теперь создадим функцию для поиска кода по номеру. Мы будем использовать тип Either String Code для представления результата, так как поиск мо-


 

 
 

жет не удаться по двум причинам – шкафчик уже занят, в этом случае нельзя сообщать код, или номер шкафчика не найден вообще. Если поиск не удался, возвращаем значение типа String с пояснениями.

lockerLookup :: Int –> LockerMap –> Either String Code lockerLookup lockerNumber map =

case Map.lookup lockerNumber map of

Nothing –> Left $ "Шкафчик № " ++ show lockerNumber ++ " не существует!"

Just (state, code) –> if state /= Taken

then Right code

 
 

else Left $ "Шкафчик № " ++ show lockerNumber ++ " уже занят!"

Мы делаем обычный поиск по отображению. Если мы получили значение Nothing, то вернём значение типа Left String, говорящее, что такой номер не существует. Если мы нашли номер, делаем допол- нительную проверку, занят ли шкафчик. Если он занят, возвращаем значение Left, говорящее, что шкафчик занят. Если он не занят, возвращаем значение типа Right Code, в котором даём студенту код шкафчика. На самом деле это Right String, но мы создали синоним типа, чтобы сделать наши объявления более понятными. Вот при- мер отображения:

lockers :: LockerMap lockers = Map.fromList

[(100,(Taken,"ZD39I"))

,(101,(Free,"JAH3I"))

,(103,(Free,"IQSA9"))

,(105,(Free,"QOTSA"))

,(109,(Taken,"893JJ"))

,(110,(Taken,"99292"))

 
 

]

Давайте попытаемся узнать несколько кодов.

ghci> lockerLookup 101 lockers Right "JAH3I"

ghci> lockerLookup 100 lockers Left "Шкафчик № 100 уже занят!" ghci> lockerLookup 102 lockers

Left "Шкафчик № 102 не существует!" ghci> lockerLookup 110 lockers


 

 
 

Left "Шкафчик № 110 уже занят!" ghci> lockerLookup 105 lockers Right "QOTSA"

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

 







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

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