# Глава 3. Киты и черепаха 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. Но дабы избежать недоразумений, определимся с понятиями. Что такое функция в математике? Вспомним школьный курс. **Функция** — это закон, описывающий зависимость одного значения от другого. Рассмотрим функцию возведения целого числа в квадрат: {#block-square} ```haskell square v = v * v ``` Функция square определяет простую зависимость: числу 2 соответствует число 4, числу 3 — 9, и так далее. Схематично это можно записать так: ``` 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 3 / (3 - 1) ``` будет 1.5. Второй пример: в Haskell есть встроенные функции `min` и `max`. Каждая из них принимает два аргумента и возвращает соответственно наименьшее или наибольшее из них значение. И вот такое выражение ```haskell min (max 2 3) 4 ``` означает следующее: вычислить `max` от 2 и 3, а затем `min` от полученного значения и числа 4. Вычислите это выражение. {.task_text} ```haskell (*) 2 (6 - 1) ``` ```consoleoutput {.task_source #haskell_chapter_0030_task_0010} ``` Это применение функции `(*)` к двум аргументам, 2 и (6 - 1). Его можно переписать как `2 * (6 - 1)`. Что равносильно `2 * 5`, то есть 10. {.task_hint} ```haskell {.task_answer} 10 ``` И ещё. Возможно, вы слышали о так называемом «вызове» функции. В Haskell функции не вызывают. Понятие «вызов» функции пришло к нам из почтенного языка C. Там функции действительно вызывают (англ. call), потому что в C, в отличие от Haskell, понятие «функция» не имеет никакого отношения к математике. Там это подпрограмма, то есть обособленный кусочек программы, доступный по некоторому адресу в памяти. Если у вас есть опыт разработки на C-подобных языках — забудьте о подпрограмме. В Haskell функция — это функция в математическом смысле слова, поэтому её не вызывают, а **применяют** к чему-то. Создайте модуль `Main`, функция `main` которого выводит в консоль число 128. {.task_text} Как вы помните, для вывода в консоль в Haskell есть встроенная функция `putStrLn`. Но она работает только со строками. Поэтому во избежание ошибки компиляции примените `putStrLn` не к самому числу, а к результату применения `show`. {.task_text} Встроенная функция `show` принимает аргумент и конвертирует его в строку. {.task_text} ```haskell {.task_source #haskell_chapter_0030_task_0020} ``` Применение функции `f` к результату, возвращаемому функцией `g` от аргумента `x`: `f (g x)`. {.task_hint} ```haskell {.task_answer} module Main where main :: IO () main = putStrLn (show 128) ``` В предыдущей задаче возникла необходимость вместо строки печатать произвольный объект. Для этого мы воспользовались цепочкой применений `putStrLn (show 128)`. Для переиспользования кода руки чешутся обернуть это в отдельную функцию для печати в консоль произвольного объекта. Но не спешите! В Haskell есть встроенная функция, делающая именно это. Называется она `print`. {.task_text} Замените цепочку из двух функций `show` и `putStrLn` на `print`. {.task_text} ```haskell {.task_source #haskell_chapter_0030_task_0030} module Main where main :: IO () main = putStrLn (show 128) ``` В теле функции `main` требуется применить функцию `print` к аргументу 128. {.task_hint} ```haskell {.task_answer} module Main where main :: IO () main = print 128 ``` Внимательный читатель спросит, каким же образом использованная в задаче функция `print` узнаёт, как именно отобразить конкретное значение в виде строки? О, это интереснейшая тема, но она относится к Третьему Киту Haskell, до подробного знакомства с которым нам ещё далеко. ## Второй Кит Итак, любое редуцируемое выражение суть применение функции к некоторому аргументу (тоже являющемуся выражением): ```haskell square 2 ``` Здесь `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`. Выведение типов делает наш код лаконичнее и проще в сопровождении. Впрочем, мы можем указать тип значения и явно, а иногда даже должны это сделать. В последующих главах я объясню, почему. Да, кстати, вот простейшие стандартные типы, они нам понадобятся: ``` 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. Не делайте этого, во избежание путаницы.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!