denisshevchenko/ohaskell.guide

View on GitHub
chapters/05-whales-n-turtle.md

Summary

Maintainability
Test Coverage
# Киты и Черепаха

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

Haskell стоит на Трёх Китах, имена которым: **Функция**, **Тип** и **Класс типов**. Они же, в свою очередь, покоятся на огромной Черепахе, имя которой — **Выражение**.

## Черепаха

Haskell-программа представляет собой совокупность выражений (англ. expression). Взгляните:

```haskell
1 + 2
```

Это — основной кирпич Haskell-программы, будь то Hello World или часть инфраструктуры международного банка. Конечно, помимо сложения единицы с двойкой существуют и другие выражения, но суть у них у всех одна:

> Выражение — это то, что может дать нам некий полезный результат.

Полезный результат мы получаем в результате вычисления (англ. evaluation) выражения. Все выражения можно вычислить, однако одни выражения в результате вычисления уменьшаются (англ. reduce), а другие — нет. Первые иногда называют редуцируемыми выражениями, а вторые — нередуцируемые. Так, выражение:

```haskell
1 + 2
```

относится к редуцируемым, потому что оно в результате вычисления уменьшится и даст нам другое выражение:

```haskell
3
```

Это выражение уже нельзя уменьшить, оно нередуцируемое и мы теперь лишь можем использовать его как есть.

Таким образом, выражения, составляющие программу, вычисляются/редуцируются до тех пор, пока не останется некое окончательное, корневое выражение. А запуск Haskell-программы на выполнение (англ. execution) — это запуск всей этой цепочки вычислений, причём с корнем этой цепочки мы уже познакомились ранее. Помните функцию `main`, определённую в модуле `app/Main.hs`? Вот эта функция и является главной точкой нашей программы, её Альфой и Омегой.

## Первый Кит

Вернёмся к выражению `1 + 2`. Полезный результат мы получим лишь после того, как вычислим это выражение, то есть осуществим сложение. И как же можно «осуществить сложение» в рамках Haskell-программы? С помощью функции. Именно функция делает выражение вычислимым, именно она оживляет нашу программу, потому я и назвал Функцию Первым Китом Haskell. Но дабы избежать недоразумений, определимся с понятиями.

Что такое функция в математике? Вспомним школьный курс:

> Функция — это закон, описывающий зависимость одного значения от другого.

Рассмотрим функцию возведения целого числа в квадрат:

```haskell
square v = v * v
```

Функция `square` определяет простую зависимость: числу `2` соответствует число `4`, числу `3` — `9`, и так далее. Схематично это можно записать так:

```haskell
2 -> 4
3 -> 9
4 -> 16
5 -> 25
...
```

Входное значение функции называют аргументом. А так как функция определяет однозначную зависимость выходного значения от аргумента, её, функцию, называют ещё *отображением*: она отображает/проецирует входное значение на выходное. Получается как бы труба: кинули в неё `2` — с другой стороны вылетело `4`, кинули `5` — вылетело `25`.

Чтобы заставить функцию сделать полезную работу, её необходимо применить (англ. apply) к аргументу. Пример:

```haskell
square 2
```

Мы применили функцию `square` к аргументу `2`. Синтаксис предельно прост: имя функции и через пробел аргумент. Если аргументов более одного — просто дописываем их так же, через пробел. Например, функция `sum`, вычисляющая сумму двух своих целочисленных аргументов, применяется так:

```haskell
sum 10 20
```

Так вот выражение `1 + 2` есть ни что иное, как применение функции! И чтобы яснее это увидеть, перепишем выражение:

```haskell
(+) 1 2
```

Это применение функции `(+)` к двум аргументам, `1` и `2`. Не удивляйтесь, что имя функции заключено в скобки, вскоре я расскажу об этом подробнее. А пока запомните главное:

> Вычислить выражение — это значит применить какие-то функции (одну или более) к каким-то аргументам (одному или более).

И ещё. Возможно, вы слышали о так называемом «вызове» функции. В Haskell функции не вызывают. Понятие «вызов» функции пришло к нам из почтенного языка C. Там функции действительно вызывают (англ. call), потому что в C, в отличие от Haskell, понятие «функция» не имеет никакого отношения к математике. Там это подпрограмма, то есть обособленный кусочек программы, доступный по некоторому адресу в памяти. Если у вас есть опыт разработки на C-подобных языках — забудьте о подпрограмме. В Haskell функция — это функция в математическом смысле слова, поэтому её не вызывают, а применяют к чему-то.

## Второй Кит

Итак, любое редуцируемое выражение суть применение функции к некоторому аргументу (тоже являющемуся выражением):

```haskell
square   2
функция  аргумент
```

Аргумент представляет собой некоторое значение, его ещё называют «данное» (англ. data). Данные в Haskell — это сущности, обладающие двумя главными характеристиками: типом и конкретным значением/содержимым.

Тип — это Второй Кит в Haskell. Тип отражает конкретное содержимое данных, а потому все данные в программе обязательно имеют некий тип. Когда мы видим данное типа `Double`, мы точно знаем, что перед нами число с плавающей точкой, а когда видим данные типа `String` — можем ручаться, что перед нами строки.

Отношение к типам в Haskell очень серьёзное, и работа с типами характеризуется тремя важными чертами:

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

Три эти свойства системы типов Haskell — наши добрые друзья, ведь они делают нашу программистскую жизнь счастливее. Познакомимся с ними.

### Статическая проверка

Статическая проверка типов (англ. static type checking) — это проверка типов всех данных в программе, осуществляемая на этапе компиляции. Haskell-компилятор упрям: когда ему что-либо не нравится в типах, он громко ругается. Поэтому если функция работает с целыми числами, применить её к строкам никак не получится. Так что если компиляция нашей программы завершилась успешно, мы точно знаем, что с типами у нас всё в порядке. Преимущества статической проверки невозможно переоценить, ведь она гарантирует отсутствие в наших программах целого ряда ошибок. Мы уже не сможем спутать числа со строками или вычесть метры из рублей.

Конечно, у этой медали есть и обратная сторона — время, затрачиваемое на компиляцию. Вам придётся свыкнуться с этой мыслью: внесли изменения в проект — будьте добры скомпилировать. Однако утешением вам пусть послужит тот факт, что преимущества статической проверки куда ценнее времени, потраченного на компиляцию.

### Сила

Сильная (англ. strong) система типов — это бескомпромиссный контроль соответствия ожидаемого действительному. Сила делает работу с типами ещё более аккуратной. Вот вам пример из мира C:

```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 тип данных следует указывать явно:

```c
double value = 122.04;
```

однако в Haskell мы напишем просто:

```haskell
value = 122.04
```

В этом случае компилятор автоматически выведет тип `value` как `Double`.

Выведение типов делает наш код лаконичнее и проще в сопровождении. Впрочем, мы можем указать тип значения и явно, а иногда даже должны это сделать. В последующих главах я объясню, почему.

Да, кстати, вот простейшие стандартные типы, они нам понадобятся:

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