學習軌道
/
R
R
/
練習
/
座標轉換
座標轉換

座標轉換

學習練習

簡介

函式在 Basics 概念中就已經介紹過了,當時的範例像這樣:

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一模一樣。

引數

與 Python、Julia 等其他指令碼語言不同,R 並不嚴格區分位置引數與關鍵字引數。

函式呼叫可以用位置或名稱來傳遞值。 後者對於引數眾多、難以記住順序的複雜函式很有用。

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

額外引數

若想接受任意數量的額外引數,可以在定義中使用 ...(省略號)。 雖然可以把函式呼叫中任何多餘的值轉成向量,但請繼續往下讀,看看使用這些「dot args」的另一種方式(在其他幾種語言中稱為「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 就在內層函式的環境裡。

嚴格來說,這稱為閉包。

環境在處理 dot args 時格外重要,因為用這種方式提供的值可以繼續傳遞給函式本體中的函式呼叫。 外層函式不需要知道、也不必在意這些 dot args 代表什麼。

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. 縮放座標

實作scale(point, s)函式,它會接收一個任意維度數的點,並依照預先定義的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}座標對建立可重複使用的轉換。

實作transform2d(dx, dy, s)函式,它會回傳一個運用閉包的函式,對點執行可重複的 2D 平移與縮放。

縮放引數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,透過 21 個概念111 個練習 和真人引導來學習並精通 R,全部免費。