Διαδρομές
/
R
R
/
Ύλη
/
Συναρτησιακός προγραμματισμός
Συ

Συναρτησιακός προγραμματισμός σε R

1 άσκηση

Σχετικά με την έννοια Συναρτησιακός προγραμματισμός

Τι είναι ο συναρτησιακός προγραμματισμός;

Αυτή είναι μια μεγάλη ερώτηση που αξίζει μια μεγάλη απάντηση (το Wikipedia δίνει μία, το Advanced R δίνει άλλη).

Για τους σκοπούς μας, αυτά είναι μερικά βασικά χαρακτηριστικά:

  • Καθαρές συναρτήσεις, που αντιστοιχίζουν εισόδους σε εξόδους με συνέπεια και χωρίς παρενέργειες.
  • Αμετάβλητα δεδομένα.
  • Συναρτήσεις πρώτης κλάσης, που μπορούν να αντιμετωπιστούν ως τιμές όπως κάθε άλλος τύπος.
  • Συναρτήσεις ανώτερης τάξης, που δέχονται άλλες συναρτήσεις ως ορίσματα ή ως τιμές επιστροφής.

Ωστόσο, η R δεν είναι μια καθαρά συναρτησιακή γλώσσα (σε αντίθεση, για παράδειγμα, με τη Haskell).

Δες την R ως μια πραγματιστική γλώσσα, που (όλο και περισσότερο) παρέχει συναρτησιακές δυνατότητες εκεί όπου αυτές είναι ένας καθαρός και εύκολος τρόπος για να γίνει μια δουλειά. Γενικά, η ανάλυση δεδομένων είναι ο στόχος, και ο προγραμματισμός είναι απλώς ένα μέσο για αυτόν τον σκοπό.

Σωλήνες

Η χρήση σωλήνων |> για τη σύνδεση συναρτήσεων είναι μια τεχνική που γίνεται όλο και πιο συνηθισμένη σε αρκετές γλώσσες.

Εν συντομία:

  • Ένα αποτέλεσμα από τα αριστερά του σωλήνα γίνεται το πρώτο όρισμα στη συνάρτηση στα δεξιά (όπου το "δεξιά" αγνοεί τις προαιρετικές αλλαγές γραμμής).
  • Τυχόν υπόλοιπα ορίσματα μπορούν να συμπεριληφθούν στην κλήση της συνάρτησης.
  • Στην R, πρέπει να κάνουμε σωλήνωση σε μια κλήση συνάρτησης, όχι απλώς σε ένα όνομα, οπότε οι παρενθέσεις απαιτούνται πάντα (αυτό διαφέρει από κάποιες άλλες γλώσσες).

Κάποιοι προγραμματιστές βρίσκουν χρήσιμο να προφέρουν τον σωλήνα ως "μετά" όταν διαβάζουν κώδικα: κάνε το βήμα 1, ΜΕΤΑ κάνε το βήμα 2...

Το παρακάτω παράδειγμα δείχνει τη σύνταξη.

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"

Αυτός ο τύπος σωλήνα προστέθηκε στη Base R στην έκδοση 4.1.0, και μπορεί να τον δεις να αναφέρεται ως "native pipe".

Παλαιότερα, η σωλήνωση γινόταν με το %>% από το πακέτο magrittr (το όνομα είναι ένα λογοπαίγνιο για τους λάτρεις της τέχνης). Ο "σωλήνας magrittr" είναι ακόμα διαθέσιμος, αλλά οι native pipes αποφεύγουν την ανάγκη για εξωτερικό πακέτο, και η υλοποίηση είναι απλούστερη σε κάποιες λεπτομέρειες.

Ο επεξεργαστής RStudio (μεταξύ άλλων) χρησιμοποιεί το Ctrl-Shift-M ως συντόμευση πληκτρολογίου για να προσθέσει έναν σωλήνα με τα κατάλληλα κενά.

Συναρτήσεις apply

Είδαμε προηγουμένως ότι πολλές συναρτήσεις στην R είναι "διανυσματοποιημένες": λειτουργούν σε ολόκληρα διανύσματα, χωρίς να χρειάζεται να γράψεις ρητούς βρόχους.

Οι συναρτήσεις που χρησιμοποιούν μόνο διανυσματοποιημένες συναρτήσεις είναι πιθανό να διανυσματοποιούνται αυτόματα και οι ίδιες. Σε ένα απλό παράδειγμα:

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

Ωστόσο, πιο σύνθετες συναρτήσεις μπορεί να μην δέχονται διανυσματική είσοδο, για παράδειγμα αν περιέχουν βρόχους ή μπλοκ if-else.

