前置きをスキップするときは「オーバーフローの検出」に進んでください。
F#はMicrosoftのDon Syme氏が開発したプログラミング言語で、命令型、オブジェクト指向、関数型の要素を併せ持つマルチパラダイム言語と位置付けられています。実装はオープンソースのMITライセンスのもと、githubのリポジトリで公開されています。
実行環境は.NET Frameworkおよび.NET CoreやMonoといったマルチプラットフォームに対応していますが、サポート状況については各プラットフォームごとの情報を参照してください(「F# の概要」など)。
言ってくれないオーバーフロー
F#のデフォルト環境では、オーバーフローが起きても警告や例外を全く発しない場面があります。たとえばこんな計算。
255 + 1 = 256
255に1を足したら256になるはずです。しかし、これをbyte
型に変えてみるとどうでしょうか。
255uy + 1uy = 0uy // (?!)
255uy
に1uy
を足すと結果は0u
になってしまいます。これがオーバーフローの一例です。
計算以外でもオーバーフローを起こす場合があります。それはキャスト(型変換)です。値によっては正負が逆転してしまうこともあります。
sbyte 384 = -128y
これでも、やはり警告や例外は出ません。こうした現象をわざと利用する場合もないことはないでしょうが、とはいえ「なんで言ってくれないの?」と思うこともあるのではないでしょうか。
そこで、ここではなぜオーバーフローが起こるのかを探り、それを検出する方法を紹介します。F#のバージョンは4.1(FSharp.Core 4.4.1.0)とします。
なぜオーバーフローするのか
オーバーフローは桁あふれともいいますが、設定しようとしている値が数値型で扱える範囲を超えてしまうことをいいます。F#では、定数で数値を表すときにu
やy
などの接尾辞をつけられます。これにより簡単に数値型を表せると同時に、オーバーフローの可能性も見通しやすくなります。ただし、これは数値の定数を表す場合に限られます。
F#では型推論のおかげで型名を書かなくて済むことも多いため、ソースコードを見ただけではオーバーフローが起こる可能性のある個所を見落としてしまうリスクがあります。結果を見て初めてオーバーフローしていたと気づく場面もあるかもしれません。
演算によるオーバーフロー
byte型では、扱える数値の範囲が0uy~255uyの整数と決まっています。これは符号なし8ビットで表せる数値を表しています。この型でオーバーフローが起きるのは、結果が255を上回るときと、0を下回るときです。
255uy + 1uy = 0uy // 結果が255を上回るときのオーバーフロー (本来なら256)
0uy - 1uy = 255uy // 結果が0を下回るときのオーバーフロー (本来なら-1)
255 + 1
の結果は本来256ですが、255uy + 1uy
ではオーバーフローして0uy
と値が小さくなってしまいます。2進法で表すと255は0b1111_1111
となります(最下位ビットから上位に4桁ごとで区切っています)。これに1を加えると、本来なら0b1_0000_0000
となるのですが、byte型は8ビットですから桁数が足りません(ここではビット数を2進数での桁数とします)。そのため最上位の桁はメモリに収まらず(桁あふれを起こして)、結果が0uy
となるのです。
次の0uy - 1uy
の結果は255uy
で、上記とは逆に数値が大きくなります。これがなぜオーバーフローかというと、最上位よりも上位の桁から1を借りようとするからです。
ただ、実際の引き算(-:減算)では2の補数(すべてのビットを反転させて1を足した値で、正負を反転させた値に相当)を足すという処理が行われるため、この計算では桁借り(繰り下がり)が起こりません(qiita内にも2の補数についての説明がいくつかあるようです)。
では0uy - 1uy
はどう計算するかというと、これを0uy + (-1)
ととらえ、-1
を1uy
の2の補数、つまり~~~1uy + 1uy = 255uy
で置き換えます(byte
型は符号なしなので負の数にはなりません)。よって0uy - 1uy
は0uy + 255uy
に置き換えられ、結果が255uy
となるのです。
こうした処理は、定数とuy
などの接尾辞が書かれていれば気づきやすいですが、関数内でどのような値がどう使われているかがわからなければ気づかないかもしれません。そうなると、オーバーフローが起こる可能性のある個所を特定するのが難しくなります。
キャストによるオーバーフロー
今度はキャストによるオーバーフローを探っていきます。
F#の数値型は扱える範囲が異なるものが複数存在するため、範囲の広さや符号の有無によって、キャスト(型変換)の際に意図しない結果を生むことがあります。
以下はできれば行うべきでないキャストの例です。
ここではint16型からsbyte型にキャストする場合を取り上げます。
sbyte 384s = -128y // 正の数 → 負の数
sbyte -192s = 64y // 負の数 → 正の数
sbyte 384s = -128y
はint16
(16ビット)からsbyte
(8ビット)へのキャストです。どちらも符号ありの型なので負の数を扱えますが、キャストによってこのように正負が逆転してしまうことがあります。扱える数値の範囲が広い型からそれが狭い型に変換されるときは、上位のビットが削られます。384
は2進法で0b1_1000_0000
と表されますが、下位8ビットだけを見ると0b1000_0000
です。これをsbyte
型では-128y
として扱います(最上位ビット1は負の数を表すため)。よってsbyte 384s = -128y
となり、正負が反転してしまうのです。
一方、sbyte -192s = 64y
ですが、-192s
は2進法で表すと0b1111_1111_0100_0000s
です。下位8ビットだけに注目すれば0b0100_0000
で、これが結果の64y
です(最上位ビット0は正の数を表す)。よって、この場合も正負が逆転してしまうのです。
ここまでオーバーフローの原因として演算とキャストを取り上げましたが、こうした処理が行われても、F#のデフォルトでは警告も例外も発してくれません。ですが、F#ではこれを検出する方法が提供されていますので、これを使えばオーバーフローが起きた時でも対応できるようになります。
オーバーフローの検出
F#における演算とキャストによるオーバーフローを検出するにはMicrosoft.FSharp.Core.Operators.Checkedモジュールを設定します。元に戻すにはMicrosoft.FSharp.Core.Operatorsモジュールを設定し直します。
open Microsoft.FSharp.Core.Operators.Checked // を実行
open Microsoft.FSharp.Core.Operators // を実行
このCheckedモジュールが設定されると、上記のような演算およびキャスト(型変換)によってオーバーフローが起きたときにSystem.OverflowException
が発生するため、オーバーフローを検出できるようになります。そのため、たとえばオーバーフローが発生する可能性がある処理を行うときは結果をoption
型にするといった対応もとれます。
open Microsoft.FSharp.Core.Operators.Checked // オーバーフローを検出する設定
// オーバーフローが起こる可能性がある処理
let f x = x + 1uy
// オーバーフローの検出を考慮して結果をoption型にする
let tryOption f x =
try Some(f x) with
| :? OverflowException -> None
| _ -> reraise()
tryOption f 255uy // None (オーバーフローを検出した)
tryOption f 1uy // Some 2uy (オーバーフローを検出しなかった)
この設定で対応できる演算子は以下のとおりです。
- 掛け算 - (*)
- 足し算 - (+)
- 引き算 - (-)
- マイナス(正負の反転) - (~-)
open Microsoft.FSharp.Core.Operators.Checked
// 以下はすべて例外発生「System.OverflowException: 算術演算の結果オーバーフローが発生しました。」
128uy * 3uy
255uy + 1uy
-128y - 1y
同じくこのモジュールで対応できるキャストの型は以下のとおりです。
- byte, sbyte
- char
- int, int16, int32, int64
- uint16, uint32, uint64
- nativeint, unativeint
open Microsoft.FSharp.Core.Operators.Checked
// 以下はすべて例外発生「System.OverflowException: 算術演算の結果オーバーフローが発生しました。」
byte 256
uint16 -128y
デフォルトでオーバーフロー時に例外発生する処理
ここまで、F#におけるオーバーフローの例と、それを検出する方法を取り上げましたが、上記の設定をしなくてもオーバーフローによる例外が発生する処理もありますので、最後にその例を紹介しておきます。
pown 2 31 // 例外発生「System.OverflowException: 算術演算の結果オーバーフローが発生しました。」
System.Decimal.MaxValue + 0.5M // 例外発生「System.OverflowException: Decimal 型の値が大きすぎるか、または小さすぎます。」
F#を使う皆さんの参考になれば幸いです。