Языки программирования, базовый курс.

Функции и процедуры

Не можешь ? Научим! Не хочешь ? Заставим!

Написав уже первые несколько программ, люди стали думать над тем, как этот процесс упростить. Исторически первым способом использования уже написанного кода стала организация подпрограмм или, иначе, процедур. В языке Fortran уже есть возможность описывать процедуры и функции. Быстро выяснилось, что кроме первоначального назначения их как способа не писать один и тот же код дважды, подпрограммы служат мощным средством структурирования программы и организации процесса программирования.

Действительно, можно легко разделить большую программу на части, если только описать, как именно будут взаимодействовать между собой процедуры. Можно даже поручить написание процедур разным людям. Поэтому самой сложной частью аппарата функций и процедур в языках программирования всегда была передача параметров и организация общих переменных. Те параметры, что написаны в тексте процедуры, называются формальными, а те, которые пишутся в вызывающей программе - фактическими. При вызове процедуры или функции формальным параметрам ставятся в соответствие фактические.

Вызов процедуры чем-то похож на оператор GOTO, только при вызове процедуры каким-то образом запоминается место, откуда она была вызвана, а по окончании выполнения происходит возврат (то есть еще один GOTO) на запомненное место вызова. Другая интерпретация семантики вызова процедуры состоит в том, чтобыпросто скопировать ее код в место вызова и подставить вместо формальных параметров фактические.

Некая терминологическая проблема состоит в том, что в разных языках процедуры и функции называются разными словами. В C все они называются функциями, в Algol, наоборот, процедурами, в Fortran используетсяслово «подпрограмма» (subroutine) итп. Не обращая особого внимания на принятую терминологию, мы рассмотрим два концептуальных класса объектов, которые будем называть «процедуры» и «функции», хотя последовательно разница между ними проводится в очень немногих языках, среди которых можно назвать Ada. Разница состоит не в написании, а в назначении: функции предназначены только для того, чтобы вычислять значение, а процедуры - чтобы сделать что-то полезное. Все вместе процедуры и функции будем называть подпрограммами, имея в виду, что их основная цель - как-то обособить участок программы.

Макроподстановки

Программист должен быть ленив, это заставляет его больше думать и меньше делать, и в итоге приводит к более качественному коду. Разумеется, никто не хочет писать один и тот же кусок программы несколько раз. Первый способ, который находит для себя начинающий программист, называется «китайский метод повторного использования кода copy/paste» - простое копирование кусков программы. Но даже этот способ ленивые программисты сумели автоматизировать. Макроподстановка или просто макро позволяет выделить какой-то текст, дать ему имя и далее копировать сколько угодно, причем средствами языка а не текстового редактора. При правильном использовании это дает более читаемый текст.

#include <stdio.h>
#define sayit printf(“I will always use Google before asking dumb
questions\n”) main() { sayit; sayit; sayit; }

В макроопределения можно вводить параметры, и они будут подставлены в текст программы без какой-либо проверки. Макроподстановка - опасный инструмент, с его помощью можно легко запутать любую программу.

#define int float
#define float int
...

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

Но это еще не все. Некоторые языки программирования используют макроопределения и макроподстановки вкачестве основного инструмента и главной идеи, на которой строится весь язык.

Передача параметров.

Передать параметры в процедуру или функцию можно многими разными способами. Единственным в своем роде исключением тут можно считать некоторые ранние диалекты языка Basic, в которых передачи параметров в процедуры нет вообще. Хотите передать значение - кладите его в переменную. Во всех остальных распространенных языках используются следующие способы передачи параметров:

1. Передача значений

Процедуре передаются копии значений параметров. Соответственно, изменить какие-то переменные вызывающей программы процедура не может.

procedure test(x:integer);
begin
	writeln(x);
	x:=x+1;
	writeln(x);
end;
var  xx:integer;
begin
	xx:=1;
	test(xx); (* xx не изменяется после выполнения процедуры *)
	writeln(xx);
end.

