関数型プログラミングとは何でしょうか?
ここでは、いくつかの重要な特徴を挙げます。
ただし、Rは純粋な関数型言語ではありません(たとえばHaskellとは対照的です)。
Rは実用的な言語と捉えましょう。(ますます)関数型の機能を提供し、それが仕事を片付けるためのクリーンで簡単な手段となる場面で役立ちます。 一般に、目標はデータ分析であり、プログラミングはそのための手段にすぎません。
関数をつなぐパイプ|>の使用は、いくつかの言語でますます一般的になっている手法です。
簡単に言うと、次のとおりです。
())が必要です(これは他の言語とは異なります)。コードを読むときに、パイプを「次に」と読むとわかりやすいと感じるプログラマーもいます。ステップ1をして、次にステップ2をする、という具合です。
次の例は、この構文を示しています。
library(stringr)
"Monday-25-February" |>
str_split("-") |>
unlist() |> # [1] "Monday" "25" "February"
str_sub(1, 3) |> # [1] "Mon" "25" "Feb"
str_to_upper() |> # [1] "MON" "25" "FEB"
str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"
このタイプのパイプはv4.1.0でBase Rに追加されました。「ネイティブパイプ」と呼ばれることもあります。
RStudioのエディターなどでは、Ctrl-Shift-Mが、適切なスペース付きでパイプを追加するキーボードショートカットとして使われています。
以前見たように、Rの多くの関数は「ベクトル化」されています。明示的なループを書かなくても、ベクトル全体に作用します。
ベクトル化された関数だけを使っている関数は、それ自体も自動的にベクトル化される可能性が高いです。 簡単な例を見てみましょう。
triple <- function(x) return(3 * x)
triple(1:10)
[1] 3 6 9 12 15 18 21 24 27 30
しかし、より複雑な関数はベクトル入力を受け付けないことがあります。たとえば、ループやif-elseブロックを含む場合です。
ベクトルに関数を適用する、より一般的な方法が必要です(他の言語のmap()に似ています)。
Rの基本言語には多様で豊富なデータ構造があるため、それらを操作する*apply()関数のファミリーも存在します。
より専門的には、これらは関数を引数として受け取るため、プログラマーは「高階関数」、数学者は「汎関数」と呼ぶことがよくあります。
ここでは、lapply()とsapply()を見てみましょう。
lapply()「List-apply」はリストの入力に対して動作するよう設計されていますが、必要に応じてベクトルは暗黙のうちにリストに変換されます。
出力はリストなので、ベクトルを得るには展開が必要になることがあります。
h <- function(size) {
switch(size,
"small" = 6,
"medium" = 8,
"large" = 10
)
}
v <- c("small", "large", "medium")
h(v) # bad use of an unvectorized function
#> Error in switch(size, small = 6, medium = 8, large = 10) :
#> EXPR must be a length 1 vector
lapply(v, h) # output is a list
#> [[1]]
#> [1] 6
#> [[2]]
#> [1] 10
#> [[3]]
#> [1] 8
lapply(v, h) |> unlist() # output is a vector
#> [1] 6 10 8
Switchのコンセプトに親しんでいる方は、スカラーのswitchを、dplyrライブラリのベクトル化されたrecode_values()に置き換えることもできると気づくかもしれません。
ベクトル化は良いことであり、現代のRはそれを実現する複数の方法を提供しています。
library(dplyr)
h <- function(size) {
recode_values(size,
"small" ~ 6,
"medium" ~ 8,
"large" ~ 10
)
}
v <- c("small", "large", "medium")
h(v)
#> [1] 6 10 8
sapply()「Simplified-apply」はlapplyと似たことをしますが、戻り値の簡略化も試みます。たとえば、リストではなく名前付きベクトルを返します。
状況によっては、unname()を使う必要があるかもしれません。
sapply(v, h)
small large medium
6 10 8
sapply(v, h) |> unname()
#> [1] 6 10 8
purrrライブラリlapplyとsapplyはシンプルで、ほとんどのRユーザーにとって親しみやすく、ベクトルや単純なリストに対して今でもかなり人気があります。
Base Rには、他にも_たくさん_のapply系関数があります。 この種の混乱に対して、よりシンプルで一貫性のある代替を提供することが、Tidyverseの重要な目標です。
現在ではpurrrパッケージがあり、map()のようないくつかの追加オプションを提供しつつ、設計の一貫性とエラーメッセージの明瞭さを向上させています。
このパッケージは、base Rよりも後方互換性に縛られていないため、他の言語に見られる現代的な関数型プログラミングにRを大きく近づけます。
purrrには多様で多くの関数があり、簡単にはまとめられません。
ライブラリを理解するのに役立つリソースには、次のようなものがあります。
map()関数は、入力の各要素に関数を適用し、常にリストを返します。
ここまでは、名前が変わっただけで、おそらくエラーメッセージが良くなっただけのlapply()のように聞こえます。
しかし、map()は_大きな_ファミリーの一員にすぎません。
関数を選ぶときに考えるといい質問がいくつかあります。
map()は常にリストを返しますが、特定の型の原子ベクトルが欲しい場合は、文字列にはmap_chr()、整数にはmap_int()などを使います。imap()のように、関数名の前にiを付けます。mapをmap2に置き換えます。2つの入力(たとえば値と重みで加重平均を計算する)に対するmap2_dbl()のように。pmapに置き換えます。pmap_chr()のように。そう、ハードウェアレベルの並列処理も可能です。map_if()、位置インデックスの条件を指定するmap_at()、要素が述語を満たさなくなるまで入力を処理するhead_while()を検討します。mapの代わりにmodify()関数を、再帰的な適用にはmodify_tree()を使います。これらの関数は、パイプとうまく連携するように、可能な限り入力データを最初の引数に取ります。
また、purrrは(意図的に)Base Rよりも細かく厳格です。何か特定のものを要求すれば、関数は_まさに_それを返すか、エラーメッセージを出して停止します。
厳しいですが、見つけにくいバグを減らすのにはとても効果的です。
map系の関数は、一般に入力と同じ長さの戻り値を持ちます。
他の関数の中には、データの次元を_減らす_ものがあります。たとえばsum()です。
sum(1:100)
#> [1] 5050
上の例では、長さ100のベクトルを長さ1の整数に変換しています。 行列と配列に関する今後のコンセプトで、次元の削減という考え方がより一般的であることを見ていきます。
sum()関数は組み込みです(他の多くの統計関数も同様です)。
しかし、map()に相当する何らかの高階関数を使って、任意の次元削減関数をデータ構造全体に適用する方法が必要です。
他のいくつかの言語では、fold、foldl、foldrといった関数名が使われます。Rでは、該当する関数はreduce()で、(アルゴリズム好きには)よく知られたMapReduceフレームワークに対応します。
最初の引数は、いつもどおり入力データです。2番目の引数は2引数の関数で、多くの場合匿名関数ですが、+のような算術演算子程度にシンプルなこともあります。
# surround an infix operator with backticks, to use as a function
reduce(1:100, `+`)
#> [1] 5050 # same as sum(1:100)
# anonymous function, alternately add and subtract
reduce(1:10, \(accum, nextval) ifelse(nextval %% 2 == 0, accum + nextval, accum - nextval))
#> [1] 7
関数を入力の先頭から適用するか末尾から適用するかは、重要でしょうか?
+や*のような結合的な演算子では重要ではありません。
明らかに、(1 + 2) + 3 == 1 + (2 + 3)です。
対照的に、-と/は結合的ではありません。
(1 - 2) - 3
#> [1] -4
1 - (2 - 3)
#> [1] 2
方向が重要な関数には、省略可能な.dir引数があります。
# default is .dir = "forward"
reduce(1:10, `-`)
#> [1] -53
reduce(1:10, `-`, .dir = "backward")
#> [1] -5
map()やmap2()と同様に、2つの入力ベクトルを並行して操作するreduce2()関数もあります。
Rでは、ベクトルを扱うのが好みです。最終的な単一の値ではなく、途中のすべてのステップを見たいとします。
そのためには、対応する関数としてaccumulate()とaccumulate2()があります。
構文はreduce()に似ていますが、出力型を指定するための追加の省略可能な引数がいくつかあります。
accumulate(1:10, `+`)
#> [1] 1 3 6 10 15 21 28 36 45 55
よくあるaccumulateの種類には、それぞれ専用の関数があります。
ベクトル関数のコンセプトでは、Base Rのcumsum()とcumprod()を扱いました。また、dplyrライブラリには、数値の平均のためのcummean()、論理ベクトルのためのcumall()とcumany()があります。
よくあるのは、何らかの述語に一致する入力の要素を含めたい(または除外したい)場合です。
ベクトルのフィルタリングのコンセプトで、これを行う1つの定型的な方法を見ました。
v <- 1:3
v[v >= 2]
#> [1] 2 3
これは伝統的なRでよく使われますが、現代的な関数型のパイプラインにはあまりなじみません。
そのため、purrrは述語関数のセットを提供しています。これらはパイプと連携するために、f(input, func, ...)という通常の引数の慣習に従います。
名前は他のほとんどの言語とは少し異なります(filterやdropのような名前は、Rではすでにまったく別の意味を持っています)。
チーズクラブを立ち上げます。このクラブでは、機械学習(ML)を使って、チーズ好きのお客様の履歴と好みに基づき、提供する新しいチーズを選びます。
新しく入会するメンバーには、最初のアンケートに回答してもらいます。これは、手始めに使う基本的なデータを集めるためです。 このアンケートを通じて、評価にニュアンスを欠いた、極端な評価をするお客様が一定数いることが分かりました。 こうした評価は、よりきめ細やかなアルゴリズムに取り返しのつかない偏りをもたらしかねないため、そうしたお客様のニーズに対応するための専用のアルゴリズムを別に用意しています。 そこで、そうしたお客様のデータを扱うための補助的な関数をいくつか用意してもらいます。
以下のタスクにはいくつもの解き方がありますが、どのタスクも2つ以下の高階関数で解くことができます。
評価システムは5段階の星を基準にしており、単純に整数1:5で表します。
極端な評価をするお客様は1か5の評価しか付けません。そこで、あるお客様がこの行動を示すかどうかを知りたいのです。
all_15()を実装してください。これは評価のベクトルを受け取り、すべての評価が1か5であればtrueを、そうでなければfalseを返します。
all_15(c(2, 3, 4, 4, 1))
#> [1] FALSE
all_15(c(1, 5, 5, 1, 5))
#> [1] TRUE
お客様の名前(またはID)と、その評価を関連付ける必要があります。
name_customers()を実装してください。これは名前のベクトルと評価のリストを受け取り、それらをリストのリストにまとめます。
内側のリストの要素には、nameとratingという名前を付けてください。
names <- c("c1", "c2")
ratings <- list(c(2, 3, 5), c(1, 1, 5))
name_customers(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c1"
#> [[1]]$rating
#> [1] 2 3 5
#> [[2]]
#> [[2]]$name
#> [1] "c2"
#> [[2]]$rating
#> [1] 1 1 5
より極端な評価をするお客様を、それ以外のお客様から分ける必要があります。
emphatics()を実装してください。これはお客様と評価を受け取り、1か5の星評価しか使わないお客様だけに絞り込んだリストを返します。
# names and ratings as in the task 2 example
emphatics(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c2"
#> [[1]]$rating
#> [1] 1 1 5
極端な評価をするお客様は1と5の評価しか使いません。そのため、これらを0と1に変換しておくと、計算上都合が良くなります。
to_binary()を実装してください。これは極端な評価のベクトルを受け取り、2値の評価を返します。ここでは1が0に、5が1に変換されています。
to_binary(c(1, 1, 5, 5, 1))
#> [1] 0 0 1 1 0
すべてのお客様(極端な評価をするお客様だけでなく)について、好みをより深く理解するにつれて、時間をかけて推薦を改善し続けたいと考えています。
satisfactions()を実装してください。これは評価のベクトルを受け取り、その時点までの累積平均評価のベクトルを返します。結果は小数点以下2桁に丸めてください。
数値がどんどん伸びて、お客様が幸せになりますように!
satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00