Tuesday, February 3, 2009

Функциональное программирование в “не функциональных” языках: ленивые вычисления

Сейчас модно говорить о ФП – что это такое, зачем и как его используют. Мне же хотелось бы рассматривать ФП как некоторый способ более эффективно реализовывать поставленную задачу. Т.е. не как просто новомодный (хотя ФП уже старо как мир) финт ушами, а как реальное практическое средство.

Один из элементов ФП, позволяющих эффективно его использовать являются “ленивые вычисления”. Принцип такой – зачем вычислять то, что возможно нам не понадобится.

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

Допустим у нас есть задача: написать функцию, принимающую три числовых параметра  (a, b, c), и возвращающую сумму a+b если a>0 и a+c если a<0.

Простейшая реализация (C#):

int function(int a, int b, int c) 
{
return a+a>0?b:c;
}


Но при вызове этой функции нам понадобится передать все три параметра. Но ведь реально нужны только 2 (в зависимости от значения первого или второго параметра). А если вычисление значений каждого из параметров является трудоёмка задача (выполнение запросов к базе данных, вычитка из большого документа или просто тяжело-вычисляемое значение), то нет смысла его делать.



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



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



Теперь небольшой пример из совсем почти не функционального языка – C# 2.0:



Для начала перепишем функцию так, чтоб она оперировала с функциями (“делегатами”):



        private delegate int IntFunction();
private static IntFunction funct(IntFunction a, IntFunction b, IntFunction c)
{
return delegate
{
int aValue = a(); return aValue > 0 ? aValue + b() : aValue + c();
};
}


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



        private static IntFunction CreateInt(int a)
{
return delegate
{
Console.WriteLine("Computing for long..long time, resulting "+a);
return a;
};
}


Ну а теперь сам вызов:



Console.WriteLine("Result is {0}",funct(CreateInt(1), CreateInt(2), CreateInt(3))());


В результате получим:



Computing for long..long time, resulting 1
Computing for long..long time, resulting 2
Result is 3


Как видно,  функция, возвращающая “3” даже не была запущенна.



Ну и само-собой, можно передавать результат выполнения функции как параметр для самой же функции, что позволяет выстраивать конструкции по типу:



Console.WriteLine("Result is {0}", funct(funct(CreateInt(1), CreateInt(2), CreateInt(3)), CreateInt(4), CreateInt(5))());


В результате которых будет  реально толь3о 3 раза вызвана наша “долгая” функция, вместо 5, как было бы при обычном подходе.



Вот такой простой и имхо понятный пример того, как можно понять смысл использования “ленивых вычислений”, и использовать в “не функциональных” языках.