Типы и классы
Поверь в типы
Ранее мы уже говорили, что Haskell является статически типизированным языком. Тип каждого выражения известен во время компиляции, что ведет к безопасному коду. Если вы напишете программу, которая попытается поделить булевский тип на число, то она даже не скомпилируется. Это хорошо, потому что лучше ловить такие ошибки на этапе компиляции вместо того, чтоб ваша программа падала во время работы. Все в Haskell имеет свой тип, так что компилятор может сделать довольно много выводов о вашей программе перед ее компиляцией.
В отличие от Java или Pascal, у Haskell есть механизм вывода типов. Если мы напишем число, то нам не надо говорить языку, что это число. Haskell может вывести это сам, так что нам не надо явно указывать типы наших функций и выражений. Мы изучили некоторые основы Haskell только очень поверхностно упомянув типы. Тем не менее, понимание системы типов является очень важной частью обучения языку Haskell.
Тип – это нечто вроде ярлыка, который есть у каждого выражения. Он говорит нам, к какой категории вещей относится выражение. Выражение True – булевское, "hello" – это строка, и так далее.
А сейчас воспользуемся GHCi для определения типов нескольких выражений. Мы сделаем это с помощью команды :t. Давайте попробуем.
ghci> :t 'a' 'a' :: Char ghci> :t True True :: Bool ghci> :t "HELLO!" "HELLO!" :: [Char] ghci> :t (True, 'a') (True, 'a') :: (Bool, Char) ghci> :t 4 == 5 4 == 5 :: Bool
Мы увидели что делает :t с выражениями – печатает сами выражения, затем следует :: и их тип. :: читается как «имеет тип».У явно указанных типов первый символ всегда в вернем регистре. 'a' как можно увидеть, имеет тип Char. Не сложно сообразить, что это обозначает character - символ.. True – это тип Bool. . Выглядит логично. Ну а как на счет этого? Исследуя тип "HELLO!" получим [Char]. Квадратные скобки указывают на список, так мы прочтем это как «список символов». В отличие от списков, каждый кортеж любой длины имеет свой тип. Так выражение (True, 'a') имеет тип (Bool, Char), тогда как выражение ('a','b','c') будет иметь тип (Char, Char, Char). 4 == 5 всегда вернет False, поэтому его тип Bool.
У функций тоже есть типы. Когда мы пишем свои собственные функции, мы можем указывать их тип явно. Обычно это считается хорошей практикой, исключая случаи написания очень коротких функций. Здесь и далее мы будем декларировать типы для всех создаваемых нами функций. Помните «выражение списка», который мы использовали раньше, и которое фильтровало строку так, что оставались только прописные буквы? Вот как это выглядит с объявлением типа.
removeNonUppercase :: [Char] -> [Char] removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]
removeNonUppercase имеет тип [Char] -> [Char], и означает, что строке сопоставляется строка. Это потому, что она принимает одну строку в качестве параметра, и возвращает вторую как результат. Тип [Char] синоним String, поэтому для большей ясности запишем тип как removeNonUppercase :: String -> StringМы не обязаны были задавать для этой функции объявление типа, потому что компилятор сам может вычислить, что это функция преобразования из строки в строку, но всё равно мы это сделали. А как нам записать тип функции, которая принимает несколько параметров? Вот простая функция, которая принимает три целых числа и складывает их вместе:
addThree :: Int -> Int -> Int -> Int addThree x y z = x + y + z
Параметры разделены с помощью -> , и здесь нет никакого различия между параметрами и типом возвращаемого значения. Возвращаемый тип это последний элемент в объявлении, а параметры — перевые три. Позже мы увидим, почему они просто разделяются с помощью ->, вместо того чтобы как-то специально отделить тип возвращаемого значения от типов параметров, например Int, Int, Int -> Int или что-то в этом духе.
Если вы хотите объявить тип вашей функции, но не уверены, каким он должно быть, то всегда можно написать функцию без него, а затем проверить тип функции с помощью :t. Функции — тоже выражения, так что :t будет работать с ними без проблем.
А вот обзор некоторых часто используемых типов.
Int обозначает целое число. Он используется для целых чисел. 7 может быть типа Int но 7.2 — нет. Int Int ограничен, и это значит, что у него есть минимальное и максимальное значение. Обычно, на 32-битных машинах максимально возможный Int - это 2147483647 , а минимально возможный — -2147483648.
Integer обозначает эээ… тоже целое число. Основная разница в том, что он не имеет ограничения, поэтому он может представлять действительно большие числа. Имеется в виду — действительно большие. Между тем, Int более эффективен.
factorial :: Integer -> Integer factorial n = product [1..n]
ghci> factorial 50 30414093201713378043612608166064768844377641568960512000000000000
Float – это вещественное число с плавающей точкой одинарной точности.
circumference :: Float -> Float circumference r = 2 * pi * r
ghci> circumference 4.0 25.132742
Double – это вещественное число с плавающей точкой с удвоенной точностью!
circumference' :: Double -> Double circumference' r = 2 * pi * r
ghci> circumference' 4.0 25.132741228718345
Bool это булевский тип. Этот тип может принимать только два значения: True и False.
Char представляет символ. Их выделяют одинарными кавычками. Список символов – это строка.
это типы, но тип кортежа зависит от его длины и от типа его компонентов. Так что, теоретически, существует бесконечное количество типов кортежей — а это многовато, чтобы перечислить их все в этом руководстве. Заметьте, что пустой кортеж () — это тоже тип, который может содержать единственное значение: ()
Переменные типа
Как вы думаете, какой тип у функции head? head принимает список любого типа и возвращает первый элемент, так какой же у нее тип? Давайте проверим!
ghci> :t head head :: [a] -> a
Хммм! Что такое a? Тип ли это? Помните, раньше мы говорили что типы пишутся с большой буквы, так что это точно не может быть типом. Так как начинается не с заклавной буквы, в действительности, это переменная типа. Это значит, что a может быть любым типом. Это похоже на «дженерики» в других языках, но только в Хаскеле они гораздо более мощные, так как позволяют нам легко писать очень общие функции, конечно, если эти функции не используют какие-нибудь специальное свойства конкретных типов. Функции, в объявлении которых встречаются переменные типа называеются полиморфными функциями. Объявление типа функции head выше означает, что она принимает список любого типа и возвращает один элемент того же типа.
Несмотря на то, что переменные типа могут иметь имена, состоящие более чем из одной буквы, обычно они называются of a, b, c, d …
Помните функцию fst? Она возвращает первый компонент в паре. Давайте проверим ее тип.
ghci> :t fst fst :: (a, b) -> a
Можно заметить, что fst принимает в качестве параметра кортеж, который состоит из двух типов, и возвращает элемент того же типа как первый компонент пары. Поэтому мы можем применить fst к паре, которая содержит два любых типа. Заметьте, что из-за того что a и b различные переменные типа, они вовсе не обязаны обозначать разные типы. Такая запись обозначает, что типы первого компонента и возвращаемого значения одинаковы.
Азбука классов типов
Класс типов — это что-то вроде интерфейса, который определяет некоторое поведение. Если тип является частью класса типов, это означает что он поддерживает и реализует поведение, описываемое этим классом. Множество людей, приходящих из ООП, путаются в классах типов, потому что думают, что они похожи на классы в объектно-ориентированных языках. Вообще-то они совсем не похожи. Можете думать о них как об интерфейсах в Java, только лучше.
Какая сигнатура типа для функции ==?
ghci> :t (==) (==) :: (Eq a) => a -> a -> Bool
Интересно. Мы видим здесь новую вещь, символ =>. Всё что идет до => называется ограничением класса. Мы можем прочитать предыдущие объявление типа так: функция равенства принимает два значения одинакового типа и возвращает Booll. Тип этих двух значений должен быть членом класса Eq (это и есть ограничение класса).
Класс типа Eq предоставляет интерфейс для проверки на равенство. Каждый тип, для значений которого операция проверки на равенство имеет смысл, должен быть членом класса EqВсе стандартные типы Хаскеля, кроме IO (тип для работы со вводом и выводом) и за исключением функций — входят в класс типов Eq.
У функции elem тип (Eq a) => a -> [a] -> Bool, потому что она использует оператор == над элементами списка, чтобы проверить, есть ли в этом списке значение, которое мы ищем.
Несколько базовых классов типов:
Eq используется для типов которые поддерживают проверку равенства. Интерфейс этого типа реализует две функции — == и /=. Так что если у нас есть ограничение класса Eq для переменной типа в функции, то она может использовать == или /= внутри своего определения. Все типы которые мы упоминали ранее, за исключением функций, входят в Eq, и, следовательно, могут быть проверены на равенство.
ghci> 5 == 5 True ghci> 5 /= 5 False ghci> 'a' == 'a' True ghci> "Ho Ho" == "Ho Ho" True ghci> 3.432 == 3.432 True
Ord предназначен для типов, которые поддерживают упорядочение.
ghci> :t (>) (>) :: (Ord a) => a -> a -> Bool
Все типы упомянутые ранее, за исключением функций, являются частью Ord. Ord содержит все стандартные функции сравнения, такие как >, <, >= и <=. . Функция сравнения принимает два члена Ordодного и того же типа, и возвращает отношение порядка между ними. Тип Ordering может принимать значения GT, LT or EQ, означая, соответственно, «больше чем», «меньше чем» и «равно».
Чтобы стать членом Ord, тип должен для начала иметь членство в престижнои и эксклюзивном клубе Eq.
ghci> "Abrakadabra" < "Zebra" True ghci> "Abrakadabra" `compare` "Zebra" LT ghci> 5 >= 2 True ghci> 5 `compare` 3 GT
Члены класса типов Show могут быть представлены как строки. Все типы описанные ранее, кроме функций, являются частью Show. Наиболее используемая функция в классе типов Show — это функция show. Она берет значение, чей тип принадлежит Show, и представляет его в виде строки.
ghci> show 3 "3" ghci> show 5.334 "5.334" ghci> show True "True"
Read — это нечто противоположное классу типов Show. Функция read принимает стоку и возвращает тип, который является членом Read.
ghci> read "True" || False True ghci> read "8.2" + 3.8 12.0 ghci> read "5" - 2 3 ghci> read "[1,2,3,4]" ++ [3] [1,2,3,4,3]
Отлично. Еще раз повторю, все описанные ранее типы входят в этот класс типов. Но что случится, если попробовать сделать read "4"?
ghci> read "4" <interactive>:1:0: Ambiguous type variable `a' in the constraint: `Read a' arising from a use of `read' at <interactive>:1:0-7 Probable fix: add a type signature that fixes these type variable(s)
Это GHCI пытается нам сказать, что он не знает что именно мы хотим получить в результате. Заметьте, что во время предыдущих вызовов read мы потом что-то делали с результатом функции. Таким образом, GHCI мог вычислить, какой тип ответа из read мы хотим получить.Когда мы использовали результат как boolean, он знал, что надо вернуть Bool. А в данном случае он знает, что нам нужен какой-то тип, входящий в класс Read, но не знает какой именно. Давайте посмотрим на сигнатуру функции read.
ghci> :t read read :: (Read a) => String -> a
Видите? Функция возвращает тип являющийся частью Read, но если мы не воспользуемся им позже, то у компилятора не будет способа определить какой именно это тип. Вот почему используются явные аннотации типа. Аннотации типа — это способ явно указать, какого типа должно быть выражение. Делается это с помощью добавлени :: в конец выражения и указания типа. Смотрите:
ghci> read "5" :: Int 5 ghci> read "5" :: Float 5.0 ghci> (read "5" :: Float) * 4 20.0 ghci> read "[1,2,3,4]" :: [Int] [1,2,3,4] ghci> read "(3, 'a')" :: (Int, Char) (3, 'a')
Для большинства выражений компилятор может вывести тип самостоятельно. Но иногда он не знает, вернуть ли значение типа Int или Float для выражения, вроде read "5". To see what the type is, Haskell would have to actually evaluate read "5". But since Haskell is a statically typed language, it has to know all the types before the code is compiled (or in the case of GHCI, evaluated). So we have to tell Haskell: "Hey, this expression should have this type, in case you don't know!".
Enum members are sequentially ordered types — they can be enumerated. The main advantage of the Enum typeclass is that we can use its types in list ranges. They also have defined successors and predecesors, which you can get with the succ and pred functions. Types in this class: (), Bool, Char, Ordering, Int, Integer, Float and Double.
ghci> ['a'..'e'] "abcde" ghci> [LT .. GT] [LT,EQ,GT] ghci> [3 .. 5] [3,4,5] ghci> succ 'B' 'C'
Bounded members have an upper and a lower bound.
ghci> minBound :: Int -2147483648 ghci> maxBound :: Char '\1114111' ghci> maxBound :: Bool True ghci> minBound :: Bool False
minBound and maxBound are interesting because they have a type of (Bounded a) => a. In a sense they are polymorphic constants.
All tuples are also part of Bounded if the components are also in it.
ghci> maxBound :: (Bool, Int, Char) (True,2147483647,'\1114111')
Num is a numeric typeclass. Its members have the property of being able to act like numbers. Let's examine the type of a number.
ghci> :t 20 20 :: (Num t) => t
It appears that whole numbers are also polymorphic constants. They can act like any type that's a member of the Num typeclass.
ghci> 20 :: Int 20 ghci> 20 :: Integer 20 ghci> 20 :: Float 20.0 ghci> 20 :: Double 20.0
Those are types that are in the Num typeclass. If we examine the type of *, we'll see that it accepts all numbers.
ghci> :t (*) (*) :: (Num a) => a -> a -> a
It takes two numbers of the same type and returns a number of that type. That's why (5 :: Int) * (6 :: Integer) will result in a type error whereas 5 * (6 :: Integer) will work just fine and produce an Integer because 5 can act like an Integer or an Int.
To join Num, a type must already be friends with Show and Eq.
Integral is also a numeric typeclass. Num includes all numbers, including real numbers and integral numbers, Integral includes only integral (whole) numbers. In this typeclass are Int and Integer.
Floating includes only floating point numbers, so Float and Double.
A very useful function for dealing with numbers is fromIntegral. It has a type declaration of fromIntegral :: (Num b, Integral a) => a -> b. From its type signature we see that it takes an integral number and turns it into a more general number. That's useful when you want integral and floating point types to work together nicely. For instance, the length function has a type declaration of length :: [a] -> Int instead of having a more general type of (Num b) => length :: [a] -> b. I think that's there for historical reasons or something, although in my opinion, it's pretty stupid. Anyway, if we try to get a length of a list and then add it to 3.2, we'll get an error because we tried to add together an Int and a floating point number. So to get around this, we do fromIntegral (length [1,2,3,4]) + 3.2 and it all works out.
Notice that fromIntegral has several class constraints in its type signature. That's completely valid and as you can see, the class constraints are separated by commas inside the parentheses.