https://www.youtube.com/watch?v=AEq5bb5XQFA
前回までは Swift API Design Guidelines の中から「命名規則」に着目して API の描き方をみていきましたけれど、今日から「表現方法」について眺めていきますね。
———————————————————————— 熊谷さんのやさしい Swift 勉強会 #18
00:00 開始 00:43 全般的な表現方法 01:36 プロパティーの計算量 16:39 didSet 18:51 焼却計算量 21:12 オーダー表記 22:13 min 関数の計算量 22:29 プロパティーにするかメソッドにするか 25:25 フリーな関数を使う場面 26:47 明確な主体が存在するか 27:14 何にも制約されないとき 28:48 確立された表記方法のとき 29:45 フリーな関数を使う例 31:47 関数を型に所属させる例 34:42 制約のない主体を扱う場面 38:51 確立された分野の表記方法を扱う例 43:24 フリーな関数を使おうと思ったときは 44:40 大文字小文字の区別 45:55 キャメルケース 49:01 頭字語の扱い 53:31 頭字語における例外的扱い 55:34 次回の展望 ————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #18
今日は、引き続きAPIデザインガイドラインのお話です。今回は新しいセクションに入ります。以前はガイドラインの基本項目やドキュメントコメントについて、また命名規則についても学びました。命名規則については特に長くじっくり見てきましたが、今日は表現方法についてです。
一見、命名規則と同じように感じるかもしれませんが、APIデザインガイドラインでは表現方法は別のセクションとして記載されています。メソッドの名前に直接関与する部分もありますが、そうでない部分もあり、もう少し広い視点でAPIを読みやすくデザインしていこうという内容になります。
まず最初に挙げられているのは、ドキュメントコメントについての追加ルールです。計算量についてのガイドラインです。プロパティは一般的にオーダー1(O(1)
)だと思われがちです。要はプロパティにアクセスすれば、瞬時に答えが返ってくるという感覚です。何か値を入力したらすぐに応答が返ってくるという時間感覚があります。そのため、プロパティがO(1)
でないときにはドキュメントコメントで明記しましょうというのがガイドラインに含まれています。
具体的にどういったことかというと、例えば変数に値が入っていて、それを使おうと思ったときに、グローバルであれば普通に値が取得できるといった感覚があります。これがオブジェクトになるともう少し分かりやすいかもしれません。例えば、ストラクトがあり、そのストラクトがvalue
というプロパティを持っているとします。このインスタンスを作成してvalue
にアクセスするとき、データ構造体が持っているvalue
がすぐに取得できると思うじゃないですか。しかし、これが実際には30分かかるとは想像しないですよね。
そのため、プロパティがO(1)
でない場合には、それをドキュメントコメントに書いておくべきです。保存型プロパティについては基本的にそんなに遅くなることはないと思いますが、計算型プロパティの場合は処理が重くなることがあります。計算型プロパティではゲッターを使って値を計算するため、重たい処理を含むことがあり、その場合にはドキュメントコメントに計算量を明記しておくことが重要です。
例として、O(1)
でなく、計算量が非常に大きい関数があるとします。ドキュメントコメントに「オーダー O(n^2)
」と書いてあれば、「これは重い処理だな」と事前に分かります。また、非同期処理にするなどの対応が必要だと判断できます。このように、期待される計算量から逸脱している場合には、ドキュメントコメントに詳細を明記しておきましょうというガイドラインになっています。
例えば、文字列のカウントプロパティなどは一般的には瞬時に返ってくると考えられます。しかし、重たい処理を含む場合にはその旨をドキュメントコメントで示す必要があります。そうすることで、利用者がプロパティの使いどころを適切に判断できるようになります。
要は、計算量が直感的でないプロパティやメソッドについてはドキュメントコメントで詳細を記載しておき、利用者にその旨を伝えることが大事です。これがAPIデザインガイドラインの一部として重要視されています。 こうすると配列のカウントはもしかすると数えているかもしれません。ドキュメントコメントには特にオーダーは書いてないですけどね。右側には今ドキュメントコメントが出るようにしています。でも、多分配列のカウントは数えています。Array
型は何かをやってるかもしれませんけど、自分で作ったコレクションなんかは絶対数えますよね。Sequence
とかでね。インデックスがあるから早いのか、だからSequence
はカウントを持たないんですね。だからコレクションは大丈夫なんです。インデックスの範囲を取ればいいからですね。ディクショナリーも一緒です。
とりあえず、isEmpty
の方が最適化されて早くなるみたいです。みんないい感じの補足を入れてくれています。とりあえず、カウント数を数えるのと空であるか否かを判定するのはちょっと違うみたいですね。文字列の場合も、確かisEmpty
が最適化されているはずです。確か、Objective-Cの頃にそういうのを調べた気がします。
今、計算型プロパティと保存型ストアドプロパティの話をしています。それと、アレイのremoveFirst
がもしかするとと指摘してくれた方がいます。removeFirst
はちょっとメソッドになっちゃうので、プロパティの話とは少し違いますけど、それでもメソッドの場合も計算量が多いようなら書いてあげると親切でしょう。これがvar
になってないからですね。親切になることは間違いないです。
計算量を意識して書いたコメントをつけてあげましょうということで、ドキュメントコメントに@complexity
を使って書くことが基本になってくるみたいですね。例えば、O(n)
と書いてあげる感じです。ありがとうございます。
続いて、計算型プロパティと保存型プロパティについてですが、ストアドプロパティは直感的に見て、普通保存されているものを取り出すだけなのでサクサクっと行きそうな気がします。でも、didSet
やwillSet
なんてものがあるから、処理が重くなることもあります。それで計算型プロパティについてガイドラインに従うことが大事です。計算量が多かった場合には、ちゃんとコメントをつけてあげるのが親切ですね。読みやすくて理解しやすいコードになります。
同様に、メソッドにも計算量がかさむ場合はしっかり伝えていくと親切です。例えば、removeFirst
とかも計算量がかさむときには計算量についてコメントをつけてあげるのがいいですね。
それから、didSet
があると内部的には実質的に計算型プロパティとして扱われるという感じです。何かフックが入るので、確かにゲッターが追加されるイメージになります。ここまででOKですかね。こんな風にプロパティの計算量をちゃんとコメントに書いていくことが重要です。
計算量の話については前回もちょこっと出てきましたね。何の話で出たのか忘れちゃいましたけど。それについてのO(1)
と、コメントに特にオーダーが書いてないのとでは大きな違いがあります。これが計算量をどれくらいかってわからないと、ガイドラインに沿っているのかどうかがわかりません。
他にも、values
のappend
とかも計算量がO(1)
って書いてあります。この話が前のサーバーサイドLT会議でちょうど話題に上がっていて、配列のappend
ってO(n)
じゃないのかという話がありました。でも、これはO(1)
って評価するんだというのが面白かったです。Swiftもその評価に倣ってO(1)
になっていて、平均を取るんですね。 超たくさんの呼び出しが起こった時に、一般的にどういう計算量になるか、みたいな話です。焼却計算量という考え方があります。これはリストの末尾に要素を追加する処理などで使われます。この「焼却計算量」がO(1)ではない理由が面白いので、興味ある人はぜひ調べてみてください。また、サーバーサイドのLT会で阿部さんに聞いてみるのも面白いかもしれません。
正直、計算量の話って軽く流してしまいがちですよね。私もあんまり理解できていない部分が多いです。計算量を具体的にどうやって求めるか、オーダー記法(O(n)など)を理解しているつもりですが、実際は深く理解できているわけではないです。
オーダー記法O(1)はデータ量に影響せず、O(log n)はデータ量のログに依存します。O(n)はデータ量に比例し、他にもO(n^2), O(n^3), O(2^n), O(n!)などがあります。それぞれの計算量がどのように変化するかを理解することが重要です。
最小の要素の話ですが、自分がファースト例として挙げたものでしたが、これはミニマム配列の話だった気がしますね。バリューを保持している配列で、それ自体が貫通として扱われるのです。
設計手法によってプロパティでもメソッドでもどちらでも良い場合があります。昔、計算量がO(1)のものはプロパティ、それよりも大きい計算量のものはメソッドにした方が良いという意見をどこかで見たことがあります。公式なガイドラインではないかもしれませんが、計算量の観点から判断するのも一つの手かもしれません。
たとえば、min
のように内部的に処理を行う場合はメソッドにするという価値観もありますが、これはケースバイケースです。ドキュメントコメントに計算量を書いておくことも重要です。
次に、フリーな関数(グローバル関数)についてです。Swiftではフリーな関数を特別な場面に限って使うガイドラインがあり、明確な主体が存在しない時に限りフリーな関数を使うべきとしています。明確な主体が存在する場合、その主体に対してメソッドとして提供するのが適切です。
例えば、最小の要素を求める際にグローバル関数を使うのは、良い例かもしれません。 次に、制約のないジェネリックス関数について説明します。言葉だけで説明すると難しいかもしれませんが、エニー型を取る関数について話します。ここで言うエニー型とは、引数として取る場合や主体として取る場合のことです。
主体があるかないかがこのルールで重要です。主体がないと所属できないような状況です。逆に、主体が広すぎると関係ないところにも波及してしまいます。オブジェクト指向で考えると分かりやすいかもしれません。エニープロトコルは全ての型が準拠します。他にも、例えばコレクションプロトコルのように、全てのコレクションに波及するものもあります。このように波及する範囲がとても広いわけです。
そのため、波及範囲が曖昧な場合、関係のないものとして備わってしまうことがあります。制約がないまたは弱い場合には、所属させずに外に置くことで余計なものを避けるという感じです。
また、前回の専門用語の話でもありましたが、文化的に独立して一般に使われるものの場合、例えばサイン関数があります。サイン関数は不動小数点数型を主体として取りますが、不動小数点数型に直接サイン関数を所属させずに、外側に置いて一般的な習慣に従った表現方法を取るというルールがあります。
具体例があると分かりやすいですね。例えば、最小値を求める min
関数について考えてみましょう。数字1個目、数字2個目、数字3個目があるときに、どれが一番小さいかという機能を実装したい場合、A、B、Cのどれも対等で主体というわけではありません。この場合、Swiftの原則として、主体にメソッドを持たせることになりますが、主体がない場合は、関数として外側に置くしかありません。
以下のように書きます:
func min<T: Comparable>(_ values: T...) -> T {
return values.min()!
}
このようにして、関数を用意します。これは、制約のない関数を使う適切な場面の一例です。
同じ最小値を求める場合でも、配列の要素の中から最小値を取るときには、配列の持っている値が主役になります。この場合には以下のように要素に対してメソッドを持たせることになります。
extension Array where Element: Comparable {
var minValue: Element? {
return self.min()
}
}
このように、配列に対して minValue
プロパティを用意します。ガイドライン的には、このように主体がある場合には、フリーな関数ではなく、主体にメソッドやプロパティを持たせる方が良いということです。 次に、何にも制約されないジェネリック関数についてお話しします。これについては特に細かく説明する必要はなさそうですね。ここでは主に print
関数が使われていますが、他に何かあるのでしょうか。やはり print
が最も分かりやすい例のようですね。
例えば、Swift では実際に実現不可能なことですが、fatalError
の使用例も面白いですね。確かにこれはおもしろい例です。fatalError
で何かメッセージを出すというのも具体的な例のほうが良いかもしれません。
次に、文字列型に対して拡張(extension)を利用する場合の話です。例えば、文字列に対して何かメソッドを追加する際、その範囲が広すぎると捉えることもできます。ここでの例としては、String
型に対して関数を追加する場合などが挙げられます。それに対して、fatalError
のような方法でメッセージを出力するのは少し無理があるかもしれません。
具体的に言うと、print(self)
を使った方法です。この場合、戻り値の型を求められるので、適切な型を返す必要があります。例えば、fatalError
を使うことが考えられますが、これをあまり呼ぶ気にはなりません。しかし、例としては有効かもしれません。ただし、fatalError
は中二病っぽいイメージがあるので慎重に使うべきです。
次に、確立された分野、例えば数学の関数のような場合です。例えばサイン関数(sin)は、主体が Double
であっても外部に定義するのが妥当でしょう。ここで、C言語のライブラリを利用して sin
関数を呼び出す方法を紹介します。この場合、Double
型に対して拡張を行い、sin
メソッドを追加する方法が考えられます。ただし、一般的な文化に基づいて、sin
関数をフリー関数として用意するのがガイドラインとなります。x.sin
という形式はやや違和感があるため、標準的な方法として sin(x)
を使うべきでしょう。
以上が、今回はジェネリック関数や拡張、fatalError
の使用についての詳細な説明です。 とりあえず、こういう風に文化にしっかり根付いているものは、それを使いましょうっていうガイドラインです。あんまりフリーな関数を定義する場面っていうのは、一般プログラマーにはそんなに機会がないのかなという感じがします。このガイドラインをじっくり眺めてみると、そんなところかなと思います。
フリーな関数を使う場面が決められているので、手軽に書けるじゃないですか。グローバル関数って簡単に書けるんで、ついつい何かのために使ってしまいがちですけれども、ずっと書こうとすることがあったとしたら、その時に「これはどれが主体なのか」「どこに所属するのが妥当なのか」と一回手を止めて考えてから書くようにする。そうすることで、Swiftコードらしくなってきます。これは結構大事なガイドラインになってきます。
これを守るか守らないかっていうのが、プロトコル思考っていう価値観に大きく寄っていくかどうかの境目にもつながっていると思うので、結構大事なポイントです。今日一番覚えて帰るとしたら、多分ここですね。覚えて帰るほどたくさんは紹介しないですけどね、時間の都合でね。
もう一個言っておきましょうか。あと10分あるんでね。大文字小文字の区別。これね、Swiftを当たり前にやってる人にとっては当たり前なんですけど、他の言語をメインでやっててたまにSwiftをやるっていう人にとっては、とっても大事な命名規則です。大文字小文字の区別って書いてありますけど、要はシンボル名、引数名も含めて、名前付けの時には必ずキャメルケースを使う必要があります。Swiftの場合は、型やプロトコルの名前は大文字から始まるアッパーキャメルケース、それ以外には必ず小文字から始まるローワーキャメルケースを使っていきます。
この原則はどの場面でも絶対ですね。外れてることは今はないはずです。昔は外れてたんですけど、Swift 2の頃は。ここが大事なポイント。キャメルケースっていうのは、単語をつなげていく時に単語間のスペースを詰めて、各単語ごとの頭文字を大文字にする。ただし、先頭だけは型やプロトコルの名前を除いて小文字にするのが基本です。だから、型は大文字、メソッドは小文字、グローバル関数も小文字です。この原則を守ることで、Swiftコードらしさが保たれます。
モジュール名も大文字で始めますね。これも他の言語だと往々にしてアッパーキャメルケースを使っていくみたいなことがあります。関数名とかが大文字で始まることがあったりしますが、Swiftの場合はこれを守ることが大事です。うっかり他のつもりでやってしまったりすると、何の実害もないわけではありませんが、効率や信頼性に影響します。人とコミュニケーションを図るときには、シンボル名が大文字で始まっているとついつい型だと思ってしまうので、例えば fatalError
という関数が大文字で始まっていると、それをインスタンス化していると勘違いしてしまいます。これが小文字の f
になっただけで、違和感なく読み取れるんです。
このような市民性が出てくるので、これも結構大事なポイントになってきます。では、大体この辺にして、次に行きたいと思います。他にもいろんなケースがありますが、それはまたの機会にします。 とりあえず、これ面白いですね。今みたいにロワーキャメルケースやアッパーキャメルケースについてお話ししましたが、アメリカ英語において全てを大文字で表記するという英単語パターンがあるわけです。そういった場合、登場場所に応じて一律に大文字または小文字で表現するというルールがあります。
例えば、最初に自分が結構混乱したのですが、以下のコードがわかりやすいかもしれません。ASCII
というのは、American Standard Code for Information Interchangeの略語です。これが変数名で使われるときには、原則としてロワーキャメルケースを使用します。しかし、ASCII
のような言葉がキャメルケースの大文字表現の部分で出てきたときには、それに続くアルファベットはすべて大文字にします。例として、isASCII
のように書きますが、先頭の小文字のパートでASCII
のようなすべて大文字の言葉が出てくるときには、すべて小文字に変えて表現します。
例えば、一番上の例がわかりやすいです。UTF
は、ユニバーサルなんだっけ、まあいいや、UTF
は大文字で始まる3つの単語です。これも先頭にある場合は小文字でutf
と書き、途中でUTF
が出てくる場合には、例えばisUTF8
のように、UTF
の部分は大文字で書くというルールになっています。
コメントに書いてもらったように、URL
は結構、小文字で書き始めて、そのまま小文字で書いてしまうことがあります。自分もよくやっていて、ガイドラインを読み直すうちにやっと身についてきましたが、URL
と書きたがることが多いんですよね。しかし、このルールに従うと、URL
の部分は大文字になります。インポートすると、Foundationの中で例えば何か出てくるかな。パッと見たところでは、あまりいい例は出てきませんが、URLのエントリーポイントなどを定義する際には、このように書きがちです。しかし、ガイドライン的にはこうなるといった感じです。
また、キャッシュドURLレスポンスなんてのもありますが、意外とガイドラインにしっかり沿っています。このガイドラインには、「その他の当該語は一般語と同じルールで表記」と記されています。例えば、レーダー
やスキューバ
などがそうです。これらは英語表現として、R
だけ大文字でadar
が小文字、スキューバ
ならS
だけ大文字でcuba
が小文字になります。レーダー
は、Radio Detecting and Rangingの大文字を集めて作った単語です。スキューバはSelf-Contained Underwater Breathing Apparatusの略語です。要するに、略語でも表記通りに扱おう、スキューバが途中に入っていても全部大文字で書くことはしない、というルールです。
この辺りについては、知っていようが知らなかろうが、表記通りなので助かります。もしこれがすべて大文字で書くルールだったら、日本人にはちょっと辛い感じがしますが、まあこんな感じです。
では、今日は時間になったのでこれぐらいで終わりにしましょう。引き続き、また表現方法について見ていこうと思います。次回もよろしくお願いします。お疲れ様でした。