함수는 기초 개념에서 다음과 같은 예제와 함께 소개했어요:
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
squareit의 정의를 자세히 살펴보면 여러 부분을 구분할 수 있어요:
x를 받아요.{ }로 감싸요.squareit에 할당돼요.R에서 함수는 숫자나 문자열과 마찬가지로 일급 객체예요.
따라서 squareit <- function...은 문법적으로 x <- 42와 똑같은 할당이에요.
R은 Python이나 Julia 같은 다른 스크립트 언어와 달리 위치 인자와 키워드 인자를 뚜렷하게 구분하지 않아요.
함수를 호출할 때 값은 위치로 넘기거나 이름으로 넘길 수 있어요. 후자는 인자가 많아 순서를 기억하기 어려운 복잡한 함수에 유용해요.
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
함수 정의에서 인자의 기본값을 지정할 수 있는데, 기본값이 없는 인자들 뒤에 와야 해요.
그러면 기본값을 그대로 쓸지, 다른 값으로 덮어쓸지 선택할 수 있어요.
g <- function(x, y = 2) x / y
# default y value
g(6)
#> [1] 3
# explicit y value
g(6, 3)
#> [1] 2
임의 개수의 추가 인자를 받으려면 정의에 ... (줄임표)를 사용해요.
함수 호출에서 넘긴 여분의 값들을 벡터로 바꿀 수도 있지만, 이 ... 인자를 활용하는 다른 방법도 있으니 계속 읽어봐요 (다른 여러 언어에서는 "varargs"라고 불러요).
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
앞에서 매개변수와 본문이 모두 함수의 구성 요소라고 했어요.
사실 세 번째 구성 요소가 더 있어요. 바로 함수가 정의된 환경이에요.
이것은 중첩 함수의 예로 잘 설명할 수 있어요:
outer_func <- function(x) {
inner_func <- function(y) {
x * y
}
inner_func(3)
}
outer_func(5)
#> [1] 15
함수 호출은 바깥 함수에 x = 5를 넘기고, 이 값은 그 함수 본문 안에서 사용할 수 있어요.
안쪽 함수는 바깥 함수 본문의 일부이고, x의 값에 접근할 수 있어요.
다르게 말하면 x = 5가 안쪽 함수의 환경에 있는 거예요.
기술적으로는 이것을 클로저라고 해요.
환경은 ... 인자에서 특히 중요한데, 이렇게 넘긴 값은 함수 본문 안의 함수 호출로 그대로 전달할 수 있거든요.
바깥 함수는 ... 인자가 무슨 뜻인지 알 필요도, 신경 쓸 필요도 없어요.
f_var <- function(x, ...) {
sum(x, ...)
}
x <- c(1, 2, NA, 6)
# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA
# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9
이 기법은 stringr 같은 Tidyverse 라이브러리에서 아주 많이 쓰여요.
stringr의 많은 함수는 stringi나 base R의 저수준 함수를 사용하기 편하게 감싼 래퍼예요.
str_*() 함수에 넘긴 추가 인자는 그대로 그 저수준 함수로 전달돼요.
바깥 함수는 안쪽 함수(이름이 있든, 아래에서 설명하는 익명 함수든)를 정의하고 그것을 반환값으로 사용할 수 있어요.
반환된 함수는 자신이 정의된 환경을 함께 담고 있어요.
times_y <- function(x) {
# anonymous function - see next section
\(y) x * y
}
f <- times_y(3)
class(f)
#> [1] "function"
f(4)
#> [1] 12
함수를 정의할 때는 보통 만들어진 함수 객체를 변수에 묶어요:
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
이렇게 하면 스크립트 뒤에서 함수를 쓰기 편하지만, 꼭 변수에 묶어야 하는 건 아니에요. 짧고 한 번만 쓰는 함수는 그 자리에서 바로 유용하게 쓸 수 있어요. 이름을 붙이지 않은 함수를 익명 함수라고 해요.
익명 함수는 워낙 자주 쓰여서 (R v4.1.0부터) 정의하는 축약 문법이 생겼어요. function이라는 단어를 백슬래시 \로 바꾸면 돼요.
이 문법의 예는 반환값을 다룬 앞 섹션에서 사용했어요.
R에서는 벡터의 개별 원소에 값을 할당할 수 있어요. 벡터를 함수 인자로 넘기고, 반환하기 전에 함수 본문에서 수정하면, 수정된 벡터를 얻게 돼요.
그런데 원래 벡터는 어떻게 됐을까요?
f <- function(vec) {
vec[1] <- 42
vec
}
vals <- c(1, 3, 4)
# f() returns a modified vector
f(vals)
#> [1] 42 3 4
# the original vector is unchanged.
vals
#> [1] 1 3 4
R은 데이터 과학을 위해 만들어진 언어예요. 그 데이터를 모으는 데는 시간과 노력이 많이 들고, 어쩌면 어마어마한 비용이 들 수도 있어요. 그러니 데이터를 손상시키지 않는 게 중요해요!
일반적인 원칙은 (몇 가지 예외를 빼면) 수정 시 복사예요. 어떤 객체(예를 들어 벡터)가 나중에 문제를 일으킬 수 있는 방식으로 바뀌면, R은 수정된 복사본을 돌려주고 원본은 그대로 둬요.
큰 데이터 구조를 복사하는 데는 계산 비용이 많이 들 수 있지만, 대안이 데이터 손상이라면 보통 이쪽이 더 나은 선택이에요.
같은 데이터를 반복해서 복사하게 만드는 연산을 조심해요.
(Tidyverse 저자) Hadley Wickham의 말을 옮기면:
루프는 원래 느리지 않아요. 하지만 루프 안에 느린 연산을 넣기 쉽게 만드는 건 위험할 정도예요.
벡터화나 고차 함수가 이런 종류의 성능 저하를 피하는 데 도움이 돼요.