何を当たり前なことを、と思うでしょうか。しかし、Rにおいてこれはそんなに簡単なことではありません。
Tidyverse design guideは、第|||部まるまる、8章分を割いて関数のデフォルト値がどうあるべきかについて議論しています。 それは、関数のデフォルト値が重要であるとともに、静的型付け言語ではないRではどうがんばっても限界があるところだからです。
とはいえ、原則は10章のタイトルになっている次のひとことだけです。これだけ覚えておけば間違いありません。
必須の引数はデフォルト値を持つべきではない(Required args shouldn’t have defaults)
これです。10章の冒頭を少し引用してみましょう。
The absence of a default value should imply than an argument is required; the presence of a default should imply that an argument is optional.
デフォルト値を持たないということはその引数は必須だという意味だし、デフォルト値を持つということはその引数は必須ではないという意味だ、と。
When reading a function, it’s important to be able to tell at a glance which arguments must be supplied and which are optional. Otherwise you need to rely on the user having carefully read the documentation.
引数をひと目見て分かるようにしておかないと、ドキュメントをちゃんと読むユーザーしか使えない関数になっちゃうよ、と。
といって、言うは易し、というやつで、tidyverseのパッケージもそれをちゃんと守れているかというとそんなことはなかったりします。例えば2019年になってもまだこんなissueがあったり。
弘法も筆の誤り、Hadleyもデフォルト値の誤り。では、なぜそんな難しいことになっているのでしょうか。
引数間に依存関係があるケース
例えば、11章ではrep()
がケーススタディとして取り上げられています。
この関数はtimes
、each
、length.out
の3つの引数を持ちますが、すべてを同時に指定することはできません。
また、どれも指定しないことも許されません。エラーが出ます。
こういう複雑な依存関係はデフォルト値では示すことができません。 個人的に、ここはR(というか動的型付け言語)の限界だと思っていて、静的型付け言語であれば関数のオーバーロードとかがあるので、
rep(Vector v, int times); rep(Vector v, int times, int length.out); rep(Vector v, int times[]); ...
みたいな感じで書けるんじゃないかな?いう予感がしています(R以外の言語をあんまりよく知らないので違ったらすみません...)
関数を分けよう
ここで鋭い方は、いや、お前そんなわかったようなことを言ってるけど、
rep(Vector v, int times); rep(Vector v, int each); rep(Vector v, int length.out);
の区別つかなくない?何も解決してなくない??というツッコミをすることでしょう。 そうなんです。なので、この3つの引数をひとつの関数に押し込めようなんてことは考えず、11章で提案されているように、
rep_full()
rep_each()
の関数に分ける、みたいなことをしていたはずです。Rはなまじ遅延評価とかでマジカルなことができるばっかりに変なところに迷い込んでしまいがち、 という感じではないかな、と思っています*1。
ちょっと話はずれますが、C++のデフォルト引数についての記事をたまたま見つけて、面白かったです。やっぱり言語が違ってもみんないろいろ考えたり悩んだりしてるんですね。
タブ補完
ここからは個人的な好みですが、関数を分けるのはタブ補完という観点でも引数やオプションを増やすよりいいと思っています。
オプションはタブ補完が効きませんが、関数名ならタブ補完が効きます。具体的には、stringr::str_detect()
は正規表現か固定の文字列かをfixed()
とかregex()
で指定しますが、何が指定できるかはドキュメントを読まないとわかりません。
stringiならstringi::stri_detect_
までタイプすると候補が出てきて一目瞭然です。
もちろん、「ドキュメントを読まないとわからない」ことがいいことなのか悪いことなのかは、じっくり考える必要があると思います。 また、関数が増すぎて覚えられない、というデメリットもあります。唯一の正解はなさそうです。
指定できるオプションを示すために使う
12章は少し話が変わって、「その引数に指定できるオプションを示す」ためにデフォルト値を使う話が出てきます。
具体的にはこんな感じの関数です。
fun1 <- function(x = c("a", "b", "c")) { x <- match.arg(x) ... }
何のためにこんなことをするかというと、
The advertisement happens in the function specification, so you see in tooltips and autocomplete, without having to look at the documentation.
デフォルト値にオプションを並べておくことでドキュメントを開かなくてもマウスカーソルを重ねれば説明が出てくるようになるから、と言っています(ここではautocompleteと書かれていますが、RStudioでタブ補完効きます...?)。
しかし、個人的にはこれは「デフォルト値の乱用」だと思っています。
擬似的な型?
ここが静的型付け言語であれば、オプションを明示するのにデフォルト値を使うことはありません。「指定できるオプション」を表すのは、型です。Enumとかで。例えばGoであればこんな感じになると思います(Enumじゃないけど)。
type X int const { a X = iota b c } func fun1(x X) { ... }
賢いエディタであれば、ここからX
という型が取りうる値を推測して、タブ補完の候補に表示してくれることでしょう。これも型のおかげです。
Rにはそういうのがないので、デフォルト値で擬似的に型(的な役割)を実現しようと生まれたこの独自の戦法なわけですが、本来、
- デフォルト値 → 指定しなくてもいいという事実を示す
- 型 → 指定しないといけないものを示す
という真逆の目的を持ったもののはずです。これは混ぜるな危険だと思っていて、たとえば「必須の引数に指定できるオプションを示す」ためにデフォルト値を指定するのは、大原則である「必須の引数はデフォルト値を持つべきではない」というルールに違反しています。しかし一方で、オプションを明示する方法はこれしかないので、そうしたい気持ちもわかるなという。
まとめ
いかがだったでしょうか。「必須の引数はデフォルト値を持つべきではない」というルールは、とてもシンプルに見えて、守るのが難しいルールです。 とりあえずこのことばは頭の片隅に置いて、前に進みましょう。正解があるのかはわからないけど、進めばそこが道になる、的な。
*1:...と思ったけど、オーバーロードのないGoとかRustの話とかを読むと、この説明の仕方は間違いのなのかも。 https://internals.rust-lang.org/t/justification-for-rust-not-supporting-function-overloading-directly/7012