A funkcionális programozás alapelemei

Programozási paradigmák

A funkcionális programozás (ls. még a wikipediát) kapcsán szót ejtünk a programozási paradigmákról. A nyelvek többsége nem sorolható be egyetlen paradigma alá, inkább több különbözőből tartalmaznak elemeket, mint a Python is. A következő osztályozás leegyszerűsítő ugyan, de a legfontosabbak szerepelnek benne:

Egy tisztán funkcionális programban egy függvény adott bemenetre mindig ugyanazt a kimenetet adja, és nem változtatja a program állapotát.

A fenti feltétel nem teljesül, ha változóknak értéket adok majd azokat megváltoztatom, például az alábbi függvény más eredményt adhat akkor is, ha a paramétere ugyan az.

def f(y):
        return x + y

    x = 1
    print(f(5))
    x = -5
    print(f(5))

Ha ezt elkerüljük, akkor a függvényeink matematikai értelemben is függvények lesznek, nem pedig eljárások/procedúrák (gépi utasítások) egymás utánjai. Innen a név, funkcionális programozás.

Történelem

Guido van Rossum eredetileg ki kívánta dobni a funkcionális programozási nyelvekből ismert alapfüggvényeket a Python 3 core-ból a listaértelmezéssel helyettesíthetőnek tartva a fontosabbakat. A funkcionális programozás híveinek kemény ellenállását látva végül a lambda, map(), filter() maradt, de a reduce() kikerült az functools csomagba.

Előnyök

A funkcionális programok helyessége könnyebben igazolható, könnyebben tesztelhetők, modulárisabbak,...

Listaértelmezés, generátorkifejezés

Az első és legfontosabb funkcionális nyelvi elem a Pythonban az iterátor, amiről már volt szó. Végigmenni egy bejárható objektum elemein, kiválogatni közülük bizonyosakat valamilyen feltétel szerint és tenni velük valamit a funkcionális programozás legfőbb eleme. Pythonban a listaértelmezés (list comprehension) és a generátorkifejezés (generator expression) használható e feladatokra.

A listaértelmezés egy listát ad vissza, a generátorkifejezés egy iterátort. A szintaktikájuk azonos, kivéve hogy az előbbit []-be, utóbbit ()-be tesszük.

Példa: Sztringek listáját kapjuk egy fájlból beolvasott sorokból. A .strip() metódust használva vegyük le mindegyik sor elejéről és végéről a white space karaktereket.

A generátorkifejezés általános alakja:

(expression for expr1 in seq1
            if condition1
            for expr2 in seq2
            if condition2
                ...
            for exprN in seqN
            if conditionN )

A listaértelmezésé ugyanez, csak []-ben. Ez a következő kóddal ekvivalens:

for expr1 in seq1:
    if not (condition1):
        continue
    for expr2 in seq2:
        if not (condition2):
            continue 
        ...
        for exprN in seqN:
            if not (conditionN):
                continue
            expression

Feladat: Írjuk ki az 1890 és 1915 közti szökőéveket:

Lambda függvények

Ha egy egyszerű kifejezést szeretnénk kiértékelni, akkor ezekhez külön függvényt írni nem feltétlenül szükséges, lokálisan használhatunk egy egyszerűbb módon definiált függvényt, ez a lambda-függvény. Ennek alakja

lambda arguments: expression

Akár nevet sem kell adni az objektumnak:

A lambda-függvénynek több argumentuma is lehet:

map()

A map() egy iterálható objektum minden elemére alkalmazza a megadott függvényt és visszaad egy iterátort. (A Python korábbi változataiban egy listát adott vissza). Matematizált formulával valahogy így nézne ki: $$\mathop{\mathrm{map}}:(A\mapsto B)\times A^n\mapsto B^n\ \ (n\in \mathbb{N})$$

Listaértelmezéssel:

A map többváltozósként is használható. Ha az $f$ függvény több változós, akkor több lista megadása szükséges és a map mindegyiken szimultán halad és hattatja az $f$ függvényt.

Formalizálva valahogy így: $$\mathop{\mathrm{map}}: (A \mapsto X)\times A^n \mapsto X^n$$ $$\mathop{\mathrm{map}}: (A\times B \mapsto X) \times A^n\times B^n \mapsto X^n$$ $$\mathop{\mathrm{map}}: (A\times B \times C\mapsto X)\times A^n\times B^n\times C^n \mapsto X^n$$ $$\vdots$$

Feltételes kifejezések

lambda függvényekben használhatunk feltételes kifejezéseket is. A feltételes kifejezések formája a következő:

kifejezés if feltétel else kifejezés a feltétel hamis esetre

Feltételes kifejezésekben az else használata kötelező. Ennek egy következménye, hogy nem lesznek lefedetlen esetek a kódban.

Példa lefedetlen esetekre:

filter()

A filter függvény leszűr egy bejárható objektumot egy adott bool-függvény szerint.

A feltétel függvény egy $A\mapsto \{True, False\}$ függvény kell legyen.

$$\mathrm{filter} : (A\mapsto \{True, False\})\times A^n\mapsto A^m \text{ ahol } m\leq n$$

zip()

any() és all()

A kvantoroknak megfelelő függvények eldöntik, hogy egy bejárható objektum elemei közt van-e True, illetve hogy mindegyik True értékű-e.

reduce

A reduce (a functools csomag függvénye) egy 2-változós függvényt alkalmaz egy iterálható objektum (pl. lista) elemeire balról jobbra haladva, míg végül egyetlen értéket nem kap. Alakja:

reduce(*függvény*, *iterálható*[, *kezdőérték*])

A kétváltozós függvény $A\times B\mapsto A$ alakú kell legyen, az iterálható elemek $B$ beliek, a kezdőérték $A$-beli. A kezdőértéket a többi elem elé teszi. Ez a visszaadott érték üres lista esetén. Ha nincs kezdőérték, és a lista egyelemű, akkor azt az elemet adja vissza.

Egy $(a, b, c)$ tuple-re és egy $x$ kezdőértékre a reduce a következőt adja: $$\mathop{\mathrm{reduce}} (f, (a,b,c)) = f(f(a, b), c)$$ $$\mathop{\mathrm{reduce}} (f, (a,b,c), x) = f(f(f(x, a), b), c)$$

Ezzel az összegzés tétele elvileg bármikor elvégezhető és a szélsőérték keresés ennek egy alesete.

Két példa

A belső szorzat általánosítása

A skalár (belső) szorzat egy lehetséges implementációja:

Általában az $f(g(x_1, y_1), g(x_2, y_2), \ldots g(x_n, y_n))$ akalú kifejezéseket lehet a skalár szorzás általánosításának nevezni.

Könnyű implementálni funkcionálisan.

Vagy ha a $g$ függvény asszociatív, akkor lehet két változósnak is tekinteni és reduce-al számolni.

Tegyük ugyanezt a 'vagy'-ok 'és'-ével és az 'és'-ek 'vagy'-ával. Ehhez az or és and műveletek függvényváltozata kell. Vagy csinálunk ilyet, vagy az operator modulból betöltjük:

8 királynő

Hányféleképp lehet nyolc királynőt föltenni egy sakktáblára, hogy semelyik kettő ne üsse egymást?

Az ilyen bástyaelhelyezések száma 8! = 40320. A királynők azonban átlósan is üthetik egymást. Az i-edik és j-edik sorban lévő királynő pontosan akkor nem üti egymást, ha $$|i - j| \ne |x[i] - x[j]|,$$ ahol $x$ egy permutációja a $0,1,\dots,7$ számoknak. A permutációk listázásához töltsük be az itertools csomag permutations függvényét, mely a for ciklus minden ciklusában ad egy következő permutációt, amíg a végére nem ér. Így a megoldás: