Кодим на Python по-функціональномуПізнаємо силу функціональної парадигми програмування
Зміст статті
Які парадигми? Давайте кодити!
Коли тобі треба написати щось, то ти, напевно, найменше морочишся щодо того, яку парадигму програмування вибрати. Швидше, ти або вибираєш найбільш підходящу мову, або відразу починаєш кодувати на своєму улюбленому, що віддається перевагу і перевіреному роками. Воно й вірно, хай про ідеологію думають ідеологи, наша справа програмити :). І все-таки, програмуючи, ти обов'язково дотримуєшся якоїсь парадигми. Розглянемо приклад. Спробуємо написати щось просте… ну, наприклад, порахуємо площу кола.
Можна написати так:
Площа кола (варіант перший)
double area_of_circle(double r) return M_PI*pow(r,2); > int main() double r = 5; cout
Площа кола (варіант другий)
class Circle < double r; public: Circle(double r) < this->r = r; > double area() < return M_PI*pow(this->r,2); > void print_area() cout area() print_area();>
Можна й по-іншому… але як не намагайся, код буде або імперативним (як у першому випадку), або об'єктно-орієнтованим (як у другому). Це відбувається не через відсутність уяви, а просто тому, що C++ «заточений» під ці парадигми.
І найкраще (чи гірше, залежно від прямоти рук), що з його допомогою можна зробити – це змішати кілька парадигм.
Як ти вже здогадався, однією і тією ж мовою можна писати, дотримуючись кількох парадигм, причому іноді навіть декільком відразу. Розглянемо основні їхні представники, адже без цих знань ти ніколи не зможеш вважати себе професійним кодером, та й про роботу в команді тобі теж, швидше за все,доведеться забути.
Імперативне програмування
«Спочатку робимо це, потім це, потім ось це»
Мови: Майже всі
Абсолютно зрозуміла будь-якому програмісту парадигма: "Людина дає набір інструкцій машині". З імперативної парадигми всі починають вчити/розуміти програмування.
Функціональне програмування
«Вважаємо вираз і використовуємо результат для чогось ще».
Мови: Haskell, Erlang, F#
Абсолютно незрозуміла програмісту-початківцю парадигма. Ми описуємо не послідовність станів (як і імперативної парадигмі), а послідовність дій.
Об'єктно-орієнтоване програмування
"Обмінюємося повідомленнями між об'єктами, моделюючи взаємодії в реальному світі".
Мови: Майже всі
Об'єктно-орієнтована парадигма зі своєю появою міцно увійшла до нашого життя. На ООП побудовано практично всі сучасні бізнес-процеси.
Логічне програмування
«Відповідаємо питанням пошуком рішення».
Мови: Prolog
Логічне програмування – досить специфічна штука, але водночас цікава та інтуїтивно зрозуміла. Достатньо простого прикладу:
У той час, як кожен програміст за визначенням знайомий з імперативним та об'єктно-орієнтованим програмуванням, з функціональним програмуванням у чистому вигляді ми стикаємося рідко.
Функціональне програмування протиставляють імперативному.
Імперативне програмування передбачає послідовність змін стану програми, а змінні служать зберігання цього стану.
Функціональне програмування, навпаки, передбачає послідовність дій з даних. Це схоже на математику – ми довгопишемо на дошці формулу f(x), а потім підставляємо x і отримуємо результат.
І вся сіль функціонального програмування полягає в тому, що тут формула – це інструмент, який ми застосовуємо до ікса.
Дволикий пітон
Немає кращої теорії, ніж практика, тож давай вже щось напишемо. А ще краще – напишемо на пітоні:). Порахуємо суму квадратів елементів масиву «data» імперативно та функціонально:
Імперативний Пітон
data = [. ] sum = 0 для 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 і x або -x для x у xrange(10) if x%2==0]
Як ти вже зрозумів, необов'язково повністю слідувати функціональній парадигмі, достатньо вміло використовувати її у поєднанні з імперативною, щоб спростити собі життя. Проте я весь час говорив про імперативну парадигму... і нічого не сказав про ОВП та ФП.
Що ж, ООП – це фактично надбудова над імперативною парадигмою, і якщо ти перейшов від ІП до ООП, то наступним кроком має бути застосування ФП в ООП. На закінчення скажу кілька слів про рівень абстракції. Так ось, чим він вищий – тим краще і саме поєднання ОВП та ФП дає нам цей рівень.
На диск я поклав нові дистрибутиви пітона для віндусоїдів. Лінуксоїдам допомога не потрібна:).
Декілька хороших ресурсів для тих, кому хочеться дізнатися більше:
Якщо тобі не сподобався пітон, то не засмучуйся – ти можеш успішно застосовувати ідеї функціонального програмування та в інших мовах високого рівня.