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:
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.
A |> pipe-ok használata a függvények összekapcsolására egyre elterjedtebb technika számos nyelvben.
Röviden:
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.
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
purrr csomagAz 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:
vapply() hasonló az sapply() függvényhez, de van egy kötelező paramétere, amellyel megadható a kimenet formátuma.apply() mátrixokhoz és magasabb dimenziós tömbökhöz.mapply() több lista vagy vektor argumentum párhuzamos feldolgozásához.tapply() nehezebben magyarázható, de csoportosítási műveletekhez és szabálytalan tömbökhöz használható.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:
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 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:
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.i betűt, például imap().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)pmap-re, mint a pmap_chr() esetében. És igen, hardverszintű párhuzamosság is lehetséges.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.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.
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.
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.
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.