Киты и Черепаха

Итак, проект создали, теперь мы готовы начать наше путешествие.

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, числу 39, и так далее. Схематично это можно записать так:

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 очень серьёзное, и работа с типами характеризуется тремя важными чертами:

  1. статическая проверка,
  2. сила,
  3. выведение.

Три эти свойства системы типов 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. Не делайте этого, во избежание путаницы.