В предыдущей главе мы познакомились с функциями и выражениями, увидев близкую связь этих понятий. В этой главе мы познакомимся с функциями поближе, а также узнаем, что такое «чисто функциональный» язык и почему в нём нет места оператору присваивания.
Применение функции нам уже знакомо, осталось узнать про объявление и определение, без них использовать функцию не получится. Помните функцию square
, возводящую свой единственный аргумент в квадрат? Вот как выглядит её объявление и определение:
square :: Int -> Int
square v = v * v
Первая строка содержит объявление, вторая — определение. Объявление (англ. declaration) — это весть всему миру о том, что такая функция существует, вот её имя и вот типы, с которыми она работает. Определение (англ. definition) — это весть о том, что конкретно делает данная функция.
Рассмотрим объявление:
square :: Int -> Int
Оно разделено двойным двоеточием на две части: слева указано имя функции, справа — типы, с которыми эта функция работает, а именно типы аргументов и тип вычисленного, итогового значения. Как вы узнали из предыдущей главы, все данные в Haskell-программе имеют конкретный тип, а поскольку функция работает с данными, её объявление содержит типы этих данных. Типы разделены стрелками. Схематично это выглядит так:
square :: Int -> Int
имя тип тип
функции аргумента вычисленного
значения
Такое объявление сообщает нам о том, что функция square
принимает единственный аргумент типа Int
и возвращает значение того же типа Int
. Если же аргументов более одного, объявление просто вытягивается. Например, объявление функции prod
, возвращающей произведение двух целочисленных аргументов, могло бы выглядеть так:
prod :: Int -> Int -> Int
имя тип тип тип
функции первого второго вычисленного
аргумента аргумента значения
Идею вы поняли: ищем крайнюю правую стрелку, и всё что левее от неё — то типы аргументов, а всё что правее — то тип вычисленного значения.
Мы не можем работать с функцией, которая ничего не вычисляет. То есть аналога C-функции void f(int i)
в Haskell быть не может, так как это противоречит математической природе. Однако мы можем работать с функцией, которая ничего не принимает, то есть с аналогом C-функции int f(void)
. С такими функциями мы познакомимся в следующих главах.
Теперь рассмотрим определение функции square
:
square v = v * v
Схема определения такова:
square v = v * v
имя имя это выражение
функции аргумента
А функция prod
определена так:
prod x y = x * y
имя имя имя это выражение
функции первого второго
аргумента аргумента
Определение тоже разделено на две части: слева от знака равенства — имя функции и имена аргументов (имена, а не типы), разделённые пробелами, а справа — выражение, составляющее суть функции, её содержимое. Иногда эти части называют «головой» и «телом»:
square v = v * v
голова функции тело функции
(англ. head) (англ. body)
Обратите внимание, речь здесь идёт именно о знаке равенства, а никак не об операторе присваивания. Мы ничего не присваиваем, мы лишь декларируем равенство левой и правой частей. Когда мы пишем:
prod x y = x * y
мы объявляем следующее: «Отныне выражение prod x y
равно выражению x * y
». Мы можем безопасно заменить выражение prod 2 5
выражением 2 * 5
, а выражение prod 120 500
— выражением 120 * 500
, и при этом работа программы гарантированно останется неизменной.
Но откуда у меня такая уверенность? А вот откуда.
Haskell — чисто функциональный (англ. purely functional) язык. Чисто функциональным он называется потому, что центральное место в нём уделено чистой функции (англ. pure function). А чистой называется такая функция, которая предельно честна с нами: её выходное значение всецело определяется её аргументами и более ничем. Это и есть функция в математическом смысле. Вспомним функцию prod
: когда на входе числа 10
и 20
— на выходе всегда будет 200
, и ничто не способно помешать этому. Функция prod
является чистой, а потому характеризуется отсутствием побочных эффектов (англ. side effects): она не способна сделать ничего, кроме как вернуть произведение двух своих аргументов. Именно поэтому чистая функция предельно надёжна, ведь она не может преподнести нам никаких сюрпризов.
Скажу больше: чистые функции не видят окружающий мир. Вообще. Они не могут вывести текст на консоль, их нельзя заставить обработать HTTP-запрос, они не умеют дружить с базой данных и прочесть файл они также неспособны. Они суть вещь в себе.
А чтобы удивить вас ещё больше, открою ещё один секрет Haskell.
В мире Haskell нет места оператору присваивания. Впрочем, этот факт удивителен лишь на первый взгляд. Задумаемся: если каждая функция в конечном итоге представляет собою выражение, вычисляемое посредством применения каких-то других функций к каким-то другим аргументам, тогда нам просто не нужно ничего ничему присваивать.
Вспомним, что присваивание (англ. assignment) пришло к нам из императивных языков. Императивное программирование (англ. imperative programming) — это направление в разработке, объединяющее несколько парадигм программирования, одной из которых является знаменитая объектно-ориентированная парадигма. В рамках этого направления программа воспринимается как набор инструкций, выполнение которых неразрывно связано с изменением состояния (англ. state) этой программы. Вот почему в императивных языках обязательно присутствует понятие «переменная» (англ. variable). А раз есть переменные — должен быть и оператор присваивания. Когда мы пишем:
coeff = 0.569;
мы тем самым приказываем: «Возьми значение 0.569
и перезапиши им то значение, которое уже содержалось в переменной coeff
до этого». И перезаписывать это значение мы можем множество раз, а следовательно, мы вынуждены внимательно отслеживать текущее состояние переменной coeff
, равно как и состояния всех остальных переменных в нашем коде.
Однако существует принципиально иной подход к разработке, а именно декларативное программирование (англ. declarative programming). Данное направление также включает в себя несколько парадигм, одной из которых является функциональная парадигма, нашедшая своё воплощение в Haskell. При этом подходе программа воспринимается уже не как набор инструкций, а как набор выражений. А поскольку выражения вычисляются путём применения функций к аргументам (то есть, по сути, к другим выражениям), там нет места ни переменным, ни оператору присваивания. Все данные в Haskell-программе, будучи созданными единожды, уже не могут быть изменены. Поэтому нам не нужен не только оператор присваивания, но и ключевое слово const
. И когда в Haskell-коде мы пишем:
coeff = 0.569
мы просто объявляем: «Отныне значение coeff
равно 0.569
, и так оно будет всегда». Вот почему в Haskell-коде символ =
— это знак равенства в математическом смысле, и с присваиванием он не имеет ничего общего.
Уверен, вы удивлены. Как же можно написать реальную программу на языке, в котором нельзя изменять данные? Какой прок от этих чистых функций, если они не способны ни файл прочесть, ни запрос по сети отправить? Оказывается, прок есть, и на Haskell можно написать очень даже реальную программу. За примером далеко ходить не буду: сама эта книга построена с помощью программы, написанной на Haskell, о чём я подробнее расскажу в следующих главах.
А теперь, дабы не мучить вас вопросами без ответов, мы начнём ближе знакомиться с Китами Haskell, и детали большой головоломки постепенно сложатся в красивую картину.
В процессе работы Haskell-программы в памяти создаётся великое множество различных данных, ведь мы постоянно строим новые данные на основе уже имеющихся. За их своевременное уничтожение отвечает сборщик мусора (англ. garbage collector, GC), встраиваемый в программы компилятором GHC.