Об одном терминологическом заблуждении

Несколько лет назад я проходил собеседование в одной фирмочке, занимающейся разработкой софта. «Собеседование» — громко сказано, заключалось оно в рассказе потенциального работодателя о прелестях их фирмы, озвучивании предложения по зарплате (19 тыр на испытательный срок, затем 25) и нескольких вопросах ко мне. Вопросы были такие: чем отличается функция от процедуры, как я себе представляю процесс разработки ПО и какую последнюю книгу по программированию я читал.

Видимо, мои ответы оказались «хорошими», потому что мне позвонили на следующей неделе и были согласны на 25 тысяч сразу. Я все равно отказался, уж слишком неудобно располагался потенциальный работодатель — на диметрально противоположном конце Москвы. Если вторые два вопроса малоинтересны (я описал что-то типа «водопада», а на вопрос о книжке упомянул Кормена — такую фамилию не знал сам «собеседователь»), то вопрос о разнице между функцией и процедурой кажется более интересным.

Разумеется, «отличник», заучивший определение, должен ответить что-то типа «В Бейсике и Паскале функция возвращает значение, а процедура не возвращает, в Си различия между функциями и процедурами нет, функция, которая не возвращает значение, имеет специальный тип void». Тем более удивительно, что я так и ответил, даже зная наизусть то определение функции, которое на мехмате давал А. Г. Кушниренко (в курсе «Программирование и работа на ЭВМ», как это не странно). Почему-то функция в математике и функция в программировании были для меня разнородными понятиями, просто вторая была отдаленно похожа на первую.

Вообще, история понятия функции довольно интересна. Впервые его ввел еще Лейбниц, понимая под функцией «нечто», задаваемое формулой. Функции отождествлялись с графиками, затем к середине XIX века появилось более-менее современное определение числовых функций — как правила, сопоставляющего одному числу (аргументу) другое (значение). Кроме того, это понятие обобщалось на случай функций многих переменных, вектор-функций, многозначных функций и прочих подобных понятий. Наконец, в самом начале XX века было дано современное определение функции — как тройки из множества определения X, множества значений Y и подмножества f из их декартова произведения X*Y, такого, что для любого x из X существует единственный элемент <x, y> из f, где y — некоторый элемент Y. Это определение начисто выносит мозг первокурсникам, пришедшим на первую лекцию по математическому анализу — ведь многие искренне считают, что функция — это правило, по которому одному числу ставится в соответствие другое — так написано в старых школьных учебниках для шестого класса. В более новых учебниках приведено в смягченной форме мозгоразрывающее определение — там X и Y — обязательно числовые множества, а вместо «подмножества декартова произведения» говорится просто — «правило, ставящее в соответствие».

Кстати, нельзя начинать определение функции с «правила». Правило, то есть подмножество декартова произведения, стоит лишь на третьем месте. Для начала надо определить множества, где функция определена, и какие значения она принимает.

Так вот, мозгосносящее определение адски сильно — так как X и Y могут быть чем угодно. Например, пусть X — это множество текстовых строк, а Y — множество неотрицательных 32-битных чисел. Тогда strlen прекрасно описывается в этих терминах. Декартовым произведением X и Y будет множество пар из текстовой строки и числа, а «выделить» их следует так, чтобы текстовой строке соответствовала ее длина. Можно сделать областью определения множество пар 32-битных чисел с плавающей запятой, а областью значений — двухэлементное множество значений типа bool — а функция будет просто сравнивать эти числа. В общем, вы поняли.

На этом месте любой программист должен возмутиться — как же так, ведь функция обычно должна что-то делать, а возвращаемое значение зачастую никому и не нужно, возьмем хоть тот же printf. Это чудовищное заблуждение тех, кто видел только процедурные языки программирования, вроде Паскаля. «Что-то полезное» не зря называют «побочным эффектом» — у «настоящей» функции побочных эффектов быть не должно.

Достаточно «честно» функции реализованы, скажем, в Лиспе и других подобных языках. Если учебники по другим языкам программирования сразу начинаются с описания понятия переменной и оператора присваивания, то в Лиспе прекрасно можно обойтись и без них. Поддерживаемая в Паскале модель выполнения программы предполагает, что работа программы — это последовательное выполнение каких-то операций с переменными и присваивание им значений. В Лиспе, если не вводить оператор присваивания, работа программы — это вычисление функций (то есть нахождение их значений) без побочных эффектов. Функции в Лиспе вообще очень близки к математическому понятию функции.

Для чего это может быть нужно? «Настоящая» функция зависит только от своих аргументов, не изменяет ничего лишнего, а единственным результатом ее работы является вычисленное значение. Это позволяет, например, легко распараллелить программу, написанную в функциональном стиле (то есть в том, который я описал, как присущий Лиспу). Попробуйте «распараллелить» длинную цепочку каких-нибудь присваиваний, уверен, что это потребует ручной работы. А «функциональная» программа паралеллится легко и непринужденно. Кроме того, не содержащая присваиваний функциональная программа потокобезопасна, а для «обычных» программ потокобезопасность — это темный лес и страшное вуду. Я почти не шучу, буквально месяц назад читал откровения эмбеддеров по поводу того, как надо использовать слово volatile.

А вот слово «процедура» само по себе обозначает взаимосвязанную последовательность действий — и, следовательно, «функция» в ее понимании «обычным» программистом — это некий противоестественный гибрид из процедуры и функции в смысле математики.

Конечно, не все можно сделать с помощью «чистых функций» без побочных эффектов. Например, ввод-вывод в большинстве случаев таким «побочным эффектом» и является. Но надо понимать, что «побочные эффекты» — это не самое главное в понятии функции.

Совершенно не стыкуется с этим понятием и передача параметров по ссылке (или еще хуже — введенная в Си «подмена понятий» с передачей указателя). Обычно ее устраивают для того, чтобы функция смогла «вернуть» что-то лучшее, чем какой-нибудь int или double — но из-за ограничений Си или Паскаля приходится вводить параметры, которые изменятся после вызова подпрограммы.

Кто-то в комментариях утверждал, что обучение Паскалю (давно устаревшему) в российских вузах очень полезно. На самом деле, если человек нормально знает один только Паскаль, то и все остальное программирование он будет воспринимать «через Паскаль» — и, допустим, «настоящее» различие между функцией и процедурой будет ему недоступно.