13 Łącznik
R ma bezpośrednio wbudowane mechanizmy łączenia kodu R z kodem napisanym w językach C i Fortan. W tym rozdziale skupimy się jednak na przykładach łączenia R z innymi, popularnymi współcześnie językami programowania C++ i Pythonem (sekcje 13.1 i 13.2) oraz z systemową linią komend (sekcja 13.3). Inne możliwe połączenia R to z językami takimi jak Rust, JavaScript, czy Java.
13.1 C++
C++ jest jednym z najczęściej używanych kompilowanych języków programowania. Jest to spowodowane kilkoma zaletami tego języka, w tym jego wysoką wydajnością, niezależnością od konkretnej platformy systemowej, czy uniwersalnością.
Język C++ posiada zarówno wiele podobnych do R konstrukcji i koncepcji, ale też różni się w pewnych kluczowych koncepcjach. Najważniejsze cechy C++, które wyróżniają go od R i które warto znać na początku:
- Jest językiem kompilowanym
- Pozwala na używanie tylko
=
jako operatora przypisania - Zakłada głównie statyczną kontrolę typów
- Posiada typ skalarny
- Domyślnie nie używa wektoryzacji
- Większość linii kodu należy kończyć znakiem średnika
;
- Konieczne jest zwracanie wartości używając
return
Przykładowy kod R do przeliczania temperatury ze stopni Fahrenheita na Celsjusza wygląda w poniższy sposób (sekcja 3.8):
function(temperatura_f){
konwersja_temp =- 32) / 1.8
(temperatura_f }
To samo obliczenie wykonane w języku C++ może wyglądać w ten sposób:
double konwersja_temp_cpp(double temperatura_f){
double temperatura_c = (temperatura_f - 32) / 1.8;
return temperatura_c;
}
Odnosząc się do punktów z wcześniej wymienionej listy:
- Jest językiem kompilowanym - gdybyśmy chcieli użyć powyższą funkcję jako program C++ musielibyśmy stworzyć kolejną funkcję
main()
, a następnie skompilować kod. Nie jest możliwe wykonywanie tego kodu linia po linii - Pozwala na używanie tylko
=
jako operatora przypisania - nie możemy w nim użyć operatora<-
czy->
- Zakłada głównie statyczną kontrolę typów - w powyższym przykładzie musieliśmy zadeklarować, że nasza funkcja
konwersja_temp_cpp
, nasz argumenttemperatura_f
oraz zmiennatemperatura_c
będzie typudouble
. Zrobiliśmy to poprzez dodanie nazwy typu przed nazwą funkcji/argumentu/zmiennej. Co ważne, w tym języku też typy (zazwyczaj) nie są automatycznie konwertowane do innych typów jak ma to miejsce w R (sekcja 5.9). - Posiada typ skalarny -
double
może przechowywać tylko jedną wartość. - Domyślnie nie używa wektoryzacji - powyższa funkcja
konwersja_temp_cpp()
zwróci błądExpecting a single value
w przypadku podania wektora numerycznego jako obiekt wejściowy. Aby użyć wektor wartości na wejściu konieczne jest napisane pętli lub użycie innych podobnych konstrukcji. - Większość linii kodu należy kończyć znakiem średnika
;
. Nie dotyczy to linii definiujących powstanie funkcji, rozpoczynających i kończących pętle czy wyrażenia warunkowe - Konieczne jest zwracanie wartości używając
return
. W R użycie funkcjireturn()
było opcjonalne.
Obecnie ponad dwa tysiące pakietów R łączy się z językiem C++ używając pakietu Rcpp (Eddelbuettel et al. 2020). Dodanie języka C++ do pakietu R często ma na celu przyspieszenie pewnych wymagających obliczeniowo zadań lub połączenie R z istniejącymi zewnętrznymi bibliotekami napisanymi w C++.
library(Rcpp)
Pakiet Rcpp pozwala na zarówno wywoływanie kodu C++ wewnątrz skryptów R (sekcja 13.1.1), jak używając zewnętrznych plików o rozszerzeniu .cpp
(sekcja 13.1.2).
Ta część książki ma na celu pokazanie zupełnych podstaw łączenia R z C++. Więcej informacji na ten temat można znaleźć na stronie http://www.rcpp.org/, w rozdziale Rewriting R code in C++ książki Wickham (2014), sekcji Rcpp książki Gillespie and Lovelace (2016), oraz na stronie Unofficial Rcpp API Documentation.
13.1.1 Wywoływanie kodu C++ wewnątrz skryptu R
W przypadku krótkich fragmentów kodu C++ możliwe jest umieszczenie ich wewnątrz skryptu R jako obiekt tekstowy.
Poniższej stworzono nowy obiekt rcpp_fun1
, który zawiera wcześniejszą funkcję C++.
"
rcpp_fun1 =double konwersja_temp_cpp(double temperatura_f){
double temperatura_c = (temperatura_f - 32) / 1.8;
return temperatura_c;
}
"
W kolejnym kroku konieczne jest skompilowanie powyższego kodu i stworzenie połączenia pomiędzy nim a R za pomocą funkcji cppFunction()
.
cppFunction(rcpp_fun1)
Od tego momentu możliwe jest korzystanie z funkcji konwersja_temp_cpp()
.
Możemy sprawdzić jej działanie poprzez podanie wybranej przez nas wartości, na przykład 75
.
konwersja_temp_cpp(75)
#> [1] 23.9
Warto jednak nadal pamiętać, że powyższa funkcja nie jest zwektoryzowana - możliwe jest podanie w niej tylko obiektu o długości 1. W przypadku zadeklarowania dłuższego obiektu wejściowego otrzymamy błąd:
konwersja_temp_cpp(c(0, 75, 110))
#> Error in eval(expr, envir, enclos): Expecting a single value: [extent=3].
W sekcji 8.1.3 stworzyliśmy funkcję mile_na_km()
, która przyjmuje i zwraca obiekt o klasie lista i zamienia wartości elementów tej listy z mil lądowych na kilometry.
function(odl_mile) {
mile_na_km = vector("list", length = length(odl_mile))
odl_km =for (i in seq_along(odl_mile)) {
odl_mile[[i]] * 1.609
odl_km[[i]] =
}
odl_km }
Ta sama funkcja w języku C++ może wyglądać w ten sposób:
List mile_na_km_cpp(List odl_mile){int odl_mile_len = odl_mile.size();
List result(odl_mile_len);for (int i = 0; i < odl_mile_len; i++){
1.609;
result[i] = odl_mile[i] *
}return result;
}
Zawiera ona szereg różnic od kodu R.
Oprócz definicji typów, używania średnika i operatora return
, widać tutaj także inną metodę wywołania funkcji oraz inny sposób definiowania pętli for
.
Zadaniem linii int odl_mile_len = odl_mile.size();
jest stworzenie nowej zmiennej skalarnej (odl_mile_len
) o typie integer (int
).
Ta nowa zmienna jest wynikiem działania funkcji size()
, która jest odpowiednikiem używanej w R length()
.
W przypadku używania R wywołanie funkcji ma jednak postać, w której podajemy nazwę funkcji, a następnie w nawiasie okrągłym obiekt wejściowy.
C++ pozwala też na inny sposób wywoływania funkcji - używając wbudowanych metod.
Odbywa się to poprzez podanie nazwy obiektu (odl_mile
), a następnie po kropce (.
) podania nazwy funkcji (size()
).
W C++ pętle można definiować używając poniższej składni:
for (inicjalizacja zmiennej; warunek zakończenia; aktualizacja zmiennej) {
// Kod do wykonania
}
Po pierwsze należy przekazać w miejscu inicjalizacja zmiennej
stworzenie zmiennej, na podstawie której będzie oparta pętla.
int i = 0
oznacza, że tworzymy zmienną o typie integer i
, która przyjmuje wartość 0.
Jest to spowodowane ważną różnicą między C++ a R - w tym pierwszym języku liczenie rozpoczynamy od 0.
Przykładowo w C++, a[0]
pozwoli na wybranie pierwszego elementu z wektora a
.
Drugim elementem jest warunek trwania
, czyli określenie do kiedy pęlta trwa.
i < odl_mile_len
oznacza, że pętla będzie działała tak długo aż i
będzie mniejsze niż odl_mile_len
.
Ostatni element, aktualizacja zmiennej
, mówi co ma się stać ze stworzoną zmienną po każdym przebiegu pętli.
i++
to skrót w języku C++ mówiący, że z każdą pętlą wartość i
będzie rosła o 1.
Jest to odpowiednik kodu i = i + 1
.
W powyższej składni też widać sposób definiowania komentarzy w języku C++, gdzie używa się operatora //
.
Zainicjujmy funkcję C++ mile_na_km_cpp()
używając cppFunction()
.
"List mile_na_km_cpp(List odl_mile){
rcpp_fun2 = int odl_mile_len = odl_mile.size();
List result(odl_mile_len);
for (int i = 0; i < odl_mile_len; i++){
result[i] = odl_mile[i] * 1.609;
}
return result;
}"
cppFunction(rcpp_fun2)
Możemy sprawdzić jej działanie na przykładowym wektore odl_mile
używając kodu poniżej.
list(142, 63, 121)
odl_mile =mile_na_km_cpp(odl_mile)
#> [[1]]
#> [1] 228
#>
#> [[2]]
#> [1] 101
#>
#> [[3]]
#> [1] 195
Dodatkowo warto porównać prędkość rozwiązania w R z C++ używająć listy o długości 10001 i funkcji mark()
z pakietu bench.
as.list(0:10000)
odl_mile2 = bench::mark(
wynik =mile_na_km(odl_mile2),
mile_na_km_cpp(odl_mile2)
)
wynik#> # A tibble: 2 × 6
#> expression min median itr/s…¹ mem_a…²
#> <bch:expr> <bch> <bch:> <dbl> <bch:b>
#> 1 mile_na_km(odl_mile2) 888µs 905µs 1066. 291.2KB
#> 2 mile_na_km_cpp(odl_mile2) 447µs 460µs 2149. 84.8KB
#> # … with 1 more variable: `gc/sec` <dbl>, and
#> # abbreviated variable names ¹`itr/sec`, ²mem_alloc
Mimo otrzymania tego samego wyniku, czas wykonania funkcji napisanej w C++ był około 1.97 raza mniejszy.
13.1.2 Wywoływanie kodu z plików .cpp
Powyższy przykład sprawdza się w przypadku małych fragmentów kodu C++.
W momencie jednak, gdy kod staje się bardziej złożony, znacznie lepsze jest umieszczenie go w oddzielnym pliku o rozszerzeniu .cpp
.
Taki plik może też być umieszczony wewnątrz pakietu R (rozdział 15).
Używanie kod C++ z pliku z poziomu R wymaga jednak pewnych dodatkowych działań. Konieczne jest dodane do tego pliku kilku linii nagłówków, które umożliwią interakcję pomiędzy C++ a R.
Pierwsza z nich ma na celu umożliwienie dostępu do funkcji z pakietu Rcpp, poprzez podanie nazwy pliku Rcpp.h
w linii #include
.
#include <Rcpp.h>
Dalej, opcjonalnie można dodać również linię using namespace Rcpp;
.
W przeciwnym razie każde wywołanie funkcji z pakietu Rcpp (np., List
) musielibyśmy poprzedzać Rcpp::
(np., Rcpp::List
).
using namespace Rcpp;
Ostatnią kwestią przed dodaniem kodu funkcji C++ jest zdecydowanie czy konkretną funkcję chcemy udostępnić i używać w R. Gdybyśmy nie dodali poniższego kodu, funkcja nie byłaby widoczna z poziomu R.
// [[Rcpp::export]]
Kompletny kod funkcji można zobaczyć poniżej.
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
List mile_na_km_cpp(List odl_mile){int odl_mile_len = odl_mile.size();
List result(odl_mile_len);for (int i = 0; i < odl_mile_len; i++){
1.609;
result[i] = odl_mile[i] *
}return(result);
}
Można go zapisać do pliku .cpp
, np. mile_na_km_cpp.cpp
, a następnie skompilować i udostępnić dla R z użyciem funkcji sourceCpp()
.
sourceCpp("mile_na_km_cpp.cpp")
Teraz też możliwe jest jego sprawdzenie na przykładowym obiekcie:
list(142, 63, 121)
odl_mile =mile_na_km_cpp(odl_mile)
#> [[1]]
#> [1] 228
#>
#> [[2]]
#> [1] 101
#>
#> [[3]]
#> [1] 195
13.2 Python
Python jest współcześnie najpopularniejszym językiem programowania. Podobnie jak R, ten język jest interpretowalny, otwarty, i można uruchomić na różnych systemach operacyjnych (Windows, Mac OS i Linux). Jest to uniwersalny język programowania znajdujący zastosowanie od aplikacji internetowych, poprzez pisanie skryptów sterujących innym oprogramowaniem (jak np. QGIS), aż do projektów związanych ze sztuczną inteligencją i uczeniem maszynowym.
Różni się on od R szeregiem cech, wśród których na samym początku można zauważyć, że Python:
- Ma inne wbudowane typy danych. Przykładowo, wektor atomowy w R odpowiada podobnemu typowi - liście w Pythonie.
- Obowiązkowe jest stosowanie wcięć jako elementu języka. W R stosowanie wcięć jest rekomendowane, ale nie wymagane. W Pythonie kod bez odpowiednich wcięć nie zadziała.
- Ma inny sposób pracy na obiektach.
- Stosuje indeksowanie zaczynające się od 0. W Pythonie wybranie pierwszego i trzeciego elementu z listy wymaga podania wartości indeksu 0 i 2.
Python także pozwala na tworzenie i udostępnianie modułów i pakietów rozszerzających możliwości tego języka. Czasem podczas pracy nad jakimś projektem w R może okazać się, że konieczne jest zastosowanie rozwiązania, którego w R nie ma, a jego implementacja wymagałaby znaczącego wkładu czasowego. Pomocny w takiej chwili może okazać się jeden z wielu pakietów Pythona. Aby go użyć, nie musimy jednak opuszczać R i przenosić wszystkich elementów programu do innego języka. Polecenia Pythona można wywołać z poziomu R używając pakietu o nazwie reticulate (Ushey, Allaire, and Tang 2020). Może to mieć miejsce w czterech trybach: (1) stosowania poleceń Pythona w R Markdown, (2) importowania modułów Pythona do R, (3) uruchomiania skryptów Pythona w R, oraz (4) interaktywnego korzystania z konsoli Pythona wewnątrz R wraz z dostępem do tworzonych obiektów. Pełną dokumentację tego pakietu wraz z szeregiem przykładów można znaleźć pod adresem https://rstudio.github.io/reticulate/. Istnieje także możliwość połączenia tych języków w drugą stronę, przykładowo używając modułu rpy2.
13.3 Powłoka systemowa
Współcześnie kontaktujemy się z komputerami najczęściej używając przeróżnych interfejsów graficznych poprzez kliknięcia myszką czy naciśnięcia klawiatury. Jednocześnie większość systemów operacyjnych posiada swoje powłoki systemowe (ang. shell). Pełnią one rolę pośrednika pomiędzy systemem operacyjnym a użytkownikiem i pozwalają one uruchamiać programy, sterować nimi poprzez wprowadzanie poleceń, czy zwracać wyniki ich działania.
R pozwala na łączenie się z powłoką systemową używając funkcji system2()
.47
W ten sposób możliwe jest zarówno wywoływanie poleceń wbudowanych w powłokę systemową, uruchomianie skryptów powłoki systemowej (np. system2("moj_bash_skrypt.sh")
), czy też zewnętrznych aplikacji (w taki sposób pakiet rgrass7 (Bivand 2019) łączy się z programem GRASS GIS).
Przykładowo, w systemach opartych o UNIX polecenie wc
pozwoli na określenie liczby wyrazów w wybranym pliku.
Używając system2()
oraz polecenia wc
możemy sprawdzić ile wyrazów znajduje się w tym rozdziale książki.
system2("wc", args = "-l 14-lacznik.Rmd")
13.4 Zadania
- Napisz funkcję
f_to_c_r()
w języku R do przeliczania wartości ze stopni Fahrenheita na stopnie Celsjusza. Funkcja ta powinna przyjmować wektor wartości, np.c(0, 75, 110)
i także zwracać wektor na wyjściu. - Stwórz nowy plik
f_to_c_c.cpp
zawierający funkcję C++ do przeliczania wartości ze stopni Fahrenheita na stopnie Celsjusza. W przeciwieństwie do kodu przedstawionego w funkcjikonwersja_temp_cpp()
powyżej, nowa funkcja powinna pozwalać na przeliczanie wektorów numerycznych składających się z wielu wartości. Sprawdź działanie tej funkcji z poziomu R. - (Dodatkowo) Stwórz moduł Pythona
f_to_c_p.py
również przeliczający wartości ze stopni Fahrenheita na stopnie Celsjusza. Sprawdź działanie tego modułu z poziomu R. - Stwórz wektor numeryczny od -1000 do 1000 co 0.5.
Sprawdź prędkość działania funkcji stworzonych we wcześniejszych zadaniach używając funkcji
mark()
z pakietu bench.
Bibliografia
Bivand, Roger. 2019. Rgrass7: Interface Between Grass 7 Geographical Information System and R.
Eddelbuettel, Dirk, Romain Francois, JJ Allaire, Kevin Ushey, Qiang Kou, Nathan Russell, Douglas Bates, and John Chambers. 2020. Rcpp: Seamless R and C++ Integration.
Gillespie, Colin, and Robin Lovelace. 2016. Efficient R Programming: A Practical Guide to Smarter Programming. " O’Reilly Media, Inc.".
Ushey, Kevin, JJ Allaire, and Yuan Tang. 2020. Reticulate: Interface to ’Python’. https://CRAN.R-project.org/package=reticulate.
Wickham, Hadley. 2014. Advanced R. Chapman and Hall/CRC.
W R istnieje również funkcja
system()
, która jest mniej uniwersalna niżsystem2()
i różni się od niej składnią↩︎