ПРИМЕЧАНИЕ. В консоли интерпретатора GHCi невозможно определять программные сущности в нескольких строках. Но в данной книге нам иногда приходится разбивать определения на несколько строк, чтобы код помещался на странице. В противном случае книга оказалась бы такой широкоформатной, что для неё вам пришлось бы купить гигантский книжный шкаф!
Почти закончили. Теперь давайте модифицируем функцию, чтобы получить треугольники, периметр которых равен 24.
ghci> let rightTriangles' = [ (a,b,c) | c <– [1..10], b <– [1..c], a <– [1..b], a 2 + b 2 == c 2, a+b+c == 24]
ghci> rightTriangles'
[(6,8,10)]
Вот и ответ! Это общий шаблон в функциональном программировании. Вы берёте начальный набор решений и затем применяете преобразования и фильтруете их, пока не получите результат.
2
Типы и классы типов
Поверь в типы
Мы уже говорили о том, что Haskell является статически типизированным языком. Тип каждого выражения известен во время компиляции – это залог безопасного кода. Если вы напишете программу, которая попытается поделить булевский тип на число, то она даже не скомпилируется.
И хорошо, потому что уж лучше ловить такие ошибки на этапе компиляции, чем наблюдать, как ваша программа аварийно закрывается во время работы! Всему в языке Haskell назначен свой тип, так что компилятор может сделать довольно много выводов о программе перед её компиляцией.
В отличие от языков Java или Pascal, у Haskell есть механизм вывода типов. Если мы напишем число, то нет необходимости указывать, что это число. Язык Haskell может вывести это сам, так что нам не приходится явно обозначать типы функций и выражений.
Мы изучили некоторые основы языка, лишь вскользь упомянув о типах. Тем не менее понимание системы типов – очень важная часть обучения языку Haskell.
Тип – это нечто вроде ярлыка, который есть у каждого выражения. Он говорит нам, к какой категории относится данное выражение. Выражение True
– булево, "привет"
– это строка, и т. д.
Явное определение типов
А сейчас воспользуемся интерпретатором GHCi для определения типов нескольких выражений. Мы сделаем это с помощью команды :t
, которая, если за ней следует любое правильное выражение, выдаст нам тип последнего. Итак…
ghci> :t 'a'
'a' :: Char
ghci> :t True
True :: Bool
ghci> :t "ПРИВЕТ!"
"ПРИВЕТ!" :: [Char]
ghci> :t (True, 'a')
(True, 'a') :: (Bool, Char)
ghci> :t 4 == 5
4 == 5 :: Bool
Мы видим, что :t
печатает выражения, за которыми следуют ::
и их тип. Символы ::
означают: «имеет тип». У явно указанных типов первый символ всегда в верхнем регистре. Символ 'a'
, как вы заметили, имеет тип Char
. Несложно сообразить, что это сокращение от «character» – символ. Константа True
имеет тип Bool
. Выглядит логично… Идём дальше.
Исследуя тип "ПРИВЕТ!"
, получим [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` ['А'..'Я']]
Функция removeNonUppercase
имеет тип [Char] –> [Char]
. Эта запись означает, что функция принимает одну строку в качестве параметра и возвращает другую в качестве результата.
А как записать тип функции, которая принимает несколько параметров? Вот, например, простая функция, принимающая три целых числа и складывающая их:
addThree :: Int –> Int –> Int –> Int
addThree x y z = x + y + z