https://youtu.be/MpPjOLcKIAE
前回はThe Basics
の 数値型変換
セクションに入るに当たって、そこから連想する numericCast
について眺めていきましたけれど、今回はいよいよ本編に入っていきます。型変換の作法というのか、それより手前の型変換の必要性みたいな初歩的なところをおさらいします。どうぞよろしくお願いしますね。
———————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #119
00:00 開始 00:11 BinaryInteger のビット幅 01:33 FixedWidthInteger はビット幅を想定している 02:46 BinaryInteger もビット幅を想定している 05:21 BinaryInteger でも幅の大きい方に揃えて比較可能 08:47 BinaryInteger 同士の型揃え 09:21 BinaryInteger は動的ビット幅も表現可能 12:20 整数の扱い 12:47 正の数が想定されている場面でも一般に Int を使う 14:12 整数型と整数リテラルと型推論の相乗効果 15:18 サイズが明記された整数型を使ったとすると 18:52 正の整数しか想定しない場面でも Int 型を推奨 22:51 サイズが明記された整数型を使う場面 26:35 表現力の違いで UInt を選ぶ場面 29:20 正の値であると判断するために UInt を使う場面 29:55 絶対値を UInt で表現する場面 34:30 絶対値のオーバーフロー判定を行う 36:44 偶発的なオーバーフロー検出と、データの性質を暗黙的に文書化 39:54 次回の展望 ————————————————————————————————————
Transcription: 熊谷さんのやさしい Swift 勉強会 #119
はい、じゃあ始めていきますね。今日は数値型変換、前回はこの「タイトルからニューメリックキャストに発想を広げて、その部分だけ見ていたら時間が終わっちゃったんですけど、あのとき変換の話をしてたんですけど、ニューメリックキャストからまたちょっと派生したお話なので、ニューメリックキャストはあんまり気にしないでもらって大丈夫なんですけど...
この中でニューメリックキャストはジェネリックプログラミングでは役に立ちそうだね、みたいなお話をしたかと思うんですけど、その中でジェネリックの対象としてバイナリーインテジャーとフィックスウィズインテジャー、その2つが対象として挙げられる、というそんな話が出てきて。バイナリーインテジャーは二進数表現の整数だけれども、特にビット幅にはこだわってないよ、みたいなお話をしました。
そこから、バイナリーインテジャーを継承したフィックスウィズインテジャーになると、初めてビットサイズの幅について言及するようになってくる、考慮に入ってくる、みたいな話をしたんですけど、どうやらそれは適切ではなかったようで、その補足をしますね。
どんなあたりが適切じゃなかったかというと、バイナリーインテジャーはビット幅を想定していない。フィックスウィズインテジャーはここから初めてそういう概念が生まれたっていう話をしましたけれども、そこが適切ではなかったという感じです。というのも、例えば何でもいいんですけど、ジェネリックプログラミングである型Tが、ナンバーにしようかな、ナンバーがバイナリーインテジャーの前にフィックスウィズインテジャー、こっちに準拠しているよみたいな形になってたとします。そうすると、このTじゃないや、ナンバーだ、ナンバーっていう型は、型は何か知らないけど、とにかくフィックスウィズインテジャーに準拠したものっていう形になるじゃないですか。
そのときにビット幅っていうものがプロパティで参照できる、ここまでは前回もお話ししたので、思い返してもらえると、思い出せる人もいるかと思うんですけど、問題はですよ。前回のお話の中で、バイナリーインテジャーだけに準拠している定数型は標準では存在していないので、なかなかこれのバイナリーインテジャーだけを想定する状況っていうのは検証しにくいというお話をしました。でもよくよく考えると、ジェネリックプログラミングを使えば、これだけで具体的な型が存在していようといまいと、ナンバー型というものはバイナリーインテジャーだけに準拠していて、フィックスウィズインテジャーには準拠していないっていう状況を簡単に作れましたね。
これを踏まえてやってみたところ、気づいたことは、そんなに大したことには気づいてないんですけど、まずナンバー型はビット幅っていうものを想定していないっていう状況、ここまでは問題なく前回の話のとおりでした。ただ、バイナリーインテジャーがビット幅を想定していないかっていうと、そんなことはなくて、インスタンスプロパティにはビット幅のプロパティが存在していることを知りました。なので、ランタイム上でバイナリーインテジャーのビット幅を取ることはできる。型には存在してないんで、コンパイルタイムには取れないんですけど、こういった性格になっていて、ちょっとサイズを返してみようかな。
ビット幅の型は何になっているかというと、Int型になっているので、ちょっとInt型を返すようにしてあげます。こうしてあげると、例えばInt型のビット幅はいくつかっていうのが、こうやって名前を付けて、せっかくだから関数名を変えてみますか。ビットウィズオブ、オブは中だね。こうだね。こんな感じな関数にしてあげると、スペル間違えた。こうね。で、こうやって例えばInt型のサイズを取れば64ビットだし、これがInt8型だったら8ビットだし、こういうふうにランタイム上では取ることができた。
なので、ここから言えることとして、バイナリーインテジャー型だからといってビットサイズというものを想定していない、全く発想にも浮かんでいないということはなかった。ランタイム上では判断できるというのは前回お話したとおりなんですけど、ここで比較演算として片側がバイナリーインテジャー、もう片側がバイナリーインテジャーの比較演算、間違えた。こうね、型パラメーターとして用意してあげて、こうやって両方を取って比較演算するときに、ここでビットサイズが大きいほうに揃えたい、っていうときにビットサイズの幅をちゃんと取れる。スイッチで左辺のビット幅と右辺のビット幅を比較して、それで大きかったらこっちに合わせて比較する。なんか間違ってるな。使ってない、こうね。こうしてあげて、これでビット幅の大きいほうに合わせてあげる。
ここでニューメリックキャスト、ね。これで比較する。そうじゃなかった場合にはニューメリックキャストで右辺に寄せてあげる。これでコンパイル取るよね。こういうふうに書いていける。実際、バイナリーインテジャーしか想定していない透過比較演算子なのにもかかわらず、やっぱりオーバーフローしなかったんですよ。もう片側より値が大きいときに、右辺と左辺を入れ替えたりとかいろいろしてみてもオーバーフローしなかったのは、やっぱりこうやって多分ランタイム上で比較して、それに応じてキャストで合わせて演算する。こうすると、例えば片側がInt8型で、でも片側がInt8の範囲を超えるInt64とか、そういった具体的な値でもちゃんと比較できるよっていう、こういった感じ。
これが逆だと、例えば64にしたから分かりにくいね、32とかにして、こっちを64とかにすると、ここのパスね、今こっちのパスが動いてますけど、こっちが動くはずなんですよね。 こういうふうに、型を揃えるにしてもオーバーフローせずに……ここ、ちょっとオーバーフローしないですけど、ちゃんと値を大きくしてもオーバーフローするんで、アンサインド(無符号)とか使っていくといい感じにオーバーフローさせていけるかと思うんですが、とりあえずこうやってちゃんとバイナリーインテジャー(整数型)にもビットの概念があった、これをちょっと伝えたかったのと、ついでにバイナリーインテジャーのときには具体的な型変換ができない、要はライトハンドサイドとレフトハンドサイドみたいな。
こういう型変換がイニシャライザーが存在していないのでできないけれど、ニュメリックキャスト(数値キャスト)を使えば簡単にバイナリーインテジャー同士の型合わせができていけるよみたいな感じです。
なので、バイナリーインテジャーはちゃんとビット幅を考慮したコードが書ける。コメントで面白かったのが、ソースコードのコメントで「FixedWidthInteger型のインスタンスでは定数が得られるよ」って書いてあった。それというのは、FixedWidthIntegerじゃなかったときには定数とは限らないっていうことがここで伺えるとすれば、Int32とかInt64とかは普通にFixedWidthInteger型なので64だ、32だ、どんな値が入っててもそういった定数が得られてるわけですけど、もし誰かがFixedWidthIntegerじゃないバイナリーインテジャー型を作ったときには、それが保持している値に応じてビット幅が変わる。要は動的ビット幅っていう型があり得るかもしれないというふうに考えていくと、あくまでも固定されていない、約束がされていない。
要は配列のカウントみたいな感じ。配列のカウントって中に入ってる要素に応じて数変わるじゃないですか。今なら3だし、こうやって1個増えれば4だし、こういうふうな感覚で数値のビット幅が増えていけばビットウィズ数値が上がっていくみたいな、そういうふうな表現になっていくと思われます。
なので、もし無限長整数型みたいなのを誰かが作ったときには、このビットウィズが入ってる値に応じてめまぐるしく変わっていくみたいな、そういうふうな世界観になっていくんでしょうね。そんなところが個人的に面白いなあと思ったんですけど、こういうのって面白いと感じるものですかね?どうなんだろうね。興味があったら、ぜひこの辺り遊んでみたら面白いし、意外と今まであんまり気にしてなかったんですけど、改めて見てみるとニュメリックキャスト、バイナリーインテジャーと絡めていったときに使いどころは色々ありそうな感じがするんで、1回遊んでみると、実際に実践の中でも活用していけるのかなみたいな雰囲気がなんとなく持てたので、いい成果だったなと自分自身の中では思ったところです。
じゃあ、本編に入っていきますね。本編に入っていくと、すごい基本的なところを見ていく形になるわけですけど、まずはとりあえずスライド読んでいきましょうか。整数の扱い、型変換の話なのに、いきなりInt型の話になるのか。これは、型変換に絡んでのお話ですね。ちょっとセクションにとらわれないほうがいいかもしれないですね。
とりあえず、整数型はどう扱っていくかっていう一般的なお話ですね。負の数を取らないと分かっていても、すべての一般的な整数を扱う定数や変数ではInt型、要はInt64みたいな型が具体的に明記されていない普通のInt型、汎用的なInt型を使っていくよっていう話が書いてあります。要はInt型推奨っていうことですね。このお話はそんなに遠くない以前にもお話していたので、思い出せる方はその辺り思い出しつつ聞いてもらえたらと思いますが、とりあえずそうやってInt64とかを積極的に使っていかない。
要はC言語とかだと積極的にInt64とかを使っていく場面、Int32とかInt8とか、いろんな事情でそういった明記して使っていくっていうこともないことなかった。意外とあったかなと思うんですよね。でも、そういうふうにではなく、できる限りInt型推奨で。なぜそうなったかっていうと、そういう方針になっているかというと、定数や変数を相互利用しやすくできる、速やかに相互利用できる。あと、定数リテラルの値が推論された型と一致する、特にこの2個目が重要かなと個人的に感じるところとしては。型推論っていうものが存在している都合で、Swiftでコードを書いていく中で定数を扱うときに、その変数の型が何なのかって書かないことが多くなってる。そうすると、Int64とかInt32とか明記しているところが出てきていると、型推論されたInt型と型を明記して定義した数値、その2つの運用が難しくなってくる。要は型変換が常に必要になってくるとか、そういった状況が起こってしまうわけですよ。型推論と相性がちょっと悪い。 なので、できる限りInt型に揃えておくと、いい感じにコードが書けていくよ、ということですね。例えば、型推論が苦手だなと思って、バリューとしてInt64とかを使ったとします。これを関数とか明記しなくていい場合、例えば、これをステータスコードとして使用するとします。でも、これはあまり良い例じゃないですね。適切な型を明記する必要があります。
他の人が作っている関数で、例えばInt型を取るものの場合、整数型を使いたい時に型推論で型の幅を含む整数型を使用していると、それを関数に渡す際にそのままでは渡せなくなります。なので、Int型に型変換する必要があります。これによって、一番わかりやすい問題は、コードが冗長になることです。これ自体はそんなに大きな問題ではないと感じるかもしれませんが、これが多くなってくると、やはりちょっと問題が出てきます。
他にも問題があるかもしれませんが、基本的にはそれに閉じるような気がします。例えば、Intを返す関数があり、それを利用したいときなど、足し算でステータスに値を足したい場合、型が一致しないと問題になります。このときにはどちらに揃えるかを考えます。別に何の問題もなく、Int型に揃えるならこうですし、Int64に揃えるならInt64にすればいいのです。また、ステータス型に揃えたい場合はnumericCastを使うこともできます。
とにかく、型を揃えることを明記してあげれば問題なく処理できるため、全体的には本質的に問題はありません。ただし、Int型にしておくことによって、一般的なInt型と揃えられるため、冗長なコードにならず読みやすくなります。それとは逆のパターンについても触れておきます。関数を定義する時に、ここは正の整数しか想定していないからUIntでいいや、となると、型推論でステータスというパラメータを設定した12行目などがエラーになることがあります。
ここで、正の整数しか取らないというのが引数からちゃんと伝わってくるため、変換する必要が出てきます。何も悪いことをしていませんが、少し煩わしくなります。また、ステータスが負の値だった場合にオーバーフローするリスクがあるので、その辺のチェックも必要になります。
例えば、ガード文で安全性を確保するために、guard let value = UInt(exactly: status) else { fatalError("異常系の処理") }
のように書き、変換が正しく行われた場合に値を使用する、という流れになります。この方法は問題なく、むしろ安全性が高いとも言えます。 なので、万全を期すならこういうふうになってくるわけですよ。万全を期すなら、つまり、おさらいすると、この「アクション」というメソッドが想定し得る値を型で制限してあげて、そうすると制限されることによって適切な型しか取れなくなってくるので、必然的にコンパイラによって適切な型に合わせていくという制御が働いて、プログラマーは安全なコードを書かざるを得なくなってくるというね。
なので、安全を考慮したときにはこれが素晴らしいコードになってくるわけですけど、Swiftではそこまで必ずしも求めてなくて、一般的に想定し得る限りで、言うほどのことがなければ問題なく動くものだったらば、Int型に揃えていきましょう。そのほうがシンプルなコードが書けますよねっていうね。そういう感じなのかな。そうやって相互運用性が高まっていくというね、そういった効果。
あとね、型推論との相性がとてもいいよ。型推論との相性がいい理由は、型推論の整数リテラルの推論ね。整数リテラルの推論と相性がいいって言ってるのは、この整数リテラルが規定ではIntegerLiteralType型に変換される。このIntegerLiteralTypeはInt型になっているっていうね。この都合なので、いい感じに統一感が出てくるよっていう感じ。そんな理由でIntを推奨していくよっていう感じ。
下に書いてあるのがさっきお話ししたことにつながるわけですけど、他の整数型、要はInt型以外、アンサインドインテジャーも含めて、UIntとかInt8とかInt16とか、そういったものは特別に必要とされる場合に限って使うこと。つまり、さっきお話ししたことをちょっと思い出しておくと、ここら辺だね。これでいいんだ。アクションが負の整数を渡されてもらっちゃ困るんだみたいな仕様になっていたときね。こういうふうに厳密にそうでなくては困るんだっていう状況、これが特別に必要とされる場合。そういったときに限ってUIntとかを使っていくんだよっていう方向性。
外部から明示的にサイズが指定された場合とか、要は「ここのデータにはUInt8型で渡してくださいね」みたいなとき、C言語で作られたAPIとかの文字列、要はC文字列を受け取りたがってるAPIとかでよくありますよね。UInt8の配列をくれ、とか、配列というかポインターをくれとか、あとそのほかにパフォーマンスやメモリー使用量、その他の必要な最適化のときに。そういったときには使っていくかもねっていうようなとき。
以前この勉強会でも話を聞かせてもらったことありますけど、例えばInt8じゃないとメモリー量が膨大になってしまう。Int64とかやってたら大変な量取ることになっちゃったりするし、あとスタティックIntの配列、スタティックアレイInt、ちょっと表現変だな。要はスタティックメンバー、グローバルの変数とかもそうですけど、クラスがあって、そこにスタティックバーとして、レッドでいいや、何かしらのテーブルがあってこれがIntの配列みたいなときで、この配列の中身が膨大なとき、こういったときにはIntとかだと、例えば仮に数値255まであれば十分っていうような整数値だったときに、ここはUInt8とかにしておくと、スタティックなメンバーっていうのはコンパイルタイムにプログラム領域とかだったかな、データ領域違う、スタティック領域ってありますね。今のプログラムでは常識ではね。
なので、スタティック領域にずばっと書き込まれちゃうんで、ここでサイズを削減してランタイム時に型変換をしていくみたいな。そういったのもありですよね。膨大過ぎるときは。そういったふうにメモリ使用量とか最適化とか、そういったのの都合、あとは受け取る先がInt8を得意としてる環境、フィットCPUとかそういうようなときとか、そういったパフォーマンスを睨んだりとか、そういったふうに使う場面が出てきたりしたときに限って使う。
ってコメントちょっと寄せられてたのは何だろう。SwiftでUIntを使う場面ってどういうときなんだろう。
そうね。普段でUIntを使った回って確かにね、閃かないかもしれないし、あと自分のUIntを使った思い出って言うとね、なんかUIntは使わないほうが良かったなみたいな結論にたどり着いたみたいな、そういった思い出ぐらいしかないですけど。
そうね、UIntを使う場面、一つ考えられるシンプルなところで言うと、IntとUIntの大きな違いとして、最大値が大きく違ってくるっていうところがある。要は表現力が。桁数が高い、桁の表現力が高いっていうのがまず一つありますよね。 「なので、このINTMaxがちょっと分かりにくいかなと思います。8ビットでやってみると分かりやすいですよね。桁が小さくなるんでね。例えば、桁が増えて分かりやすくはなるけど、一般的にINTが使われるから、こっちのほうが良いかもしれません。
例えば、INTMaxというのは、桁数が7ビットで表現できる範囲、つまり2^63-1までの値を表現できます。でも、もしそれよりも大きな数のアイテムを扱う場合には、あらかじめUINTを使ってレコードのIDを管理しておくなどの対策をとらなければなりません。INT8のほうが分かりやすいと思いますが、これはUINTの話ですね。感覚的には、200個までのアイテムを表現するならINT8ではなくUINT8を使うべきです。ただ、この場合、INT16もあるのでちょっと話が変わってきます。
また、コメントで指摘された通りなんですが、UINT8は確かに存在しますが、INT64やUINT64はあんまり使われません。また、念のためのアサーションフェイラーを使用する際、負の値が存在しないと書かれている場合に、念のためチェックを上回って行いたいような場合に持ち出すことが多いです。
具体的に使われている場面として、標準ライブラリーの中で絶対値の話を例に挙げます。UINT8の最小値は0ですが、INT8の最小値はマイナス128なので、絶対値を取るとオーバーフローが発生します。例えば、ABS(INT8.min) のようにすると、ランタイムエラーが起きます。これを防ぐためには、次の型にキャストしてから処理する必要があります。
だからINT8型の絶対値を取るプロパティ magnitude
が用意されているわけです。具体的には、INT8型のインスタンスに対して magnitude
プロパティを使用すると、値がUINT8型で返されます。これは絶対値の表現力を超えないように配慮しているためです。
こうした例は、ローカルな場所ですが適切に使われています。INT8型の magnitude
プロパティによって、特別な場面で適切な型キャストが行われているわけです。
INT型の magnitude
も同じようにUINTで取れます。でも、もしINT.minのUINTの絶対値を考えると、これは適切にUINTで表現されます。どちらもUINTなので、適切に管理されています。このように、INT型の magnitude
に関してはUINTを使うという特別なケースがあるわけです。これが非常に良い解決策となっています。」 これね、そうした上で問題ないんだったらこれでいいし、UIntになってもらっちゃ困るんだよねっていうようなときに初めて型チェックができるじゃないですか。素晴らしいですよね、これね。これをIntとして使いたいんだで、Intであれば問題ないんだみたいなときにはこれでいいですよね。オーバーフローしますけどね。今はね、minなのでね。こうやってここが問題だっていうときにはExactly使って、問題があったらnilにしてもらって、nilになってくれたらこっちのもんですよね。ここでnilだったときにはゼロとして使いたいんだとか、nilだったときには別の処理をしたいんだとかね。そういうふうなエラー生成ができてくるんで、とてもいい感じに型チェックが働いてね。
ああそうだ、if letね。こうやってね。こういうふうな感じ、いい感じにクリティカルなポイントとしてプログラマーにプッシュしていけるっていう。こういったときにはいい具合に、純粋に絶対値だから負の値は取らないよね。だからUIntだよねっていう発想を超えた感じ出てますよね、これね。だから特別な場合っていうのは、負の値を取らないからとかそういう理由じゃなくて、負の値を取ったときにやばいからとかね。それだと表現できなくなっちゃうから、表現できなくなっちゃうと手詰まり。もうね、それ以上手がないっていう状況になっちゃう。Intの絶対値はちょっと普通には取れないよねっていう状況になっちゃうっていうね。そういった困りごと、それが特別な場面っていう雰囲気みたいね。
他にUIntが使われている場面ってどんなのがあるんだろうな。とりあえずちょっとスライド1枚これ終わらせてから、時間があったら話します。
今話したとおりなんですけど、特別な場面で明示的にサイズが指定された型、UIntもサイズが指定された型に含まれると捉えたら、その偶発的なオーバーフローを捕捉したり、まさにさっきの、捕捉しましたよね。minだったときに限ってオーバーフローしちゃうっていうのを捕捉したり、あとデータの性質を暗黙的に文脈化する。絶対値はこれ両方含めてますね。絶対値は絶対に負の値を取らないので、そういった性質あるものですよみたいな感じ。でもやっぱ個人的には前半かな。着目するのはこの偶発的なオーバーフロー捕捉。
最終的にはここに行き着く感じがして、単純にデータの性質を示したいっていうときにはコメントで書いとくっていうのが一般的で、さらにはここはC文字列だからInt8で取らないといけないんだみたいな、それぐらいにならないとデータの性質っていうふうにはいかないのかな。
こんな感じです。なのでSwiftでは全体として見て、Int8とかInt16とかではなく、Intのほうが都合がいいから推奨していくよ。それになる理由としては、いろいろnilチェックとかいろんな事情からオーバーフロー検査をしやすい環境ができてるとはいえ、あんまりExactlyっていうパラメーターを取るイニシャライザーを使う人ってそんなにいないというか、そもそも知らないから使ってないみたいな人もいるかと思うんですけど、こういうときにとても役立つんで、型変換をちょっと精密に扱っていきたいよっていうようなときにはExactlyっていうのを覚えておいてもらうというか、思い出してもらうとSwift的に安全なコードを書ける形に一気に変わるんで、これを知っておくのがおすすめです。
こんなふうにしてナチュラルに普通にIntで使っていけば問題ないよねパターンと、問題が出てくるよねパターンを両方ともInt型で表現できちゃうっていう。こういったところを踏まえて、かつ定数リテラルの推論が一般的にInt型ですよとか、みんなはだいたいInt使うよねとか踏まえていくと世界観的にIntがばっちり合ってくるわけですよ。C言語とかだと、逆にオーバーフローとか怖いなとか思っていくとInt8とかInt16とかで型でしっかり制約をかけていくっていうのが相性がいいわけですよね。どっちかっていうと、こんな感じでInt型を積極的に使っていきましょうっていう。クリティカルでなければInt型というのを押さえておくといい感じに話が進んでいくでしょう。
それを踏まえた上で具体的な型を使って、そうすると型変換っていうものが必要になってくるよっていうお話をまた次回とか、そんな感じの次回その次ぐらいで話していこうかなと思います。じゃあ、いい具合の時間になったので今日はこれで終わりにしましょう。お疲れ様でした。ありがとうございました。