引き続き、自分の中で評判な技術ブログ「その Swift コード、こう書き換えてみないか」を眺めていきます。今回は、前回の最後に見ていた isMultiple(of:)
の実装でいまひとつ理解できなかったところをもう少し細かく見ていきますね。なかなか精密なところみたいで(少なくとも自分が)ここにピンと来ていないということは、つまりはバグを含ませる危険性があるはず⋯みたいな気持ちで眺めてみれたらきっと良いことありそうです。よろしくお願いしますね。
—————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #256
00:00 開始 00:27 今回も isMultiple(of:) の話から 02:48 0の倍数について再確認 05:47 倍数計算で magnitude が使われる理由 06:57 剰余演算でオーバーフローの可能性 09:29 ゼロを中心に対照でない — とは 11:13 magnitude は表現範囲が拡張される 12:48 比較演算は型のサイズに柔軟に対応 15:29 剰余は割る数の表現範囲も重要 17:31 magnitude の定義を見てみる 19:49 abs より magnitude が安全 20:20 適切に -1 の剰余を計算する工夫 21:21 際どいケースを想定してコードが書けるかどうか 23:25 そもそも、マイナスの剰余とは? 23:51 -1 の剰余がオーバーフローする理由は? 26:02 そもそもの剰余とは⋯? 27:58 クロージングと次回の展望 ——————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #256
では始めていきます。今日は引き続き、技術ブログを見ていきます。今回は、自分の中でヒットしている技術ブログ「このコード、こう書いてみないか」というテーマの前回からの続きです。これを見ていこうと思うのですが、次に進める前に気になるのが isMultiple(of:)
の部分です。
前回も少し触れましたが、この関数のエッジケースについては、確実に理解しておきたいと思います。理解が及んでいないコードは、危険性があるため基本的にはよくないです。わかるまで調べてから次へ進むことが重要です。なので、スッキリしないまま進むのではなく、もう少し調べて納得してから次へ進めたいと思います。そのうち面白い発見があるかもしれません。
そのため、前回話題にした「0の倍数」の話を調べてみることにします。ここでのポイントは、0
の倍数は常に 0
だということです。バイナリーインテジャーの場合、自分自身と同じ値になるかどうかを調べることになりますが、セルフに限定する必要についても考えました。
ここまでが本質的な話ではないので、次回にまた詳しく見ていくことにしましょう。 まずは本質的なところを見ていきます。悪数が0だったときには、自分が0であるかどうかで判断します。これは少し不思議な感じがしますが、大丈夫でしょうか。0の倍数という考え方に違和感を覚えるのですが、任意のy
に対して0
をかけても0しかありえないから、これは正しいと言えるでしょう。
この特例が書いていない場合、0で割ることができないため、ある値x
をy
で割った余りが0だったらという処理が必要となり、その際に例外が発生してしまうのです。そのための特別な処理をしているのだとわかります。
次に、マグニチュード
が使われているところを見ていきます。マグニチュード
(絶対値) を使って常用を取る計算をすると、HKSがきわどい状況 (オーバーフロー等) を避けることができます。もしマグニチュード
を使わなかったとすると、0周りで対処できない可能性があるのです。
例えば、Int8
型の-128
という値で常用を計算すると、オーバーフローが発生することがあります。このような場合にマグニチュード
を使うことで、値が負の数であっても正しく計算され、オーバーフローを防ぐことができます。
次にいくつかの例を試してみます。例えば、-128
を-1
で割るとオーバーフローが発生しますが、マグニチュード
を使うと正しく計算されます。
また、Uint
に対して-5
を使ってみると、これはまた別の話になります。ここでもマグニチュード
を使うことで値が正しく処理されるのです。こうしたことから、0周りや負の値に対しても適切に対応するためにマグニチュード
を使用していることがわかります。
この点についてさらに調べるには、他の資料やツールを使うと良いでしょう。例えば、ChatGPTのようなAIツールを使って追加の情報を得るのも一つの方法です。
以上が、今回のSwift言語の具体的な仕様や処理の理解についての説明になります。 とりあえず、最初の部分を見ていくと、適切な型を使っているかどうかわからないところがありますね。8
という値が出てきますが、これは何を指しているのかよくわかりません。他にも疑問点がありますが、とりあえずバイナリインテージャーについて検討してみましょう。バイナリインテージャーにおいて、マグニチュードを取る必要があるかどうかはわかりにくい部分ですね。
マグニチュード(絶対値)を取ることで、自然な動作が期待できます。しかし、この点について完全には理解できていないようです。もう一つ面白い話があります。それはタイムインテージャーの実装に関するものです。
少し余談になりますが、マグニチュードについてもう少し掘り下げてみましょう。これがどのようによく設計されているかというと、例えば Int8
の最小値と最大値を見てみると、-128
から 127
まであります。マイナスの値の絶対値を取ると、その値は正の値になりますが、例えば -128
の絶対値は 128
になり、オーバーフローしてしまいます。この点が難しいところです。
一方で、x
のマグニチュードを使うと、オーバーフローしない形で絶対値が得られます。これは、Unsigned Integer
(符号なし整数)を使用することで、通常の絶対値計算が可能になるためです。これにより、128
も表現できるのです。
さらに、これについての興味深い点は、例えば Int8
と UInt8
という異なる型を比較する場合でも、この仕組みがうまく機能する点です。これは、バイナリインテージャーの構造によるものです。異なる型でもバイナリインテージャーに所属していれば、比較が可能になります。
この設計のおかげで、型が違っても正しく比較できるのですね。これは Swift の言語仕様に非常によく適合しています。ただし、型の範囲が広すぎる場合や、どちらの型を使用するかの問題が発生することがあります。
具体的には、精度の高い方の型で値を返す必要がありますが、どちらが精度が高いかを判断するのは難しいことがあります。その場合、特定の型に依存するようなコードを書くのは避けるべきです。しかし、このような仕組みが Swift らしいと感じます。どちらの型を返すかの問題を解決するために、Any
や AnyBinary
を使う手もあります。
とはいえ、これらの型を使うとメモリの消費が増えるため、効率的ではありません。特に整数のようなプリミティブな演算では、効率が重要です。そのため、万能な解決策とは言えません。
最終的に、マグニチュードについての話は興味深いです。この定義はバイナリインテージャーに直接実装されています。具体的には、BinaryInteger
のマグニチュードは次のように定義されています:
var magnitude: UnsignedInteger {
return self.magnitude
}
このように、Unsigned Integer
を使用することで、絶対値の計算が効率的に行えるのです。 表現の範囲を絶対に超えないので、マグニチュードは変わりません。SignedInteger
のマグニチュードを見たらUnsignedInteger
になるのか、もともとバイナリインテージャーのエクステンションのマグニチュードかどうか確認しましたが、ありませんでしたね。したがって、Numeric
のマグニチュードで良いと思います。これを見ると、Numeric
数値のマグニチュードは独自の型で、型パラメータを関連付けて持っていることがわかります。Numeric
であって、比較可能であれば自分自身でなくても良いという規定です。つまり、Int8
のマグニチュードはUInt8
で表現できるということになっています。
これがもしマイナス127から127までしか表現できないという大前提があれば、わざわざこんなことはせずにセルフで表現しますが、いろいろな事情を考慮すると表現力を高める必要が出てきます。そして、このように型パラメータが使われるというのは面白い設計です。これは非常に重要な設計と言えます。なるべくランタイムエラーを起こさないようにするという発想で、先に述べた絶対値の型表現よりも現代的な表現になっています。とても興味深いですね。
このような部分も細やかに考慮することで、より良いコードが書けると思いますが、難しいことでもあります。そういったところが特に難しいと感じさせられたのが、Int
の「isMultiple(of:)」の実装部分です。こちらにはもう一つ前提条件が増えています。「other」がマイナス1だったときには無条件でtrue
を返すという条件です。
最初に自分がプレイグラウンドで実験してハマったのはこの「エッジケース」で、マイナス1で常用を取ろうとするとオーバーフローしてしまうという問題がありました。これを回避するために、オーバーフローするけれども、マイナス1の常用であるかどうかは常にtrue
なわけです。1で割り切れるか、マイナス1で割り切れるかどうかですので、ケースによってはチグる可能性もありますが、マイナス1のときにはオーバーフローを回避しつつまずtrue
を返すという重要な条件です。
これがちゃんと書けるかどうかは、プログラマーの実力に大きく左右されるところです。テストケースをしっかり書ければ良いのですが、テストケースを思いつくかどうかも含めて重要なポイントです。こういう境界値の問題は思いつかなかったりすると、バグが生まれやすいです。このような「isMultiple(of:)」を作る際にはリーダブルなコードを書くことが重要ですが、安易に実装してここだけを書いて終わってしまうことが起こりそうです。
今までの経験から、その素晴らしさに感心しました。Swiftのテストケースも完全には見ていないですが、Swiftにはテストケースがしっかり書かれているので、このコードが存在するということはきっとテストも用意されているのだと思います。これは自分が感動した部分なので、ぜひ紹介したいと思いました。 負の数における剰余の計算って難しくないですか?マイナスで終わる時のことを、これまであまり考えたことがなかったなと思いました。この辺りについて詳しく見ていくと、どういう動きをするか思い描けるようになりますよね、いろんなエッジケースも。実は、マイナス1で剰余を計算する時にもったいないことになるんですよね。なぜオーバーフローするのか、あまりわかっていない部分があるようです。
例えば、x
をマイナス1で剰余をとったときに、これがオーバーフローするわけです。割り算の場合はどうなのでしょうか?マイナス1で割ると、例えばマイナス128割ることのマイナス1で、その結果は 128 になります。マイナス2で割るとどうなるのでしょうか?その場合は 64 になりますね。
マイナスの場合、過去に計算したものと比較すると、意外とシンプルに見えますが、マイナス1でオーバーフローする理由を見ていくと、剰余を出す時にどのように計算するかが重要ですね。エッジケースがあるかもしれませんが、予測できない部分もあります。
以下の計算式を見てみましょう:
let x = -128
let y = x % -1
これはオーバーフローするんですよね。どうしてかを念頭に置くと、次のように考えることができます。
剰余の計算式は以下のように表せます:
let remainder = y - (x / 2) % 2
この計算式にマイナスの値を代入するとどうなるか、確かに注意が必要です。基本的な部分をもう一度振り返って、間違えないようにしないといけませんね。マイナスの計算において何か誤解があるかもしれませんが、もし理解が進むと意外と単純なのかもしれません。
負の数に対する剰余の計算が難しいですが、これを自分でしっかり理解して説明できると強みになります。負の整数の商とあまりについて調べると面白い記事が見つかるので、一緒に勉強しましょう。次回のテーマは「負の整数に対する商と余り」にして、それに関連するエッジケースなども解説できるように準備します。
それでは、今回はここまでにしましょう。お疲れ様でした。次回もよろしくお願いしますね。ありがとうございました。