Московский физико-технический институт
Опубликован: 23.12.2005 | Доступ: свободный | Студентов: 2868 / 252 | Оценка: 4.61 / 4.44 | Длительность: 27:18:00
ISBN: 978-5-9556-0051-2
Лекция 5:

Функции

Разгадка парадоксов

Единственное решение всех загадок, которые мы только что сами себе загадали, состоит вот в чем. Раз генерируемая функция имеет доступ к контексту вызова генерирующей; и раз неизвестно, какие именно локальные переменные (или аргументы) из этого контекста ей понадобятся - то сохранять необходимо весь контекст! Иными словами, раз функция 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 нет никаких видимых полей, зато появились функции доступа к созданному нами приватному полю. Правда, особо злостный нарушитель порядка может заменить эти наши функции своими. Однако и здесь можно помочь делу - если вы очень хотите защитить ваши объекты от вмешательства, вы можете запретить изменение переменных, ссылающихся на объекты-функции доступа к приватному полю. Делается, это, правда, только с помощью недокументированных функций, но, тем не менее, возможность такая есть. А о деталях вы узнаете в следующей лекции.