учебники, программирование, основы, введение в,

 

Рекурсивные функции и множества

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

  1. функция;
  2. множество;
  3. тип.

Заметим, что рекурсивно определенный (т.е. построенный посредством рекурсии) объект, в свою очередь, носит название рекурсивного.
Заметим также, что определение рекурсивных объектов в математике происходит по индукции. При этом сначала формулируется базис индукции, как рекурсивное определение исключительных случаев при построении типа или множества (или вычисления функции), а затем шаг индукции, как рекурсивное правило построения того же объекта.
Для формализации рекурсивных функций и множеств в дальнейшем будут использоваться комбинаторная логика и теория вычислений Д. Скотта, основанная на понятии домена (последнее уточняется в ходе лекции).
В качестве практической иллюстрации определения понятия рекурсии рассмотрим ряд примеров рекурсивных функций, описанных на языке программирования SML.
Начнем с описания известной нам функции факториала на языке SML:
fun fact n = if (n<2) 1 else n*fact(n-1);
Как следует из описания, значением функции является 1, если значение аргумента не превышает 1, и произведение чисел натурального ряда от 2 до заданного в противном случае. Заметим, что идентификатор функции fact явно присутствует как в левой, так и в правой части описания. Отметим также, что настоящий пример, по сути, представляет собой линейный вариант записи математического определения функции факториала по индукции. Наконец, еще одной особенностью рекурсии является многократность вызова одной и той же функции с различными значениями аргумента.
Далее рассмотрим описание функции, которая вычисляет длину списка:
fun length lst = if (lst==[]) 0
else 1 + length(tl(lst));
Заметим, что функция length использует встроенную функцию tl (получение "хвоста" списка) в ходе вычислений. Отметим также, что реализация рекурсивной обработки списка (который, кстати, представляет собой встроенный рекурсивный тип языка SML) выглядит лаконично и является весьма наглядной.
Наконец, рассмотрим рекурсивное определение функции sumpos, суммирующей первые n чисел натурального ряда (и повторяющей отмеченные особенности функции fact):
fun sumpos n = if (n<2) 1
else n + sumpos (n-1);
В ходе обсуждения примеров рекурсивного определения функций на языке программирования SML было упомянуто понятие рекурсивного типа для списков.
Рассмотрим достаточно формальные определения важнейших рекурсивных типов, а именно, списка и дерева, выраженные в словесном виде.

