Беларусь, рогачёв |
Функции
Разгадка парадоксов
Единственное решение всех загадок, которые мы только что сами себе загадали, состоит вот в чем. Раз генерируемая функция имеет доступ к контексту вызова генерирующей; и раз неизвестно, какие именно локальные переменные (или аргументы) из этого контекста ей понадобятся - то сохранять необходимо весь контекст! Иными словами, раз функция genFunc в операторе return возвратила объект-функцию, то контекст вызова genFunc не будет уничтожен до тех пор, пока сгенерированный объект-функция существует! Это, конечно, может привести к изрядному расходу памяти в том случае, когда последовательность функций, генерирующих друг друга, достаточно длинна. Ведь сохранять придется все их контексты, вплоть до самого первого! (К тому же, в некоторых случаях вместо цепочки мы будем иметь дело с деревом или даже более общего вида графом.) Тем не менее, никаких ограничений на генерацию функций в ActionScript не накладывается. Видимо, авторы языка справедливо посчитали, что в тех (в общем-то, довольно редких) случаях, когда без многоступенчатой генерации функций не обойтись, полученный выигрыш в гибкости намного превзойдет сложности, связанные с дополнительными затратами памяти (да и не так уж много ее тратится, все-таки). Так что теперь самое время продемонстрировать наиболее впечатляющие "фокусы", возможность проделывать которые нам дает механизм многоступенчатой генерации функций.
Использование генераторов функций
Одна из наиболее показательных задач - это построение сложной функции из более простых на основе действий пользователя (задача, часто встречающаяся при разработке образовательных продуктов по математике). Предположим, что пользователю предоставлена таблица достаточно простых и распространенных функций, которые он может складывать, умножать, вычитать, делить, возводить в степени, применять друг к другу и т.д. Затем программа должна построить график получившейся функции (или просто вывести значения этой функции в нескольких точках). Сейчас мы на крайне упрощенном примере, с очень ограниченным набором функций и их преобразований, увидим, как применяются в этом случае генераторы функций.
// Сначала определяем три генератора: // сложение функций _global.funcPlus = function(func1, func2){ return function(x){return func1(x) + func2(x);}; } // умножение функций _global.funcTimes = function(func1, func2){ return function(x){return func1(x)*func2(x);}; } // композиция функций _global.funcComposition = function(func1, func2){ return function(x){return func1(func2(x));}; } // и формируем массивы генераторов и обычных функций var operators_array = [funcPlus, funcTimes, funcComposition]; var functions_array = [function(x){return 1/x;}, Math.sin, Math.exp]; // Массив instructions_array, передаваемый в следующую функцию, // содержит указания пользователя - какие операции с какими // функциями производить. Этот двумерный массив, содержит // много субмассивов из двух элементов. Первый элемент - // операция, второй - одна из функций. _global.processInstructions = function(start_func, instruc- tions_array){ var func = start_func; for (var i=0; i<instructions_array.length; i++){ func = operators_array[instructions_array[i][0]]( func, functions_array[instructions_array[i][1]] ); } return func; } var start_func1 = function(x){return x*x;} // Этот массив должен был бы генерироваться на основе // действий пользователя var instriction_set1 = [[1,1], [2,0]]; // Описывает функцию sin(1/x)/(x*x) var resultFunc = processInstructions(start_func1, instriction_set1); trace("resultFunc(0.5) = " + resultFunc(0.5)); trace("resultFunc(0.1) = " + resultFunc(0.1)); // Протестируем, действительно ли сгенерировано sin(1/x)/(x*x) testFunc = function(x){return Math.sin(1/x)/(x*x);} trace("testFunc(0.5) = " + testFunc(0.5)); trace("testFunc(0.5) = " + testFunc(0.1));
На выходе этой программы получаем:
resultFunc(0.5) = 3.63718970730273 resultFunc(0.1) = -54.402111088937 testFunc(0.5) = 3.63718970730273 testFunc(0.5) = -54.402111088937
Tо есть генераторы функций в данном случае работают именно так, как мы того ожидали.
Что лучше: генератор или функция с параметром?
Этот вопрос напоминает другой, о том, кто сильнее, слон или кит. Тем не менее, поразмышляем над ним. Генератор функций, как мы уже видели, тоже дает возможность параметризовать (сгенерированную) функцию. Однако, если использовать его только в целях параметризации, то приходится все-таки, писать две функции: и генерирующую (хоть и совсем простую), и генерируемую. В случае функции с параметром достаточно одной. По этой же причине функции с параметром легче укладываются в голове. Таким образом, если вам нужна только параметризация (и параметр у вас один, в крайнем случае - два) - скорее всего, вас устроит именно функция с параметром, прикрепленным в качестве поля объекта-функции. Если у вас параметров штук пять (например, коэффициент ы полинома) - уже имеет смысл подумать о генераторе. Ведь из аргументов генератора параметры доставать удобнее, чем из полей объекта-функции. Впрочем, в случае полинома можно обойтись и одним полем в виде массива. Но уж если у вас возникает задача, вроде описанной выше задачи построения произвольных функций, - генераторы, безусловно, лучшее, что здесь можно придумать.
Реализация приватных полей во Флэш МХ
Напоследок рассмотрим совсем необычный способ применения эффекта сохранения контекста генератора при генерации функций. С++ и Java-программистов может раздражать то, что при работе на ActionScript (в отличие от ActionScript 2) нет возможности скрыть от пользователя детали реализации. Всегда существует опасность, что не в меру любопытный и самоуверенный пользователь начнет менять значения полей наших объектов, доступ к которым мы не хотим ему предоставлять. Конечно, во Флэше существуют свойства. То есть у объекта можно сделать фиктивные поля, при записи в которые будет вызываться соответствующая функция. Но ведь полученную этой функцией информацию нужно где-то хранить. И, желательно, хранить в таком месте, до которого добраться просто так нельзя. Так вот, если функции для получения и уст ановки значений свойства генерировать другой специальной функцией, то полученные функции будут иметь доступ к контексту вызова генератора. Там-то мы и спрячем наше поле! (И, кстати, делать именно свойство нам совершенно необязательно. Мы вполне можем удовольствоваться приватным полем и функциями для доступа к нему. Так мы и поступим - чтобы работа с нашим приватным полем была максимально похожа на то, что мы писали бы на C++ или Java.) Вот код, выражающий эти идеи.
_global.makePrivateField = function(getter, newGetterName, setter, newSetterName){ var field; this[newGetterName] = function(){ return getter.call(this, field); } this[newSetterName] = function(val){ field = setter.call(this, val); } } car = {}; makePrivateField.call( car, function(wheelsN){ // Функция - getter return "Wheels number = " + wheelsN; }, "getWheelsNumber", function(wheelsN){ // Функция - setter return Math.round(wheelsN); }, "setWheelsNumber" ); car.setWheelsNumber(3.8); trace(car.getWheelsNumber());
Возвращает такой код строчку
Wheels number = 4
Выведя в отладочное окно список переменных программы ( Ctrl+Alt+V ), мы получим следующее:
Global Variables: Variable _global.makePrivateField = [function 'makePrivateField'] Level #0: Variable _level0.$version = "WIN 6,0,21,0" Variable _level0.car = [object #2, class 'Object'] { getWheelsNumber:[function 'getWheelsNumber'], setWheelsNumber:[function 'setWheelsNumber'] }
Мы видим, что в объекте car нет никаких видимых полей, зато появились функции доступа к созданному нами приватному полю. Правда, особо злостный нарушитель порядка может заменить эти наши функции своими. Однако и здесь можно помочь делу - если вы очень хотите защитить ваши объекты от вмешательства, вы можете запретить изменение переменных, ссылающихся на объекты-функции доступа к приватному полю. Делается, это, правда, только с помощью недокументированных функций, но, тем не менее, возможность такая есть. А о деталях вы узнаете в следующей лекции.