Заметим, что в языке C этот способ является единственным.

2. Передача ссылок

proceduretest(var x:integer);
begin
	writeln(x);
	x:=x+1;
	writeln(x);
end;
var  xx:integer;
begin
   xx:=1;
   test(xx); (* xx после выполнения процедуры увеличится на 1 *)
   writeln(xx);
end.

Отличие состоит в том, что в процедуру передается ссылка на переменную, и переменная x в процедуре и xx в вызывающей программе это одна и та же переменная. Этот способ передачи параметров является единственно возможным в языке Fortran. Возникает естественный вопрос: Что, если в качестве параметра будет передано выражение ? Константа ? Можно выражения в качестве параметров-ссылок вообще запретить. Так и сделано в языке Pascal. Но что делать, если других способов передачи параметров вообще нет в языке ? Разработчики Fortran нашли весьма своеобразное решение этой проблемы. Если в качестве параметра указана переменная, то процедуре передается ссылка на эту переменную. А если там выражение, то создается специальная безымянная переменная, ссылка на которую передается процедуре. Все изменения этой переменной, произведенные процедурой, теряются. С константой все интереснее. Некоторые компиляторы Fortran не создавали копию константы, а передавали ссылку на место хранения самой константы, в надежде, что программист знает, что делает и не будет менять значение в процедуре. Таким образом Fortran давал возможность изменить значение константы. Что, разумеется, приводило к непредсказуемым последствиям.

3. Передача по имени. Рассмотренные два способа передачи параметров являются основными. Но был еще один странный способ в языке Algol,который интересно рассмотреть. Он заключался в том, что переданное в процедуру выражение вычислялось каждый раз так, как если бы вместо формальных параметров были подставлены их значения, вычисленные в контексте вызывающей программы. По сути это нечто среднее между макроподстановкой и вызовом подпрограммы. Разобраться с этим способомнам поможет пример, называемый «прием Йенсена» по имени изобретателя.

begin
procedure p (a,b);
name a, b;integer a, b;
begin
  for a:=1 step1 until 10 do
    b := 0
end p;
integer i; integer array s [1:10];
  p (i, s[i])
end

При выполнении процедуры p на каждом шаге цикла вместо a подставляется i, а вместо b подставляется s[i], и эта процедура просто обнуляет массив s. Проблем с таким способом передачи параметров очень много, а из преимуществ можно назвать только прием Йенсена. Поэтому передача параметров по имени так и осталась курьезом из языка Algol. Но способ этот оказал влияние на дальнейшее развитие языков программирования.

4. Передача невычисленных выражений (отложенное вычисление).

Рассмотрим пример на языке С

void f1(int a)
{
   if (a) printf("yes");
}
void f2(int a,intb)
{
   if (a || b) printf("yes");
}
main()
{
	int x = 1, y=2, z=0;
	f1((x==1) || (y/z == 1)); // работает
	f2((x==1),(y/z== 1));	// не работает
}

Происходит это потому, что если в выражении (x==1) || (y/z ==1) первое условие выполнено, то дальше считать не имеет смысла, на результат это не повлияет. Но сделать то же самое внутри процедуры мы не можем, потому что значения параметров должны быть вычислены до вызова процедуры. Заметим, что в случае передачи параметров по имени оба способа будут работать.

В некоторых языках программирования существует еще один способ передачи параметров, который называется отложенными или «ленивыми»(lazy) вычислениями. Он похож на передачу параметра по имени, и отличается тем, что вычисление выражения происходит один раз, но только тогда, когда это значение действительно понадобится. В таких языках оба примера будут работать, если только параметры процедур объявлены как «ленивые».

Функции

