Mi is az a funkcionális programozás?
A mi szempontunkból ezek a legfontosabb jellemzői:
Az R azonban nem tiszta funkcionális nyelv (ellentétben például a Haskell-lel).
Tekints az R-re úgy, mint egy pragmatikus nyelvre, amely egyre inkább biztosít funkcionális lehetőségeket ott, ahol ezekkel tisztán és egyszerűen elvégezhető a munka. Á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 több nyelvben is.
Röviden:
Néhány programozó hasznosnak tartja, ha a kód átnézésekor a pipe-ot „then”-nek ejti ki: csináld meg az 1. lépést, THEN 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 4.1.0-s verzióban adták hozzá a Base R-hez, és találkozhatsz vele „natív pipe” néven is.
Az RStudio szerkesztő (többek között) a Ctrl-Shift-M billentyűkombinációt használja, hogy megfelelő szóközökkel illesszen be egy pipe-ot.
Korábban láttuk, hogy az R-ben sok függvény „vektorizált”: egy egész vektoron működnek, anélkül hogy kifejezett ciklusokat kellene írnunk.
Azok a függvények, amelyek csak vektorizált függvényeket használnak, maguk is valószínűleg automatikusan vektorizáltak lesznek. Egy egyszerű példán:
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.
Általánosabb módszerre van szükségünk, hogy függvényeket alkalmazzunk vektorokra (hasonlóan a map() függvényhez néhány más nyelvben).
Mivel az R alapnyelvében gazdag választéka van az adatszerkezeteknek, egy egész családnyi *apply() függvénnyel is rendelkezik, hogy ezeken dolgozzon.
Technikailag ezeket a programozók gyakran „magasabb rendű függvényeknek”, a matematikusok pedig „funkcionáloknak” nevezik, mert függvényeket vesznek át argumentumként.
Egyelőre nézzük a lapply() és a sapply() függvényt.
lapply()A „List-apply” lista bemeneteken való működésre készült, de a vektorokat szükség esetén csendben listává alakítja.
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
A Switch fogalmat ismerő tanulók észrevehetik, hogy a skaláris switch helyett használhatnánk a vektorizált recode_values() függvényt a dplyr könyvtárból.
A vektorizáció jó, és a modern R többféle módot kínál rá, hogy elérjük.
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” a lapply-hoz hasonló dolgot tesz, de meg is próbálja egyszerűsíteni a visszaadott értéket: például egy nevesített vektort 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 könyvtárA lapply és a sapply egyszerű, a legtöbb R-felhasználó ismeri őket, és még mindig elég népszerűek vektorokhoz és egyszerű listákhoz.
A Base R-nek is van sok más apply-függvénye. Az ilyen fajta zűrzavar helyett egy egyszerűbb és következetesebb megoldás nyújtása a Tidyverse egyik kulcscélja.
Ma már létezik a purrr csomag, amely számos további lehetőséget kínál, például a map() függvényt, miközben a tervezés következetességét és a hibaüzenetek érthetőségét is javítja.
A visszafelé kompatibilitás kevésbé köti, mint a Base R-t, így 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 és változatos függvény található, amelyeket nem könnyű egyszerűen összefoglalni.
Néhány forrás, ami segít eligazodni a könyvtárban:
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 a 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, amin érdemes elgondolkodni, 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észekhez, és így tovább.i-t, például imap().map-et map2-re, mint a map2_dbl() két bemenethez (például értékek és súlyok, egy súlyozott átlag kiszámításához)pmap-re párhuzamos bemenetek listájához, mint a pmap_chr(). És igen, hardverszintű párhuzamosság is lehetséges.map_if() függvényre egy predikátum (Boolean) szerinti szűréshez, a map_at() függvényre pozíciós indexfeltételek megadásához, a head_while() függvényre pedig arra, hogy addig dolgozd fel a bemenetet, amíg egy elem meg nem bukik egy predikátumon.map helyett modify() függvényeket, vagy modify_tree() függvényt a rekurzív alkalmazáshoz.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, vagy hibával leállnak.
Szigorú, de nagyon hatékony a nehezen megtalálható hibák csökkentésében.
A map-függvények visszaadott értéke általában ugyanolyan hosszú, mint a bemenet.
Néhány más függvény csökkenti az adat dimenzióinak számát, például a sum().
sum(1:100)
#> [1] 5050
A fenti esetben egy 100 hosszú vektort alakítunk egy 1 hosszú egésszé. Egy későbbi, 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, hogy tetszőleges dimenziócsökkentő függvényeket alkalmazzunk egy adatszerkezeten, egy map()-pel egyenértékű magasabb rendű függvény segítségével.
Több más nyelvben olyan függvényneveket használnak, mint a fold, a foldl és a foldr. Az R-ben a megfelelő függvény a reduce(), amely megfelel a (algoritmusok szerelmesei számára) jól ismert MapReduce keretrendszernek.
Az első argumentum, mint általában, a bemeneti adat. A második argumentum egy kétargumentumos függvény: gyakran egy 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 függvényt a bemenet elejétől vagy a végétől alkalmazzuk?
Nem, ha az operátor asszociatív, mint a + vagy a *.
Nyilvánvalóan (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 opcionális .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.
Az R-ben szeretünk vektorokkal dolgozni. Tegyük fel, hogy egyetlen végső érték helyett az összes köztes lépést szeretnéd látni.
Erre valók a megfelelő függvények: a accumulate() és az accumulate2().
A szintaxis hasonló a reduce()-hoz, néhány további opcionális argumentummal a kimeneti típus megadásához.
accumulate(1:10, `+`)
#> [1] 1 3 6 10 15 21 28 36 45 55
Az accumulate néhány gyakori fajtájának saját függvénye van.
A Vektorfüggvények fogalom a Base R-beli cumsum() és cumprod() függvényt tárgyalta, a dplyr könyvtárban pedig van cummean() a numerikus átlagokhoz, valamint cumall() és cumany() a logikai vektorokhoz.
Gyakran szeretnénk a bemenet bizonyos predikátumnak megfelelő elemeit megtartani (vagy kizárni).
A Vektorszűrés fogalomban láttunk egy idiomatikus módot erre.
v <- 1:3
v[v >= 2]
#> [1] 2 3
Ezt a hagyományos R-ben sokat használják, de nem illik jól a modern funkcionális munkafolyamatokba.
Ezért a purrr kínál egy sor predikátumfüggvényt, amelyek a szokásos f(input, func, ...) argumentumkonvenciót követik, hogy pipe-okkal is működjenek.
A nevek kicsit eltérnek a legtöbb más nyelvétől (az olyan nevek, mint a filter és a drop, az R-ben már egészen mást jelentenek).
A megfelelő purrr függvények közé tartozik a keep() és a discard(), valamint néhány további.
Ezenkívül ott van az every(), a some() és a none(), amelyek egyetlen logikai érték visszaadásában helyettesítik az all() és az any() függvényt.
Indítunk egy sajtklubot, amely ML segítségével választja ki az új sajtokat, hogy a sajtkedvelő ügyfeleinknek kínálhassa őket, az előzményeik és az ízlésük alapján.
Az új tagoknak ki kell tölteniük egy kezdeti kérdőívet, hogy legyen néhány alapadatunk, amivel elindulhatunk. Ebből kiderült, hogy az ügyfeleknek van egy csoportja, a hangsúlyos ügyfeleké, akiknek a kritikáiból hiányzik minden árnyalat. Mivel ez helyrehozhatatlanul eltorzíthat egy árnyaltabban működő algoritmust, külön algoritmust állítottunk fel az igényeik kiszolgálására. A feladatod, hogy néhány segédfüggvényt készíts, amelyekkel kezelni tudod az adataikat.
Bár a következő részfeladatokat többféleképpen is meg lehet oldani, mindegyik megoldható legfeljebb két magasabb rendű függvénnyel.
Az értékelési rendszer alapja az öt csillag, ami egyszerűen az 1:5 egész számokból áll.
A hangsúlyos ügyfelek csak 1 vagy 5 értékelést adnak, és szeretnénk tudni, hogy egy ügyfél mutatja-e ezt a viselkedést.
Készítsd el az all_15() függvényt, amely egy értékeléseket tartalmazó vektort kap, és true-t ad vissza, ha minden értékelés 1 vagy 5, egyébként pedig false értéket.
all_15(c(2, 3, 4, 4, 1))
#> [1] FALSE
all_15(c(1, 5, 5, 1, 5))
#> [1] TRUE
Az ügyfél nevét (vagy azonosítóját) össze kell kapcsolnunk az értékeléseivel.
Készítsd el a name_customers() függvényt, amely egy nevekből álló vektort és egy értékeléseket tartalmazó listát kap, és ezeket egy listákból álló listába fésüli össze.
A belső listák elemeinek a neve name és rating legyen.
names <- c("c1", "c2")
ratings <- list(c(2, 3, 5), c(1, 1, 5))
name_customers(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c1"
#> [[1]]$rating
#> [1] 2 3 5
#> [[2]]
#> [[2]]$name
#> [1] "c2"
#> [[2]]$rating
#> [1] 1 1 5
A hangsúlyosabb ügyfeleket külön kell választanunk a többiektől.
Készítsd el az emphatics() függvényt, amely ügyfeleket és értékeléseket kap.
Egy olyan listát ad vissza, amely csak azokra korlátozódik, akik kizárólag 1 vagy 5 csillagos értékelést adnak.`
# names and ratings as in the task 2 example
emphatics(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c2"
#> [[1]]$rating
#> [1] 1 1 5
Mivel a hangsúlyos ügyfelek csak 1 és 5 értékelést használnak, számítási szempontból kényelmesebb lesz, ha ezeket 0-ra és 1-re változtatjuk.
Készítsd el a to_binary() függvényt, amely a hangsúlyos értékelések vektorát kapja.
Bináris értékeléseket ad vissza: az 1 helyére 0, az 5 helyére 1 kerül.
to_binary(c(1, 1, 5, 5, 1))
#> [1] 0 0 1 1 0
Szeretnéd az idő múlásával egyre jobb ajánlásokat adni minden egyes ügyfélnek (nem csak a hangsúlyosaknak), ahogy egyre jobban megérted az ízlésüket.
Készítsd el a satisfactions() függvényt, amely egy értékeléseket tartalmazó vektort kap.
Az eddigi halmozott átlagos értékelések vektorát adja vissza.
Az eredményeket kerekítsd két tizedesjegyre.
Reméljük, hogy a számok egyre csak nőnek, a boldog ügyfelek kedvéért!
satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
Iratkozz fel az Exercism-re, hogy megtanuld és elsajátítsd a(z) R nyelvet 21 fogalom111 feladat segítségével, valódi emberi mentorálással, mindez ingyen.