Наши типы

Вот мы и добрались до Второго Кита Haskell — до Типов. Конечно, мы работали с типами почти с самого начала, но вам уже порядком надоели все эти Int и String, не правда ли? Пришла пора познакомиться с типами куда ближе.

Знакомство

Удивительно, но в Haskell очень мало встроенных типов, то есть таких, о которых компилятор знает с самого начала. Есть Int, есть Double, Char, ну и ещё несколько. Все же остальные типы, даже носящие статус стандартных, не являются встроенными в язык. Вместо этого они определены в стандартной или иных библиотеках, причём определены точно так же, как мы будем определять и наши собственные типы. А поскольку без своих типов написать сколь-нибудь серьёзное приложение у нас не получится, тема эта достойна самого пристального взгляда.

Определим тип Transport для двух известных протоколов транспортного уровня модели OSI:

data Transport = TCP | UDP

Перед нами — очень простой, но уже наш собственный тип. Рассмотрим его внимательнее.

Ключевое слово data — это начало определения типа. Далее следует название типа, в данном случае Transport. Имя любого типа обязано начинаться с большой буквы. Затем идёт знак равенства, после которого начинается фактическое описание типа, его «тело». В данном случае оно состоит из двух простейших конструкторов. Конструктор значения (англ. data constructor) — это то, что строит значение данного типа. Здесь у нас два конструктора, TCP и UDP, каждый из которых строит значение типа Transport. Имя конструктора тоже обязано начинаться с большой буквы. Иногда для краткости конструктор значения называют просто конструктором.

Подобное определение легко читается:

data  Transport  =    TCP  |    UDP

тип   Transport  это  TCP  или  UDP

Теперь мы можем использовать тип Transport, то есть создавать значения этого типа и что-то с ними делать. Например, в let-выражении:

  let protocol = TCP

Мы создали значение protocol типа Transport, использовав конструктор TCP. А можно и так:

  let protocol = UDP

Хотя мы использовали разные конструкторы, тип значения protocol в обоих случаях один и тот же — Transport.

Расширить подобный тип предельно просто. Добавим новый протокол SCTP (Stream Control Transmission Protocol):

data Transport = TCP | UDP | SCTP

Третий конструктор значения дал нам третий способ создать значение типа Transport.

Значение-пустышка

Задумаемся: говоря о значении типа Transport — о чём в действительности идёт речь? Казалось бы, значения-то фактического нет: ни числа никакого, ни строки — просто три конструктора. Так вот они и есть значения. Когда мы пишем:

  let protocol = SCTP

мы создаём значение типа Transport с конкретным содержимым в виде SCTP. Конструктор — это и есть содержимое. Данный вид конструктора называется нульарным (англ. nullary). Тип Transport имеет три нульарных конструктора. И даже столь простой тип уже может быть полезен нам:

checkProtocol :: Transport -> String
checkProtocol transport = case transport of
  TCP  -> "That's TCP protocol."
  UDP  -> "That's UDP protocol."
  SCTP -> "That's SCTP protocol."

main :: IO ()
main = putStrLn . checkProtocol $ TCP

В результате увидим:

That's TCP protocol.

Функция checkProtocol объявлена как принимающая аргумент типа Transport, а применяется она к значению, порождённому конструктором TCP. В данном случае конструкция case-of сравнивает аргумент с конструкторами. Именно поэтому нам не нужна функция otherwise, ведь никаким иным способом, кроме как с помощью трёх конструкторов, значение типа Transport создать невозможно, а значит, один из конструкторов гарантированно совпадёт.

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

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

Обратите внимание на форматирование, когда ментальные «ИЛИ» выровнены строго под знаком равенства. Такой стиль вы встретите во многих реальных Haskell-проектах.

Значение типа Day отражено одним из семи конструкторов. Сделаем же с ними что-нибудь:

data WorkMode = FiveDays | SixDays

workingDays :: WorkMode -> [Day]
workingDays FiveDays = [ Monday
                       , Tuesday
                       , Wednesday
                       , Thursday
                       , Friday
                       ]
workingDays SixDays = [ Monday
                      , Tuesday
                      , Wednesday
                      , Thursday
                      , Friday
                      , Saturday
                      ]

Функция workingDays возвращает список типа [Day], и в случае пятидневной рабочей недели, отражённой конструктором FiveDays, этот список сформирован пятью конструкторами, а в случае шестидневной — шестью конструкторами.

Польза от типов, сформированных нульарными конструкторами, не очень велика, хотя встречаться с такими типами вы будете часто.

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

А теперь мы можем познакомиться с типами куда более полезными.