Теперь поговорим о функциях. Цель функции — получить значение, в отличие от процедуры, цель которой — произвести какое-то действие. Если функция все-таки изменяет среду исполнения, например, выводит какое-то число на дисплей, это называется «побочным эффектом». К проблеме побочных эффектов в императивных языках есть два диаметрально противоположных подхода. В языке Ada побочные эффекты функций просто запрещены. Смысл в этом есть, ведь в выражении, подобном (x==1) || f(y) , функция f(y) может так и остаться никогда не вычисленной. Если она производит какое-то действие кроме вычисления значения, этодействие так и не произойдет. Это может привести к довольно сложно обнаруживаемым ошибкам. С другой стороны, в языке C вообще нет разницы между функциями и процедурами. В C и процедуры и функции формально считаются функциями, причем процедуры возвращают специальное значение, принадлежащее к типу void,которое символизирует отсутствие результата. А тот факт, что в вышеприведенном выражении функция f(y) при x равном 1 не будет вычислена, считается дополнительным удобством для программиста.

Наследники языка C идут в этом отношении еще дальше. Стандартный способ аварийного прекращения программы в языках PHP и PERL такой:

($x == 1) or die(“X не равен 1, аварийное завершение программы“)

Функция die выводит сообщение и завершает выполнение программы. Формально это выражение, состоящее из логического условия и вызова функции, возвращающей значение логического типа. Результат этой функции неизвестен - программа завершается раньше, чем он может быть использован.

Рекурсивные функции

Для математика вполне естественно определять функции посредством рекуррентных соотношений. Например,

Факториал числа:

N! равен 1 при N равном 1
N! равен N*(N-1)! при остальных N

Числа Фибоначчи

Fn+2 = Fn + Fn+1
F0 = 1
F1 = 1

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

int factorial(intx)
{
	if (x == 0) 
	   return 1;
	else 
	   return x * factorial(x-1);
}

При вычислении функции factorial прежде,чем умножать x на факториал x-1, последний надо вычислить. А при его вычислении понадобится факториал x-2. Все промежуточные результаты вычислений (x,x-1, x-2 итд) должны где-то запоминаться. Для этого можно использовать уже знакомую нам структуру данных - стек. При вызове любой процедуры или функции нужно выполнить некоторые вспомогательные действия, а именно,запомнить в стеке место вызова, выделить (опять же в стеке) память для всех переменных, объявленных в функции, а также формальных параметров, а по окончании работы функции вернуть все к первоначальным значениям. Этот процесс на ранних компьютерах требовал значительного объёма моделирования.

Запись результата функции

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

int sqr(int x){	return x*x;}

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

function sqr(x:integer):integer;
begin  
  sqr:=x*x;
end;

С виду все просто, но на практике такая запись оказывается очень неудобной. Если программист решит переименовать какую-то сложную функцию, то ему придется переименовывать и каждое вхождение переменной-результата. Более серьезная проблема состоит в том, может ли эта переменная появляться справа от знака присваивания ? С одной стороны, это вроде бы обычная переменная. С другой - в языке Pascal, где используется именно этот метод, вызов функции без параметров ничем не отличается от обращения к переменной.

var q:integer;
function question:integer;
begin   
    question:=q;   
	if question = 0 then 
	begin 
	  q:=100; 	
	  question:= question+1;    
	end;   
	q:=q-1;
end;

Появляется неоднозначность: question в операторе if и справа от знака присваивания может означать либо рекурсивный вызов этой же функции, либо переменную-результат. Заметим, что этотстранный пример будет работать в любом случае, хотя и будет возвращать разные последовательности чисел. В результате в языке Pascal появление переменной-результата в том контексте, где требуется значение, запрещено. Несмотря на неудобство, этот способ по историческим причинам оказался широко распространен. Со временем сложилась программистская практика, устраняющая основные недостатки переменных-результатов. Она состоит в том, чтобы самому объявить еще одну вспомогательную переменную, вычислить результат и в конце функции переложить его в переменную-результат.

function sqsum(a,b:integer):integer; // (a+b)^2
var res:integer;
begin  
  res:=a+b;  
  res:=res*res;  
  sqsum:=res;
end;

Метод оказался настолько удачным, что в языке Object Pascal (Delphi) ввели специальную автоматически объявляемую переменную с именем result.