Χρειαζόμαστε έναν πιο γενικό τρόπο να εφαρμόζουμε συναρτήσεις σε διανύσματα (παρόμοιο με τη map() σε κάποιες άλλες γλώσσες).

Επειδή η R έχει μεγάλη ποικιλία δομών δεδομένων στη βασική γλώσσα, έχει επίσης μια ολόκληρη οικογένεια συναρτήσεων *apply() για να λειτουργεί πάνω τους. Πιο τεχνικά, αυτές συχνά ονομάζονται "συναρτήσεις ανώτερης τάξης" από τους προγραμματιστές και "συναρτησιακά" από τους μαθηματικούς, επειδή δέχονται συναρτήσεις ως ορίσματα.

Για την ώρα, ας εξετάσουμε τη lapply() και τη sapply().

lapply()

Η "List-apply" είναι σχεδιασμένη να δουλεύει με εισόδους λίστας, αλλά τα διανύσματα μετατρέπονται σιωπηλά σε λίστες όταν χρειάζεται.

Η έξοδος είναι μια λίστα, οπότε μπορεί να χρειαστεί αποσυσκευασία για να πάρεις ένα διάνυσμα:

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

Όσοι έχουν εξοικειωθεί με την έννοια Switch μπορεί να προσέξουν ότι θα μπορούσαμε εναλλακτικά να αντικαταστήσουμε το βαθμωτό switch με τη διανυσματοποιημένη recode_values() από τη βιβλιοθήκη dplyr. Η διανυσματοποίηση είναι καλή, και η σύγχρονη 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()

Η "Simplified-apply" κάνει κάτι παρόμοιο με τη lapply, αλλά προσπαθεί επίσης να απλοποιήσει την τιμή επιστροφής: για παράδειγμα, ένα ονομασμένο διάνυσμα αντί για λίστα. Ανάλογα με τα συμφραζόμενα, μπορεί να χρειαστεί η χρήση της unname():

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

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

Η βιβλιοθήκη purrr

Οι lapply και sapply είναι απλές, γνωστές στους περισσότερους χρήστες της R, και ακόμα αρκετά δημοφιλείς για διανύσματα και απλές λίστες.

Η Base R έχει επίσης αρκετές άλλες συναρτήσεις apply:

  • Η vapply() είναι παρόμοια με τη sapply() αλλά έχει επίσης μια υποχρεωτική παράμετρο για να ορίσεις τη μορφή εξόδου.
  • Η apply() για μήτρες και πίνακες υψηλότερης διάστασης.
  • Η mapply() για πολλαπλά ορίσματα λίστας ή διανύσματος παράλληλα.
  • Η tapply() είναι πιο δύσκολο να εξηγηθεί, αλλά δουλεύει με πράξεις ομαδοποίησης και ανομοιόμορφους πίνακες.
  • Η vectorize() είναι μια μάλλον μυστηριώδης συνάρτηση, που φαίνεται να είναι ένα περιτύλιγμα για τη mapply().

Αρκετά πια!

Η παροχή μιας απλούστερης και πιο συνεπούς αντικατάστασης για αυτού του είδους τη σύγχυση είναι βασικός στόχος του Tidyverse.

Υπάρχει τώρα το πακέτο purrr, που παρέχει αρκετές επιπλέον επιλογές όπως η map(), βελτιώνοντας παράλληλα τη συνέπεια στον σχεδιασμό και τη σαφήνεια στα μηνύματα σφάλματος.

Λιγότερο περιορισμένο από τη συμβατότητα προς τα πίσω σε σχέση με τη base R, αυτό το πακέτο φέρνει την R σημαντικά πιο κοντά στον σύγχρονο συναρτησιακό προγραμματισμό όπως τον συναντάς σε άλλες γλώσσες.

Υπάρχουν πολλές και ποικίλες συναρτήσεις στο purrr, που δεν συνοψίζονται εύκολα. Πόροι που βοηθούν να περιηγηθείς στη βιβλιοθήκη περιλαμβάνουν:

Note

Η βιβλιοθήκη purrr λειτουργεί σχεδόν αποκλειστικά σε μονοδιάστατα δεδομένα: διανύσματα και λίστες.

Θα δούμε σε μια επόμενη έννοια ότι η βιβλιοθήκη dplyr παρέχει ισοδύναμη λειτουργικότητα για δισδιάστατα dataframes.

Οι πολλές συναρτήσεις map

Η συνάρτηση map() εφαρμόζει μια συνάρτηση σε κάθε στοιχείο της εισόδου, και επιστρέφει πάντα μια λίστα.

