https://youtu.be/ZAk-1sU-48c
The Basics
の 型安全と型推論
について引き続き眺めていきます。今回は初期値を与えたときの型推論に着目し、リテラルと絡めてその挙動を見ていく感じになりそうです。どうぞよろしくお願いしますね。
————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #115
00:00 開始 01:14 独自の型でコンパイル時の検査はできる? 04:40 Playground でエラーがコンパイル時かを知る方法 05:21 リテラル型を独自のものに変えられる? 06:02 _ExpressibleByBuiltinIntegerLiteral 08:00 Builtin 名前空間 09:07 ここまでで分かったコンパイル時のリテラルの値範囲を絞っていく方法 10:25 エラーと警告の扱いについて 12:25 今回の展望 12:42 Builtin 名前空間を使う方法 14:44 Builtin.IntLiteral を普段の数値型に変換できる? 19:34 リテラル型への変換時の fatalError の扱いは? 21:11 初期値と型推論 21:31 型推論が特に有効な場面 22:32 リテラル 23:48 リテラルの推論のされ方 26:29 リテラルは型を持たない 27:54 リテラルと型推論の相乗効果 31:13 リテラルの既定の型 36:57 クロージング —————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #115
では、始めましょう。今日は「型安全」と「型推論」の話の続きを進めていきますが、その前に前回のお話について少し触れておきます。
前回、リテラルがコンパイル時にオーバーフローチェックをするという話をしました。このことについて、独自の型でも同じことができないかという話が出ました。しかし、試してみた結果、結構な量のコードを書かないといけなさそうで、途中で諦めたということでした。
実際に試してみてわかったことなのですが、アプローチが間違っていたと感じました。たとえば、Int8
に200を代入しようとすると、オーバーフローがコンパイル時に発生するという話です。これを実装しようと考えた際、ビット幅を想定するからIntrinsicが関与するのではないかと思ったのですが、違ったようです。
具体的には、min
とmax
があるのでこの部分が関係するかと思いきや、最小値と最大値を決めるためにValueType
のインスタンスを必要とすることになり、このインスタンスを用意すると結局は型のインスタンスが関与する場所が明確になりました。
リテラルから値への変換にはExpressibleByIntegerLiteral
というプロトコルが重要ですね。リテラルから画面上のValue
型へ変換する際に、エラーを出すかどうか、コンパイル時にエラーとして判定するかどうかが、このプロトコルに依存してきます。
このプロトコルにはイニシャライザが含まれており、このイニシャライザが値の範囲を判定する可能性があります。integerLiteralType
が関与するのですが、この例ではInt
型になっています。つまり、let value: Int8 = 200
のように書いたときに、このイニシャライザが呼ばれる際にintegerLiteralType
が渡される部分でコンパイルエラーを解決できるはずです。
たとえば、Int8
ならInt8
の範囲、Int16
ならInt16
の範囲に区切られます。こうして、Int8
を取ろうとして500
などを渡すとどうなるかというと、多分コンパイルタイムにエラーが出るでしょう。
これを確認するために、プレイグラウンドで適宜コードを書いてエラーが出るかどうかを見るのが良いでしょう。たとえば、以下のようなコードで確認できます。
let value: Int8 = 200
この場合、Int8
なのでコンパイル時にエラーが出ます。Int16
ならコンパイルが通り、正常に動くでしょう。このように、リテラルの範囲でコンパイル時にエラーが出るかどうか確認することができます。
カスタムな範囲を作り、コンパイルタイムエラーを出すためには、独自のリテラル型を作り、この型にExpressibleByIntegerLiteral
を適用させる必要がありますが、実際にはそれほど簡単ではないかもしれません。特にバイナリー整数型に適用すると難しい部分もありますが、挑戦してみる価値はあるかもしれません。 とりあえず、理解しやすいところから見ていきましょう。コンパイル中のXcodeに出てくるエラーは、変なところがピックアップされて分かりにくいことがあります。今回の場合は独自の型を定義しましたが、「Expressible by Integer Literal」に準拠していないというエラーが出ています。コンソールにはもう少し詳細な情報が表示されます。これは、「Expressible by Built-in Integer Literal Protocol」に準拠していないといけないというエラーです。これに準拠させれば、独自の型がリテラルとして使用できるようになります。
しかし、問題となるのはアンダースコアがついたシンボルです。アンダースコアがついたシンボルに一般ユーザーが手を出すと、将来のバージョンで使えなくなる可能性があったり、隠蔽されていて使えない場合があります。今回も残念ながら隠蔽されているものです。
このように、「Expressible by Built-in Integer Literal Protocol」に準拠しても、特定のパラメータが「Built-in Namespace」内の「Int Literal」型に対応していないとエラーが出ます。そして「Built-in」系の名前空間が通常の実行環境では触れないようになっています。これが見つからないため、独自のリテラルを定義することが非常に難しくなっています。
続いて、補足のお話です。前回エラーと警告についてお話ししましたが、そのときは具体的な指針が示せませんでした。ただ、Appleがエラーや警告についての具体的な指針を示していた情報があるはずです。例えば、エラーには何を書くべきか、警告には何を書くべきかといったガイドラインです。これが「Swift.org」やGitHubのAppleのSwift、またはSwift Evolutionに存在していると思いますが、今回は見つけられませんでした。
これからは、型安全と型推論の話に入っていきます。 このテキストでは、Swiftの初期値と型推論について学習しようとしていますが、その前に「ビルドインにアクセスする方法」についてコメントがありました。ビルドセッティングの型スイートフラッグについても触れています。具体的には「型スイートフラッグ図にパースSTPを追加する」という点についてです。
まず、型スイートフラッグを使ってみるとどうなるか試してみます。このプロジェクトのビルドセッティングで型スイートフラッグを使うと、新しい設定方法に関する実験が可能です。実際に試してみたところ、インポートファンデーションを使用せずにエラーが出ないかどうかも確認しました。
さらに実験を進めると、ビルドインティペラル
という型が出てきました。これを見たところ、数値を入力しても反応しませんでした。数値を型に入れようとした場合も動作せず、エラーが発生しました。ここから推測すると、ビルドインのインテジュアリティ型では、数値の範囲を指定することが難しいようです。
その後、メモリレイアウトを調査し、ビルドインイントリペラル
のサイズが表示されました。結果として16ビットのサイズが返ってきました。この試行錯誤を通じて、インテジュアリティ型の扱いについての理解が深まりました。
また、ユイント64
にキャストして数値を処理しようと試みましたが、それでもエラーが発生しました。リテラルからリテラル互換型を作り直し、最終的にイニシャライザーを使用してビルドを試みたところ、何とか成功しましたが、行動が複雑になってしまいました。
全体として、新しい設定や型を扱う際には、いろいろなエラーや問題に直面することがあります。その中で試行錯誤を重ねることで、徐々に理解を深めることができます。おそらく、ビルドイン型の取り扱いについてはもう少し詳細なドキュメントやガイドが必要だと感じます。 ここでフェイタルエラーを出したときに、コンパイルタイムエラーとなるのかランタイムエラーとなるのか考えてみましょう。フェイタルエラーが残っている場合、これはランタイムエラーになります。今回のテーマで扱ったリテラルの値の範囲を独自に作成し、かつコンパイルエラーで検出するというのはユーザー側が実装するのは非常に難しいでしょう。もしかすると不可能に近いかもしれません。
コンパイラーがコンパイル時に検出してエラーを出しているようですが、具体的な段階は不明です。独自の範囲でビルドエラーを出せたらかっこいいと思いますが、現実的には難しそうですね。しかし、組み込みの機能が使えるというのは夢が広がります。
初期値と多態性理論については、過去の勉強会でも話しましたので馴染みのあるところかもしれませんが、初めての人にとっては新しいかもしれません。多態性理論は、係数や変数の宣言時に初期値が伴うときに特に有効です。必ずしも特段有効というよりは、一般的に馴染み深い箇所で使われるため有効とされています。
たとえば、メソッドチェーンなどを行うときには多態性理論が非常に有用です。リテラルの割り当てについても、この勉強会では見ていくことになります。
リテラルとは、言語によって多少の違いがあるかもしれませんが、基本的にはソースコード内に直接記述される値のことです。Swiftでは、たとえば 42
や 3.14159
などがリテラルとされます。変数に値を代入する場合、その変数の型が自動的に推論されます。たとえば、変数に 42
を代入すると、その型は Int
になります。
さらに、不動小数点数のリテラルに対しても型を指定しないと、自動的に Double
型として推論されます。これは、常に Float
型ではなく Double
型が選ばれるという仕様になっているためです。この点については勉強会で話したとおりです。 ここはそれほど重要なポイントではありませんが、「常に」と言うのは言い過ぎですね。後で少し紹介しましょう。そしてもう一つ見ておきたい点があります。文脈内で整数リテラルと不動小数点リテラルを組み合わせると、文脈から Double
型と推定されると記されています。これは反例があるかもしれませんが、整数と不動小数点リテラルを組み合わせると、文脈から Double
型と推測されるだけでなく、前の行と関連して別の型と推定されることもありそうです。これも後で説明します。
また、スライドに色を変えて書いてあるリテラル地産の話ですが、リテラル地産はここでの一番最後の部分に出てくるもので、主に文脈から Double
と推定される理由や流れを解説しています。とりあえず、そのあたりは後でプレイグラウンドで試してみるとして、ここで注目したい点は、以前の話に少し触れたことがある、リテラル自体には明確な型が無いということです。これは Swift のリテラルの面白いところです。
他の言語のリテラルは明確に型を持っていることが一般的です。たとえば、C言語のリテラルは整数リテラルは明確に整数型です。しかし、Swiftの場合、リテラルそのものには型が無いのです。ただし、突き詰めていけば最終的には型に落ち着くことになりますが、それはコンピューター内部の話であり、リテラルそのものには型が無いというのが Swift のリテラルの特徴です。このため、文脈に応じて効果的に利用することが可能です。
つまり、変数 A
が Int
型だった場合、リテラル 10
は Int
型として扱われ、変数 B
が Double
型ならリテラル 10
は Double
型として扱われます。このように、型が無いからこそリテラルが直接変換されるわけです。これを支えているのが ExpressibleByIntegerLiteral
と ExpressibleByFloatLiteral
という2つのプロトコルです。このプロトコルに準拠することで、リテラルから直接適切な型へ変換されます。
例えば、Int
型は ExpressibleByIntegerLiteral
に準拠しており、Double
型は ExpressibleByIntegerLiteral
と ExpressibleByFloatLiteral
の両方に準拠しています。このため、Double
型は整数リテラルと不動小数点リテラルの両方から変換可能です。一方で、Int
型は ExpressibleByIntegerLiteral
のみに準拠しているため、不動小数点リテラルを代入しようとするとエラーになります。
こうした点を抑えておくと良いでしょう。最後に、スライドで話した型推論の話に戻りますが、例えば1行目で A
が Int
型だと明記されていれば、その後も文脈に応じて型が決まっています。 リテラルは型がそもそも決まっていません。そのため、最終的にランタイムに持っていくときには型を決めないといけません。そのとき、Int
型は整数リテラルから変換できるプロトコルに準拠しているので、「このリテラルはInt
型だよね」というふうに型推論されます。例えば、今回のスライドの話では、Int
型と明示しなくても文脈からA
がInt
型として判断されます。こういうふうに変数が型推論されるのは特に有用です。
例えば、一行目だけを見たとき、「A」という変数の型は決まっていません。そして、リテラルである10
も型がありません。この時点では、文脈から何型にすればいいかは人間には分かりますが、コンピューター的には型が決まらないと思うかもしれません。しかし、Swift言語では、リテラルが文脈から型を決められないときには、A
の型としてInt
型と判断します。これはSwiftの独自のルールです。
実際、コード全体を見たときに、変数A
の型が分からず、リテラルの10
の型が分からないときには、リテラル型はInt
とみなされます。これにより、右側がInt
なので、左側もInt
と推論され最終的にはA
がInt
型となります。このように、規定の型が「インテジャリテラルタイプ」としてデフォルトでInt
型として決まっています。
要するに、このコンテキストではリテラルがInt
型と推論されますが、例えばDouble
型を明示することで、途端にリテラルの型もDouble
と解釈されるようになります。これがリテラルの面白いところです。
また、整数リテラルが必ずしもInt
型に解釈されるとは限らないという点についてお話しました。これはフロート型にも同様で、フロートリテラルタイプがFloat
型として解釈される可能性があります。つまり、リテラルが盲目的にInt
型やFloat
型と解釈されるわけではないと理解しておくことは、時には非常に役立つかもしれません。
では、今日はこれで時間になりましたので終わりにしましょう。お疲れ様でした。ありがとうございました。