Опубликован: 04.04.2012 | Уровень: для всех | Доступ: платный
Лекция 7:

Введение в лямбда исчисление

< Лекция 6 || Лекция 7: 12345 || Лекция 8 >

Механизмы, подобные агентам

Язык C# вводит понятие делегатов, предназначенных для тех же целей, что и агенты.

Если не принимать во внимание дух и нотацию, можно сказать, что главная разница между делегатами C# и агентами Eiffel в том, что цель делегата не может быть открытой. Выражение agent {STOP}.close не имеет прямого эквивалента в C#. В приложении, посвященному языку C#, о делегатах говорится подробнее.

Язык Smalltalk вводит понятие блока (block) — сегмента кода, который может передаваться как объект. Заметьте, что Smalltalk является нетипизированным языком, поэтому здесь нет способа проверить во время компиляции, что передаваемый блок будет использоваться с подходящими аргументами, — любые несоответствия приводят к появлению ошибок в период выполнения.

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

(defun f (x y) ("expression involving x and y"))

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

(curry f)

Сама функция curry может быть определена в Lisp. Язык был определен на базе бестипового лямбда-исчисления, так что нет ничего удивительного, что многое из того, что мы видели в этой лекции, без труда выразимо в Lisp. Все это применимо и для более современных функциональных языков, таких как Haskell и ML. Следует отметить два момента.

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

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

Программы как аргументы

Некоторые языки программирования позволяют передавать методы как аргументы другим методам с примерно таким синтаксисом:

integral (f:function(x: REAL):REAL ; a, b: REAL): REAL

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

В сравнении с агентами или замыканиями такое решение имеет ограничения.

  • "Метод" является аргументом специального типа, который не соответствует в полной мере системе типов языка.
  • Обычно информация о методе не представляет хорошо определенное значение, как в случае агента или замыкания, и, следовательно, метод не может быть присвоен переменной (для которой, учитывая предыдущий пункт, было бы трудно задать соответствующий тип) — он может быть только использован как аргумент.
  • Из-за отсутствия подходящей типизации аргументов невозможно или, по крайней мере, не просто прейти на следующий уровень абстракции и определить функции, такие как композиция или карринг.
  • Все, что можно делать с аргументом, представляющим метод, — это вызвать его. В противоположность этому агенты являются полноправными объектами, чьи компоненты обеспечивают информацию об ассоциированном методе.
  • Могут возникать проблемы, когда методам нужен доступ к глобальным переменным. В первую очередь проблемы появляются у разработчиков компиляторов, но могут касаться и программистов.
  • Подход не согласуется в полной мере с ОО-схемой, так как использует данные, не являющиеся объектами.

Однако этот подход удовлетворяет многим основным потребностям, он успешно применяется в не ОО-языках, начиная с Фортрана и продолжаясь в Паскале и его последователях.

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

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

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

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

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

Все эти приемы важны для компилятора в процессе генерирования кода, а не для прикладного программиста, когда он пишет свою программу. Компилятор скрывает адреса методов под одним или несколькими слоями абстракции, позволяя программисту думать в терминах более высокого уровня — методах, объектах, агентах.

Языки С и С++ позволяют передавать имя функции (процедуры рассматриваются как функции, возвращающие значение void ) как фактический аргумент или присваивать его переменной. Тогда, если f является соответствующим формальным аргументом или переменной, можно вызвать функцию следующим образом:

(*f ) (args)

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

"Много маленьких оберток" и вложенные классы

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

Его главный недостаток — необходимость писать много маленьких классов, часто включающих только один метод.

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

Дальнейшее чтение

  1. J. Roger Hindley and Jonathan P. Seldin: Introduction to Combinators and X-Calculus, London Mathematical Society Student Texts, Cambridge University Press, 1986. Классическое руководство по лямбда-исчислению и теории комбинаторов (служит непосредственным базисом некоторых языков программирования). Математический текст, не ориентированный на специалистов по информатике. Замечательно ясный текст, определяет все необходимые концепции.
  2. Chris Hankin: An Introduction to Lambda Calculi for Computer Scientists, King's College Publications, London, 2004. Написана для специалистов по информатике.