Μέχρι στιγμής, αυτό ακούγεται σαν τη lapply(), απλώς μετονομασμένη, και ίσως με καλύτερα μηνύματα σφάλματος.

Ωστόσο, η map() είναι μόνο ένα μέλος μιας μεγάλης οικογένειας.

Μερικές ερωτήσεις για να σκεφτείς όταν διαλέγεις μια συνάρτηση:

  • Τι θέλεις να επιστρέψεις; Η map() επιστρέφει πάντα μια λίστα, αλλά αν θέλεις έναν συγκεκριμένο τύπο ατομικού διανύσματος χρησιμοποίησε τη map_chr() για συμβολοσειρές, τη map_int() για ακέραιους, κ.λπ.
  • Χρειάζεσαι κάθε θέση από την είσοδο; Βάλε πρόθεμα i στα ονόματα των συναρτήσεων, όπως η imap().
  • Θέλεις να επεξεργαστείς πολλαπλές εισόδους παράλληλα;
    • Αντικατέστησε το map με map2 στο όνομα, όπως η map2_dbl() για δύο εισόδους (π.χ. τιμές και βάρη, για να υπολογίσεις έναν σταθμισμένο μέσο όρο)
    • Αντικατέστησε με pmap για μια λίστα παράλληλων εισόδων, όπως η pmap_chr(). Και ναι, ο παραλληλισμός σε επίπεδο υλικού είναι εφικτός.
  • Θέλεις να επεξεργαστείς όλα τα στοιχεία της εισόδου; Αν όχι, σκέψου τη map_if() για να εφαρμόσεις ένα φίλτρο κατηγορήματος (λογικό), τη map_at() για να ορίσεις κριτήρια θέσης, τη head_while() για να επεξεργαστείς την είσοδο μέχρι ένα στοιχείο να αποτύχει σε ένα κατηγόρημα.
  • Είναι τα δεδομένα εισόδου βαθιά εμφωλευμένα; Υπάρχουν συναρτήσεις για αυτή την περίπτωση.
  • Θέλεις η τιμή επιστροφής να είναι πάντα του ίδιου τύπου με την είσοδο; Χρησιμοποίησε τις συναρτήσεις modify() αντί για τη map, ή τη modify_tree() για αναδρομική εφαρμογή.

Αυτές οι συναρτήσεις έχουν τα δεδομένα εισόδου ως πρώτο όρισμα όποτε είναι δυνατόν, ώστε να δουλεύουν καλά με σωλήνες.

Επίσης, το purrr είναι (σκόπιμα) πιο σχολαστικό από τη Base R. Αν ζητήσεις κάτι συγκεκριμένο, οι συναρτήσεις θα επιστρέψουν ακριβώς αυτό ή θα σταματήσουν με ένα μήνυμα σφάλματος. Σκληρό, αλλά πολύ αποτελεσματικό στη μείωση των δυσεύρετων bugs.

Συναρτήσεις reduce

Οι συναρτήσεις map έχουν γενικά τιμή επιστροφής με το ίδιο μήκος με την είσοδο.

Κάποιες άλλες συναρτήσεις μειώνουν τη διαστασιμότητα των δεδομένων, για παράδειγμα η sum().

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

Στην παραπάνω περίπτωση, μετατρέπουμε ένα διάνυσμα μήκους 100 σε ακέραιο μήκους 1. Θα δούμε σε μια μελλοντική έννοια για μήτρες και πίνακες ότι η έννοια της μείωσης διαστάσεων είναι πιο γενική.

Η συνάρτηση sum() είναι ενσωματωμένη (όπως και πολλές άλλες στατιστικές συναρτήσεις). Ωστόσο, χρειαζόμαστε έναν τρόπο να εφαρμόζουμε αυθαίρετες συναρτήσεις μείωσης διαστάσεων σε μια δομή δεδομένων, χρησιμοποιώντας κάποια συνάρτηση ανώτερης τάξης ισοδύναμη με τη map().

Σε αρκετές άλλες γλώσσες, χρησιμοποιούνται ονόματα συναρτήσεων όπως fold, foldl και foldr. Στην R, η σχετική συνάρτηση είναι η reduce(), που αντιστοιχεί στο γνωστό (στους λάτρεις των αλγορίθμων) framework MapReduce.

Το πρώτο όρισμα είναι, ως συνήθως, τα δεδομένα εισόδου. Το δεύτερο όρισμα είναι μια συνάρτηση 2 ορισμάτων: συχνά μια ανώνυμη συνάρτηση, αλλά ενδεχομένως τόσο απλή όσο ένας αριθμητικός τελεστής όπως το +.

