https://www.youtube.com/watch?v=7LzVzBT_jZI
今回は Swift API Design Guidelines にある「表現方法」の後半、引数の扱い方についての指針に着目して眺めていきます。
引数の表現を工夫することで、引数がその用途を説明するだけでなく、もう少し広い API 全体に意味を補足するみたいな、そんな表現手法を垣間見る気持ちで眺めてみると、何か新しい発見があったりするかもしれないです。
————————————————————————— 熊谷さんのやさしい Swift 勉強会 #20
00:00 開始 00:14 オリエンテーション 02:47 引数の表現方法 04:25 ドキュメントになる引数名を選ぶ 09:25 この勉強会についての補足 14:27 内部名と外部名 16:16 表に現れないラベル名 24:03 質疑応答 24:14 ラベル名のある言語文化 25:24 気がつくということ 27:56 みんなが楽できるようにしておく 28:39 変数名とラベル名の視点の違い 31:40 外部名をつけるときの判断 32:29 引数の既定値を活用 33:47 特徴を読み取る練習 36:11 引数の既定値で明瞭化する 40:48 プロトコルによる既定値の表現 44:18 カスタマイズポイントを用いた既定値表現 —————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #20
画面に出ている例を見比べていくと、わかると思います。今回の例では、目に見えるAPIデザインの範囲には、それほど大きな影響が出ていない感じはしますが、ドキュメントには特に良い影響があります。具体的には、例えばArrayを返すエレメントのPredicateで説明されている内容がわかりやすくなっています。
上の例だと、配列(Array)を返すメソッドで、内容がPredicateで説明されています。説明されているものが、自分自身の中のPredicateにマッチするものを返すという意味です。ここで重要なのは、predicate
というラベル名がコメントにもそのまま出てきており、的確なコメントを構成している点です。
具体例として、Array
, self
, predicate
のように、具体的な表現がコメント内に自然な英文法として埋め込まれている状況です。逆に、英文法として違和感を感じる例としては、下の例が挙げられています。例えば、Array
, self
は良いですが、satisfyingInclusion
という引数名をコメントにそのまま使うと、英文法として非常に違和感があります。このような状況は好ましくないとされています。
上の例だと引数が明確に説明されており、引数だけに注目すれば意味が取れやすいです。だから、どっちが良いかというのはその時々の価値観や好みによります。Swiftの価値観で言えば、上の方が良いという程度だと思ってもらえればと思います。
現在話している内容は、Apple公式のAPIデザインガイドラインを日本語に訳してスライド1枚に収まるように要所だけ切り抜いた資料に基づいています。原本は存在しており、それを評価や個人的見解を抜きに紹介しています。この勉強会では、公式文書をより深く見ていこうという内容です。ガイドラインを頭に入れておくことで、ガイドラインに照らしてどちらが良いか考え、こっちの方がブラッシュアップされる感がある時に積極的に取り入れると、ガイドラインに書いてある以上の成果が出てくると思います。
特に、どっちでも大きな支障がない場面では、個々の裁量が生きてくる場面だと思いますので、ガイドラインにとらわれずに柔軟に取り組むことをお勧めします。しかし、ガイドラインから外れてしまうとバラバラになってしまい、誤解を招く可能性が高いと感じるガイドラインもあります。そうしたところでは、ガイドラインに従う方が良いと思います。
まずは、プレディケイトの例を紹介しましたが、次に他の例としてreplaceRange
メソッドを挙げています。これは非常にわかりやすい例で、あえてひどい例を持ってきた感じがします。 ですからね。実際に replaceSubrange(with: newElement)
のように書けば、サブレンジを新しい要素で置き換えるという非常にわかりやすい説明になります。しかし、ただ単に replaceR(with:)
と書くと、何の説明もなく非常にわかりにくいものになってしまいます。
この例で言うと、例えば replaceRange(ofElementsIndicatedBy: R, withTheContentOf: newElements)
のように、シンボル名をそのままコメントに埋め込むやり方は避けるべきです。せめてコメントの中で、R
が何なのか、with
の後に何を取っているのかをちゃんと説明しないと、コメント自体が台無しになってしまいます。個人的には、これはコメントが悪い例であり、引数名が悪いという問題以前のことだと思います。
その上で、確かに with
をそのまま使うのはわかりにくいです。せっかく内部変数名と外部ラベル名を分けられるという Swift 言語の仕様が活かされていません。これを活かしきれていないという捉え方ができます。
外部名と内部名を分ける文化が今まで主流でなかったことが影響しているのかもしれません。他の言語でもその傾向はあるでしょう。API デザインガイドラインを把握して使う人であれば、replaceRange(with:)
のような API 名を取ることは分かります。しかし、使う際に自然に使えるものの、発想が及ばず、with
のまま残ってしまうのはよく見られることです。そのままコメントを拾って使ってしまうと、メリハリがないまま使用されてしまいます。
以上のことから、Swift ならではの気遣いという面では慣れが必要です。徐々に改善していく過程で気づきを促すことも重要かもしれません。
例えば、配列をマップする場合を考えてみましょう。コード補完では map
の後に transform
が現れますが、実際にコードを書くときにはクロージャを使い、element
に対して何かを行う形になります。この際、transform
という言葉は表には出てこなくなります。
このガイドラインを紹介したのもそのためです。この transform
という言葉をどう活かすかがポイントです。これが裏地のようにオシャレに使われると、見る人には「気が利いてるな」と思わせる効果があります。これができると、「この人は余裕があり、コード実装にも気配りができる」と見られるでしょう。with
に関しても同様です。
ここまで解説してきましたが、Swift の仕様をより効果的に活用して、読みやすく理解しやすいコードを目指すのが大切です。これができると、やはりコード実装においても余裕や気配りが感じられ、全体として良い印象を与えるようになるでしょう。 いい感じのお話ですが、replaceだったかな?さっきの例をちょっと上げておきますね。わざわざ実装する必要もないかもしれませんが、例として挙げてみます。
まず、replaceSubrange
。さっきの例で、subrange
を入れないとわからないことが多いです。インデックスとしてR
を使います。ここでsubrange
なんて型が用意されているのか見てみると、indices
なんて型があるようです。そして、ここでwith
を使います。置き換える部分には新しい要素の配列を指定します。
次に、replaceSubrange
を呼び出す感じになりますね。APIデザインガイドラインを考慮すると、ラベル名まではまだ付けられていませんが、こんな雰囲気になるでしょう。
この時、with
をそのまま使うと、表向きには問題ありませんが、コード補完の時に現れてしまうため、コードの読みやすさに影響します。コードを置き換える際、内部変数名をきちんと付けてあげると、可読性が高まります。
例えば、インデックスが3番目から5番目までを置き換える場合のコードを書いてみると、表向きには現れないように意識します。ただ、実際にコードを書いていくうちに、どうしてもわかりにくいものになってしまうことがあります。
もし難しい書き方をすると、その分面倒になってしまいます。手続き的な言語を扱うのが面倒に感じるようになってきたのは、おそらく概念的なコードが好まれるようになってきたからでしょう。応急処置で済ませるのも良くないので、そういう時にはやはりしっかりとした実装が必要です。
たとえば、ArraySlice
の置き換えをする際も、正確に書くことを心がけるべきです。内部変数名がきちんと付いているだけでも、実装が明瞭になります。そして、そのことでコードが読みやすくなり、結果的にバグが減り、アプリケーションの品質が向上します。
APIデザインガイドラインでは、そのあたりのことを見据えてコードの品質向上を図るように導いています。こうした点を念頭に置くことで、ガイドラインのありがたみを感じてもらえると思います。
また、コメントを通じて、外部ラベルや内部変数名の使い方についても色々と意見をもらっています。SwiftやObjective-Cといった外部ラベルを持つ言語に慣れてくると、その重要性がより明確に見えてきます。 ただ、意外と少ないですね。iOSをやっている人にとっては、Objective-CとSwiftが全てみたいなところがあるから分かりますけど、他の言語をやっていると、なかなかそういう文化を持っていないまま育って、そのままiOSに入ってきたりすると、目立つところですね。
そこを気づかせていくことも、仮に指導する立場にいる場合は、大事なポイントなのかもしれないですね。自分ができることを気づくのはなかなか難しいですから。前回のお話でも、オーバーロードの仕組みが難しいと感じる人もいるでしょうし、当たり前の文化として捉えて何も感じない人もいるでしょう。
すべて気にしすぎると時間ばかりかかって大変なので、立ち加減が難しいですね。気持ちの汲み取り方というのも、大事な資質だと思います。それに、指導する側だけでなく、逆の立場も同じです。コードを書いていく上で「このコードは見た相手にとって読みやすいかな」とか「どこに気が配られていないかな」と思える能力、感じ取れる能力が大事です。その能力が、コードの安全性にも繋がりますからね。
たとえば、if
文で条件を漏らさないようにするなど、聞くばりや感受性がプログラマーとして磨くべき部分なのかもしれません。車の運転でいうと、危険予測みたいなものですね。そういった感覚を養うのは結構難しい分野だと思いますが、意識できるか否かが大事なステップです。意識できる人とできない人では成長度が大きく違うからです。
そういったところにも意識を向けながら、ガイドラインを生かしていけると良いですね。プログラマーの美徳という面でも、「将来みんなが楽できるようにしておく」という考えが重要です。自分だけが楽をしようとすると、それはただの怠惰ですからね。
ラベルがあるために、引数名とラベル名の整合性を取ることも大切です。外部ラベル名はAPIを利用する側に示す説明で、内部引数名は実装する側で適切な名前を付けます。実装を見る側、つまりチーム側が適切に意思疎通でき、誤解を生まないための説明です。
営業トークのように、お客さん目線での説明と技術目線での共有は異なります。その視点を意識すると、名前の付け方の一つの指針に繋がってきます。ガイドラインと結びつくと、内側の目線で名前を付ける際にドキュメントコメントに反映される名前と近い印象を持つかもしれません。
例えば、new elements
をwith
だけにするかという問題も、ドキュメントコメントにnew elements
が綺麗に反映されているのなら、それを反映すべきでしょう。その違いを気を配ることが非常に奥深くて面白い部分だと思います。
名前の付け方について、自分なりの判断材料や指針を作ってみることも勉強になると思います。その名前をつける際に感じるものが出るポイントが重要です。何気なくつけるのではなく、意識することが重要です。
次に進みましょう。引数の既定値を活用する話に移ります。ここまでは名前付け的な話でしたが、次は表現方法についてです。引数をどう活用していくかを考える中で、既定値を活用するとメソッドの表現力が広がります。
例えば、compare
で比較オプションや比較範囲、ローカライズ(ロケール)を想定してコンペアするオプションを取れることなどを紹介します。このオプションがあるかないかが非常に大事です。次のスライドで詳しく説明しますが、まずは雰囲気を捉えてください。
このように、プログラミングの表現力を広げるために、引数の既定値を意識すると非常に興味深いポイントがたくさんあると思います。 原点0、原点Oを取ります とか、マイナスの方には値がないとか、そういういろんな特徴を汲み取るという授業があったのをふと思い出したんですが、今表示されているスライドはまさにそういう画面です。
ここから何が感じ取れますか?というところですが、ここから見ると、「オプション」を取りますね、「レンジ」というのを取りますね、「ロケール」というのを取りますね、といったことがわかります。戻り値を返しますね、取ろうと思えば取れますし、あと、compare
が動詞だから戻り値を返すよね、とか、いろいろ考えられることが出てくるかと思うんですが、その中で今回大事になってくるポイントとしては、上の行と下の行が同じ機能を持っているよねっていうのが感じられたら大成功なんです。
同じ機能があると捉えられるようになると、「オプションズ」と「レンジ」、「ロケール」は省略されているという雰囲気がつかめてくると思うんです。同じ機能だと捉えられると、それを感じさせるAPIデザインが紹介されているんです。
この時に、上の例と下の例は全く同じ表現をしているんですよ、定義の中で。compare
の第1引数を取る例と、第2引数を取る例、第2引数からの配列を取る例、からどうでもいいんだ、既定値が与えられているか否かですね。そして、第2引数の他にも第3引数、第4引数も全く同じ。取る場合もあれば既定値を与える場合もあるという定義。それを定義していくと、下のようにたくさんのメソッドをオーバーロードで定義していくということができるようになってくるんです。
もうちょっとしっかり網羅していくとすると、まだまだ増えていくんですけど、オプションでは取らないけどレンジだけを取る例とか、いろいろ定義していって、仮にそれを全部網羅させたとしましょう。そうした時に、定義としては上も下もどっちも完璧になるんですよね。ただ、それを読む側、実装を見る、使う側にも影響してきますが、見た時に果たして同じ機能なのだろうかという疑問を、上の例は持たせないんです。1個しかメソッドが定義されていないんだから1個の機能でしょ、と認識するわけです。通常はその認識通りに動くんです。プログラマーが中で意地悪しなければ、普通は期待通りにcompare
が動くわけです。
ただ、下の例になってくると、いろんなパターンのcompare
が今回の例だと4つ定義されていて、この4つの中から求めるものはどれなのか探さないといけなくなるんです。見た感じでは、ロケールがないパターンはどういう動きをするんだろう?ロケールがあるパターンはどういう動きをするんだろう?ってなります。そして、それを見比べていくと、他と見比べて、あ、雰囲気が一緒だね、同じ機能を持っていそうだね、という風になり、最終的には、ああ同じだね、という結論になるわけです。そこまでの負担がかかるわけです。
ただ、上の例だと一発で、その負担がいきなり消えて、「ああ、これはcompare
だね」という風に労力が減るわけです。これがとても大事なポイントになっているんです。
人は1日に判断できる数が限られているという主張が自分は好きなんですけど、仮にそれが正しいとすると、ここで判断力を無駄に消費せずに、いきなりcompare
1個を理解できた方が、その先の判断すべき数が増えるわけです。その理屈を信じるとすればね。だから、その面でも負担を減らせるし、とても大事なポイントなんです。 「なので、オプションとして提供できる、要は規定値を添えられるものであったとすれば、引数に規定値を定義する、上の例を積極的に使っていきましょう。そうすると、今お話ししたような効能が得られますよ、というのがこのガイドラインになりますね。
プロトコルでの扱いが面倒だというコメントが寄せられていますね。確かにプロトコルだと規定値を添えられないんですよね。それはどうしたものかという疑問が湧いてきますね。
プロトコルというのはインターフェースを規定するようなもので、規定値がどうこうという具体的な話とは少し違うのです。プロトコルにはインターフェースがあって、例えばファンクションとして action
何もパラメータを取らないアクションとします。もう一つのファンクションとして、オプションを取るようにしましょう。
例えば struct
で何かサンプルタイプを作ります。インターフェースを備え、ファンクションを実装することを求められます。そこでパラメータを取るものを作成しようとすると、まだ実装されていないとエラーメッセージが出ますね。パラメータを取らないものも同じように求められます。ここで規定値をつけるとどうなるのか、疑問が湧きます。
例えば = true
と規定値をつけてみても、実際には賢く動作しません。私はプロトコル拡張でデフォルト実装を提供することが多いです。デフォルト実装によって、プロトコルを使用する側で実装を省略できるのです。これにより、プロトコル拡張がデフォルト実装を提供しているため、デフォルトインプリメンテーションが添えられていることが確認できます。
プロトコル表現としての規定値が見事に表現されています。この方法でコードを見ると、型の実装としての規定値と同じように感じます。書き方は全然違いますけどね。
もう一つ大事なポイントとして、これを完全なデフォルト実装として表現するかどうかです。プロトコルに宣言を書くことで、カスタマイズポイントを提供するという意味があります。カスタマイズポイントを提供するとは、独自に実装した場合に独自の実装が優先されるかどうかというポイントのことです。このようにして、型の実装の規定値が同時に存在していることが見えます。」 とりあえず、こういう風な実装を添えたときと、こういう風にね、アブストラクトも綴り合ってたからちょっと自信ないけど、まあいいや。こういう風に実装を添えたときってこれをインスタンス化して、インスタンス化したときに SUM
を呼ぶっていう風にしたときに、コンクリートってプリントされると思うんですけど、動いてないか? 動きそうですね。
これがカスタマイズポイントがあるかどうかで、オブジェクト思考みたいな動きを見せるという話をすると、余計混乱するかもしれないですね。説明しても分かりにくいかもしれませんが、雰囲気だけでも掴んでもらえればと思います。
動いていないな、どこかエラーでも出ているのかもしれませんが、ダイレクトにアクションを実行したときには、コンクリート
っていう、こちらの型に実装したものがちゃんと表示されるはずです。
それでは、これをプロトコル、まず分かりにくいかもしれませんが、インターフェースとして扱ったときには、ここでは コンクリート
ってちゃんと出るんですけどね。動かないですね、コンクリート
って表示されます。それはなぜかというとカスタマイズポイントが定義されているからです。コンクリートをプロトコル型で扱ったとしても、ちゃんとカスタマイズポイントがあるかを見てくれて、あった場合にはそれを動かしてくれます。オーバーライドに似たような動きを見せてくれます。
ここでカスタマイズポイントをなくしてしまうと、これでね、ちゃんとコードは成立するんですけど、そのときにカスタマイズポイントとして規定されていないインターフェースを呼ぶので、カスタマイズされているかどうかを見ずに、インターフェースの方のデフォルト実装が呼ばれるという動きを見せます。この大事なポイントとしては、プロトコルが意図した機能を提供するか、独自に変えていいかという大きな差につながってきます。独自に変えていいよっていう風にしない、要は宣言しないとすると、確実にここを橋渡しして、定位置として false
を渡してくれる。あくまでもプロトコル型として一様に扱ったときの話になってくるので、型を直接に扱うときとはまた話が分かれてきます。
そこはまたプロトコルの回でお話できたらいいかなと思います。とりあえず、こうするとカスタマイズポイントがフラグを取る場合だけに限られて、フラグがなかったときには既定値 false
ですよという感じの定義ができるので、主体はあくまでもフラグを取るもの。フラグを取らないものは便宜的に存在するものという雰囲気が出てきます。それによって、より型にデフォルト引数を添えたものと似たような雰囲気が出てきます。
あくまで似たような雰囲気ね。全然違う感じはすると思うんですけど、全然違うものだとも思うんですけど、実際にね。ここは本当に、プロトコルのなかなか難しい、頭が混乱しがちなところですけどね。概念的なものと具象的なもの、その辺の世界が全然違う感じというのを意識していくと、時間をかけて徐々に整理されていくかなという気がするところです。
とりあえず、最後にいい感じの例がありましたね。自分もあまり意識できてなかったところも見えて良かったかな。時間的にもね、ちょうどいい感じになってきたので、次回はこの続きからAPIデザインガイドラインを見ていくということにしましょう。
では今日の Swift の勉強会はこれでおしまい。また水曜日によろしくお願いしますね。お疲れ様でした。