# Глава 4. Неизменность и чистота
В предыдущей главе мы познакомились с функциями и выражениями, увидев близкую связь этих понятий. В этой главе мы познакомимся с функциями поближе, а также узнаем, что такое «чисто функциональный» язык и почему в нём нет места оператору присваивания.
## Объявляем и определяем
Применение функции нам уже знакомо, осталось узнать про объявление и определение, без них использовать функцию не получится. Помните функцию `square`, возводящую свой единственный аргумент в квадрат? Вот как выглядит её объявление и определение:
```haskell {.example_for_playground .example_for_playground_001}
square :: Int -> Int
square v = v * v
```
Первая строка содержит объявление, вторая — определение.
**Объявление** (англ. declaration) — это весть всему миру о том, что такая функция существует, вот её имя и вот типы, с которыми она работает.
**Определение** (англ. definition) — это весть о том, что конкретно делает данная функция.
Рассмотрим объявление:
```haskell
square :: Int -> Int
```
Оно разделено двойным двоеточием на две части: слева указано имя функции, справа — типы, с которыми эта функция работает, а именно типы аргументов и тип вычисленного, итогового значения. Как вы узнали из предыдущей главы, все данные в Haskell-программе имеют конкретный тип, а поскольку функция работает с данными, её объявление содержит типы этих данных. Типы разделены стрелками. Выглядит это так:
```haskell
square :: Int -> Int
```
Такое объявление сообщает нам о том, что функция `square` принимает единственный аргумент типа `Int` и возвращает значение того же типа `Int`. Если же аргументов более одного, объявление просто вытягивается. Например, объявление функции `prod`, возвращающей произведение двух целочисленных аргументов, могло бы выглядеть так:
```haskell
prod :: Int -> Int -> Int
```
Идею вы поняли: ищем крайнюю правую стрелку, и всё что левее от неё — то типы аргументов, а всё что правее — то тип вычисленного значения.
Мы не можем работать с функцией, которая ничего не вычисляет. То есть аналога C-функции `void f(int i)` в Haskell быть не может, так как это противоречит математической природе. Однако мы можем работать с функцией, которая ничего не принимает, то есть с аналогом C-функции `int f(void)`. С такими функциями мы познакомимся в следующих главах.
Теперь рассмотрим определение функции `square`:
```haskell
square v = v * v
```
Здесь `square` — имя функции `v` — имя аргумента, `v * v` — выражение.
А функция `prod` определена так:
```haskell
prod x y = x * y
```
Определение тоже разделено на две части: слева от знака равенства — имя функции и имена аргументов (имена, а не типы), разделённые пробелами. А справа от знака равенства — выражение, составляющее суть функции, её содержимое. Иногда эти части называют «головой» и «телом».
Вернемся к определению функции `square`: в нем `square v` до знака равенства — голова функции. А `v * v` после знака равенства — тело:
```haskell
square v = v * v
```
Обратите внимание, речь здесь идёт именно о **знаке равенства,** а никак не об операторе присваивания. Мы ничего не присваиваем, мы лишь декларируем равенство левой и правой частей. Когда мы пишем:
```haskell
prod x y = x * y
```
мы объявляем следующее: «Отныне выражение `prod x y` равно выражению `x * y`». Мы можем безопасно заменить выражение `prod 2 5` выражением `2 * 5`, а выражение `prod 120 500` — выражением `120 * 500`, и при этом работа программы гарантированно останется неизменной.
Но откуда у меня такая уверенность? А вот откуда. Haskell — чисто функциональный язык. Но прежде чем мы перейдем к обсуждению этого, остановимся на задаче.
Напишите функцию `triangleArea`, принимающую два аргумента типа `Double`: длину `b` основания треугольника и его высоту `h`. По ним функция должна вернуть значение типа `Double` — площадь треугольника. Формула простая: половина произведения основания на высоту. Например, `triangleArea` от `b=4` и `h=8` равен `16.0`. {.task_text}
```haskell {.task_source #haskell_chapter_0040_task_0010}
module Main where
-- Your code here
main :: IO ()
main = do
print (triangleArea 3 5)
print (triangleArea 1 6)
print (triangleArea 9 1)
print (triangleArea 4 8)
```
В объявлении функции укажите, что функция принимает два аргумента типа `Double` и возвращает значение типа `Double`. В определении используйте формулу `b * h / 2.0`. {.task_hint}
```haskell {.task_answer}
module Main where
triangleArea :: Double -> Double -> Double
triangleArea b h = b * h / 2.0
main :: IO ()
main = do
print (triangleArea 3 5)
print (triangleArea 1 6)
print (triangleArea 9 1)
print (triangleArea 4 8)
```
Напишите функцию `clamp`, которая принимает три аргумента типа `Int`: минимальное значение `a`, предпочитаемое значение `val` и максимальное `b`. Если `val` лежит внутри интервала от `a` до `b`, то функция возвращает `val` без изменений. Иначе — ближайшее к нему значение (`a` либо `b`). {.task_text}
Для реализации воспользуйтесь встроенными функциями `min` и `max`, принимающими два аргумента и возвращающими соответственно минимальный или максимальный из них. {.task_text}
```haskell {.task_source #haskell_chapter_0040_task_0020}
module Main where
-- Your code here
main :: IO ()
main = do
print (clamp 0 128 100)
print (clamp 0 (-3) 100)
print (clamp 0 41 100)
```
Последовательно примените `min` к `b` и результату применения `max` от `a` и `val`. Либо наоборот — `max` к `a` и результату применения `min` к `b` и `val`. Так выглядит применение функции `f` к аргументу 3 и результату применения функции `g` от 2-х аргументов: `f 3 (g 1 2)`. {.task_hint}
```haskell {.task_answer}
module Main where
clamp :: Int -> Int -> Int -> Int
clamp a val b = min (max a val) b
main :: IO ()
main = do
print (clamp 0 128 100)
print (clamp 0 (-3) 100)
print (clamp 0 41 100)
```
## Чисто функциональный
Haskell — чисто функциональный (англ. purely functional) язык. Чисто функциональным он называется потому, что центральное место в нём уделено чистой функции (англ. pure function). А чистой называется такая функция, которая предельно честна с нами: её выходное значение всецело определяется её аргументами и более ничем. Это и есть функция в математическом смысле. Вспомним функцию `prod`: когда на входе числа `10` и `20` — на выходе всегда будет `200`, и ничто не способно помешать этому. Функция `prod` является чистой, а потому характеризуется отсутствием побочных эффектов (англ. side effects): она не способна сделать ничего, кроме как вернуть произведение двух своих аргументов. Именно поэтому чистая функция предельно надёжна, ведь она не может преподнести нам никаких сюрпризов.
Скажу больше: чистые функции не видят окружающий мир. Вообще. Они не могут вывести текст на консоль, их нельзя заставить обработать HTTP-запрос, они не умеют дружить с базой данных и прочесть файл они также неспособны. Они суть вещь в себе.
А чтобы удивить вас ещё больше, открою ещё один секрет Haskell.
## Присваивание? Не, не слышал!
В мире Haskell нет места оператору присваивания. Впрочем, этот факт удивителен лишь на первый взгляд. Задумаемся: если каждая функция в конечном итоге представляет собою выражение, вычисляемое посредством применения каких-то других функций к каким-то другим аргументам, тогда нам просто не нужно ничего ничему присваивать.
Вспомним, что присваивание (англ. assignment) пришло к нам из императивных языков. Императивное программирование (англ. imperative programming) — это направление в разработке, объединяющее несколько парадигм программирования, одной из которых является знаменитая объектно-ориентированная парадигма. В рамках этого направления программа воспринимается как набор инструкций, выполнение которых неразрывно связано с изменением состояния (англ. state) этой программы. Вот почему в императивных языках обязательно присутствует понятие «переменная» (англ. variable). А раз есть переменные — должен быть и оператор присваивания. Когда мы пишем:
```c
coeff = 0.569;
```
мы тем самым приказываем: «Возьми значение `0.569` и перезапиши им то значение, которое уже содержалось в переменной `coeff` до этого». И перезаписывать это значение мы можем множество раз, а следовательно, мы вынуждены внимательно отслеживать текущее состояние переменной `coeff`, равно как и состояния всех остальных переменных в нашем коде.
Однако существует принципиально иной подход к разработке, а именно декларативное программирование (англ. declarative programming). Данное направление также включает в себя несколько парадигм, одной из которых является функциональная парадигма, нашедшая своё воплощение в Haskell. При этом подходе программа воспринимается уже не как набор инструкций, а как набор выражений. А поскольку выражения вычисляются путём применения функций к аргументам (то есть, по сути, к другим выражениям), там нет места ни переменным, ни оператору присваивания. Все данные в Haskell-программе, будучи созданными единожды, уже не могут быть изменены. Поэтому нам не нужен не только оператор присваивания, но и ключевое слово `const`. И когда в Haskell-коде мы пишем:
```haskell
coeff = 0.569
```
мы просто объявляем: «Отныне значение `coeff` равно `0.569`, и так оно будет всегда». Вот почему в Haskell-коде символ `=` — это знак равенства в математическом смысле, и с присваиванием он не имеет ничего общего.
Что выведет этот код? В случае ошибки напишите `error`. {.task_text}
```haskell {.example_for_playground}
module Main where
x = 2
x = 3
main :: IO ()
main = print x
```
```consoleoutput {.task_source #haskell_chapter_0040_task_0030}
```
Единожды присвоив `x` значение 2, мы не можем изменить его и сказать, что теперь в `x` хранится 3. Такой код не скомпилируется. {.task_hint}
```haskell {.task_answer}
error
```
Уверен, вы удивлены. Как же можно написать реальную программу на языке, в котором нельзя изменять данные? Какой прок от этих чистых функций, если они не способны ни файл прочесть, ни запрос по сети отправить? Оказывается, прок есть, и на Haskell можно написать очень даже реальную программу. За примером далеко ходить не буду: сама эта книга построена с помощью программы, написанной на Haskell, о чём я подробнее расскажу в следующих главах.
Напишите функцию `eqAbs`, проверяющую два числа с плавающей точкой `a` и `b` на равенство c точностью до `eps` включительно. В теле `eqAbs` воспользуйтесь встроенной функцией `abs`, возвращающей модуль разности двух чисел. {.task_text}
Например, `eqAbs 37.001 37.002 0.1` вернет `True`, а `eqAbs 37.001 37.002 1e-5` вернет `False`. {.task_text}
```haskell {.task_source #haskell_chapter_0040_task_0040}
module Main where
-- Your code here
main :: IO ()
main = do
print (eqAbs 10010.0 10020.0 1.0e+1)
print (eqAbs 10.0 10.002 1e-1)
print (eqAbs 5.01 5.02 1e-4)
print (eqAbs 37.06 37.059 0.1)
print (eqAbs 37.6 37.59 0.0001)
```
Модуль разности чисел `a` и `b` должен быть меньше или равен `eps`. {.task_hint}
```haskell {.task_answer}
module Main where
eqAbs :: Double -> Double -> Double -> Bool
eqAbs a b eps = abs (a - b) <= eps
main :: IO ()
main = do
print (eqAbs 10010.0 10020.0 1.0e+1)
print (eqAbs 10.0 10.002 1e-1)
print (eqAbs 5.01 5.02 1e-4)
print (eqAbs 37.06 37.059 0.1)
print (eqAbs 37.6 37.59 0.0001)
```
А теперь, дабы не мучить вас вопросами без ответов, мы начнём ближе знакомиться с Китами Haskell, и детали большой головоломки постепенно сложатся в красивую картину.
## Для любопытных
В процессе работы Haskell-программы в памяти создаётся великое множество различных данных, ведь мы постоянно строим новые данные на основе уже имеющихся. За их своевременное уничтожение отвечает сборщик мусора (англ. garbage collector, GC), встраиваемый в программы компилятором GHC.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!