Изучай Haskell ради Добра! Типы и классы типов Типы и классы типов Поверь в типы Ранее мы уже говорили, что 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 ограничен, и это значит, что у него есть минимальное и максимальное значение. Обычно, на 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 выше означает, что она принимает список любого типа и возвращает один элемент того же типа. Несмотря на то, что переменные типа могут иметь имена, состоящие из более чем одной буквы, мы обычно называем их a, b, c, d ... Помните функцию fst? Она возвращает первый компонент в паре. Давайте проверим ее тип. ghci> :t fst fst :: (a, b) -> a Можно заметить, что fst принимает в качестве параметра кортеж, который состоит из двух типов, и возвращает элемент того же типа как первый компонент пары. Поэтому мы можем применить fst к паре, которая содержит два любых типа. Заметьте, что поскольку a и b – различные переменные типа, они вовсе не обязаны быть разного типа. Это лишь значит, что тип первого компонента и тип возвращаемого значения одинаковы. Азбука классов типов Класс типов — это что-то вроде интерфейса, который определяет некоторое поведение. Если тип является частью класса типов, это означает что он поддерживает и реализует поведение, описываемое этим классом. Множество людей, приходящих из ООП, путаются в классах типов, потому что думают, что они похожи на классы в объектно-ориентированных языках. Вообще-то они совсем не похожи. Можете думать о них как об интерфейсах в Java, только лучше. Какая сигнатура типа для функции «==»? ghci> :t (==) (==) :: (Eq a) => a -> a -> Bool Заметьте: оператор равенства, «==» — это функция. Функциями тоже являются «+»,«*»,«-»,«/» и почти все остальные операторы. Если имя функции содержит только специальные символы, по умолчанию подразумевается что это инфиксная функция. Если мы захотим проверить ее тип, передать ее другой функции, или вызвать как префиксную функцию, мы должны поместить ее в круглые скобки. Интересно. Мы видим здесь что-то новое, символ «=>». Все, что находится перед =>, называется ограничением класса. Мы можем прочитать предыдущее объявление типа так: функция равенства принимает два значения одинакового типа и возвращает Bool. Тип этих двух значений должен быть членом класса 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 или 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" :1:0: Ambiguous type variable `a' in the constraint: `Read a' arising from a use of `read' at :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". Чтобы узнать, какой у него тип, Haskell должен был бы фактически вычислить read "5". Но так как Haskell — статически типизированный язык, он должен знать все типы до того, как скомпилируется код (или, в случае GHCI, вычислится). Так что, мы должны сказать языку: "Эй, это выражение должно иметь вот этот тип, если ты вдруг не сможешь сам его понять!" Членами класса типов Enum являются последовательно упорядоченные типы, они могут быть перечислены. Основное преимущество класса типов Enum в том, что мы можем использовать его типы в диапазонах списков. Кроме того, у них есть предыдущие и последующие элементы, которые можно получить с помощью функций succ и pred. Типы входящие в этот класс: (), Bool, Char, Ordering, Int, Integer, Float и Double. ghci> ['a'..'e'] "abcde" ghci> [LT .. GT] [LT,EQ,GT] ghci> [3 .. 5] [3,4,5] ghci> succ 'B' 'C' Члены класса типов Bounded имеют верхнюю и нижнюю границу. ghci> minBound :: Int -2147483648 ghci> maxBound :: Char '\1114111' ghci> maxBound :: Bool True ghci> minBound :: Bool False minBound и maxBound интересны тем, что имеют тип (Bounded a) => a. В этом смысле они являются полиморфными константами. Все кортежи также являются частью Bounded, если их компоненты принадлежат Bounded. ghci> maxBound :: (Bool, Int, Char) (True,2147483647,'\1114111') Num — класс типов для чисел. Его члены могут вести себя как числа. Давайте проверим тип некоторого числа. ghci> :t 20 20 :: (Num t) => t Похоже, что все числа также являются полиморфными константами. Они могут вести себя как любой тип, являющийся частью класса типов Num. ghci> 20 :: Int 20 ghci> 20 :: Integer 20 ghci> 20 :: Float 20.0 ghci> 20 :: Double 20.0 Это типы, которые находятся в классе типов Num. Если проверить тип оператора «*», можно увидеть, что он принимает любые числа. ghci> :t (*) (*) :: (Num a) => a -> a -> a Он принимает два числа одинакового типа и возвращает число этого же типа. Именно поэтому (5 :: Int) * (6 :: Integer) приведет к ошибке, а 5 * (6 :: Integer) будет работать нормально и вернет Integer — потому что 5 может вести себя и как Integer, и как Int. Чтобы присоединиться к Num, тип должен подружиться с Show и Eq. Integral — тоже числовой класс типов . Num включает в себя все типы, включая действительные и целые числа, а Integral включает в себя только целые числа. В этот класс входят типы Int и Integer. Floating включает в себя только числа с плавающей точкой, то есть Float и Double. Очень полезной функций для работы с числами является fromIntegral. Вот ее объявление типа: «fromIntegral :: (Num b, Integral a) => a -> b». Из этой сигнатуры мы видим, что она принимает целое число (Integral) и превращает его как более общее число (Num). Это окажется полезно, когда потребуется, чтобы целые числа и числа с плавающей точкой могли «сработаться» вместе. Например, функция длины length имеет объявление «length :: [a] -> Int» вместо того, чтобы использовать более общий тип «(Num b) => length :: [a] -> b». Думаю, это произошло по историческим причинам или из-за чего-то подобного, хотя по-моему это довольно глупо. В любом случае, если мы попробуем вычислить длину списка и добавить к ней 3.2, то получим ошибку, потому что мы попытались сложить вместе Int и число с плавающей точкой. Чтобы обойти это, можно использовать fromIntegral (length [1,2,3,4]) + 3.2 и всё заработает. Заметьте, что у fromIntegral есть несколько ограничений в сигнатуре типа. Все верно, и, как вы можете видеть, ограничения класса разделяются внутри круглых скобок с помощью запятых. ------------------------------------------------------------------------------- http://translated.by/you/learn-you-a-haskell-for-great-good-chapter-types-and-typeclasses/into-ru/trans/ Original (English): Learn you a Haskell for Great Good:chapter - Types and Typeclasses (http://learnyouahaskell.com/types-and-typeclasses) Translation: © olegchir, Dmitry-Leushin, asinitsyn, Oleg Avdeev, Yasir Arsanukaev, Николай, slayzx. License: Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License translated.by crowd