트랙
/
R
R
/
연습 문제
/
좌표 변환
좌표 변환

좌표 변환

학습 연습 문제

소개

함수는 기초 개념에서 이미 소개했어요. 다음은 그때 다룬 예시 중 하나예요:

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은 수정된 복사본을 반환하고 원본은 그대로 둬요.

큰 데이터 구조를 복사하는 데는 계산 비용이 많이 들 수 있지만, 대안이 데이터 손상이라면 보통 이쪽이 더 나은 선택이에요.

Caution

같은 데이터를 반복해서 복사하게 만드는 연산은 조심해요.

(Tidyverse 저자) Hadley Wickham의 말을 옮기면:

루프가 원래 느린 건 아니지만, 루프 안에 느린 연산을 넣기 너무 쉽게 만들어요.

벡터화나 고차 함수가 이런 종류의 성능 저하를 막아줄 수 있어요.

지침

회사에서는 웹 페이지를 만들기 위해 지도 데이터를 다뤄 왔어요. 몇 가지 논의 끝에 일부 계산을 동적으로 수행하기 위해 R을 사용하기 시작하기로 했어요.

점을 나타내는 벡터를 직접 다루는 것을 목표로 해봐요. 별도의 x, y 좌표로 나눌 필요는 없어요.

1. 좌표 스케일링하기

임의의 개수의 차원을 가진 점을 받아 미리 정의된 s만큼 스케일링하는 scale(point, s) 함수를 구현해요.

# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4

2. 좌표 평행이동하기

각 좌표가 생략 부호에 담긴 값만큼 이동한 새로운 점을 반환하는 translate(point, ...) 함수를 구현해요.

이 함수는 모든 차원의 점을 처리할 수 있어야 하며, 생략 부호로 받는 인자의 개수는 그 차원과 일치해야 해요.

point2d <- c(2, 3)

# supply dx, dy translations
translate(point2d, 0.5, 0.6)
#> [1] 2.5 3.6

point3d <- c(2, 3, 4)

# supply dx, dy, dz translations
translate(point3d, 0.5, 0.6, 0.7)
#> [1] 2.5 3.6 4.7

3. 2D 점 변환하기

팀 동료 중에는 R에 익숙하지 않은 사람도 있어서, {x, y} 좌표 쌍을 위한 재사용할 수 있는 변환을 만들기 위해 함수 클로저를 사용하기로 해요.

클로저를 이용해 점의 2D 평행이동과 스케일링을 반복해서 수행하는 함수를 반환하는 transform2d(dx, dy, s) 함수를 구현해요.

스케일링 인자 s는 선택 사항으로, 기본값은 1이에요.

반환된 함수는 2D 점을 받아 다음을 수행해야 해요.

  1. 미리 정의된 dx와 dy만큼 점을 평행이동해요.
  2. s만큼 점을 스케일링해요.
# scale all coordinates by the same amount (2) in this example
# your code needs to be able to handle a vector `s`

tf2d <- transform2d(0.5, 0.6, 2)
class(tf2d)
#> [1] "function"

tf2d(c(2, 3))
#> [1] 5.0 7.2

# Accept default for `s`
tf2d <- transform2d(0.5, 0.6)

tf2d(c(2, 3))
#> 2.5 3.6

연산 순서가 중요해요. 평행이동한 뒤 스케일링하면 올바른 결과가 나오지만, 스케일링한 뒤 평행이동하면 그렇지 않아요. 전문 용어로 말하자면, 두 연산은 서로 교환되지 않아요.

4. 3D 점 변환하기

지도 데이터에는 높이 정보도 포함될 수 있는데, 이는 화면에 음영과 등고선을 추가하는 데 사용돼요.

함수를 반환하는 transform3d(dx, dy, dz, s) 함수를 구현해요.

스케일링 인자 s는 선택 사항으로, 기본값은 1이에요.

반환된 함수는 3D 점을 받아 미리 정의된 값만큼 평행이동한 뒤, s만큼 스케일링해야 해요.

# using default `s`
tf3d <- transform3d(0.5, 0.6, 0.7)
point <- c(2, 3, 4)
tf3d(point)
#> [1] 2.5 3.6 4.7

# using vector `s`
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
tf3d(point)
#> [1] 1.25 2.16 3.29
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
R Exercism

좌표 변환 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 R 트랙을 개념 21개연습 문제 111개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.