Глобальные переменные.

Использование подпрограмм вызвало еще одну проблему. У каждой функции или процедуры могут быть локальные переменные, которые, кроме нее самой, недоступны никому. Но очень часто нужно иметь доступ к каким-то переменным из нескольких процедур, или же сохранять значение переменной между последовательными вызовами функции. Самый распространенный пример - генератор последовательности чисел.

var seed:integer;
function nextval:integer;
begin
  seed:=seed+1;
  nextval :=seed;
end;

procedure setseed;
begin  
  seed:=0;
end;

Обратите внимание, что в данном примере присутствуют два варианта использования глобальных переменных. Функция nextval и процедура setseed обращаются к одной и той же глобальной переменной seed,а последовательные вызовы nextval используют тот факт, что переменная seed является внешней по отношению к этой функции и сохраняет присвоенное значение. Таким образом, один и тот же механизм глобальных переменных решает обе поставленные задачи.

Но так было не всегда. По какой-то непонятной причине разработчики языков Fortran и Algol считали,что процедура или функция не должна «видеть» ничего, находящегося за ее пределами. Поэтому упомянутые задачи решали по отдельности. В языке Algol было введено специальное ключевое слово own, означавшее, что значение объявленной таким способом переменной сохраняется между вызовами подпрограммы. В языке Fortran для этого использовалось ключевое слово SAVE. На Algol функция nextval выглядела бы так:

real procedure nextval;
begin 
  own integer seed;
  seed:=seed+1;
  nextval:=seed;
end;

В языке Fortran проблема глобальных переменных решается при помощи специального ключевого слова COMMON.

	SUBROUTINE SETSEED 
	INTEGER SEED
	СOMMON /GLOBALSEED/ SEED
	SEED = 0
	END	
	
	FUNCTION NEXTVAL()	
	INTEGER NEXTVAL	
	INTEGER SEED	
	COMMON /GLOBALSEED/ SEED	
	SEED = SEED+1	
	NEXTVAL = SEED	
	END

В отличие от Pascal и C,в Fortran каждая подпрограмма содержит свое собственное описание переменной SEED. Если эти описания будут разными, результат окажется непредсказуемым. Такое странное решение было принято с целью обеспечить возможность независимой компиляции подпрограмм.

Раздельная компиляция.

Основная идея, лежащая в основе раздельной компиляции такова: сделать участки кода как можно более независимыми друг от друга, компилировать каждый из них только один раз и хранить где-то полученный машинный код. Это позволяло сэкономить время работы компилятора, которое было весьма дорогим на старых компьютерах. Если какая-то подпрограмма работает правильно, нет необходимости компилировать ее каждый раз заново, достаточно знать, как ее вызвать. Такие подпрограммы можно объединять в библиотеки, и вызывать по мере необходимости. На реализацию такой функциональности обращали особое внимание при создании языка Fortran, и тогда же сформировались три главные проблемы раздельной компиляции:

Надо сказать, что ранние языки (включая язык C) не предоставляли вообще никаких средств для решения этих проблем. Предполагалось, что для программиста будет написана некая инструкция, описывающая правильную работу с библиотекой. А программист все сделает правильно и в соответствии с инструкцией. Даже в языке C есть возможность вызвать любую подпрограмму сколь угодно неправильным способом. Например:

/* #include<stdio.h> */
main()
{	
	int x =printf(0.5);
}

Формально компилятор должен выдать предупреждение о том, что тут происходит нечто не совсем правильное. Основная идея решения проблемы проверки правильности вызовов состоит в том, чтобы создать какой-то отдельный файл с описаниями правил вызова библиотечных функций, но без их содержания. Компиляция этого файла тоже понадобится, но займет намного меньше времени. В языке C для этого служат так называемые include-файлы. В языке Modula-2 они называются def-файлы. В Turbo-Pascal файл с библиотечными подпрограммами делится на секции interface и implementation. Иметь один файл с описаниями намного лучше, чем дублировать описания в каждой подпрограмме. Одновременно получается документ, которым может пользоваться и программист.

