Изучай Haskell ради Добра! Типы и классы типов

Miran Lipovača, “Learn you a Haskell for Great Good:chapter - Types and Typeclasses”, public translation into Russian from English More about this translation.

Translate into another language.

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

Поверь в типы

Ранее мы уже говорили, что 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

Pages: ← previous Ctrl next
1 2

Original (English): Learn you a Haskell for Great Good:chapter - 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

Like this translation? Share it or bookmark!