Язык Python не зря пользуется популярностью в среде программистов Гугла и редакторов Хакера одновременно :). Этот поистине мощный язык позволяет писать код, следуя нескольким парадигмам, и сегодня мы попробуем разобраться, в чем же между ними разница и какой из них лучше следовать.

 

Какие парадигмы?! Давайте кодить!

Когда тебе надо написать что-то, то ты, наверное, меньше всего заморачиваешься относительно того, какую парадигму программирования выбрать. Скорее, ты либо выбираешь наиболее подходящий язык, либо сразу начинаешь кодить на своем любимом, предпочитаемом и годами проверенном. Оно и верно, пусть об идеологии думают идеологи, наше дело – программить :). И все-таки, программируя, ты обязательно следуешь какой-либо парадигме. Рассмотрим пример. Попробуем написать что-нибудь простое… ну, например, посчитаем площадь круга.

Можно написать так:

Площадь круга (вариант первый)

double area_of_circle(double r) {
return M_PI*pow(r,2);
}
int main() {
double r = 5;
cout << "Площадь: "<< area_of_circle(r)<< endl;
}

А можно и так:

Площадь круга (вариант второй)

class Circle {
double r;
public:
Circle(double r) { this->r = r; }
double area() { return M_PI*pow(this->r,2); }
void print_area() {
cout << "Площадь: "<< this->area() << endl;
}
};
int main() {(new Circle(5))->print_area();}

Можно и по-другому… но только как не старайся, код будет или императивным (как в первом случае), или объектно-ориентированным (как во втором).
Это происходит не из-за отсутствия воображения, а просто потому, что C++ «заточен» под эти парадигмы.

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

 

Парадигмы

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

 

Императивное программирование

«Сначала делаем это, потом это, затем вот это»

Языки: Почти все

Абсолютно понятная любому программисту парадигма: «Человек дает набор инструкций машине».
С императивной парадигмы все начинают учить/понимать программирование.

 

Функциональное программирование

«Считаем выражение и используем результат для чего-нибудь еще».

Языки: Haskell, Erlang, F#

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

 

Объектно-ориентированное программирование

«Обмениваемся сообщениями между объектами, моделируя взаимодействия в реальном мире».

Языки: Почти все

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

 

Логическое программирование

«Отвечаем на вопрос поиском решения».

Языки: Prolog

Логическое программирование – довольно специфическая штука, но, в то же время, интересная и интуитивно понятная.
Достаточно простого примера:

{задаем правила}
witch(X) <= burns(X) and female(X).
burns(X) <= wooden(X).
wooden(X) <= floats(X).
floats(X) <= sameweight(duck, X).
{задаем наблюдения}
female(girl).
sameweight(duck,girl).
{задаем вопрос}
? witch(girl).

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

Функциональное программирование противопоставляют императивному.

Императивное программирование подразумевает последовательность изменений состояния программы, а переменные служат для хранения этого состояния.

Функциональное программирование, наоборот, предусматривает последовательность действий над данными. Это сродни математике – мы долго пишем на доске формулу f(x), а потом подставляем x и получаем результат.

И вся соль функционального программирования в том, что здесь формула – это инструмент, который мы применяем к иксу.

 

Двуликий питон

Нет лучшей теории, чем практика, так что давай уже что-нибудь напишем. А еще лучше – напишем на питоне :).
Посчитаем сумму квадратов элементов массива «data» императивно и функционально:

Императивный Питон

data = [...]
sum = 0
for element in a:
sum += element ** 2
print sum

Функциональный Питон

data = [...]
sq = lambda x: x**2
sum = lambda x,y: x+y
print reduce(sum, map(sq, data))

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

Но если сами «функционалы» и не получили широкого распространения, то отдельные идеи перекочевали из них в скриптинговые (и не только) языки программирования.
Оказалось, что совершенно необязательно писать полностью функциональный код, достаточно украсить императивный код элементами функционального.

 

Питон в действии

Оказывается, концепции ФП реализованы в Питоне более чем изящно. Ознакомимся с ними подробнее.

 

?-исчисления

Lambda исчисления – это математическая концепция, которая подразумевает, что функции могут принимать в качестве аргументов и возвращать другие функции.
Такие функции называются функциями высших порядков. ?-исчисления основываются на двух операциях: аппликация и абстракция.
Я уже привел пример аппликации в предыдущем листинге. Функции map, reduce – это и есть те самые функции высших порядков, которые «апплицируют», или применяют, переданную в качестве аргумента функцию к каждому элементу списка (для map) или каждой последовательной паре элементов списка (для reduce).

Что касается абстракции – здесь наоборот, функции создают новые функции на основе своих аргументов.

 

Lambda-абстракция

def add(n):
return lambda x: x + n

adds = [add(x) for x in xrange(100)]

adds[34](5)

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

Замыкание – это определение функции, зависящей от внутреннего состояния другой функции. В нашем примере это lambda x. С помощью этого приема мы делаем что-то похожее на использование глобальных переменных, только на локальном уровне.

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

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

 

Чистые функции и ленивый компилятор

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

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

Как мы уже выяснили, в функциональной парадигме можно распоряжаться функциями как угодно. Но больше всего выгоды мы получаем, когда пишем «чистые функции». Чистая функция – это функция без побочных эффектов, а значит, она не зависит от своего окружения и не изменяет его состояния.

Применение чистых функций дает нам ряд преимуществ:

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

Для увеличения производительности в ФП также используются ленивые вычисления. Яркий пример:

print length([5, 4/0, 3+2])

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

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

 

Списочные выражения и условные операторы

Чтобы жизнь (и программирование) не казались тебе медом, разработчики питона придумали специальный «подслащающий» синтаксис, который буржуи так и называют – «syntactic sugar».
Он позволяет избавиться от условных операторов и циклов… ну, если не избавиться, то уж точно свести к минимуму.

В принципе, ты его уже видел в предыдущем примере – это adds = [add(x) for x in xrange(100)]. Здесь мы сразу создаем и инициализируем список значениями функций. Удобно, правда?
Еще есть такая штука, как операторы and и or, которые позволяют обходиться без громоздких конструкций типа if-elif-else.

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

 

Императивный код

L = []
for x in xrange(10):
if x % 2 == 0:
if x**2>=50:
L.append(x)
else:
L.append(-x)
print L

 

Функциональный код

print [x**2>=50 and x or -x for x in xrange(10) if x%2==0]

 

Итоги

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

Что ж, ООП – это, фактически, надстройка над императивной парадигмой, и если ты перешел от ИП к ООП, то следующим шагом должно быть применение ФП в ООП. В заключение скажу пару слов об уровне абстракции. Так вот, чем он выше – тем лучше и именно сочетание ООП и ФП дает нам этот уровень.

 

CD

На диск я положил свежие дистрибутивы питона для виндусоидов. Линуксоидам помощь не нужна :).

 

WWW

Несколько хороших ресурсов для тех, кому хочется узнать больше:

 

INFO

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

1 комментарий

  1. iamnotaprogrammer

    03.01.2017 at 13:39

    После прочтения осталось несколько вопросов:
    print length([5, 4/0, 3+2])
    length — это самописная функция или опечатка должно быть len?
    ну и если len то python2 выдает ZeroDivisionError
    что автор имел в виду?

Оставить мнение

Check Also

Кеш-атаки по сторонним каналам. Что произошло в области утечек на аппаратном уровне за последние два года

Несмотря на то что до 2016 года существовало лишь несколько публикаций о кеш-атаках на сма…