Главная / Курсы / О Haskell по-человечески / Неизменность и чистота
# Глава 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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!