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

Ако сте посещавали лекции във ФМИ, несъмнено знаете че съществуват и по-екзотични начини за писане на код от Java и C#. Един от тях е функционалното програмиране. Характерно за него са „функции от по-висок ред“ – такива чиито аргументи или връщани стойности също са функции. Един от първите и най-известни езици за функционално програмиране е LISP. Той предлага някои много елегантни решения на доста проблеми, но едва ли ще се сблъскате с него в днешни дни, освен ако не учите във ФМИ или не пишете макроси за emacs. Въпреки, че едва ли някой пише цели приложения на LISP в днешни дни, някои от идеите на функционалното програмирне са съвсем приложими в „модерните“ езици… Не става въпрос просто за функции от по-висок ред – указатели към функции има още в C. Там най-често те се ползват за callback. В обектно-ориентираните езици тези неща стават по-добре с предаването на обект, който имплементира определен интерфейс/наследява определен клас, което се поддържа доста по-лесно. Готиното нещо в LISP са closure-ите – анонимни функции, които имат достъп до променливите на блока, в който са дефинирани. Тази простичка идея прави някои неща много по-лесни за изразяване. Едно от тях е работата със списъци. Например:

people = ["Eddie Vedder", "Chris Cornell", "Mark Arm", "Layne Staley"]
family_names = people.map() { |person| person.split(‘ ‘)[1] }

people е масив, а map е негов метод, който взема анонимна функция. Самата тя се дефинира с { |person| person.split(' ')[1] } – взема един аргумент perosn, разцепва го на думи и връща втората. map работи така – изпълнява приетата функция за всеки елемент от масива на който е извикана и връща нов масив, който съдържа върнатите стойности.

Има логика, нали? Кода се чете лесно – „map-ни всеки елемент от people със person.split(' ')[1]„. Функцията map идва от LISP. Друга такава е filter (в ruby се казва find_all), която връща нов списък, съдържащ тези елементи, за които подадената функция върне true. Ако искаме да изведем само тези фамилии, които имат шест символа, може да се направи така:

result = people.map() { |p| p.split(‘ ‘)[1] }.find_all() { |f| f.length() == 6 }

Традиционния вариант изглежда така: :ruby: result = [] for person in people do family_name = p.split(‘ ‘)[1] if family_name.length() == 6 do result << family_name end end

Предимствата? Първия вариант е по-кратък, по-прост и съответно се чете много по-лесно. Също така, е по-близко до това което искате да направите – чете се като „трансформирайте имената до фамилии и оставете шестбуквените“, за разлика от втория, който звучи по-скоро като „създай нов списък, обходи иметата, извади фамилии, провери дали фамилиите са шестбуквени и ако да, запиши ги в новосъздадения списък“. Цялата идея за предаване на функция, която да се изпълни за всеки елемент е много блага и лесна за прочит.

Освен работата със списъци, има още един случай в които closure-ите са ужасно полезни – когато имате някакъв стандартен try/catch блок, който искате да презползвате. Класическия пример за това е работата в файлове:

File.open(‘foo.txt’, ‘w’) do |my_file|
  my_file.write(‘Stuff!’)
  # …
end

Когато взема блок код, функцията open работи по следния начин – отваря съответния файл и изпълнява кода, предавайки отворения файл като аргумент (в случая като my_file). След изхода му, бил той нормален или с изключение, open затваря файл. Така няма да забравите да освободите ресурса, а и кода отново е по-близо до замисъла ви – „направи с този файл това“, вместо „отвори файла, опитай да направиш това, finally затвори файла“. Други примери за употреба на такива блокове са връзки към бази данни и транзакции – вярвам, че няма нужда да давам пример.

