Итак, проект создали, теперь мы готовы начать наше путешествие.
Haskell стоит на Трёх Китах, имена которым: Функция, Тип и Класс типов. Они же, в свою очередь, покоятся на огромной Черепахе, имя которой — Выражение.
Haskell-программа представляет собой совокупность выражений (англ. expression). Взгляните:
1 + 2
Это — основной кирпич Haskell-программы, будь то Hello World или часть инфраструктуры международного банка. Конечно, помимо сложения единицы с двойкой существуют и другие выражения, но суть у них у всех одна:
Выражение — это то, что может дать нам некий полезный результат.
Полезный результат мы получаем в результате вычисления (англ. evaluation) выражения. Все выражения можно вычислить, однако одни выражения в результате вычисления уменьшаются (англ. reduce), а другие — нет. Первые иногда называют редуцируемыми выражениями, а вторые — нередуцируемые. Так, выражение:
1 + 2
относится к редуцируемым, потому что оно в результате вычисления уменьшится и даст нам другое выражение:
3
Это выражение уже нельзя уменьшить, оно нередуцируемое и мы теперь лишь можем использовать его как есть.
Таким образом, выражения, составляющие программу, вычисляются/редуцируются до тех пор, пока не останется некое окончательное, корневое выражение. А запуск Haskell-программы на выполнение (англ. execution) — это запуск всей этой цепочки вычислений, причём с корнем этой цепочки мы уже познакомились ранее. Помните функцию main
, определённую в модуле app/Main.hs
? Вот эта функция и является главной точкой нашей программы, её Альфой и Омегой.
Вернёмся к выражению 1 + 2
. Полезный результат мы получим лишь после того, как вычислим это выражение, то есть осуществим сложение. И как же можно «осуществить сложение» в рамках Haskell-программы? С помощью функции. Именно функция делает выражение вычислимым, именно она оживляет нашу программу, потому я и назвал Функцию Первым Китом Haskell. Но дабы избежать недоразумений, определимся с понятиями.
Что такое функция в математике? Вспомним школьный курс:
Функция — это закон, описывающий зависимость одного значения от другого.
Рассмотрим функцию возведения целого числа в квадрат:
square v = v * v
Функция square
определяет простую зависимость: числу 2
соответствует число 4
, числу 3
— 9
, и так далее. Схематично это можно записать так:
2 -> 4
3 -> 9
4 -> 16
5 -> 25
...
Входное значение функции называют аргументом. А так как функция определяет однозначную зависимость выходного значения от аргумента, её, функцию, называют ещё отображением: она отображает/проецирует входное значение на выходное. Получается как бы труба: кинули в неё 2
— с другой стороны вылетело 4
, кинули 5
— вылетело 25
.
Чтобы заставить функцию сделать полезную работу, её необходимо применить (англ. apply) к аргументу. Пример:
square 2
Мы применили функцию square
к аргументу 2
. Синтаксис предельно прост: имя функции и через пробел аргумент. Если аргументов более одного — просто дописываем их так же, через пробел. Например, функция sum
, вычисляющая сумму двух своих целочисленных аргументов, применяется так:
sum 10 20
Так вот выражение 1 + 2
есть ни что иное, как применение функции! И чтобы яснее это увидеть, перепишем выражение:
(+) 1 2
Это применение функции (+)
к двум аргументам, 1
и 2
. Не удивляйтесь, что имя функции заключено в скобки, вскоре я расскажу об этом подробнее. А пока запомните главное:
Вычислить выражение — это значит применить какие-то функции (одну или более) к каким-то аргументам (одному или более).
И ещё. Возможно, вы слышали о так называемом «вызове» функции. В Haskell функции не вызывают. Понятие «вызов» функции пришло к нам из почтенного языка C. Там функции действительно вызывают (англ. call), потому что в C, в отличие от Haskell, понятие «функция» не имеет никакого отношения к математике. Там это подпрограмма, то есть обособленный кусочек программы, доступный по некоторому адресу в памяти. Если у вас есть опыт разработки на C-подобных языках — забудьте о подпрограмме. В Haskell функция — это функция в математическом смысле слова, поэтому её не вызывают, а применяют к чему-то.
Итак, любое редуцируемое выражение суть применение функции к некоторому аргументу (тоже являющемуся выражением):
square 2
функция аргумент
Аргумент представляет собой некоторое значение, его ещё называют «данное» (англ. data). Данные в Haskell — это сущности, обладающие двумя главными характеристиками: типом и конкретным значением/содержимым.
Тип — это Второй Кит в Haskell. Тип отражает конкретное содержимое данных, а потому все данные в программе обязательно имеют некий тип. Когда мы видим данное типа Double
, мы точно знаем, что перед нами число с плавающей точкой, а когда видим данные типа String
— можем ручаться, что перед нами строки.
Отношение к типам в Haskell очень серьёзное, и работа с типами характеризуется тремя важными чертами:
Три эти свойства системы типов Haskell — наши добрые друзья, ведь они делают нашу программистскую жизнь счастливее. Познакомимся с ними.
Статическая проверка типов (англ. static type checking) — это проверка типов всех данных в программе, осуществляемая на этапе компиляции. Haskell-компилятор упрям: когда ему что-либо не нравится в типах, он громко ругается. Поэтому если функция работает с целыми числами, применить её к строкам никак не получится. Так что если компиляция нашей программы завершилась успешно, мы точно знаем, что с типами у нас всё в порядке. Преимущества статической проверки невозможно переоценить, ведь она гарантирует отсутствие в наших программах целого ряда ошибок. Мы уже не сможем спутать числа со строками или вычесть метры из рублей.
Конечно, у этой медали есть и обратная сторона — время, затрачиваемое на компиляцию. Вам придётся свыкнуться с этой мыслью: внесли изменения в проект — будьте добры скомпилировать. Однако утешением вам пусть послужит тот факт, что преимущества статической проверки куда ценнее времени, потраченного на компиляцию.
Сильная (англ. strong) система типов — это бескомпромиссный контроль соответствия ожидаемого действительному. Сила делает работу с типами ещё более аккуратной. Вот вам пример из мира C:
double coeff(double base) {
return base * 4.9856;
}
int main() {
int value = coeff(122.04);
...
}
Это канонический пример проблемы, обусловленной слабой (англ. weak) системой типов. Функция coeff
возвращает значение типа double
, однако вызывающая сторона ожидает почему-то целое число. Ну вот ошиблись мы, криво скопировали. В этом случае произойдёт жульничество, называемое скрытым приведением типов (англ. implicit type casting): число с плавающей точкой, возвращённое функцией coeff
, будет грубо сломано путём приведения его к типу int
, в результате чего дробная часть будет отброшена и мы получим не 608.4426
, а 608
. Подобная ошибка, кстати, приводила к серьёзным последствиям, таким как уничтожение космических аппаратов. Нет, это вовсе не означает, что слабая типизация ужасна сама по себе, просто есть иной путь.
Благодаря сильной типизации в Haskell подобный код не имеет ни малейших шансов пройти компиляцию. Мы всегда получаем то, что ожидаем, и если должно быть число с плавающей точкой — расшибись, но предоставь именно его. Компилятор скрупулёзно отслеживает соответствие ожидаемого типа фактическому, поэтому когда компиляция завершается успешно, мы абсолютно уверены в гармонии между типами всех наших данных.
Выведение (англ. inference) типов — это способность определить тип данных автоматически, по конкретному выражению. В том же языке C тип данных следует указывать явно:
double value = 122.04;
однако в Haskell мы напишем просто:
value = 122.04
В этом случае компилятор автоматически выведет тип value
как Double
.
Выведение типов делает наш код лаконичнее и проще в сопровождении. Впрочем, мы можем указать тип значения и явно, а иногда даже должны это сделать. В последующих главах я объясню, почему.
Да, кстати, вот простейшие стандартные типы, они нам понадобятся:
123 Int
23.5798 Double
'a' Char
"Hello!" String
True Bool, истина
False Bool, ложь
С типами Int
и Double
вы уже знакомы. Тип Char
— это Unicode-символ. Тип String
— строка, состоящая из Unicode-символов. Тип Bool
— логический тип, соответствующий истине или лжи. В последующих главах мы встретимся ещё с несколькими стандартными типами, но пока хватит и этих. И заметьте: имя типа в Haskell всегда начинается с большой буквы.
А вот о Третьем Ките, о Классе типов, я пока умолчу, потому что знакомиться с ним следует лишь после того, как мы поближе подружимся с первыми двумя.
Уверен, после прочтения этой главы у вас появилось множество вопросов. Ответы будут, но позже. Более того, следующая глава несомненно удивит вас.
Если вы работали с объектно-ориентированными языками, такими как C++, вас удивит тот факт, что в Haskell между понятиями «тип» и «класс» проведено чёткое различие. А поскольку типам и классам типов в Haskell отведена колоссально важная роль, добрый вам совет: когда в будущих главах мы познакомимся с ними поближе, не пытайтесь проводить аналогии из других языков. Например, некоторые усматривают родство между классами типов в Haskell и интерфейсами в Java. Не делайте этого, во избежание путаницы.