Разумеется, без комментариев такой файл имеет ограниченную полезность, но за много лет стало совершенно ясно, что задача заставить программиста комментировать код в процессе его написания находится где-то между «очень сложно» и «практически невозможно». Если в случае Fortran от программиста требовался довольно пространный текст, то в случае C или Pascal отделить описание функции, которое нужно компилятору для правильного ее вызова - дело не сложное, и комментарии требуются в меньшем объёме. В более поздних языках, например, в C++, вызов функции, не имеющей описания, вообще невозможен (что не мешает сделать неправильное описание).

Что касается третьей проблемы - несовместимости версий - то, несмотря на многочисленные попытки ее решить, она существует и сегодня.

Запись вызова функций и подпрограмм

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

	CALL f(x)

В дальнейшем от использования ключевого слова отказались, и стали писать просто f(x); В записи же функций долгое время особого разнообразия не наблюдалось,поскольку запись была заимствована из языка математики. Некоторое разнообразие существовало только в отношении функций и процедур, не имеющих аргументов. Вопрос стоял так: писать ли пустые скобки ? В Pascal скобки писать не нужно, и это решение имеет два важных последствия. Во-первых, можно писать программы, похожие на текст на английском языке:

begin
initialize_variables;
open_files;
perform_calculations;
write_results;
close_files;
end.

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

Указатели на функции

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

void map(double*x, int length, double (*fun)(double))
{	
	int i;	
	for(i=0;i<length;i++)
		x[i] =fun(x[i]);
}

часто пишут что-то вроде

typedef enum {FN_SIN, FN_COS }  FUNCTION;

void mapnf(double*x, int length, FUNCTION f)
{	
	int i;	
	for(i=0;i<length;i++)		
	switch(f)		
	{		
	case FN_SIN:			
		x[i] =sin(x[i]); break;		
	case FN_COS:			
		x[i] =cos(x[i]); break;		
	}
}

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

Функции с переменным числом аргументов

Тенденцияк переносу средств ввода-вывода из языка в библиотеки, о которой мы будем говорить позже, привела к необходимости появления процедур спеременным числом аргументов (и функций тоже, но они нужны реже). Одна из первых попыток была предпринята в языке Pascal. Там есть процедуры с переменным числом аргументов, но это были «специальные» процедуры, по существу, операторы языка, лишь внешне напоминавшие обычные. Способа создать свою процедуру спеременным числом аргументов в классическом Pascal нет. В языке С процедуры с переменным числом аргументов уже можно создавать, и имеется стандартный механизм, позволяющий эти аргументы получать. Для этого нужно или иметь возможность определить число аргументов, анализируя первые несколько переданных значений, или принять соглашение о том, что какое-то определенное значение будет являться признаком окончания списка аргументов. Таким образом, в C могут быть два вида вызовов процедуры с переменным числом аргументов: f(n,a1, a2 ... an); и f(a1,a2 ... an, NN); где n-число аргументов, NN - константа, которая не может встретиться среди значений аргументов, обычно это 0.

Способа узнать,сколько параметров было в действительности передано, в C не существует, и программист может легко создавать трудноуловимые ошибки. Вот как выглядит описание функции с переменным числом аргументов:

int add_them_up(int arg_count, ... )
{
	va_list ap;
	int sum = 0;
	va_start(ap, arg_count );
	for ( ;arg_count > 0; arg_count-- )	
	  sum += va_arg( ap, int );
	va_end(ap);
	return sum;
} 

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

int func(int x,int y=0, int z=0);

func(1,2) // x=1,y=2, z=0
func(5)   // x=5,y=0, z=0

Другие способы вызова подпрограмм.

