Операции, функции и специальные формы в императивных языках программирования

786f8601a19c82d74032b44dc217113d

В этой статье мы разъясним довольно тонкий семантический вопрос, который часто остаётся за кадром при изучении программирования на императивных языках.

Если читатель владеет одним из языков семейства Лисп (Common Lisp, Scheme, Clojure и т.д.), а в особенности если читал SICP, то ему излагаемый вопрос не в новинку, и он может пропустить эту статью. Если читатель использует Хаскель или другой язык, основанный на модели ленивых вычислений, то там всё немножко по-другому, и впрямую изложение материала в данной статье к таким языкам не относится, хотя фундаментальные принципы в основе лежат те же.

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

Операции

Большинство императивных языков программирования использует синтаксис выражений, приближенный к математическим формулам, где, помимо собственно вызовов функций, используются операции (operators) со специальным синтаксисом, обычно одноместные префиксные или двуместные инфиксные, например, ~x, *p или 5+2. Без какой-либо потери общности эти операции можно было бы (а в языке Лисп так и сделано) представить в обычной функциональной форме вызова, например, not(x), target(p) или plus(5,2). Поэтому, говоря об операциях, мы фактически с тем же успехом можем говорить о функциях или, по крайней мере, о чём-то синтаксически подобном. Далее мы можем специально не выделять операции среди прочих функциональных объектов.

Функции

На применении функций (включая операции) фактически построено всё императивное программирование. В объектно-ориентированном программировании входящие с состав класса функции называют методами, но фактически это те же самые функции.

Для нас сейчас в механизме применения функций важен механизм передачи параметров.

Подавляющее большинство современных императивных языков программирования использует в конечном итоге два способа передачи параметров — по значению и по ссылке. (Этими способами природа и человеческая мысль не ограничиваются, и, например, в языке Алгол использовался выпадающий из нашего хода рассуждений способ передачи параметров по имени, но впоследствии решили, что тех же самых результатов гораздо красивее можно добиться передачей по значению некоторого лямбда-выражения).

Передача параметров как по значению, так и по ссылке заключается в том, что сначала вычисляется значение фактического параметра, а затем само это значение или ссылка на него передаётся внутрь функции для использования в качестве формального параметра. (Не все языки, поддерживающие передачу параметров по ссылке, поддерживают использование выражения для ссылки, но это для нас не важно).

Для строгости изложения заметим, что и сама вызываемая функция тоже по существу является параметром своего вызова, используемым по значению — например, конкретная функция может вычисляться через обращение к массиву функций.

Таким образом, способ вызова функции такой: вычисляем вызываемую функцию, вычисляем значения всех её фактических параметров, вызываем функцию с параметрами. (В большинстве императивных языков точный порядок вычисления параметров и самой функции не определён).

Например:

*func (x+3, ++i);

Здесь мы вычисляем тройку значений выражений *func, x+3, ++i (поскольку это язык Си, то вычисления происходят не обязательно в таком же порядке) и после этого вызываем функцию, на которую указывает func, с переданными ей вычисленными значениями x+3 и ++i.

x+y

Здесь мы определяемся со значением операции +, вычисляем значение x, вычисляем значение y, после чего применяем операцию + к вычисленным значениям x и y.

Специальные формы

Специальной формой принято называть конструкцию, которая синтаксически выглядит, как вызов функции (в том числе операция), но семантика вычисления её параметров отличается от установленной для функций.

В большинстве императивных языков реализовываются три специальные формы: логическое И, логическое ИЛИ и условная операция.

В операции x && y значение параметра y вычисляется только в том случае, если значение параметра x является истинным.

В операции x || y значение параметра y вычисляется только в том случае, если значение параметра x является ложным.

В операции x ? y : z вычисляется только один из параметров y или z в зависимости от истинности параметра x.

Что произойдёт, если мы попробуем заменить операцию && в языке Си на написанную нами функцию And?

bool And (bool x, bool y) {
  return (x&&y)
}

Такая замена семантически не эквивалентна. Например, мы имеем право написать:

if ((i >= 0) && (array[i] > 0)) ...

но не имеем право заменить такую конструкцию на:

if (And (i >= 0, array[i] > 0)) ...

В первом случае, если i < 0, то обращения к массиву не произойдёт, так как второй параметр операции && вычисляется только в случае истинности первого.

Во втором случае, если i < 0, то второй параметр функции And всё равно будет вычисляться, исходя из общего порядка вызова функции, и произойдёт выход за границы массива при этом вычислении.

По той же самой причине функции вроде coalesce не являются полноценной заменой условной операции, так как не представляют собой специальные формы.

Обычно новую, изобретённую программистом, специальную форму, в отличие от функции, невозможно добавить в программу средствами языка.

© Habrahabr.ru