Jump to content

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

Оцени ову тему


Juanito

Препоручена порука

Мотивација

 

Кад програмер напише нешто у стилу

a = 5
b = 3
c = a + b

рачунар то отприлике овако преводи и одрађује: 1) иди на локацију а у меморији и покупи шта тамо нађеш; 2) уради исто то на локацији b; 3) сабери вредности са претходних локација; 4) резултат упиши на локацију c.

 

Историја програмских језика је историја сакривања ових ружних детаља иза прекидача. Уместо да спајамо и растављамо жице да бисмо упалили и угасили сијалицу, можемо једноставно да притиснемо прекидач. Или да га фино подесимо на жељену јачину светла. Или да пљеснемо дланом о длан кад пожелимо светло. Или пак да замолимо нашег дигиталног асистена да светло упали за нас када паркирамо ауто у гаражи и исључи кад напустимо кућу. Тако је некако и у програмским језицима. Имамо променљиве, контролне структуре, процедуре, класе и разне друге могуће и немогуће апстракције, које скривају меморијске адресе и остале але и аждаје. Упркос томе, многи делови просечних програма и даље изгледају као да их је писао компјутер, а не човек. Ако на пример имамо листу имена и листу презимена, и желимо да направимо нову листу која садржи све могуће комбинације истих, а одрасли смо у свету потомака C језика, обично прва ствар која ће нам пасти напамет изгледа овако некако:

func makeNames(names: [String], surnames: [String]) -> [String] {
    var result = [String]()
    for name in names {
        for surname in surnames {
            result += ["\(name) \(surname)"]
        }
    }
    return result
}

Шта ми овде говоримо рачунару? Направи низ у кога ћеш да пакујеш резултате, како надолазе. Прочитај прво име из листе имена. Прочитај презиме из листе презимена, спој га са тренутним именом и резултат упиши у резултујући низ. Пређи на следеће презиме и понови то исто. Кад завршиш са презименима, пређе на слeдеће име, па опет све из почетка. Кад све завршиш, врати ми резултат. Ово изгледа као инструкције малом детету, које по први пут учи како да одради неки посао. ”Иди до оне беле кутије која се зове фрижидер, погледај доњу полицу, узми црни метални ваљак, затвори фрижидер и донеси ваљак тати.” А све што желимо да кажемо је: ”Дедер, донеси тати један Гинис!” Овај пешачки приступ, поред тога што открива небитне детаље, има једну велику ману - није баш пријатељски настројен према порасту броја листа. Шта ако имамо четири листе, две са именима и две са презименима, и желимо да из тих листа направимо сва могућа шпанска имена? Требаће нам нова функција са четири петље. А шта ако имамо десет или петнаест листа? Постоји нешто што је веома заједничко свим тим потенцијалним функцијама које бисмо морали да напишемо - све оне узимају одређен број листa и онда некако комбинују по један елемент из сваке листе, док не истроше све елементе и не врате нову листу са резултатима. Оно што бисмо желели је да напишемо једну функцију или једну ”конструкцију” која ће да ради било шта, на било ком броју листа. Желимо нешто овако:

result = doSomething list1 list2 list3 list4 list5... listN

Постоје различити начини да се овај проблем реши и они су у многоме условљени конкретним програмским језиком. Приступ кога бих овде желео да покажем је развијен у оквиру академске заједнице, али постаје све популарнији у "mainstream" језицима. Не само да су се појавили језици који су специјално прављени за тај стил програмирања (Scala, F#...), већ и они језици који су били све, само не функционалниу својим новијим инкарнацијама добијају алаткице за олакшану имплементацију функционалних техника (Java 8, C++11 и новији). Функционално програмирање је по својој природи декларативно - програмира се тако што се рачунару каже шта желимо да уради, а не како.

 

Скривена лепота функционалног програмирања је у томе што је оно засновано на математици, а математика тежи да све генерализује. У овом уводном посту сам за пример узео листе или низове и задржаћу се подуже на њима, јер су у питању врло конкретни објекти, познати и лако схватљиви сваком програмеру. Али све што ћемо рећи за листе важи за читаву класу ојеката, од којих су неки потпуно апстрактни. Фора је у томе што не морате да знате математичке детаље да бисте се овим лепотама користили. Довољно је интуитивно препознати који објекат је сличан листи, а у томе нам може помоћи и синтакса језика. На пример типови List<T> или Array<T> изгледају слично као SomeFancyAbstractConcept<T> и та сличност није случајна и није само површинска. Ја ћу се трудити да математичке детаље потпуно заобиђем, осим можда као коментаре, које можете потпуно прескочити. Циљ ми је да покажем како се разне функционалне технике могу користити у свакодневном досадном раду, који доноси новац и да нису само пука занимација залудних професора.  :)

 

