Кодим на Python по-функциональному: Познаем силу функциональной парадигмы программирования
Язык Python не зря пользуется популярностью в среде программистов Гугла и редакторов Хакера одновременно :). Этот поистине мощный язык позволяет писать код, следуя нескольким парадигмам, и сегодня мы попробуем разобраться, в чем же между ними разница и какой из них лучше следовать.
Какие парадигмы?! Давайте кодить!
Когда тебе надо написать что-то, то ты, наверное, меньше всего заморачиваешься относительно того, какую парадигму программирования выбрать. Скорее, ты либо выбираешь наиболее подходящий язык, либо сразу начинаешь кодить на своем любимом, предпочитаемом и годами проверенном. Оно и верно, пусть об идеологии думают идеологи, наше дело – программить :). И все-таки, программируя, ты обязательно следуешь какой-либо парадигме. Рассмотрим пример. Попробуем написать что-нибудь простое. ну, например, посчитаем площадь круга.
Можно написать так:
Площадь круга (вариант первый)
Площадь круга (вариант второй)
Можно и по-другому. но только как не старайся, код будет или императивным (как в первом случае), или объектно-ориентированным (как во втором).
Это происходит не из-за отсутствия воображения, а просто потому, что C++ «заточен» под эти парадигмы.
И лучшее (или худшее, в зависимости от прямоты рук), что с его помощью можно сделать – это смешать несколько парадигм.
Парадигмы
Как ты уже догадался, на одном и том же языке можно писать, следуя нескольким парадигмам, причем иногда даже нескольким сразу. Рассмотрим основные их представители, ведь без этих знаний ты никогда не сможешь считать себя профессиональным кодером, да и о работе в команде тебе тоже, скорее всего, придется забыть.
Императивное программирование
«Сначала делаем это, потом это, затем вот это»
Языки: Почти все
Абсолютно понятная любому программисту парадигма: «Человек дает набор инструкций машине».
С императивной парадигмы все начинают учить/понимать программирование.
Функциональное программирование
«Считаем выражение и используем результат для чего-нибудь еще».
Языки: Haskell, Erlang, F#
Абсолютно непонятная начинающему программисту парадигма. Мы описываем не последовательность состояний (как в императивной парадигме), а последовательность действий.
Объектно-ориентированное программирование
«Обмениваемся сообщениями между объектами, моделируя взаимодействия в реальном мире».
Языки: Почти все
Объектно-ориентированная парадигма со своим появлением прочно вошла в нашу жизнь.
На ООП построены практически все современные бизнес-процессы.
Логическое программирование
«Отвечаем на вопрос поиском решения».
Языки: Prolog
Логическое программирование – довольно специфическая штука, но, в то же время, интересная и интуитивно понятная.
Достаточно простого примера:
В то время, как каждый программист по определению знаком с императивным и объектно-ориентированным программированием, с функциональным программированием в чистом виде мы сталкиваемся редко.
Функциональное программирование противопоставляют императивному.
Императивное программирование подразумевает последовательность изменений состояния программы, а переменные служат для хранения этого состояния.
Функциональное программирование, наоборот, предусматривает последовательность действий над данными. Это сродни математике – мы долго пишем на доске формулу 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)]
Здесь мы создали список функций, каждая из которых прибавляет к аргументу определенное число.
В этом маленьком примерчике также уместилась еще пара интересных определений функционального программирования – замыкание и карринг.
Замыкание – это определение функции, зависящей от внутреннего состояния другой функции. В нашем примере это 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
Несколько хороших ресурсов для тех, кому хочется узнать больше:
- http://www.python.org
- http://en.wikipedia.org/wiki/Programming_paradigm
- http://www.ibm.com/developerworks/library/l-prog.html
INFO
Если тебе не приглянулся питон, то не расстраивайся – ты можешь успешно применять идеи функционального программирования и в других языках высокого уровня.