Как мы уже говорили, когда обсуждали выражения, существует три способа записи: префиксный, инфиксный и постфиксный. Те варианты, которые мы сейчас рассмотрели, относятся к префиксному способу записи - имя функции ставится перед аргументами. Разновидностью этого способа является запись, принятая в языке LISP, когда имя функции вносится внутрь скобок.(SIN X)Цель такой записи - исключить неоднозначность. Когда аргументом функции можетбыть список, записываемый в виде (X Y Z ...) тозапись PLUS( MINUS (X Y) M) может читаться двояко, как вызов функции PLUS c тремя аргументами:переменной MINUS, списком(X Y) и переменной M, а также как вызов функции PLUS c двумя аргументами: результатом вызова функции MINUS с двумя аргументами X и Y, и переменной M. Постфиксная запись вызова функций выглядит так: вместо f(x)пишется x.f или x.f(). Несмотря на очевидность этого метода, широкого распространения в языках программирования он не получил, за одним исключением. На постфиксной записи построено все объектно-ориентированное программирование. Этот вид программирования настолько важен, что мы будем разбирать его в отдельной части.

Типы функций и карринг.

Допустим,у нас есть функция, возвращающая целое число, и имеющая один аргументцелого типа. Поскольку в языках со строгой типизацией «все имеет какой-то тип», логично спросить, какой тип имеет сама функция? В классических императивных языках вроде C или Pascal существуют типы для указателей на функции. Формально это обстоятельство предполагает существование типов самих функций, но создать переменную типа «функция» в языке C или Pascal не получится. В некоторых языках функция может сама по себе быть объектом «первого класса», в таких языках функции имеют типы. И на примере этих языков имеет смысл изучить типы, к которым принадлежат различные функции.

Возьмем обозначения типов в языке Haskell:

Тип Integer -> Float это вещественная функция целого аргумента. С точки зрения математики эта функция является отображением множества целых чисел Integer на множество вещественных чисел Float.

Тип Float->Float это функция, которая получает на входе вещественное число и возвращает вещественное число.

Float->Float->Float это функция, которая получает на входе два вещественных числа и возвращает вещественное число. Его можно интерпретировать и по-другому: функция, которая получает одно число типа Float и возвращает функцию типа Float->Float.

Действительно, если у функции с двумя аргументами зафиксировать один аргумент, то получится функция одного аргумента. Рассмотрим выражение A+X Если несколько необычным способом поставить скобки, то получится функция «Aплюс» от X, прибавляющая A к аргументу: (A+) (X) Далее можно записать определение функции F = (A+) где мы просто обозначаем нашу функцию, прибавляющую A,как F. Такой прием называется карринг, и широко используется в функциональных языках.

Лямбда-исчисление

Раз уж мы заговорили про функции как отдельные сущности, то попробуем пойти чуть дальше, и поговорить про лямбда-исчисление. Эта идея, лежащая в основе одной из самых красивых концепций программирования, очень проста. Но из-за непривычности ее для тех, кто знает только императивные языки, начнем немного издалека. В языке C можно совместить описание структуры и объявление переменной:

struct {
	int x,y;
	float z;
		} p;

Заметим, что тип переменной p не имеет названия. Он нужен только для того, чтобы объявить одну-единственную переменную. Теперь представим себе, что с функциями можно делать то же самое (на самом деле в C такой возможности нет):

z = int lambda (x,y){return x+y;};
y = z(2,3);

Здесь lambda - не имя функции, а ключевое слово, означающее, что далее следует описание функции. Имени у этой функции нет, и указатель на нее присваивается переменной. Запишем то же самое на языке, похожем на Pascal:

z:= lambda (x,y):integer 
    begin 
     result:= x+y; 
    end;
y := z(2,3);

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

lambda(x) {printf(”%d”,x);}(42);

Зачем могла бы понадобиться такая функциональность, и почему ее нет в Pascal и C? Прежде всего, для передачи функций как параметров. Любой, кто много пользовался в C функцией qsort, скажет, что было бы намного удобнее описывать функцию сравнения прямо в месте вызова, а не вынуждать того, кто будет читать текст программы, искать, где находится функция, которая всего-навсего сравнивает два числа. Особую роль лямбда-исчисление играет в функциональном программировании. В традиционных языках такие функции отсутствуют по простой причине: их создателям казалась неестественной мысль о том, что функция, с таким трудом написанная и скомпилированная, может быть навсегда забыта сразу после использования. Действительно, если присвоить переменной z указатель на какую-то другую функцию, то указатель на функцию, полученную с помощью lambda, будет уже недоступен.