Начнем с определения списка. Список из элементов типа t либо пуст, либо состоит из головы и хвоста, где голова - элемент типа t, а хвост - список из элементов типа t.
Приведем определение дерева (для частного случая бинарного дерева). Бинарное дерево из элементов типа t либо пусто, либо состоит из корня и двух поддеревьев , где корень - элемент типа t, а каждое из поддеревьев является деревом из элементов типа t.
Подчеркнем, что оба приведенных определения являются рекурсивными (т.е. определяемые понятия определяются c использованием самих этих понятий).
Отметим, что и список, и дерево имеют абстрактный (параметрический) тип аргументов, т.е. это, вообще говоря, полиморфные объекты. Именно таким образом список и дерево трактуются в языке программирования SML.
Заметим также, что для манипулирования рекурсивно определенными объектами (в частности, списками и деревьями) целесообразно использовать рекурсивно определенные функции (например, подсчет количества элементов списка, обход дерева и т.д.).
В качестве упражнения предлагается построить математическое описание по индукции для рекурсивной функции (скажем, факториала) и сравнить его с программой на языке SML, содержащей описание данной функции.
Изучив порядок определения рекурсивных функций в языке программирования SML, перейдем к определению рекурсивных типов.
Для задания рекурсивных типов в SML используются определения типов datatype следующего вида:
<определение типа>:: =
datatype <имя типа> = <описание типа>;
где
<описание типа>:: =
<выражение> |
<имя элемента типа> of <конструктор>
причем под конструктором понимается запись вида
<конструктор> = <тип> * ... * <тип>
Заметим, что фигурирующий в описании рекурсивного типа символ "|" является частью алфавита языка программирования SML.
Кроме того, обратим внимание на рекурсивный характер определения, который выражается в том, что типы в конструкторе могут совпадать с определяемым типом.
Наконец, отметим, что значок "*", используемый в конструкторе типов SML, семантически аналогичен конструктору "x" для декартова произведения доменов.
Проиллюстрируем сказанное о рекурсивных определениях типов содержательными примерами на языке программирования SML.
В частности (поскольку достаточно сложное понятие полиморфизма будет рассматриваться существенно позднее), рассмотрим описания списка и бинарного дерева с целочисленными элементами.
Приведем описание списка из целочисленных элементов на языке SML:
datatype intlist = nil | element of int * intlist;
Приведем описание бинарного дерева из целочисленных элементов на языке SML:
datatype inttree = empty |
node of int * inttree * inttree;
Заметим, что операция "|", применяемая для конструирования типов в SML, соответствует конструктору дизъюнктной суммы "+", а операция "*" - прямого произведения доменов "x".
Как уже отмечалось, для формализации рекурсивных вычислений будет использоваться система комбинаторной логики.
Попытаемся исследовать известные нам комбинаторы на предмет применимости для моделирования рекурсивных вычислений.
Предположим, что существует комбинатор Y, который при аппликации к любому комбинаторному терму (т.е. функции) E остается неизменным:
YE = E(YE).
К сожалению, выясняется, что ни один из ранее рассмотренных нами комбинаторов
(I)      I a = a;
(K)     К ab = a;
(S)     S abc = ac(bc);
(B)     B abc = a(bc);
(C)     C abc = acb;
(W)    W xy = xyy
требуемой возможности не обеспечивает.
Тем не менее, оказывается, что объект Y с указанной выше характеристикой действительно существует и известен в литературе как комбинатор неподвижной точки.
Для получения характеристического соотношения, задающего комбинатор неподвижной точки, необходимо сформулировать (и доказать, что, впрочем, выходит за рамки данного курса) следующую теорему.
Сформулируем теорему о неподвижной точке функции.
Для любой функции f, которую можно представить в ламбда-исчислении, существует ламбда-терм Y, такой, что он является неподвижной точкой функции f, т.е. выполняется соотношение
Yf = f(Yf).
При этом объект Y имеет конечный вид
Y = λf.(λx.f(xx))(λx.f(xx)),
или, в форме комбинаторной характеристики:
Y = WS(BWB),
где
W = CSI,
C = S(BBS)(KK),
B = S(KS)K.
Таким образом, любая функция имеет "неподвижную точку" в форме комбинатора Y, характеристику которого можно явно представить в базисе комбинаторов {K,S}.
Исследуем возможность описания комбинатора неподвижной точки Y на языке программирования SML.
Очевидно, что явным образом этот комбинатор реализован посредством рекурсивных определений функций с помощью рассмотренного нами варианта конструкции fun (или рекурсивного варианта подстановки let - конструкции let rec). Следовательно, реализация комбинатора Y посредством явной рекурсии является тривиальной задачей.
Для решения поставленной задачи возьмем за основу приведенное выше ламбда-выражение для комбинатора Y.
Далее, определим следующий тип данных:
datatype 'a t = T of 'a t -> 'a;
Теперь определим функцию:
val Y = fn f =>
(fn (Tx) => (f (fn a => x (Tx) a)))
(T (fn (Tx) => (f (fn a => x (Tx) a))));
Можно практически убедиться в том, что построенная таким образом (в полном соответствии с теорией) функция для комбинатора неподвижной точки Y действительно решает поставленную задачу.
Рассмотрим связь ставшего концептуально ясным и практически апробированного понятия рекурсии с теорией вычислений.
Ранее уже упоминалось о том, что домены в теории вычислений Д. Скотта используются для формализации семантики рекурсивно определенных функций и множеств.
Напомним, что в исследованиях Д. Скотта речь идет о денотационной семантике языков программирования. Последняя описывается уравнениями, задающими абстрактные функции на состояниях. При этом вводится специализированный метаязык, в котором переменные пробегают по доменам.
Ранее, говоря о доменах, мы ограничивались утверждением, что домены представляют собой до некоторой степени аналог множеств в теоретико-математическом смысле. Кроме того, отмечалось то обстоятельство, что домены требуют выполнения определенных дополнительных ограничений по сравнению с обычными множествами. На данном этапе представляется возможным конкретизировать форму упомянутых ограничений.
Итак, существуют следующие ограничения, которые отличают домены от множеств:

  1. все рекурсивные определения являются разрешимыми;
  2. все домены имеют неопределенные элементы, необходимые для обработки "исключительных ситуаций", как, например, в случае с элементом unbound, характеризующим невозможность связывания переменной со значением, или элемента error, характеризующего общую ошибку;
  3. производные (т.е. построенные с помощью ранее исследованных в курсе конструкторов) домены сохраняют структуру базовых;
  4. между доменами допускаются не только непосредственные, но и рекурсивные равенства.

В настоящей лекции мы ограничились рассмотрением простейшего вида рекурсии.
Однако существует еще несколько видов рекурсивных определений объектов (в частности, функций), описание которых может представлять теоретический интерес и практическую ценность.
Кратко перечислим основные виды рекурсии.
Во-первых, отметим, что тот простейший тип рекурсии, который рассматривался ранее, носит название прямой рекурсии.
Более сложным видом рекурсивных определений является так называемая взаимная рекурсия. В таком случае, скажем, при формулировке рекурсивного определения функции, функция f определяется через функцию g и наоборот.
Еще одним важным типом рекурсии является рекурсия, известная под названием частичной. В случае задания описания, например, функции с помощью частичной рекурсии вновь вводимая функция является частично определенной.
Заметим в заключение, что все перечисленные выше виды рекурсии адекватно формализуются посредством теории вычислений Д. Скотта, а также формализаций, основанных на ламбда-исчислении и комбинаторной логике.
Подводя итоги обсуждения рекурсивного определения функций, типов и множеств, выделим наиболее значимые результаты.
Во-первых, рекурсия относится к достаточно хорошо изученным областям computer science и имеет целый ряд вполне адекватных формализаций, наиболее важными из которых являются комбинаторная логика и теория вычислений Д. Скотта.
Во-вторых, рекурсивные вычисления концептуально ясно и интуитивно прозрачно реализуемы посредством функционального подхода к программированию.
В-третьих, платой за лаконичность и прозрачность (с математической точки зрения) реализации рекурсий является необходимость (многократного) повторного вызова функций.
Поскольку наше исследование языков программирования базируется на технологическом фундаменте .NET, перечислим основные результаты влияния данной платформы на реализацию рекурсии:

  1. интегрированная среда гетерогенных языков программирования позволяет сократить сроки реализации сложных (требующих рекурсивных и нерекурсивных функций) проектов;
  2. широкий спектр унифицированных предопределенных структур данных (списки, деревья, очереди и т.д.) позволяет увеличить эффективность реализации и повторно использовать код;
  3. рекурсия лучше соответствует ориентированным на обработку рекурсивных структур данных языкам функционального программирования (SML, Haskell, Scheme и др.).

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика