Kurzusok
/
R
R
/
Tanterv
/
Funkcionális programozás
Fu

Funkcionális programozás ebben a kurzusban: R

1 feladat

A(z) Funkcionális programozás fogalomról

Mi is az a funkcionális programozás?

Ez egy nagy kérdés, amely hosszú választ érdemel (a Wikipédia ad egyet, az Advanced R pedig egy másikat).

A mi szempontunkból ezek a legfontosabb jellemzők:

  • Tiszta függvények, amelyek a bemeneteket következetesen, mellékhatások nélkül képezik le a kimenetekre.
  • Megváltoztathatatlan adatok.
  • Első osztályú függvények, amelyeket értékként lehet kezelni, mint bármely más típust.
  • Magasabb rendű függvények, amelyek más függvényeket fogadnak argumentumként vagy adnak vissza visszatérési értékként.

Az R azonban nem tiszta funkcionális nyelv (ellentétben például a Haskell-lel).

Tekintsd úgy az R-t, mint egy pragmatikus nyelvet, amely egyre inkább nyújt funkcionális képességeket ott, ahol ezekkel tisztán és egyszerűen lehet elvégezni egy feladatot. Általában az adatelemzés a cél, a programozás pedig csupán eszköz ehhez.

Pipe-ok

A |> pipe-ok használata a függvények összekapcsolására egyre elterjedtebb technika számos nyelvben.

Röviden:

  • A pipe bal oldalán lévő eredmény lesz a jobb oldali függvény első argumentuma (ahol a „jobb oldali” a nem kötelező sortöréseket figyelmen kívül hagyja).
  • A többi argumentumot magában a függvényhívásban lehet megadni.
  • R-ben a pipe-ot egy függvény_hívásra_ kell alkalmazni, nem csupán egy névre, ezért a zárójelek mindig szükségesek (ez néhány más nyelvtől eltér).

Néhány programozó hasznosnak találja, hogy kódolvasás közben a pipe-ot „azután”-nak mondja ki: csináld meg az 1. lépést, AZUTÁN a 2. lépést...

Az alábbi példa szemlélteti a szintaxist.

library(stringr)

"Monday-25-February" |> 
  str_split("-") |>     
  unlist() |>               # [1] "Monday"   "25"       "February"
  str_sub(1, 3) |>          # [1] "Mon" "25"  "Feb"
  str_to_upper() |>         # [1] "MON" "25"  "FEB"
  str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"

Ezt a fajta pipe-ot a v4.1.0-ban adták hozzá a Base R-hez, és találkozhatsz vele „natív pipe” néven is.

Korábban a pipe-olást a magrittr csomag %>% operátorával végezték (a név szójáték a művészet kedvelőinek). A „magrittr pipe” még mindig elérhető, de a natív pipe-okkal nincs szükség külső csomagra, és a megvalósítás néhány részletben egyszerűbb.

Az RStudio szerkesztő (és mások is) a Ctrl-Shift-M billentyűkombinációt használja, hogy megfelelő szóközökkel beszúrjon egy pipe-ot.

Apply függvények

Korábban láttuk, hogy az R-ben sok függvény „vektorizált”: a teljes vektorokon működnek, anélkül hogy explicit ciklusokat kellene írnunk.

Az olyan függvények, amelyek csak vektorizált függvényeket használnak, valószínűleg maguk is automatikusan vektorizáltak lesznek. Egy triviális példa:

triple <- function(x) return(3 * x)
triple(1:10)
 [1]  3  6  9 12 15 18 21 24 27 30

A bonyolultabb függvények azonban lehet, hogy nem fogadnak el vektoros bemenetet, például ha ciklusokat vagy if-else blokkokat tartalmaznak.

Szükségünk van egy általánosabb módszerre, amellyel függvényeket alkalmazhatunk vektorokra (hasonlóan néhány más nyelv map() függvényéhez).

Mivel az R alapnyelvében számos különféle adatszerkezet van, egy egész családnyi *apply() függvény is tartozik hozzájuk, amelyek ezeken működnek. Technikailag ezeket a programozók gyakran „magasabb rendű függvényeknek”, a matematikusok pedig „funkcionáloknak” nevezik, mert függvényeket fogadnak argumentumként.

Egyelőre nézzük az lapply() és sapply() függvényeket.

lapply()

A „list-apply” lista bemeneteken való munkára készült, de a vektorok szükség esetén csendben listává alakulnak.

A kimenet egy lista, ezért lehet, hogy ki kell csomagolni, hogy vektort kapjunk:

h <- function(size) {
    switch(size,
           "small" = 6,
           "medium" = 8,
           "large" = 10
    )
}

v <- c("small", "large", "medium")

h(v) # bad use of an unvectorized function
#> Error in switch(size, small = 6, medium = 8, large = 10) : 
#>   EXPR must be a length 1 vector

lapply(v, h)  # output is a list
#> [[1]]
#> [1] 6

#> [[2]]
#> [1] 10

#> [[3]]
#> [1] 8

lapply(v, h) |> unlist() # output is a vector
#> [1]  6 10  8

Azok a tanulók, akik ismerik a Switch fogalmat, észrevehetik, hogy a skaláris switch helyett használhatnánk a dplyr csomag vektorizált recode_values() függvényét is. A vektorizálás jó, és a modern R többféle módot is kínál rá.

library(dplyr)

h <- function(size) {
  recode_values(size,
         "small" ~ 6,
         "medium" ~ 8,
         "large" ~ 10
  )
}

v <- c("small", "large", "medium")
h(v)
#> [1]  6 10  8

sapply()

A „simplified-apply” hasonlót tesz, mint az lapply, de megpróbálja leegyszerűsíteni a visszatérési értéket is: például egy elnevezett vektort ad lista helyett. A helyzettől függően szükség lehet az unname() használatára:

sapply(v, h)
 small  large medium 
     6     10      8 

sapply(v, h) |> unname()
#> [1]  6 10  8

A purrr csomag

Az lapply és a sapply egyszerű, a legtöbb R-felhasználó ismeri őket, és vektorokhoz meg egyszerű listákhoz még mindig meglehetősen népszerűek.

A Base R-nek több más apply függvénye is van:

  • A vapply() hasonló az sapply() függvényhez, de van egy kötelező paramétere, amellyel megadható a kimenet formátuma.
  • Az apply() mátrixokhoz és magasabb dimenziós tömbökhöz.
  • Az mapply() több lista vagy vektor argumentum párhuzamos feldolgozásához.
  • A tapply() nehezebben magyarázható, de csoportosítási műveletekhez és szabálytalan tömbökhöz használható.
  • A vectorize() egy meglehetősen rejtélyes függvény, amely úgy tűnik, az mapply() burkolója.

Elég már!

A Tidyverse egyik fő célja, hogy egyszerűbb és következetesebb alternatívát nyújtson az efféle zavar helyett.

Most itt van a purrr csomag, amely számos további lehetőséget kínál, például a map() függvényt, miközben következetesebb a felépítése és világosabbak a hibaüzenetei.

Mivel kevésbé köti a visszafelé kompatibilitás, mint a Base R-t, ez a csomag jelentősen közelebb viszi az R-t a más nyelvekben megtalálható modern funkcionális programozáshoz.

A purrr-ban sokféle függvény van, amelyeket nehéz röviden összefoglalni. Az eligazodást segítő források:

Note

A purrr csomag szinte kizárólag egydimenziós adatokon működik: vektorokon és listákon.

Egy későbbi fogalomban látni fogjuk, hogy a dplyr csomag ugyanezt a funkcionalitást nyújtja kétdimenziós dataframe-ekhez.

A sok map függvény

A map() függvény a bemenet minden elemére alkalmaz egy függvényt, és mindig egy listát ad vissza.

Eddig úgy hangzik, mint az lapply(), csak átnevezve, és talán jobb hibaüzenetekkel.

A map() azonban egy nagy családnak csupán egyetlen tagja.

Néhány kérdés, amelyet érdemes átgondolni, amikor függvényt választasz:

  • Mit szeretnél visszakapni? A map() mindig listát ad vissza, de ha egy adott típusú atomi vektort szeretnél, használd a map_chr() függvényt stringekhez, a map_int() függvényt egész számokhoz stb.
  • Szükséged van a bemenet minden pozíció szerinti indexére? Tedd a függvénynév elé az i betűt, például imap().
  • Több bemenetet szeretnél párhuzamosan feldolgozni?
    • A névben cseréld a map-et map2-re, mint a map2_dbl() esetében két bemenethez (például értékek és súlyok, súlyozott átlag kiszámításához)
    • Párhuzamos bemenetek listájához cseréld pmap-re, mint a pmap_chr() esetében. És igen, hardverszintű párhuzamosság is lehetséges.
  • A bemenet minden elemét fel szeretnéd dolgozni? Ha nem, gondolj a map_if() függvényre, amellyel predikátum (Boolean) szerinti szűrőt alkalmazhatsz, a map_at() függvényre, amellyel pozíció szerinti feltételt adhatsz meg, a head_while() függvényre, amely addig dolgozza fel a bemenetet, amíg egy elem meg nem bukik egy predikátumon.
  • Mélyen egymásba ágyazott a bemeneti adat? Vannak függvények erre a helyzetre.
  • Mindig ugyanolyan típusú legyen a visszatérési érték, mint a bemenet? A map helyett használd a modify() függvényeket, vagy rekurzív alkalmazáshoz a modify_tree() függvényt.

Ezeknél a függvényeknél lehetőség szerint a bemeneti adat az első argumentum, hogy jól működjenek a pipe-okkal.

A purrr ráadásul (szándékosan) pedánsabb, mint a Base R. Ha valami konkrétat kérsz, a függvények pontosan azt adják vissza, különben pedig hibával állnak le. Kemény, de nagyon hatékonyan csökkenti a nehezen megtalálható hibák számát.

Reduce függvények

A map függvények visszatérési értéke általában ugyanolyan hosszú, mint a bemenet.

Néhány más függvény csökkenti az adatok dimenziószámát, például a sum().

sum(1:100)
#> [1] 5050

A fenti esetben egy 100 hosszúságú vektort alakítunk egy 1 hosszúságú egész számmá. Egy későbbi, a mátrixokról és tömbökről szóló fogalomban látni fogjuk, hogy a dimenziócsökkentés fogalma ennél általánosabb.

A sum() függvény beépített (ahogy sok más statisztikai függvény is). Szükségünk van azonban egy módra, amellyel tetszőleges dimenziócsökkentő függvényeket alkalmazhatunk egy adatszerkezeten, egy map()-pel egyenértékű magasabb rendű függvény segítségével.

Több más nyelvben fold, foldl és foldr nevű függvényeket használnak. R-ben a megfelelő függvény a reduce(), amely az algoritmusok kedvelői által jól ismert MapReduce keretrendszernek felel meg.

Az első argumentum szokás szerint a bemeneti adat. A második argumentum egy kétargumentumos függvény: gyakran névtelen függvény, de lehet olyan egyszerű is, mint egy aritmetikai operátor, például a +.

# surround an infix operator with backticks, to use as a function
reduce(1:100, `+`)
#> [1] 5050  # same as sum(1:100)

# anonymous function, alternately add and subtract
reduce(1:10, \(accum, nextval) ifelse(nextval %% 2 == 0, accum + nextval, accum - nextval))
#> [1] 7

Számít, hogy a bemenet elejétől vagy a végétől kezdve alkalmazzuk a függvényt?

Az asszociatív operátoroknál, például a + és a * esetében nem. Nyilvánvaló, hogy (1 + 2) + 3 == 1 + (2 + 3).

Ezzel szemben a - és a / nem asszociatív:

(1 - 2) - 3
#> [1] -4
1 - (2 - 3)
#> [1] 2

Azoknál a függvényeknél, ahol számít az irány, van egy nem kötelező .dir argumentum.

# default is .dir = "forward"
reduce(1:10, `-`)
#> [1] -53
reduce(1:10, `-`, .dir = "backward")
#> [1] -5

Ahogy a map() és a map2() esetében, itt is van egy reduce2() függvény, amely két bemeneti vektoron dolgozik párhuzamosan.

R-ben szeretünk vektorokkal dolgozni. Tegyük fel, hogy nem csupán egyetlen végső értéket szeretnél látni, hanem az összes közbenső lépést.

Erre a célra az accumulate() és az accumulate2() függvény szolgál. A szintaxis hasonló a reduce() függvényéhez, néhány extra, nem kötelező argumentummal a kimenet típusának megadására.

accumulate(1:10, `+`)
#> [1]  1  3  6 10 15 21 28 36 45 55

Az accumulate néhány gyakori típusának saját függvénye van. A vektorfüggvények fogalomban szó volt a Base R cumsum() és cumprod() függvényéről, a dplyr csomagban pedig van cummean() a numerikus átlagokhoz, valamint cumall() és cumany() a logikai vektorokhoz.

Szűrő függvények

Gyakran szeretnénk a bemenet azon elemeit megtartani (vagy kizárni), amelyek megfelelnek egy predikátumnak.

A vektorszűrés fogalomban láttunk erre egy idiomatikus módot.

v <- 1:3
v[v >= 2]
#> [1] 2 3

Ezt a hagyományos R-ben sokat használják, de a modern funkcionális folyamatokba rosszul illeszkedik.

Ezért a purrr kínál egy sor predikátumfüggvényt, amelyek a szokásos f(input, func, ...) argumentumsorrendet követik, hogy jól működjenek a pipe-okkal.

A nevek kicsit eltérnek a legtöbb más nyelvétől (az olyan nevek, mint a filter és a drop, már egészen mást jelentenek R-ben).

A megfelelő purrr függvények közé tartozik a keep() és a discard(), valamint számos más is.

Emellett ott van az every(), a some() és a none(), amelyek az all() és az any() helyett egyetlen logikai értéket adnak vissza.

Rekurzió

A legtöbb funkcionális nyelv a rekurziót használja a ciklusok előnyben részesített alternatívájaként.

Az R-nek más, gyakran jobb lehetőségei is vannak: vektorizált függvények és magasabb rendű függvények.

A rekurzió azonban támogatott R-ben, és hasznos lehet olyan helyzetekben, mint a fastruktúrák bejárása.

A maximális rekurziós mélység alapértéke 5000, ami viszonylag bőkezű (a Python alapértéke 1000). Az alapérték felülírható.

A farokhívás-optimalizálás nem szabványos R-ben, bár néhány kerülőmegoldást kidolgoztak.

A hagyomány megköveteli, hogy itt mutassunk egy faktoriális példát, a rekurzív definíció klasszikus példáját.

fact <- function(n) ifelse(n == 0, 1, n * fact(n - 1))
fact(5)
#> [1] 120

# some more idiomatic R:
factorial(5)
#> [1] 120
prod(1:5)
#> [1] 120

Használhatsz rekurziót R-ben, és néha hasznos is, de gyakran vannak egyszerűbb megoldások.

Szerkesztés GitHubon A hivatkozás új ablakban vagy lapon nyílik meg

Tanuld meg a(z) Funkcionális programozás fogalmat