Функции левой части

Исходя из соображений симметрии,часто хочется наряду с x := min(a,b); писать min(a,b) := x; Смысл этого вполне очевидный - переменной, которая имеет минимальное значение, присвоить значение x. Ссылки в языке C++ дают нам такую возможность.

int& min(int&x, int& y)
{	
	if (x<y)
		return x;	
	else 
		return y;}
...
min(a,b) = 5;

Типы результата

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

Перегруженные (overloaded)подпрограммы.

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

В языке Fortran делодаже дошло до неформального соглашения: к названию функции приписывали букву, означающую используемый тип. Так, модуль вещественного числа назывался ABS, целого числа IABS, комплексногочисла CABS, итд.

Возложить на компилятор обязанность различать функции по типам аргументов догадались не сразу. Первые попытки состояли в том, что компилятор «заранее знал» про некоторые функции и мог подставить нужный код. Такие подпрограммы получили название полиморфных, или перегруженных. Программист не мог сам определить такую функцию. Разумеется, арифметические операции были полиморфными изначально, и в языке Algol-68 программисты получили возможность определять полиморфные операторы, но не функции. Причины, по которым введение полиморфных функций было отложено до 1976 года (язык ML), остаются не вполне понятными. В мир императивных языков полиморфные функции были введены в 1983 году с языком C++. Изобретатель языка Бьерн Страуструп настолько боялся новой идеи, что постарался, как он сказал, «изолировать» полиморфные функции. Выглядело это так (в первой версии языка):

overload print(int x);
overload print(char *x);
overload print(int n, char *x);

Использовалось ключевое слово overload, совершенно ненужное. Если программист определил функцию print с аргументом типа int, то ее так и следует вызывать, независимо от того, есть ли другие функции с тем же именем. И, наконец, непонятно, что будет, если функцию объявить как overload, но не написать никаких альтернатив. Что это - ошибка ? А если две такие функции находятся в раздельно компилируемых модулях ? В общем, во второй версии языка слово overload было отменено.

Шаблоны(generics)

Альтернативой полиморфным функциям является концепция универсальных «шаблонных» функций. Рассмотрим ее на примерах из языков C++ и Ada.

C++

template<typename T > void TSwap( T& a, T& b ) // typename T это параметр
{  
  T c;  
  c = a; 
  a = b; 
  b = c;
}

int main() 
{   
  int a,b; 
  floatc,d;
  ...   
  TSwap<int>(a,b);   // меняем местами целочисленные переменные   
  TSwap<float>(c,d); // меняем местами вещественные переменные
}

Ada:

generic type Element_T is private;  --  Element_T это параметр
	procedure Swap(X, Y : in out Element_T);

procedure Swap(X, Y : in out Element_T) is 
  Temporary : constant Element_T := X;
  begin  
    X := Y; 
	Y := Temporary;
end Swap;
  
procedure Swap_Integers is new Swap (Integer); -- создаем процедуру для целых 
procedure Swap_Chars is new Swap (Character);-- для символов
-- Используем функцию

a,b : integer;
c,d: character;
Swap_Integers(a,b);
Swap_Chars(c,d);	

В обоих случаях описывается шаблон - некая общая форма для работы с данными любого типа (или какого-то набора типов). У шаблона присутствует параметр - тип данных, скоторым надо работать. При компиляции программы вместо формального типа подставляется фактический и создается новая функция. Все это очень похоже на макроподстановку с параметрами, но в случае шаблонов компилятор имеет возможность проверить хотя бы синтаксис функции. В языке C ее имя состоит из имени шаблона к которому приписано имя типа в угловых скобках; в языке Ada имя каждой функции нужно придумывать отдельно. Каждый такой шаблон эквивалентен целому «семейству» полиморфных функций. Разница между полиморфными функциями и шаблонами состоит в том, что полиморфные функции для разных типов могут быть существенно различными, тогда как шаблоны внешне имеют один и тот же код для каждого типа, хотя полученный в результате компиляции машинный код может существенно различаться. Шаблоны в определенном смысле зависят от полиморфных функций, поскольку описывая шаблон, мы надеемся, что один и тот же код окажется правильным при подстановке разных описаний типов.

Замыкания

Нравится нам это или нет, но многие функции используют какой-то внешний контекст. Это могут быть глобальные переменные, константы, другие функции. С другой стороны, если в языке сами функции являются объектами первого класса, то их можно не только присваивать переменным, но и возвращать из других функций в качестве результата. Вот как это происходит в языке JavaScript:

function differential (f, dx) 
{  
    return function(x) 
	{ 
	  return (f(x+dx) - f(x)) / dx;  
	}; 
}

Функция differential возвращает функцию для вычисления отношения заданных малых приращений другой функции к приращению аргумента. Чем это отличается от указателя на функцию ? Очень просто: в случае указателя функция должна уже где-то существовать, в случае функции как значения она создается в процессе выполнения программы. Заметим, что переменная dx и функция f, переданные в функцию differential, «замораживаются» в возвращаемой безымянной функции. Следующий вызов differential породит другую безымянную функцию, с «вмороженными» в нее другими значениеми dx и f.

Это еще один пример так называемого контекста, то есть всех сущностей, от которых зависит значение, возвращаемое функцией.Функция вместе с контекстом в ряде языков представляет собой объект первого класса иносит название замыкания. Замыкания впервые появились в функциональных языках, потом в языках, задуманных как интерпретируемые, и только в конце первого десятилетия 21 века начали появляться в традиционных языках, таких как Cи и Pascal. Пример на Delphi

type  
	TCounter =reference to function: integer; 
	function MakeCounter: TCounter;
		var  i: integer;
	begin  i := 0;  
	result := function: integer 
						begin
						  inc(i);
						  result:= i;
						end;
	end;

Генераторы

Одно из наиболее распространенных применений функций с внешним контекстом это создание генераторов, то есть функций, возвращающих каждый раз новое значение. Это может быть порядковый номер или псевдослучайное число или что-то еще, зависящее от контекста. В любом случае функция должна как-то запомнить свое состояние и восстановить его при следующем вызове. Если при этом требуется какая-то инициализация, то необходима еще одна функция. Рассмотрим пример

int number;

void init_number(int startvalue)
{	
	number =startvalue;
}

int get_next_number()
{	
	return number++;
}

Таким образом, для простой задачи нам нужны сразу три объекта, причем если генераторов нужно несколько, то задача дополнительно усложняется. Объектно-ориентированное программирование предлагает решение проблемы, состоящее в том, чтовсе три компонента генератора «упаковываются» в целостный объект. Так гораздо удобнее, но проблема трех сущностей никуда недевается. Вот как решается эта задача в языке JavaScript, причем без явного объявления объектов:

function next_number(startvalue)
{ 
    var  number = startvalue; 
	while (true) 
	{   
		yield number++; 
	}
}

v=next_number(10);
a = v.next();  // a=10
b = v.next();  // b=11

Оператор yield возвращает специальный вид замыкания - генератор. К этому генератору можно обратиться посредством вызова next(), чтобы получить очередное значение. Оператор yield можно рассматривать как разновидность оператора return. Он как бы останавливает вычисление функции и возвращает промежуточный результат. Вызов next() продолжает выполнение до следующего yield. В языке Python генераторы выглядят более прозрачно:

# объявление функции
def countfrom(n):
   while True:
	   yield n
	   n += 1

# Использование

for i in countfrom(10): 
	print(i)

На самом деле функция countfrom возвращает объект, но это совершенно не заметно.