Adding isMultiple to BinaryInteger
01 何が問題だったのか
ある整数が別の整数の倍数かどうかを判定したい場面は、UI コード・アルゴリズム実装・テスト・チュートリアルなど幅広い文脈で登場します。特に 2 の倍数(偶奇)の判定は頻出です。Swift にはこの用途のための専用 API がなく、従来は剰余演算子 % や、場合によってはビット演算子 & を使って書かれてきました。
// UITableView の行の色を交互に変える
cell.contentView.backgroundColor = indexPath.row % 2 == 0 ? .gray : .white
// 4 バイトの倍数であることを表明する
_sanityCheck(bytes > 0 && bytes % 4 == 0, "capacity must be multiple of 4 bytes")
% を使った判定は読みにくく、意図がぼやける
x % 2 == 0 のような書き方は短いとはいえ、英文のように素直に読める形ではありません。演算子の優先順位(% は == より強い)を思い出しながら読む必要がありますし、bytes > 0 && bytes % 4 == 0 のような複合条件に埋め込まれるとさらに意図が取りづらくなります。また、ユーザーにとって「この API がどのプロトコルに属するべきか(Int? SignedInteger? FixedWidthInteger? BinaryInteger?)」は自明ではなく、サードパーティ製ユーティリティでも置き場所がばらついてきました。
負数に対する % の挙動で誤りが生じやすい
さらに % には、負数に対する挙動が言語によって異なるという落とし穴もあります。Swift の % は被除数の符号を引き継ぐため、「奇数かどうか」を value % 2 == 1 と書くと負の奇数に対して false を返してしまいます。
// Swift
7 % 2 == 1 // true
-7 % 2 == 1 // false (-7 % 2 は -1 になる)
Ruby や Python の % は常に非負の結果を返すため、他言語の感覚でこの書き方を持ち込むと静かにバグります。加えて % は右辺が 0 のときトラップするので、「0 で割れるか」を素朴に確かめる用途にも使えません。
「割り切れるか」を表す API の置き場所が定まらない
こうした問題に対し、ユーザーが自前で isEven や isDivisible(by:) のような拡張を書くことは可能ですが、標準ライブラリに存在しないと毎回書き直すことになり、サンプルコードや教材コードでは本題から注意を逸らす要因になります。また、「0 で割り切れるか」を表現する isDivisible(by:) は、「割り切れるのに割ろうとするとトラップする」という奇妙な状況を生みます。
let y = 0
if 10.isDivisible(by: y) {
let val = 10 / y // トラップ
}
以上のように、倍数判定を安全かつ読みやすく書くための標準 API が必要とされていました。
02 どのように解決されるのか
BinaryInteger プロトコルに、ある整数が別の整数の倍数であるかを判定するメソッド isMultiple(of:) を追加します。
protocol BinaryInteger {
// ...
func isMultiple(of other: Self) -> Bool
}
BinaryInteger に適合するすべての整数型(Int や UInt、Int64、ユーザー定義の任意精度整数型など)で、このメソッドを呼び出せるようになります。
isMultiple(of:) の使い方
従来 % で書いていた倍数判定を、そのまま置き換えられます。
// UITableView の行の色を交互に変える
cell.contentView.backgroundColor = indexPath.row.isMultiple(of: 2) ? .gray : .white
// 4 バイトの倍数であることを表明する
_sanityCheck(bytes > 0 && bytes.isMultiple(of: 4), "capacity must be multiple of 4 bytes")
// 奇数の要素を探す
let numbers = [-3, -2, -1, 0, 1, 2, 3]
let firstOdd = numbers.first { !$0.isMultiple(of: 2) }
// firstOdd == -3
メソッド名が英文のように読めるため、% 演算子を使った式に比べて意図が明確になります。bytes > 0 && bytes.isMultiple(of: 4) のような複合条件でも、演算子の優先順位を意識せずに素直に左から読めます。
負数や 0 でも安全に扱える
isMultiple(of:) は % と違って、被除数の符号に左右されずに数学的に正しい判定を返します。負の奇数でも、「2 の倍数ではない」と正しく判定されます。
(-7).isMultiple(of: 2) // false
(-6).isMultiple(of: 2) // true
また、右辺が 0 の場合もトラップせずに結果を返します。具体的には「0 は 0 の倍数である(0 は任意の数の倍数)」という数学的な定義に従い、次のように振る舞います。
0.isMultiple(of: 0) // true
5.isMultiple(of: 0) // false
これにより、「0 で割れるか」の確認と「0 で割ってトラップする」操作が別物として扱え、事前チェックに安心して使えます。
デフォルト実装
標準ライブラリは BinaryInteger と FixedWidthInteger & SignedInteger の 2 箇所にデフォルト実装を提供します。標準ライブラリの具象整数型(符号付き・符号なしの固定長整数)については、このデフォルト実装でほぼ最適な挙動になります。
独自の整数型、特に最小値・最大値を持たない任意精度整数(bignum)型などでは、絶対値ではなく元の値に対して直接割り切れるかを判定するほうが効率的な場合があります。そのようなケースでは、自前で isMultiple(of:) を実装することで、型の特性に合わせた最適化が可能です。
isEven / isOdd は採用されなかった
当初の提案には、2 の倍数判定を専用に扱う isEven / isOdd プロパティも含まれていました。しかしレビューの結果、採用されたのは isMultiple(of:) のみで、isEven / isOdd は標準ライブラリには入っていません。偶奇判定も、n.isMultiple(of: 2) と書く形で十分に読みやすく表現できる、というのがその判断の理由です。