Има редица други случаи, в които анонимните функции са елегантно решение, но това са двата pattern-на които най-често срещам. До каква степен се поддържат те в съвременните езици? Perl си има code references и много от нещата от CPAN ги ползват по такъв начин – например DBIx::Class за транзакции или вградените функции map, sort и grep. Ruby практически е наполовина език за функционално програмиране – цялата стандартна библиотека използва корутини много активно. В Python механизма не е чак толкова мощен – има няколко различни feature-а, които заедно покриват по-голяма част от полезните инструменти на функционалното програмиране, но липсват цялостни анонимни функци. Естествено, в PHP положението е трагично – там трябва да предавате стрингове с имена на функции, но и начина за създаване е трагичен. Closure-и няма, защото и без това scope-овете са два – локален и глобален. Java-та има нещо подобно с анонимни вътрешни класове, макар че за следващата версия може да доживеем да ги видим в по-приличен вид. В C# пък има, using израз, който е идеален за try/catch блокове. Чувам от някои хора, че Mircosoft били наели много от разработчиците на haskell, което ме кара да се надявам, че ще видим доста мощен функционален апарат в следващия .NET.

Според мен функционалното елементи могат много да допринесат в съвременното програмиране. Те ни дават по-малко код, който се чете по-лесно и е по-близо до идеята, с която е написан. Разбира се, изисква малко да разчупите начина си на мислене, за да ви хрумне да замените:

:ruby
for user in users do
  if user.invalid?() do
    databaseIntegrityCheck()
  end
end

със

if users.any?() { |user| user.invalid?() } do
  databaseIntegrityCheck()
end

И все пак, това което получавате в замяна си заслужава. Горещо ви препоръчвам да разгледате функционалните способности на езика, който използвате. Дори да ви изглежда малко странно на пръв поглед, вярвам че въобще няма да се замисляте веднъж като му хванете цаката.

7 thoughts on “Функционално програмиране

  1. А, не си прав, че в PHP не може да си направиш closure. Тъй като тялото на функцията в

    create_function
    се задава като низ, то спокойно можеш да ползваш заместването на променливите и да си свършиш работата:

    <pre>$type = &#39;baba&#39;;
    ...
    $a = array_filter($a, create_function(&#39;$x&#39;,
        &quot;return (\$x-&gt;type == &#39;$type&#39;)&quot;));
    </pre>

  2. Убаво е да се спомене и JavaScript-а (Или по-скоро ECMAScript) като език с възможности в областта. Пол Греъм му вика „Lisp in C clothes“. Специално prototype библиотеката ти дава всичките благинки като array#map, array#each и тн. Напоследък много чета Raganwald – има интересни работи по темата.

  3. този блог пее за код и гръндж! започвам да го харесвам 🙂

  4. Това е много силно казано closure, защото промени в променливата няма да останат забелязани. Да не говорим, че reference към обект дори не можеш да си помислиш да подадеш. Така че се придържам към тезата си – PHP няма closure-и.

  5. Не подценявай ц++, езика се променя и е коренно различен от преди 10 години. И има анонимни функции(lambda).

  6. C++ върви напред главно заради Boost. Даже има сериозен шанс (още не се случило) Boost да стане част от C++ стандарта.

    Радващо е, че можем да пишем много бърз с високо ниво на абстракция, но това също си има цена. Основната цел на функционалното програмиране е хем да пишем код много по-бързо (заради абстракцията), хем той да е по-четим, разбираем и ествествен.

    C++ & Boost успява само във второто и само донякъде.

    Първо, покрай всичките template-и синтаксисът се натоварва неимоверно много с ъглови скоби, които, не, не могат да се разглеждат като аналог на скобите в lisp. В C++ те нямат същата структурна роля и по-скоро пречат на четимостта.

    И второ – програмите се компилират много бавно. Освен, че се компилират, което от своя страна ни забавя доста, те се и компилират бавно. Това означава, че дори да сме си автоматизирали процеса по запазване/компилиране/изпълнение, то пак ще губим много време да чакаме препроцесора да се справи с бесните template-и.

    И все пак идеята за high-order C++ не е толкова лоша 🙂 Просто има по-ограничена среда на използване – в приложенията, за които бързодесйтвието е наистина критично. А все повече приложения не са точно такива…

Вашият коментар

Вашият имейл адрес няма да бъде публикуван. Задължителните полета са отбелязани с *