# 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

Έχει σημασία αν εφαρμόζουμε τη συνάρτηση από την αρχή ή από το τέλος της εισόδου;

Όχι με προσεταιριστικούς τελεστές όπως το + ή το *. Προφανώς, (1 + 2) + 3 == 1 + (2 + 3).

Αντίθετα, το - και το / δεν είναι προσεταιριστικά:

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

Για συναρτήσεις όπου η κατεύθυνση έχει σημασία, υπάρχει ένα προαιρετικό όρισμα .dir.

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

Όπως με τη map() και τη map2(), υπάρχει επίσης μια συνάρτηση reduce2() για να λειτουργεί σε δύο διανύσματα εισόδου παράλληλα.

Στην R, μας αρέσει να δουλεύουμε με διανύσματα. Ας υποθέσουμε ότι, αντί για μια μόνο τελική τιμή, θέλεις να δεις όλα τα ενδιάμεσα βήματα.

Για αυτό, οι αντίστοιχες συναρτήσεις είναι η accumulate() και η accumulate2(). Η σύνταξη είναι παρόμοια με τη reduce(), με μερικά επιπλέον προαιρετικά ορίσματα για να ορίσεις τον τύπο εξόδου.

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

Κάποιοι συνηθισμένοι τύποι accumulate έχουν τις δικές τους συναρτήσεις. Η έννοια Συναρτήσεις διανυσμάτων συζήτησε τη cumsum() και τη cumprod() στη Base R, και η βιβλιοθήκη dplyr έχει τη cummean() για αριθμητικούς μέσους όρους, τη cumall() και τη cumany() για λογικά διανύσματα.

Συναρτήσεις φιλτραρίσματος

Συχνά, θέλουμε να συμπεριλάβουμε (ή να αποκλείσουμε) στοιχεία της εισόδου που ταιριάζουν με κάποιο κατηγόρημα.

Στην έννοια Φιλτράρισμα διανυσμάτων είδαμε έναν ιδιωματικό τρόπο να το κάνουμε αυτό.

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

Αυτό χρησιμοποιείται πολύ στην παραδοσιακή R, αλλά ταιριάζει άσχημα στους σύγχρονους συναρτησιακούς σωλήνες.

Κατά συνέπεια, το purrr παρέχει ένα σύνολο συναρτήσεων κατηγορήματος, που ακολουθούν τη συνήθη σύμβαση ορισμάτων f(input, func, ...) για να δουλεύουν με σωλήνες.

Τα ονόματα είναι λίγο διαφορετικά από τις περισσότερες άλλες γλώσσες (ονόματα όπως filter και drop σημαίνουν ήδη κάτι εντελώς διαφορετικό στην R).

Οι αντίστοιχες συναρτήσεις του purrr περιλαμβάνουν τη keep() και τη discard(), καθώς και αρκετές άλλες.

Επίσης, έχουμε τη every(), τη some() και τη none(), για να αντικαταστήσουν τη all() και τη any() στην επιστροφή μιας μόνο λογικής τιμής.

Αναδρομή

Οι περισσότερες συναρτησιακές γλώσσες χρησιμοποιούν την αναδρομή ως την προτιμώμενη εναλλακτική στους βρόχους.

Η R έχει άλλες και συχνά καλύτερες επιλογές: διανυσματοποιημένες συναρτήσεις και συναρτήσεις ανώτερης τάξης.

Ωστόσο, η αναδρομή υποστηρίζεται στην R, και μπορεί να είναι χρήσιμη σε περιπτώσεις όπως η διάσχιση δενδρικών δομών.

Το μέγιστο βάθος αναδρομής έχει προεπιλογή 5000, που είναι σχετικά γενναιόδωρη (στην Python η προεπιλογή είναι 1000). Είναι δυνατό να παρακάμψεις την προεπιλογή.

Η βελτιστοποίηση ουραίας κλήσης δεν είναι στάνταρ στην R, αν και έχουν αναπτυχθεί κάποιες λύσεις.

Η παράδοση απαιτεί να δείξουμε ένα παράδειγμα παραγοντικού σε αυτό το σημείο, ως το κλασικό παράδειγμα αναδρομικού ορισμού.

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

Μπορείς να χρησιμοποιήσεις αναδρομή στην R, και μερικές φορές είναι πολύτιμη, αλλά συχνά υπάρχουν απλούστεροι τρόποι.

Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα

Μάθε την έννοια Συναρτησιακός προγραμματισμός