Ключевые концепции данной лекции

  • Многие программные схемы получают преимущества от механизма упаковки методов в объекты и хранения их для последующих вызовов. Соответствующие конструкции в языке могут быть названы "агентами", другие используемые термины — "делегаты" (в языке C#) и "замыкания".
  • Агент, обертывающий метод, может рассматриваться как любой другой объект, например, он может быть присвоен переменной и передан в любую часть программной структуры для вызова метода. Он может быть вызван в любое время через компонент, применимый ко всем агентам, который включает вызов ассоциированного метода, но контексту, где вызывается метод, нет необходимости знать, и обычно он и не знает, что за метод вызывается.
  • Агенты могут иметь любое число "открытых" операндов, соответствующих связанным переменным лямбда-выражения. Открытые операнды могут включать некоторые или все аргументы, так же как и цель. Закрытые аргументы (те, что не открыты) специфицируются при задании определения агента. Открытые операнды должны поставляться в форме кортежа при каждом вызове агента.
  • Агенты могут быть определены на основе существующего метода. Достаточно указать значения только закрытых операндов, если они есть. Чтобы избежать определения нового метода, когда его нет среди уже существующих, можно определить манифестный агент, написав непосредственно код метода в точке определения агента.
  • В языках программирования, не поддерживающих агентов или похожие механизмы, передача функций как данных требует использования многих обертывающих классов, или методов, передаваемых как аргументы, или указателей методов. Эти решения менее удобны, а в последнем случае и менее безопасны.
  • Теория лямбда-исчисления предоставляет нужную основу для понимания агентов.
  • Лямбда-выражение включает связанные переменные и определяет выражение (которое само может быть лямбда-выражением), включающее возможно связанные переменные, так же как и другие переменные, называемые свободными. Выражение представляет функцию. Применение функции к аргументам сводится к подстановке каждого аргумента для каждого вхождения соответствующей связанной переменной. В результате этого процесса, называемого бета-редукцией, создается новое выражение.
  • Связанные переменные лямбда-выражения являются произвольными именами. Они могут быть изменены при условии, что не создаются конфликты имен, в частности, конфликты с именами свободных переменных. Процесс переименования называется альфа-преобразованием (альфа-конверсией).
  • Каррингом функции из n аргументов называется специализация (задание значений) m аргументов (1 ≤ m ≤ n). В результате карринга функция из n аргументов трансформируется в функцию из n — m аргументов.

Новый словарь

Agent Агент

Beta-reduction Бета редукция

Closed operand Закрытый операнд

Closure Замыкание

Inline agent Манифестный агент

Lambda expression Лямбда-выражение

Nested class Вложенный класс

Open operand Открытый операнд

Operand Операнд

Prototype (C, C++) Прототип (С, С++)

Alpha-conversion Альфа-преобразование

Church-Rosser property Свойство Черча - Россера

First-class citizen Граждане первого класса

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

Many Little Wrappers pattern Образец "много маленьких оберток"

One-Song-Artist class Класс "певец одной песни"

Partial evaluation Частичное вычисление

Substitution (of a) Подстановка

variable in an expression) (переменной в выражение)

Упражнения

Словарь

Дайте определения терминам словаря.

Карта концепций

Добавьте новые концепции в карту, построенную в предыдущих лекциях.

Класс интегрирования без агентов

Смотри соответствующий раздел "Время программирования".

Объекты итераторы

Спроектируйте механизм итерирования, не использующий агентов, но основанный на классе LINEAR_ITERATOR, описывающем объекты, которые допускают итерирование специальной операцией на линейных структурах, подобных списку.

Итератор, стреляющий себе в ногу

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

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

Ручная оптимизация

Перепишите do_all итератор LINEAR так, чтобы он не применял манифестный кортеж как аргумент call, а использовал бы кортежную переменную t, которая заполнялась бы значениями перед каждым вызовом.Подсказка 1: вначале создайте объект-кортеж, затем присвойте значение.Подсказка 2: перечитайте разделы о свойствах кортежа, особенно тегах.

Посещение с агентами

Рассмотрите существующее множество классов, например, подмножество классов Traffic. Предположим, что программист может написать операцию visit, имеющую вариант для каждого из классов. Эти версии принимают целевой объект как аргумент. Цель упражнения состоит в определении компонента apply, который применяет подходящую visit операцию к любому такому объекту, переданному как аргумент без знания специфического типа (компонент apply может объявить этот аргумент имеющим тип ANY ).

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

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

Решение, а не только подсказку, можно найти в статье, которая посвящена компонентизации VISITOR, цитируемой при обсуждении проблемы.

Проблема Остановки с агентами

Спроектируйте более выразительное доказательство Проблемы Остановки, которое не использует никаких файлов, каталогов или строк, представляющих тексты программ. Работайте с программными элементами, передаваемыми как агенты.

Обращение карринга

Отмечалось, что карринг является взаимно обратной функцией. Напишите сигнатуру и определение функции uncurry, которой передается функция с одним аргументом f, чей результат — функция с одним аргументом. Функция uncurry должна возвращать функцию с двумя аргументами f такую, что f '= curry f ).

Условие бета-редукции

Покажите, что условие бета-редукции: [λx: X| exp](e) — "не должно быть свободных переменных e, появляющихся связанными в exp", сильнее, чем это фактически требуется для сохранения неформальной семантики редукции — применения функции к аргументам. Спроектируйте менее строгое, но все еще корректное условие.

Условие альфа-преобразования

Покажите, что условие альфа-преобразования: e \triangled \lambda x: X| exp в λ y: X| exp [x:= y] — "нет ни свободных, ни связанных вхождений y в e", сильнее, чем это фактически требуется для сохранения неформальной семантики редукции — применения функции к аргументам. Спроектируйте менее строгое, но все еще корректное условие.

< Лекция 6 || Лекция 7: 12345 || Лекция 8 >
Надежда Александрова
Надежда Александрова

Уточните пожалуйста, какие документы для этого необходимо предоставить с моей стороны. Курс "Объектно-ориентированное программирование и программная инженения". 

Юрий Симонов
Юрий Симонов
Россия, Москва, Московский Государственный Университет им. М.В. Ломоносова, 2011
Юрий Бедарев
Юрий Бедарев
Россия, Новосибирская область