トラック
/
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

追加の引数

任意の数の追加の引数を受け取るには、定義に ...(省略記号)を書きます。関数呼び出しで余分に渡された値はベクトルに変換できますが、この「ドット引数」(他のいくつかの言語では「varargs」と呼ばれます)の別の使い方については、このあとを読んでみてください。

var_f <- function(x, y, ...) {
  print(c(...))
}

var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"

関数の環境

さきほど、仮引数と本体はどちらも関数の構成要素だと説明しました。

実は、3つ目の構成要素があります。関数が定義された環境です。

これは、入れ子になった関数の例で説明できます。

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の言葉を言い換えると、次のようになります。

ループそれ自体が遅いわけではありません。ただ、ループの中に遅い処理をうっかり入れてしまいやすいのです。

ベクトル化や高階関数を使うと、この種のパフォーマンスキラーから身を守るのに役立ちます。

説明

会社では、Webページを作るために地図データを扱ってきました。 いくつか話し合った結果、一部の計算を動的に行うために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}の座標ペアに対して再利用できる変換をクロージャで作ることにします。

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を学んでマスターできます。すべて無料です。