02. Типы и классы типов

Типы

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

В отличие от большинства других языков программирования, Haskell имеет механизм автоматического вывода типов. То есть он умеет сам определять тип выражения, даже если оно не указано. Не обязательно указывать типы функций и переменных — они могут быть выведены компилятором, исходя из того, как они используются. Для того, чтобы определить тип выражения, введите следующее в GHCi:

ghci> :t 'a'
'a' :: Char
ghci> :t True
True :: Bool
ghci> :t 'a' > 'b'
'a' > 'b' :: Bool

Запись x :: T читается как «x имеет тип T». В данном случае 'a' имеет тип Char, a True имеет тип Bool, выражение 'a' > 'b' имеет тип Bool. Имена типов всегда пишутся с заглавной буквы. Вот еще один пример:

ghci> :t "hello"
"hello" :: [Char]
ghci> :t True : [False, False]
True : [False, False] :: [Bool]

Запись [T] читается как «список из элементов типа T».

Функции в Haskell являются такими же простыми объектами, как числа и строки, и поэтому тоже имеют вполне определенный тип. При написании собственных функций, программист может указать явно тип для этой функции. На практике хорошим стилем считается указание типов для функций на верхнем уровне. Помните, что автоматический вывод типов — это работа компилятора, а не того, кто читает код!

appendDot :: [Char] -> [Char]
appendDot s = s ++ "."

appendWithDot :: [Char] -> [Char] -> [Char]
appendWithDot x y = x ++ "." ++ y

Запись A -> B читается как «функция из A в B». Типы формальных параметров функции apendWithDot разделяются символами ->, самый правый тип — это тип возвращаемого результата. Позже будет ясно, почему тип функции записывается таким странным образом.

Типы Char и Bool являются базовыми типами, в то время как [Char] и Char -> Bool — составными. Для начала, нам хватит следующих базовых типов:

  • Int — целочисленный тип (обычно 32-х битный);
  • Integer — неограниченный целый тип (длинная арифметика);
  • Double — число с плавающей точкой;
  • Bool — булев тип;
  • Char — символьный тип.

Полиморфизм типов

Вспомним функцию конструкции списка :. Эта функция работает как со списками чисел, так и со списками символов. Но у нее должен быть конкретный тип! Давайте проверим:

ghci> :t (:)
(:) :: a -> [a] -> [a]

Поскольку имена типов начинаются в Haskell с заглавной буквы, a не может быть именем типа. В Haskell a называется переменной типа. Функции, у которых в типе присутствуют переменные типа, называются полиморфными. Целиком сигнатура типа функции (:) читается так: «для любого типа a функция (:) имеет тип a -> [a] -> [a]». Каким именно будет этот тип a, будет ясно из контекста вызова функции. Так, в выражении 's':"hell" переменная a будет равна Char. Рассмотрим другой пример:

ghci> :t fst
fst :: (a, b) -> a

Здеcь a и b — это две независимые переменные типа. Несмотря на то, что они могут принимать различные значения, они вполне могут быть одинаковыми. Например, в выражении fst ('x', True): a принимает значение Char, а bBool, а в выражении fst (True, False) обе переменные принимают значение Bool.

Классы типов

Важной частью системы типов Haskell являются так называемые классы типов. Заранее стоит отметить, что классы типов не имеют отношения к классам в объектно-ориентированном программировании. Будьте внимательны и не путайте эти понятия!

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

ghci> :t (==)
(==) :: Eq a => a -> a -> Bool

В записи типа, всё, что находится левее символа =>, называется ограничением на тип. Этот тип можно прочитать так: «для любого типа a, принадлежащего классу типов Eq, функция (==) имеет тип a -> a -> Bool».

Класс Eq предоставляет интерфейс для проверки значений на равенство при помощи функций (==) и (/=). Любой тип, для которого имеет смысл такая операция, должен быть членом этого класса. Практически все стандартные типы в Haskell (за исключением операций ввода-вывода и функций) являются членами класса Eq.

Стандартная функция elem имеет тип Eq a => a -> [a] -> Bool и проверяет наличие элемента в списке. Класс Eq нужен, чтобы использовать (==) в реализации.

Базовые классы типов

Класс Eq содержит типы, значения которых могут быть проверены на равенство. Класс предоставляет функции (==) и (/=).

Класс Ord содержит типа, значения которых можно упорядочить. Класс предоставляет функции (>), (<), (>=), (<=) и compare. Последняя принимает два аргумента и возвращает значение типа Ordering. Ordering имеет три значения — LT, GT и EQ, означающих «меньше», «больше» и «равно» соответствено.

ghci> :t compare
compare :: Ord a => a -> a -> Ordering
ghci> 34 `compare` 29
GT
ghci> :t (>)
(>) :: Ord a => a -> a -> Bool
ghci> "foo" > "bar"
True

Класс Show позволяет переводить значения в строки. Практически все типы в Haskell (кроме функций и операций ввода-вывода) являются членами этого класса. Самая используемая функция класса — функция show:

ghci> show 56
"56"
ghci> show 2.53
"2.53"
ghci> show False
"False"

Read — это в некотором роде класс, обратный Show. Метод класса read позволяет переводить строку обратно в значение:

ghci> 2 + read "3"
5
ghci> 's' : read "['h', 'e', 'l', 'l']"
"shell"

Функция read полиморфна в типе возвращаемого значения. Чтобы вернуть результат, Haskell должен знать, значение какого типа должно быть прочитано из строки. В выражении 2 + read "3" Haskell знает, что результат будет сложен с числом и автоматически выводит тип возвращаемого значения. Если Haskell не может вывести контекст, его можно задать явно, при помощи оператора аннотации типа ::.

ghci> :t read
read :: Read a => String -> a
ghci> read "123" :: Int
123
ghci> read "[1, 3, 5]" :: [Double]
[1.0, 3.0, 5.0]

Класс Enum содержит перечислимые типы. Класс предоставляет два метода: succ и pred. Члены класса: Bool, Char, Ordering, Int, Integer, Float, Double.

Класс Bounded представляет ограниченные типы. Класс содержит два метода без аргументов: minBound и maxBound.

ghci> :t minBound
minBound :: Bounded a => a
ghci> minBound :: Bool
False
ghci> maxBound :: (Bool, Int, Char)
(True, 2147483647, '\1114111')

Функции minBound и maxBound являются полиморфными константами.

Класс Num представляет числа. Класс предоставляет операции +, -, *, div и пр.

ghci> :t (*)
(*) :: Num a => a -> a -> a
ghci> :t 10
10 :: Num a => a

Числовые литералы в Haskell тоже являются полиморфными и имеют тип в зависимости от контекста.

В классе Integral содержатся целочисленные типы, такие как Int и Integer. С целочисленными типами регулярно используется функция преобразования к любому числу:

ghci> :t fromIntegral
fromIntegral :: (Integral a, Num b) => a -> b
ghci> 2.5 + fromIntegral (5 :: Int)
7.5

Класс Fractional представляет дробные числа и операцию вещественного деления (/).

ghci> :t (/)
(/) :: Fractional a => a -> a -> a

Класс Floating представляет числа с плавающей точкой и тригонометрические операции:

ghci> :t sin
sin :: Floating a => a -> a
ghci> :t exp
exp :: Floating a => a -> a
ghci> :t pi
pi :: Floating a => a