https://www.youtube.com/watch?v=RqF4C_l_p7Y
今回も Swift API Design Guidelines にある「表現方法」の中から、引数の扱い方についての指針を引き続き眺めていきます。そんな中でも引数ラベルの表現の仕方に着目して、ラベルの付け方とその表現力みたいなところを確認していきますね。
そしてもし時間があれば API Design Guidelines の最後の項、特例みたいなピンポイントに定められた指針みたいなところも眺めてみるかもしれないです。
——————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #21
00:00 開始 00:40 前回の振り返り 01:30 既定値のある引数を置く場所 03:43 既定値を使わない関数との比較 05:48 既定値のある引数は英語表現に含めなくて良い 08:43 引数ラベルの表現方法 09:05 引数を区別する必要がないとき 10:54 min 12:52 zip 18:14 zip で無限に続く配列と合わせる 21:22 演算子のラベルについて 23:42 変換イニシャライザーの引数 24:44 値を保全する型変換 25:16 狭義の型変換 25:39 ラベル名で型変換を区別する 26:33 型変換の例 30:47 2の補数表現 32:13 表現可能な範囲に丸め込む 33:32 変換イニシャライザーの第2引数以降 35:20 値を保全した変換を再変換してみる 36:26 LosslessStringConvertible 38:19 整数と小数との型変換 41:22 整数と小数との厳密な型変換 44:56 前置詞句を構成する最初の引数 46:19 どこまでをベース名に入れるか 48:10 次回の展望 49:54 基礎を見直して成長を感じる 52:05 クロージング ———————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #21
今日も引き続き「APIデザインガイドライン」を眺めていく回になります。そろそろ終盤という感じで、今日で終わるかは分かりませんが、少なくとも今週金曜日までには終わるかなという印象を持っています。
今日はその中から「引数の表現方法」を見ていきます。前回から引き続きこの部分を見ていますが、まずは前回見たところをざっくりと復習しておきます。具体的には、「ドキュメントになる引数名を選ぶ」や「規定値を活用する」といった話をしました。規定値を使うことでAPIが明瞭化されるというお話です。前回参加した方は、これを思い出していただけたら嬉しいです。
今日はその次、「規定値のある引数の置き場所」を見ていきます。次のスライドは特にコードのサンプルがないので、まずここで説明しておきます。
コードの一番上には、デフォルト値が設定されている引数が2番目、3番目、4番目に置かれている compare
メソッドがあります。今、緑の帯で説明されているAPIの部分です。この配置の仕方のガイドラインですね。端的に言ってしまうと、後ろの方に置くのが望ましいです。その理由は、略せる引数というのは、そのAPI自体の説明という観点から見て必ずしも重要ではないからです。
話すときも同じで、省略可能なものは重要性が低いから省略できます。極論すると、なくても問題ないレベルのものです。この感覚で、規定値を持つパラメーターも、メソッドの基本的な使い方に影響しないものとして扱えます。実際に、今画面に出ている赤い帯の一番最初の例では、すべての省略可能なパラメーターを省略した場合、このように compare(other)
だけにしても、ちゃんと他のものと比較するという意味が伝わります。
ですから、このような場合、規定値を持つ引数は後ろの方に配置するガイドラインが提示されています。補足するものは後で追加すれば十分だという価値観です。
このスライドで特筆すべき点は、前回も見たように、名前付けのガイドラインとも関係があることです。SwiftのAPIは英語表現を流暢に表現することが重要とされています。その中でも、デフォルトパラメーターは流暢な英語に含めなくてもいいというルールがあります。
これにより、引数の配置場所を後ろにすることで、名前付けのガイドラインとも相性が良くなります。先頭に重要なAPIの意味が自然に汲み取れ、後ろにオプションが付いてくるようなスムーズな流れができます。これにより、どのパラメーターがオプションなのかもラベル名から分かるようになります。
オプション的な部分は流暢な英語表現にするとまどろっこしい表現になりがちなので、後ろに配置することで簡潔な良い表現につながっていくのです。これが今日の内容です。 とりあえず、自分が話を引き続き行います。引数についての表現方法を見てきましたが、次は引数ラベルの表現方法に着目します。引数ラベルの使い方についてです。
まず、引数を区別する必要がないときにはラベルを省略するというガイドラインがあります。ただ、この英文をうまく訳せなかったので、感覚的にこういうことを言いたいんだろうなと思って捉えています。原文が「API Design Guidelines」の中にある、「When argument labels are not needed, they can be omitted」的な内容です。
たとえば「move(from x: Int, to y: Int)」のように始点と終点を明確にするためのラベルは必要ですが、関数 min
や zip
のようなものには特にラベルは必要ないだろうというガイドラインが示されています。
具体的に、たとえばSwiftで min
関数を自作する場合でも、ラベルを付けるとすると以下のような感じになります。
func min(value1: Int, value2: Int) -> Int {
return value1 < value2 ? value1 : value2
}
ただ、ここでわざわざラベルを付ける必要はないというのがガイドラインの意図です。この場合、引数をただ Int
型として取るだけで十分です。
また、zip
関数も同様です。zip
は2つのシーケンスを結合してタプルとして扱えるようにする関数です。このとき、それぞれのシーケンスに特別なラベルは必要ないです。例えば以下のように使います:
let sequence1 = [1, 2, 3]
let sequence2 = ["a", "b", "c"]
let zipped = zip(sequence1, sequence2)
このようにラベルがないことで、左のシーケンスが主体か、右が副次的かといった意味付けがされず、ただ2つのシーケンスを結合するということに集中できます。
ちなみに、zip
の動作について、要素数が異なる場合の仕様は少し注意が必要です。例えば、以下のように要素数が異なるリストを渡すと:
let sequence1 = [1, 2, 3]
let sequence2 = ["a", "b"]
let zipped = zip(sequence1, sequence2)
結果は以下のようになります:
[(1, "a"), (2, "b")]
要素数が合わない部分は無視されるという仕様です。これは使い方によっては微妙な場合があるので注意が必要です。
以上が引数ラベルに関する説明です。ガイドラインに従うことで、コードの可読性や意図が明確になり、シンプルに保つことができます。 微妙といえば微妙なんですよ。品数としてはいいんですけど、安全ではないんです。パラメーターで渡す場合には、意図しない結果が起きる可能性があって、そうなると 1
と a
をペアとしたストラクトなりタプルなりを一つの配列で表現したほうがアクセント感があって良いでしょう。
そうですね、ちょっと余談になりますが、仮にこの zip
が要素数が違ったときにランタイムエラーが出ると困るよねという話もあります。ですから、前提として要素数が同数であることをコードに明示しておく必要があるかもしれません。
具体的に例を挙げると、以下のように zip
の使い方を考えてみます。
let array1 = [1, 2, 3]
let array2 = ["a", "b", "c"]
let zipped = Array(zip(array1, array2))
for (num, char) in zipped {
print("Number: \\(num), Character: \\(char)")
}
今回、配列 array1
も array2
も3つの要素だけなので、それらがタプルとして結合され、この結果が表示されるわけです。
しかし、もし片方の配列の要素が4つだった場合でも3つしか返ってこないことがあります。たとえば、
let array1 = [1, 2, 3]
let array2 = ["a", "b", "c", "d"]
このような場合でも、返ってくるのは 3
つだけです。このように、要素数が異なる場合には注意が必要です。
そのため、配列の要素数が一致するかどうかを確認する必要があります。要素数が一致しない場合に、情報が落ちてしまうことを防ぐためにはガード条件を設けて対応するのが良いでしょう。
if array1.count == array2.count {
let zipped = Array(zip(array1, array2))
// 続けて処理を行う
}
また、配列1にキーがあって、それらのキーに対してランダムな値を割り当てたい場合には無限に続くイテレーターを利用することも可能です。
let keys = ["key1", "key2", "key3"]
let randomValues = AnySequence { AnyIterator { Int.random(in: 0..<100) } }
let zipped = Array(zip(keys, randomValues.prefix(keys.count)))
for (key, value) in zipped {
print("Key: \\(key), Value: \\(value)")
}
このように AnySequence
と AnyIterator
を利用して、無限に続くランダムな値を生成しつつ、必要な分だけ取り出して使うことができます。
このように、配列同士の要素数が異なる場合でも安全に処理できるような工夫が必要です。APIのデザインについても、このような要素を考慮する必要があります。同じ重さを持つ引数をラベルなしで使う場合と、特定の引数が重要な意味を持つ場合では、APIデザインの方針が変わることもあります。
例として、等価比較演算子 ==
の場合、ストリング型などに対する同値比較はラベルなしで使われることが普通ですが、特定の状況ではラベルが重要になることも考慮しておくべきです。これは、演算子の左右の操作数に意味がある場合、例えば掛け算や割り算など、順番を入れ替えると結果が異なる演算子が存在するからです。
まとめると、プログラミングの際には配列の要素数やAPIのデザインの方法に注意を払い、適切なラベルやガード条件を設けることが重要です。 だから、とりあえず原文を曖昧に訳してみたけど、多分大丈夫そうですね。次は、個人的にとても好みなラベル名の表現方法について話します。
Swiftはイニシャライザを使って型変換を行います。キャストという方法もありますが、別の型に変えるときにはイニシャライザを使って行うのが一般的です。このときにラベルを使って、その変換イニシャライザがどういう意味を含むものなのかを説明するのが重要です。
ここで大事なのが「値を保全する型変換」、つまり Value-Preserving Type Conversion
です。ある値を別の値に変換するときに、本質的な意味は変えない、そういった型変換を指します。一方で、意味を再解釈して変換する、つまり元の値が変わる可能性のある変換を Narrow Type Conversion
と言います。
この二つの種類の型変換を区別するために、ラベル名を工夫してAPIを読む側に意図を明確に伝えられるようにするのがガイドラインです。具体的には、値を保全する型変換の場合、最初の引数のラベル名を省略します。値の再解釈を伴う型変換の場合、最初のラベルでその変換の意味合いを説明する形を取るのが大事です。
例えば、ある Int
型の値を String
型に変換する場合、以下のように書きます。
let a = 10
let str = String(a)
この場合、ラベル名が指定されていないことから、元の値をそのままの形で String
に変換するという意味になります。このように、元の値が 10
だったものが文字列の 10
になることが分かります。
これがもっと複雑なケースになると、例えば Int
型の -1
を符号なし整数の UInt
に変換しようとすると、ランタイムエラーが発生します。
let b = -1
let uintB = UInt(b) // これはランタイムエラーになります
符号付きの整数を符号なしの整数で表現することはできないため、同じ値として表現することはできません。このコード行を見ることで、この変換が何を意味するのかを理解することができます。
特に情報処理の知識がある人は、符号付き整数の表現方法やビットでの表現をイメージしやすいと思います。それによって UInt(-1)
が何を引き起こすのか、誤解せずに理解できるでしょう。しかし、ラベルによって説明が付けられていない場合、その値と全く等価な重みを持つものに変換しようとしていることが分かり、-1はオーバーフローしそうだ、表現範囲外だと考えることができます。
このあたりをもうちょっと詳しく見ると、例えばビットパターンとして値を再解釈する場合、そのビット表現がイメージできる人ならばどんな値になるのかが想像しやすくなります。最上位ビットが 1
になる感じです。時々、どういう表現だったのか忘れてしまうかもしれませんが、マイナス1のビット表現は確かにそのような感じになっていたと思います。 とりあえず、この最上位ビットを1として解釈してくれるというイメージから、このような結果になるということです。全てが1か、そう言われると確かにそうだったかもしれません。8ビットの値だと127の次が確か-128ですよね。それから、-126、125といった具合に続きます。そうですね、この辺りは二の補数表現といって、計算するときにマイナスの値であろうと、プラスの値であろうと関係なくビット(二進数)として足し合わせると、自動的に適切な値に計算されます。これが二の補数表現です。
ビットパターンの話ですが、今どきのプログラミング言語の整数表現はこのような表現になっています。また、ビットパターン以外にも例えばマイナス1だった場合には、表現可能な範囲の数字に丸め込むことができる機能があります。たしか「クランピング」ですね。あまり使ったことはないので忘れがちですが、確か0が得られるんじゃないかと思います。0が得られるというのがラベル名によってはっきりと示されることもあります。ビットパターンやラベリングなど、様々なガイドラインの表現方法が存在します。
このガイドラインは、非常に重要なポイントだと思いますので、意識的に抑えておいてほしいですね。第一引数がラベルで説明される場合は第二引数以降もラベルで説明しても問題ありません。例えば、文字列を数字に変換するときに引数を取ることができ、これを16進数として受け取るようにしたときに変換の結果が変わってきます。
例えば、10
というのは16進数でアルファベットのa
と同じです。数値的に見ると10進数の10と16進数のa
は全く同じものです。この2つは見た目のテキストは違いますが、値としては全く同じ重みのあるものであり、バリュープリザービングタイプコンバージョンといいます。
第一引数のラベルは省略されますが、第二引数以降はその引数が何であるかをラベルで説明します。これがガイドライン的に問題ないとされています。バリュープリザービングタイプコンバージョンの特徴として、変換後の値と変換前の値が同じであるため、逆変換も同じ値を保持したまま行えることが挙げられます。ビットパターンも復元でき、値としての価値が変わりません。
ちなみに、Radix
を使ってInt
型で変換して元に戻すことができます。これもバリュープリザービングタイプコンバージョンの一例です。また、ロスレスストリングコンバーティブルというプロトコルもあります。これは、バリュープリザービングタイプコンバージョンを表現します。
カスタムストリングコンバーティブルもありますが、多くの人に馴染みのある型の値をしっかり表現できる形で文字列に変換するプロトコルです。これもバリュープリザービングです。さらに、文字列から自分自身のインスタンスに復元できることを約束する場合には、ロスレスストリングコンバーティブルで説明します。このプロトコルには、String
を取ってイニシャライズするメソッドが備わっており、失敗することもありますが、文字列から元の値に戻せます。
Int
型やString
型などの単純な値型は、このロスレスストリングコンバーティブルに準拠しており、お互いに値を保持したまま変換が可能です。このように、詳細な変換ルールやガイドラインを理解すると、プログラミングがさらに深く理解できるようになります。 おまけのお話ですけど、こうやって理解を進めていくと、今まで混乱しなかった問題が生じてくるんですよ。例えば、Double
型があったときに、1.5とかを整数に直してみようと思ったときに、自分は考えすぎてよくわかんなくなっていくんです。これは値を保全するタイプのコンバージョンなのか、整数として丸め込むのか。Swift的には、これは値を保全するものとして捉えていて、ラベルを省略したイニシャライザーで変換をかけるというふうになっています。
これは純粋に、不動小数点数の世界での1.5というのと、整数としての表現に限った1が同じものを表現しているという価値観の表れです。解像度が下がるけど同じ絵には変わりないよねという捉え方ですね。数学的に言うと、正六角形、正八角形、正十二角形、とずっと細かくしていくと極めて円に近づいていくのを円とみなすようなものです。これをみなせないと、パソコン上でどんなに頑張っても円が描けなくなるという、世界観的な崩壊が起こってくるわけですよ。
だから、どのあたりで解像度を抑えていくかが大事になってきます。Swiftの世界では、値を保全するものとして捉えています。もちろん、逆変換は精度が落ちてしまいますが、それは仕方ないと捉えることになります。このあたりは理解が難しいですが、慣れていくしかないですね。意外と難しいですよね。これ、哲学的な分野になってくるんじゃないかと思いますが、これはいたしかたないです。
もし解像度的に丸め込まれるのが嫌な場合には、整数型に変換するときにExactly
というラベルを使うと良いです。こうすると、1.5を整数として正確に表現することができないのでnil
になります。これは、近似的に値を取るときに使います。
例えば、数値型の変換では、ラベルを付けて、普通とは違うコンバージョンであることを説明したりします。例として、UInt64
からUInt32
に変換するときには上位互換なので安心して変換できるが、UInt32
からUInt64
に変換するときには切り詰められるかもしれないことを示唆できます。
こんな感じで、各種変換については選択肢が増えてくることがわかります。このように様々な表現ができるようになるのは大事なことです。ぜひ覚えておいてください。
さて、次に進みます。他に、全知識を構成する最初の引数というものについてもお話しします。これは、全知識のラベルに含める、つまりベース名に入れないというガイドラインになります。 だから、removeBoxes
のような名前にしないということですね。これは純粋なルールです。ただし、引数で複数のものを1つのものとして表現している場合には、ラベル名ではなく前置詞を外に持っていくというルールがあります。この辺りがややこしいところですが、意外と複雑な例になります。しかし、時間があと5分ほどあるので、話し続けましょう。
どこまでをベース名に入れるかというガイドラインについて説明します。一番下の緑と赤の例を比べてみるとわかりやすいかと思います。たとえば、move to x y
としたとき、このx
とy
で1つのロケーションを表現している場合、ガイドラインに従うと前置詞はラベルにするべきですが、to x
とy
のバランス感覚が悪くなってきます。同様に、RGBのred
, green
, blue
でもバランスが崩れてしまいます。そのため、特例として前置詞を外に持っていきます。
しかし、たとえばmove to point
のように一点を指す場合は、move to x
にしましょうというのが基本原則になります。このように迷ったら、このガイドラインに従うとよいでしょう。
引数のラベルについての表現方法には特別な事例が二三個あります。これについては次回に回します。APIデザインガイドラインをすべて見終える予定です。その後は、この勉強会でメインに扱う予定のSwiftの言語仕様を見ていきます。
今日はここまでにしましょう。何か質問や意見があればどうぞ。このガイドラインは基本的に覚えるべき内容です。一度読んだだけでは忘れてしまうことが多いですが、このような勉強会が役立つと思います。自分としてもこの勉強会のために読み返していると新たな発見が毎回あります。非常に基礎的な内容ですが、何度読んでも面白いものです。
そういった意味でも、ぜひ思い出したときには読み返してみてください。普段はよほど疑問がない限り読み返さないというのが現実かもしれませんが、数年後に見返すと捉え方が変わることもあります。
成長を感じられる機会にもなります。自分のソースコードが気持ち悪いと感じることがあると思いますが、それと同じように、基礎的なドキュメントを見返すことで、自分がどれだけ視野が広がったか実感できるかもしれません。どこか旅行に行ってきれいな景色を眺めながらついでに見るのも良いと思います。
この勉強会は非常に長く感じますが、ザクザクっと見れば10分くらいで終わるような分量です。時間もちょうど良いので、今日はここまでにしましょう。次回はAPIデザインガイドラインの最終回ですので、興味のある方はぜひ見に来てください。お疲れさまでした。