https://youtu.be/RRfzQ232MOY
今回は本編から少し離れた回としまして、前回の話の中で出てきて曖昧な感じに終わった magnitude
のあたりに注目して見ていこうと思います。それを見たら、続いて同じく前回の話題の プロトコルと拡張
で触れられなかった 条件付き拡張
の基本的なところについても眺めていく予定です。どうぞよろしくお願いしますね。
—————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #65
00:00 開始 01:15 オリエンテーション 02:22 勉強会の目的 03:25 勉強会の方向性 05:05 勉強会の想定対象者 05:29 談笑形式 06:23 話しかけるにあたっての心持ち 10:18 大事な心構え 11:36 今回の展望 13:34 magnitude と abs 15:15 マグニチュード 17:01 Absolute Value や Modules とも呼ばれる 17:56 magnitude で絶対値を取る 19:05 問題:どんなバグが含まれるでしょう 20:41 abs 関数で発生するエラー 21:56 整数値のビット構成 23:02 2の補数表現 26:05 問題の解答 26:55 Numeric における Magnitude の定義 28:15 Int 型における Magnitude の定義 28:44 magnitude と abs の特徴の違い 32:02 abs 関数の実装 34:11 unsafeBitCast と as! 36:21 条件付きキャストによる安全性 40:12 Magnitude 型が元の型と異なる場合の abs 関数の挙動 41:04 絶対値の概念が特殊な型があったときの挙動が心配 43:45 質疑応答 45:16 magnitude を使うか abs を使うか ——————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #65
ちょっとやりたいことがありまして、この勉強会の最初にオリエンテーション的な趣旨の話をしましたが、単元が変わるごとにそれを振り返って話すこともあります。今回は特に、The Swift Programming Language に移ってから単元が変わらず、あまり深く話せていませんでした。そのため、今日はまずオリエンテーションを振り返ってから本題に入ろうと思います。
今回は軽くオリエンテーションを見ていきますね。この勉強会は突発的に始まったもので、初めは粗削りのまま進めていましたが、参加者と共にどんな勉強会にしようかと模索しながら進めてきました。始めてからおそらく6ヶ月ほど経っていると思いますが、基本スタイルはだいぶ整ってきた感じがします。今後はこの勉強会を基軸にして、学習の機会をさらに広げていけたらいいなと考えています。
この勉強会の大事な目的は、プログラミング言語の基礎学力を養うことです。この基礎学力が実務において価値がないとされることもありますが、基礎知識があると何かのときに役立つため、言語仕様に長くゆっくり親しむ場所としてこの勉強会を続けています。
勉強会の方向性としては、Appleが公式に提供している The Swift Programming Language に沿ってSwiftを復習していきます。広く深く、一つひとつを丁寧に見ていく感じですので、同じことを何度も話す機会もあります。一度聞いただけでは身につきにくいので、何度も繰り返し聞いていただくことで、忘れていたことを思い出したり、新しい発見ができると思います。
時折難しい話もしますが、理解してもらうことよりも、そういったものがあるんだなと感じてもらえれば十分です。後でふと思い出して役に立つこともあるでしょう。
この勉強会の対象としては、アソシエイトエンジニアやSwiftに興味のあるすべての人を対象にしています。また、参加者同士で話しながら進めていけたらいいなと思っています。Zoomのコメント機能などを活用して話しかけてもらえればOKです。話しかけるのにも慣れが必要ですが、負担に感じる方は聞くだけでも全然構いません。
話しかける際の心構えとして、まずどんなに詳しそうな人でも些細なことを知らなかったりするので、些細なことでも話しかけてください。また、話題が正しいかどうかを気にしなくても大丈夫です。正しい正しくないを探していると話しかける機会を逃してしまいます。話す中で自然と判断されることもあるので、気にせず話しかけてもらって大丈夫です。
この勉強会中に、時折青緑色(エメラルド色)で書かれている部分がありますが、それが気になる場合も全然聞いて大丈夫です。また、何か聞きたいことがあればいつでも質問してください。 アーカイブを見ると、ついさっき話した話題がもう一度出てくることがよくあります。しかし、それを気にしなくて大丈夫です。何度も話せば身につくので、もし後で「あ、間違ったことを言っちゃった」となっても気にしないでくださいね。また訂正の機会を設けることもありますし、そうでないこともあります。でも、間違って話してしまうことなんて往々にしてありえることなので、リアルタイムでそういったことを気にしなくて大丈夫です。後悔もしないでください。話したいことや訂正したいことがあれば、次回でも全然構いません。前回のことについて話を割り込んでもらってもOKですので、気軽に話しかけてください。
最後にもう一つ大事なこととして、みんなの意見が正しいと考えています。ここにいる方々は、誰かを騙そうと思って間違ったことを言うわけではないですよね。実際に間違っていたとしても、価値観が違うだけで意見が食い違ったりすることがあると思います。ですから、相手がそれが正しいと思って言っていることを尊重していけたらいいなと思います。この勉強会はそんな感じで進めていますので、気楽に参加してもらえればと思います。それでは、今日の本題に入っていきましょう。
先ほどもJavaScriptプログラミング言語に沿って話すと言いましたが、今回はちょっと特別なお話で、前回の補足のような感じです。今日初めて参加した方でも、前回のことを知らなくても問題なく聞いていられる話題だと思いますので、気にせず参加してもらえると嬉しいです。
さて、どんな話題を話していくかというと、前回「マグニチュード」というプロパティが出てきて、そのお話が絶対値っぽいと感じましたが、少しふわっとして終わってしまいました。また、自分自身がその程度の理解しか持っていなかったので、調べてみたらいろいろ面白い話が見つかりました。その「マグニチュード」のお話と、前回型拡張の中で特徴を話しましたけど、時間の都合で話せなかった条件付き準拠についても今日触れていこうと思います。
まず「マグニチュード」のお話からいきますね。前回の振り返りですが、abs
といういろんな言語で馴染みのある絶対値を計算する関数を見ていたとき、どの数値で見ていたかは忘れましたが、abs
の定義を見たときに「マグニチュード」っていうプロパティを実装で使っているのを見て、「マグニチュードって何だっけ?」となったと思います。例えば、-10
のマグニチュードでも絶対値が取れるので、そのあたりをしっかり見ていこうというお話です。
スライドに整理してきたので、それを見ていきますね。まず「マグニチュード」とは何なのかという基本的なことから説明します。数学的に普通に使われている言葉で、数量的な大きさを意味する言葉です。定数値a
に対してのマグニチュードは次のように表現されます。これは実数直線における原点からの距離を表して絶対値と呼ばれることが多いです。要するに、マグニチュードというと実数世界における絶対値のことですね。
さらに、複素数やベクトルなどでも「マグニチュード」という言葉が使われます。例えば、複素数の場合、絶対値の意味が若干変わりますが、取り得る値によって式が変わるだけで、定数値においては馴染みのある絶対値と変わりません。一般的には「絶対値」という言葉が使われますが、数学的には「マグニチュード」という言葉も使われます。また、「アブソリュートバリュー」や「モジュールズ」という言葉でも表現されます。 マグニチュード(絶対値)という言葉は、時折ベクトルの世界でよく使われます。マグニチュードと絶対値、すなわち abs
は同等のものとして捉えれば問題ありません。そうすることで、話がとても簡単になり、マグニチュードを取れば絶対値を取得できるということになります。
例えば、変数 a
があって、その値が -5
、変数 b
が -10.43
だったとします。このとき、 a
のマグニチュードや b
のマグニチュードは、それぞれの絶対値と同じです。つまり、 abs(a)
や abs(b)
と同等の動きをするのがマグニチュードというプロパティです。
ここで、次のコードについて考えてみましょう。
let y = abs(x)
このコードにはバグ(または不具合)が含まれています。一見問題なさそうに見えますが、どこが問題でしょうか?アプリケーションを作っているときに abs(x)
と書いたとき、どのような不具合が起こり得るでしょうか?
これは多分ドキュメントにも書かれていることですが、「最小値を取得しようとするとオーバーフローが発生する」ことが問題です。具体的には、 Int.min
に対して abs
を適用しようとすると、ランタイムエラーが発生します。実行すると以下のようなエクセプションが発生します。
fatal error: arithmetic overflow
例を挙げると、 Int8
の最小値( Int8.min
)は -128
で、最大値( Int8.max
)は 127
です。このように、 abs
関数は符号ビットを変更するだけではなく、2の補数表現になっているため、 -128
の絶対値を取ると 128
となり、この時にオーバーフローが発生します。
ビットの観点で解説すると、2の補数表現では符号ビットを含めたビット操作が必要となります。例えば、 -1
のビットパターンを Int8
で表現すると 0b11111111
となり、 1
のビットパターンは 0b00000001
です。これにより、符号反転が成り立つのは不動小数点数のように明確な符号ビットが存在する場合のみで、2の補数表現の場合は単純にビットの反転だけでは済まないことになります。
まとめると、 Int
型が表現できる範囲は -128
から 127
までです。しかし、 Int8.min
の絶対値を取ると 128
となり、これによりオーバーフローが発生します。この点が先ほどのコードの「どこにバグが含まれるか」という問題の答えでした。 それで、以前、前回の勉強会でマグニチュードの定義などを見ていきました。まず、マグニチュードの方のプロパティ、プロトコルか、プロパティが指定されているプロトコルの定義を見てみます。
Numeric
というプロトコルにMagnitude
というのが用意されていて、ここで関連型としてMagnitude
が指定されています。そして、比較可能
でかつ数値
であることが条件となっています。この関連型を返すMagnitude
プロパティが規定されています。
ここで特徴的なのは、Numeric
プロトコルを準拠した型とは異なるものを割り当てることができる点です。これがMagnitude
プロパティの最大の特徴です。
では、実際にNumeric
プロトコルが適用されているInt
型のMagnitude
を観察してみましょう。Magnitude
に対してUnsigned Integer
が割り当てられていて、Magnitude
プロパティではそれを返すようになっています。
ここまでくると、勘のいい人はさっきの問題と照らし合わせて分かってきますよね。何を言いたいか、実際にやってみましょう。print
でInt
型のmin
のMagnitude
を表示してみます。今度はちゃんとランタイムエラーではなく、128
と出力されます。このように、問題なくInt
のmin
に対してもMagnitude
を取得でき、エラーになりません。
次に同じノリで、絶対値を取るabs
関数についての問題に戻ります。abs
関数でInt.min
の絶対値は取れませんでしたが、Int.min
のMagnitude
であれば、ちゃんと絶対値がエラーなく取れるという解決法です。
ここで大事な点は、Int.min
のMagnitude
はUInt
型であり、abs
関数で得られる絶対値はInt
型になるという違いです。これが大きな特徴の違いで、分かれ目となってきます。
おさらいすると、Magnitude
プロパティは元となった値と同じ型とは限りません。これが重要なポイントです。
前回、abs
関数の実装についても調べました。その時に見つけたコードを実際に見ていこうかと思います。SignedNumeric
に搭載されていた規定の実装を見てみましょう。
まずパラメーターは、SignedNumeric
かつComparable
の型で、x
を受け取ります。その上で、SignedNumeric
はNumeric
に準拠していて、Magnitude
を持っています。その受け取った型そのものと、その型が持っているMagnitude
とが同じ型だった場合には、受け取ったx
のMagnitude
プロパティをMagnitude
型ではなく、自分自身の型にキャストして返すというコードになっています。
Magnitude
はすなわち絶対値です。これが型が違う可能性がありますが、abs
関数は一般に受け取った型と同じ型を返します。Magnitude
プロパティが実際に受け取った型と同じだった場合には、それをそのまま使うことで、Magnitude
すなわちabs
として結果が最適に得られる状況になります。
ここでわざわざunsafeBitCast
を使っている状況になっていますが、これはいかなる制約もなくキャストを行うためのものです。コンパイラにとっては別のものとして捉えられる場合でも、無視できます。 なので、ここでマグニチュード型を T
型そのものにキャストしています。これにより、コンパイルを通すことができます。なかなか上手なコードだと思いますが、「上手だ」と言うと語弊があるかもしれません。同じ型なので、それは同じビット列になると言ったら良いのかなと思います。現代のコンピュータでは、すべてビット列でデータを表現していますから、この全く同じビット列で T
のマグニチュードも T
も全く同じ値を表現します。
なので、unsafeBitCast
でキャストしても全く問題なく、正確な値を返すことができます。これはプロトコル指向でプログラムを書いているときにも役立ちそうな気がします。型が違うからと無理やり頑張るということをしがちですが、例えばリテラルからの変換を許可して無理やり通すということをよくやっていました。これを知っていると、余計なプロトコル準拠をさせなくても安全に処理が通せるので、なかなか良いなと思いました。
大平さん:unsafeBitCast
を使わない場合、as?
とかでもいけるんですか?
大平さん(別の):そうですね、いける気がします。ただ、as?
を使いたくないから、そういう場合にこれが安全に使えるんですかね。
大平さん(最初の):どちらを使ったとしてもそんなに大きな問題にはならない気がします。まず、ビルドが通ることを確認して、これをリターンで x
のマグニチュード as
にします。こっちの書き方もよく使いますが、ビルドが通りますよね。
「書きたくないですけどね、unsafeBitCast
と書くのも as
と書くのも、ここまで変わらないというか、むしろ as
の特長としては型チェックが走るので、互換性のない型だったときにはランタイムエラーが起こってくれるという点があります。しかし、unsafeBitCast
はデータサイズが一致するかどうかのテストだけが走るため、例えば Int
型を Double
型にキャストするとコンパイルは通りますが、Int
のビット列を Double
型のビット列と解釈してしまうため、全然違う値になってしまいます。でも as
を使った場合はランタイムエラーになります。なので、意外と as
の方が安全かもしれません。
だから、この二つをシンプルに比較したとき、unsafeBitCast
の方が使いたくないなという感覚になるかもしれません。ただ、今回の場合、T
型と T
のマグニチュード型は同じであると判定しているので、その場合型検査は必要ありません。検査している分だけ少し処理が遅くなる可能性があります。だから、unsafeBitCast
で済ませてしまうという考え方ですね。
このように、マグニチュードを自分自身の型に合わせて返すというのがこのコードの目的です。でも、もしマグニチュードとそのものが違う場合、例えば Int
型そのものは Int
だけどマグニチュードは UInt
のような場合、この条件を満たさないので、通常の三項演算子を使った絶対値計算が行われるということがわかります。
なかなか面白い実装です。最初から三項演算子で済ませた方がいいような気もしますが、とりあえずマグニチュードを尊重して、それが尊重できないときに三項演算子を使うという実装になっているのが abs
関数だということです。
絶対値を取る場合、通常は最後のリターン式の書き方になるので、確かに成り立ちますが、もし独自の型で全く新しい世界観で Numeric
プロトコルに準拠した型を作ったとします。その場合、マグニチュードもその世界観に合わせた設計になるとします。そのとき、abs
関数に渡したときに、独自の型がさらに別の型のマグニチュードを返す設計になっていると、矛盾した値を返してしまう可能性があります。その点が少し怖いなと思いました。 とりあえず、一般的な絶対値という概念で考えれば普通に動くし、この方が効率がいいのかなと思いますが、あんまり効率良さそうには見えません。対して問題が、この型が一緒だから、マグニチュードを取るとかやっているより、いきなり最後の行を動かしちゃったほうが早そうな気もしないでもないですけど、とりあえずこんな実装になってましたね。コメントをちょっと漁ってみましょうか。
なるほど、CやJavaでも同じようなのがあった気がします。ありそうですよね、普通に常識みたいなものです。「ABS関数でintを渡すとオーバーフローする」っていうのもね。C言語だとオーバーフローしたときに確かにエラーチェック走らないかもしれませんね。そういえば確かに、だから問題の起こり方が違ってくるのでしょうか。
今、「ランタイムエラー」って言いましたけど、ランタイムエラーじゃなくて計算の間違いとして現れてくるのかもしれないですね。Javaだと意図せず符号が反転してしまうとか。なるほどね。それで配列とかで使っているとアウトオブバウンズになるのか、なるほど。
CやJavaなどのオーバーフローをそれほど意識しないというか、気にしない言語の場合、確かにそういう動きになってくるんですね。なので、なかなか親しみのあるABS
関数も怖いですね。
なので、絶対値を取るときには.magnitude
を使ったほうが安全だということですね。それ自体は確かにそうなのですが、ただ.magnitude
プロパティについてコメントに書いてありましたが、一般的にはABS
関数を使うことが推奨されています。その理由としては、おさらいになりますが、.magnitude
プロパティは絶対値が得られる代わりに元の型とは違う型が得られる可能性が出てくるからです。
例えば、UInt
の.magnitude
はUInt
ですが、Int
型のプロパティはUInt
型みたいに型が違ってきます。それに対してABS
関数は引数で渡したのと同じ型が絶対値として得られるという特徴があるので、スムーズに同じ型の世界の中で演算を重ねていけるという特徴があります。なので、使いやすいのか、または一般常識としてABS
が推奨されているのかもしれません。
そのコメントには「一般的な文脈」でもという内容がありましたが、英語の「generic context」の“ジェネリック”が一般的という意味なのか、ジェネリックスの“ジェネリック”なのかはちょっとよく分かりませんでした。ただ、仮にジェネリックスのほうだとしても、型推論が効いたとしても、流れの中で自然に同じ型として計算を進めていけるABS
関数のほうが推奨されているらしいです。そういうふうにコメントに書いてありました。
なので、.magnitude
のほうが安全だよと言っても、必ずしもそっちに傾けるというものではないようです。だから.magnitude
を使って型変換するということでも良い気がしますけど、クリティカルなInt
の絶対値を取る可能性があるのであれば、ガード文とかであらかじめエラー処理を挟んでおくみたいなことが必要になってくるみたいですね。
絶対値の話はここまでです。条件付き順序の話も今回しようと思っていましたが、時間がちょっと足りないので次回に回すことにしましょう。なので今日は絶対値の話だけになりますが、何か気づいたこととかありますか?
大体話した通りで、そんなに難しい話ではなかったと思いますし、言いたいこととしては.magnitude
が何かという部分を少し掘り下げた感じで、すっきりしたかなという感じです。特に問題ないですかね。大丈夫そうですね。
では今日はこれぐらいにしておきましょう。お疲れ様でした。ありがとうございました。