Циљ овог писанија је да самом себи разјасним неке ствари, а кажу да се најбоље себи разјашњава тако што се покуша објаснити неком другом. Поред тога, лепо је делити ствари који те се свиде, а мени се ФП јако свидело. Језик кога ћу користити за илсутрације ће бити углавном Swift, али то је потпуно небитно. Битан је концепт. Ко жели да се игра са овим техникама, а нема Mac, може да проба http://fsharp.org или http://www.scala-lang.org . Или можда https://www.haskell.org ,  који је сува лепота и елеганција, иако се од њега нећете баш финансијски овајдити, али ћете оно што у њему научите моћи (барем делимично) да примените у овим претходним језицима, од којих можете леба да једете. :) Можда и грешим, али нешто ми говори да ће се за коју годину на тржишту ценити знање ФП далеко више него што је то до сада био случај.

Link to comment
Подели на овим сајтовима

Ja sam Scala ucio. Izuzetno mocan jezik, ali je mnogo cudno programirati u func. jezicima na pocetku. S obzirom da mi je to prvi func. jezik u  zivotu, bas mi je cudno bilo. Ali zato mnoogoo znaci, pogotovu za java8, gde su uveli elemente func. programiranja, tako da se slazem sa Juan-om po pitanju potencijalne buduce potraznje za znanjem func. jezika :D

Link to comment
Подели на овим сајтовима

У самом центру функционалног програмирања је, гле чуда, функција. :) Каква функција? Често се у програмирању користе овакве ствари, које се називају функцијама:

func sum(a: Int, b: Int) -> Int {
    launchSpaceShuttle()
    return a + b
}

Потпис функције нам каже да она прима два цела броја и враћа нови цео број. Име функције нам каже да функција одрађује сумирање датих јој бројева. И тако ми наивни позовемо ову функцију, да израчунамо колико смо овог месеца потрошили на пиво, кад оно међутим:

 

Launch_of_the_Space_Shuttle_Atlantis.jpg

 

Транспарентно ко Вучићеви уговори с Арапима. Функционално програмирање тежи чистијем рачуну и дугој љубави, те избегава лансирање шатла, осим ако то није јасно и гласно наглашено. Неки језици нетранспарентно лансирање забрањују на нивоу компајлера.

 

Елем, када кажемо да је у центру и на самом почетку наше приче функција, мислимо на поштене и чисте функције, које раде само оно што кажу да раде, и за исти улаз увек дају исти и очекивани излаз. Дакле,

func sum(a: Int, b: Int) -> Int {
    return a + b
}
Link to comment
Подели на овим сајтовима

Да би функционало написани код био елегантан, јако је пожељно да језик функције третира исто као и све остале типове. Када у програмирању чујемо реч ”тип”. Обично мислимо на нешто као Int или String. Или на неку класу коју смо сами написали. Можемо да декларишемо променљиву да буде датог типа:

let name: String
let age: Int

а такође можемо да типове користимо у функцијама

func reverse(word: String) -> String
func isLegal(age: Int) -> Bool

Шта у овом контексту значи да су функције третиране као и сваки други тип? Па овако нешто, на пример:

let sumCalculator: Int -> Int
func findDuplicates(objects: [SomeType], equal: (SomeType, SomeType) -> Bool)

У првом случају је тип променљиве заправо фунција која прима Int и враћа Int. У другом случају функција за тражење дупликата прима, поред низа објеката, другу функцију коју ће да користи да тестира да си су два објекта из листе једнака. Ту функцију за тестирање једнакости можемо да проследимо функцији за тражење дупликата простим навођењем њеног имена.

findDuplicates(songs, areSongsEqual)

