Haskell

индекс
123,13

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

Неугомонный plague со товарищи продолжают переводить «Learn Haskell for Good». Я открою страшный секрет: русский перевод со всеми иллюстрациями будет позже опубликован на отдельном домене. А пока переведена очередная глава, с которой вы можете ознакомиться ниже.

Поверь в типы.



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


imageМы увидели что делает «: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 представляет символ. Их выделяют одинарными кавычками. Список символов – это строка.

Кортежи это типы, но тип кортежа зависит от его длины и от типа его компонентов. Так что, теоретически, существует бесконечное количество типов кортежей — а это многовато, чтобы перечислить их все в этом руководстве. Заметьте, что пустой кортеж () — это тоже тип, который может содержать единственное значение: ()

Переменные типа



imageКак вы думаете, какой тип у функции 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 различные переменные типа, они вовсе не обязаны обозначать разные типы. Такая запись обозначает, что типы первого компонента и возвращаемого значения одинаковы.

Азбука классов типов



imageКласс типов — это что-то вроде интерфейса, который определяет некоторое поведение. Если тип является частью класса типов, это означает что он поддерживает и реализует поведение, описываемое этим классом.
Множество людей, приходящих из ООП, путаются в классах типов, потому что думают, что они похожи на классы в объектно-ориентированных языках. Вообще-то они совсем не похожи. Можете думать о них как об интерфейсах в 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"
<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». Чтобы узнать, какой у него тип, 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 есть несколько ограничений в сигнатуре типа. Все верно, и, как вы можете видеть, ограничения класса разделяются внутри круглых скобок с помощью запятых.

Если вам понравилась статья, и вы хотели бы поучаствовать в переводе других, присоединяйтесь к plague! Следующая статься находится на стадии вычитки.
_________
Переведено толпой
Текст подготовлен в ХабраРедакторе
+26
22 октября 2009, 13:09
18

комментарии (10)

0
XaBoK #
Я не помню, говорили ли вам это уже или нет (это вроде бы уже третья статья), но ваш перевод названия книги не корректен…

<hollywar mode=«Off»/>
«Изучай Haskell ради Добра!» звучит примерно так же как «J# — познай силу Тёмной стороны!» ну или «VB — дорога вселенского Зла!»
<hollywar mode=«RemoteOnly»/>
Как бы никакого Добра от h точно нет, ну разве что кавайные картинки. Корректней было бы перевести слово «Good» с контекстным наклоном «хорошо», а не «добро». Да и вообще, учитывая что это речевой оборот «for Good», то и первод должен быть примерно таким: «Выучи Haskell раз и Навсегда!». Хотя можно и поиграть словами «Узнай Haskell дюже добре» ^_^

Не принимайте за негатив — статьи я читаю и доволен переводом и самим циклом.
+1
folone #
В предыдущих двух дискуссиях консенсус так и не был найден. Предлагаю тут в комментариях изложить правильные (по вашему мнению) варианты перевода, а я завтра (или, скажем, вечером) запущу голосовалку, чтобы раз и навсегда решить, как правильно называть цикл статей. Ну и переименую их все, если что.
0
folone #
Впрочем, я подумал и… вот голосовалка.
0
XaBoK #
Огласите потом результат, а то чтоб зайти надо подписаться на блог Haskell :/
Не люблю излишнюю бюрократию…
0
folone #
Мм. Ладно, сейчас сделаю голосование открытым.
Пока что лидирует нынешний вариант.
0
afi #
Скорей уж «Учим Haskell как следует» :)
0
crash #
«ради добра» ололо
0
folone #
Вам сюда.
0
d_a #
да, t — это наше все )
0
folone #
Внимание, результат (думаю, все, кто хотел, проголосовал). Спасибо за отзывы.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.