Нису нам потребни делегати (C#), показивачи на функције ( C ), нити било шта слично. Са функцијама радимо директно и најприродније могуће, као што радимо са бројевима или карактерима.

Link to comment
Подели на овим сајтовима

Да би функционало написани код био елегантан, јако је пожељно да језик функције третира исто као и све остале типове. Када у програмирању чујемо реч ”тип”. Обично мислимо на нешто као Int или String. Или на неку класу коју смо сами написали. Можемо да декларишемо променљиву да буде датог типа:

let name: String
let age: Int

а такође можемо да типове користимо у функцијама

func reverse(word: String) -> String
func isLegal(age: Int) -> Bool

Шта у овом контексту значи да су функције третиране као и сваки други тип? Па овако нешто, на пример:

let sumCalculator: Int -> Int
func findDuplicates(objects: [SomeType], equal: (SomeType, SomeType) -> Bool)

У првом случају је тип променљиве заправо фунција која прима Int и враћа Int. У другом случају функција за тражење дупликата прима, поред низа објеката, другу функцију коју ће да користи да тестира да си су два објекта из листе једнака. Ту функцију за тестирање једнакости можемо да проследимо функцији за тражење дупликата простим навођењем њеног имена.

findDuplicates(songs, areSongsEqual)

Нису нам потребни делегати (C#), показивачи на функције ( C ), нити било шта слично. Са функцијама радимо директно и најприродније могуће, као што радимо са бројевима или карактерима.

izvini, ne bi da ti kvarim ovu super temu, ali imam jedno pitanje, mislim mozda je glupo, ja ne poznajem rad na ovaj nacin, ali zar sve ove efekte ne mozes dobiti prostom ekstenzijom objekata u klasicnom OOP, gdje im prosirujes funkcionalnost sve do potrebnog nivoa

Link to comment
Подели на овим сајтовима

Čisto da bi mogao da kako tako pratim dalje jedno pitanje.

U čemu je razlika između prostog pozivanja funkcije u okviru druge funkcije i prosleđivanja te funkcije kao neki tip podataka.

Mislim na efekat naravno.

 

Разлике у ”ефектима” нема. Али коришћењем функције као аргумента друге фунције добијаш на флексибилности и знатно повећаваш употребљивост кода. На пример:

func divideByTwo(number: Double) -> Double {
    return number / 2
}

func divideEachByTwo(numbers: [Double]) -> [Double] {
    var results = [Double]()
    for number in numbers {
        results += [divideByTwo(number)]
    }
    return results
}

Овај код не можемо поново да искористимо. Једино је користан за дељење сваког броја у низу са два. Али ако ову доњу функцију за нијансу изменимо

func doSomethingWithNumbers(numbers: [Double], f: Double -> Double) -> [Double] {
    var results = [Double]()
    for number in numbers {
        results += [f(number)]
    }
    return results
}

моћи ћемо да је користимо за било коју врсту модификације бројева низа. Ако и даље желимо да их делимо са два, нема фрке:

doSomethingWithNumbers([1, 2, 3], divideByTwo)

Али сада можемо и овакве ствари да урадимо:

doSomethingWithNumbers([1, 2, 3], sqrt) // računa koren svakog broja
doSomethingWithNumbers([1, 2, 3], log) // računa logaritam svakog broja
doSomethingWithNumbers([1, 2, 3], sin) // računa sinus svakog broja

Ове три функције што сам их проследио су из стандардне библиотеке. Али то није све. Не само да могу да проследим било коју функцију одговарајућег потписа, коју сам написао или која је већ присутна у некој од библиотека, већ могу на лицу места да дефинишем фунцију за једнократну употребу. На пример:

doSomethingWithNumbers([1, 2, 3, 4, 5], { number in
    if number <= 2 {
        return number
    }
    return number * number
})

Ако је број два или мањи, враћам га, у супротном га квадрирам.

 

Има још једна супер ствар коју можемо да урадимо. О њој ћу детаљније после, овде је само илуструјем. Можемо да направимо сложену функцију, састављену од више мањих, и онда да проследимо ту коначну. Овако некако:

let rootLogAndSin: Double -> Double = sqrt >>> log >>> sin
doSomethingWithNumbers([1, 2, 3], rootLogAndSin)

где је '>>>' оператор за композицију функција, кога ћу касније дефинисати.

 

Можемо бити још општији. Наиме, наша функција што прима функцију не мора само радити с бројевима, већ може са било којим типовима. Али и томе касније.

Link to comment
Подели на овим сајтовима

izvini, ne bi da ti kvarim ovu super temu, ali imam jedno pitanje, mislim mozda je glupo, ja ne poznajem rad na ovaj nacin, ali zar sve ove efekte ne mozes dobiti prostom ekstenzijom objekata u klasicnom OOP, gdje im prosirujes funkcionalnost sve do potrebnog nivoa

 

Можеш, наравно. Функционални приступ има неке предности, које ћу током теме описивати. Који ћеш приступ користити у датом случају, зависи од много фактора, између осталог и од тога шта конкретни језик подржава. 

Link to comment
Подели на овим сајтовима

Upravo hteodoh da pitam isto sto i Kordun ali kad si vec odgovorio samo da napomenem da kad budes navodio prednosti, ako te ne mrzi, navedes primer te prednosti u jednom radnom okviru (moze hipoteticki framework). Dakle kakvu bi prednost imao radni okvir sa ovakvim pristupom u odnosu na sadasnje, pre svega za programere koji ne rade razvoj samog okvira nego ga koriste.

Link to comment
Подели на овим сајтовима

Inače što se c/c++ tiče i tamo je moguće proslediti funkciju kao parametar funkcije npr ovako:

#include <iostream>
using namespace std;

void f1(int i) {
    cout<< i<< " f1"<<endl;
}
void f2 (void(*f)(int)){
    f(6);
}
int main()
{
    f1(1);
    f2(f1);
    return 0;
}

Samo ja ne vidim neku veliku potrebu da se ovakve stvari rade.

Link to comment
Подели на овим сајтовима

Има потребе за тим у пракси често. На пример, имаш алгоритам за сортирање коме прослеђујеш функцију која пореди елементе зависно од њиховог типа.

 

Мада је фукнционално програмирање пуно више од тога, читава парадигма.

  • Волим 1
Link to comment
Подели на овим сајтовима

Погледајмо просту F# функцију за сабирање два броја

let sum a b = a + b

Позивамо је са

sum 7 8

Ако ово извшримо у интерактивној конзоли, добићемо, поред резултата, и једну занимљиву информацију о функцији:

val sum : a:int -> b:int -> int
val it : int = 15

Хм, потпис функције је 'a:int -> b:int -> int'??? Али зар не би требало да буде нешто као '(a:int, b:int) -> int'? Не би, јер би то онда била лаж. Функција за сумирање, коју смо горе дефинисали, заправо не прима два аргумента него само један. Ко не верује, може да провери:

sum 7

Нема грешке, све се уредно извршава, а информација о типу горњег израза је следећа:

val it : (int -> int) = <fun:[email protected]>

Дакле 'sum 7' је функција која прима int и враћа int. Шта та функција ради броју кога прима? Па додаје га седмици. :)

let addSeven = sum 7
addSeven 8

val addSeven : (int -> int)
val it : int = 15

Свака функција у F# је функција само једне променљиве. Уколико има више промељивих, онда компајлер у позадини одрађује један паметни трик - функција више променљивих узима прву променљиву и враћа функцију која узима другу променљиву и враћа функцију која узима трећу променљиву... док не истроши све променљиве и не дође до функције последње променљиве, која враћа резултат. Одатле оној нашој суми потпис  'a:int -> b:int -> int'. Овакав начин представљања функција назива се currying, a када суму позовемо само са једним, уместо са два броја ('sum 7'), то је онда парцијална апликација. OK, али чему то? Овакве функције су далеко погодније за композицију и разне друге функционалне технике и стилове.

Link to comment
Подели на овим сајтовима

Придружите се разговору

Можете одговорити сада, а касније да се региструјете на Поуке.орг Ако имате налог, пријавите се сада да бисте објавили на свом налогу.

Guest
Имаш нешто да додаш? Одговори на ову тему

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Чланови који сада читају   0 чланова

    Нема регистрованих чланова који гледају ову страницу

×
×
  • Креирај ново...