見積システムを作っていて、値引きまわりの計算を実装していた。
やりたいこと自体はそんなに複雑じゃない。
定価があって、
- 値引率を入力したら値引額を出す
- 値引額を入力したら値引率を逆算する
というだけ。
たとえば定価10万円で10%引きなら、値引額は1万円。
逆に値引額が1万円なら、値引率は10%。
まあ、普通の計算やな。
実装方法だけ確認したい場合はこちら JavaScriptで金額計算するときの浮動小数点対策
で、こういう細かい処理はAIに相談しながら書くと早いので、既存コードを渡して修正してもらった。
最初に出てきた「率 → 額」の計算はざっくりこんな感じだった。
const rate = numValue / 100;const discounted = Math.floor(listPrice * (1 - rate));const discountAmount = listPrice - discounted;割引後の税抜価格を出して、小数点以下を切り捨てる。
そこから定価との差額を値引額として表示する。
考え方としてはやりたいことと合ってる。
よしよし。
……いや、待てよ。
金額計算で普通に小数使ってるけど大丈夫なん?
ここで急に嫌な予感がした。
JavaScriptの浮動小数点。
存在自体は知ってた。
有名なのはこれ。
0.1 + 0.2 === 0.3これがfalseになるやつ。
「はいはい、コンピュータでは小数を完全には表現できないことがあるんやろ」
くらいの認識はあった。
でも今書いてるの、見積システムやぞ。
listPrice * (1 - rate)とか普通にやっとるやんけ。
しかもその直後に、
Math.floor(...)してる。
浮動小数点の誤差と切り捨て。
この2人、隣の席に座らせて大丈夫な組み合わせなんか?
気になったのでAIに聞いてみた。
「なんか浮動小数点とかなんとかを考慮せなあかんのちゃうの?」
すると、
考慮した方がいい。金額や率は整数化して計算した方が安全。
という回答が返ってきた。
だよね。
絶対考慮した方がいいよね。
……。
待てやおい。
んなら最初から考慮して入れとかんかい!
何をしれっと「ああ、当然そうだけど?」みたいに言うてんねん!
お前が最初に出してきたコードには、それ入ってなかったんやぞ!
まず謝れやコラ!
……ということをAIに言っても仕方ないので、いったん口角を上げた状態を保ちつつ、コードを直してもらうことにした。
率も整数にして計算する
値引率は小数1桁まで扱う想定だった。
たとえば、
7.3%なら、10倍して
73として扱う。
AIが出してきたコードもそんな方向だった。
const rate_x10 = Math.round(numValue * 10);7.3%なら73。
ここまでは分かる。
そして、割引後価格を求めるための倍率を1000倍スケールで作る。
本来、
7.3% = 0.073 = 73 / 1000なので、
1 - 0.073= 927 / 1000になる。
つまり、
const multiplier_x1000 = 1000 - rate_x10;でいい。
ところが、AIが「対策版です」と出してきたコードはこうだった。
const multiplier_x1000 = 1000 - rate_x10 * 10;ん?
その* 10、どっから来た。
7.3%なら、
rate_x10 = 73なのに、さらに10倍して730を引いてしまう。
1000 - 730 = 270つまり7.3%引きのつもりが、倍率0.27。
73%引きになっとるやないか。
浮動小数点の1円誤差を警戒していたら、割引率が10倍になって帰ってきた。
警備員呼んだら警備員が商品持って走っていったくらい話が違う。
「対策版です」と言われると余計に怖い
ここが今回一番ゾッとしたところだった。
最初のコードならまだ疑える。
「小数使ってるけど大丈夫か?」
と、自分で引っかかった。
でもそのあと、
「浮動小数点を考慮した安全なコード」
として出てきたものは、心理的にはちょっと信用してしまう。
こっちは一度問題を指摘してるから、
じゃあ今度は対策されたんやな
と思いやすい。
でも普通に間違ってた。
しかも構文エラーでもない。
コードとしては動く。
変数名もそれっぽい。
コメントまで付いてる。
const multiplier_x1000 = 1000 - rate_x10 * 10;これだけ見せられたら、流れでコピペしてしまう人もいると思う。
今回たまたま、
「いや、rate_x10ってもう百分率を10倍した値やん」
と気づいたから止められた。
気づかなかったら?
普通に間違った値引額を表示するシステムが出来上がる。
AIが出したから大丈夫、が一番危ない
ここで浮動小数点とは別の怖さが出てきた。
AIを使ってコードを書くこと自体はめちゃくちゃ便利。
俺も使ってる。
既存コードを渡して、
「ここをこう変えたい」
と頼めば、叩き台を作る速度はかなり上がる。
でも、
AIが出したコードを採用できることと、そのコードが正しいと判断できることは別。
今回の例なら、
7.3% → 73にした理由を理解していれば、
1000 - 73 * 10を見たときに、
「いや待て」
となる。
逆に、
「なんか浮動小数点対策で整数化してるらしい」
くらいの理解でコピペしていたら、そのまま通る可能性がある。
AIがもっともらしい変数名とコメント付きで出してくるぶん、下手すると自分で雑に書いたコードより信用してしまう。
そこが怖い。
消費税の計算も気になってきた
一度気になり始めると、他の計算も全部怪しく見えてくる。
実際、見積システムでは消費税も計算していた。
こんなコード。
tax = Math.floor(finalTotal * Const.TAX * 0.01);税率10%なら、
finalTotal * 10 * 0.01という計算。
これも0.01という小数をわざわざ使っている。
だったら少なくとも、
tax = Math.floor(finalTotal * Const.TAX / 100);のように、税率を整数として扱った方が考えやすい。
もちろんJavaScriptのNumber自体には安全に整数として扱える範囲があるので、「整数にしたら宇宙の果てまで絶対安全」という話ではない。
ただ、今回扱う通常の見積金額の範囲なら、金額と率を整数スケールで扱う方が、小数を何となく掛け回すより意図も追いやすい。
そして何より、
どこで丸めるかを仕様として決める。
これが大事だった。
「最後に何となくfloorしとけばええやろ」ではなく、
- 割引後価格を確定するとき
- 消費税額を確定するとき
- 値引額を表示するとき
それぞれ何を元に計算して、どこで端数処理するのかを決めておく。
金額計算になると、コードの問題だけじゃなく仕様の問題にもなる。
PythonならDecimalがある。じゃあ安心?
バックエンドはDjangoなので、Python側の計算も気になった。
そこで出てきたのがDecimal。
from decimal import Decimal小数を正確に扱いたいときによく使うやつ。
ここでも一個注意がある。
Decimal(0.01)と、
Decimal("0.01")は同じ気持ちで書かない方がいい。
前者は一度0.01をfloatとして作ってからDecimalに渡す。
後者は文字列の"0.01"から直接Decimalを作る。
金額計算でDecimalを使うなら、最初からDecimalとして正確な値を作る方が分かりやすい。
この辺まで来ると、
小数、お前ちょっと座れ。
という気分になってくる。
しかもAIの説明自体もレビュー対象だった
今回さらに面白かったのが、コードだけじゃない。
AIから、
「math.floor()はDecimalをfloatに変換するから危険」
という趣旨の説明も途中で出てきた。
でも、こういう説明も含めてそのまま記事にはできない。
コードだけレビューして説明文は信用する、では意味がない。
AIが生成したものなら、
- コード
- 数式
- APIの挙動
- 「これは安全」という断言
全部が確認対象になる。
今回の記事を書くにあたっても、会話中に出てきた説明をそのまま「正解集」として並べるのではなく、実際に確認できたことと、改めて技術検証が必要なことを分けることにした。
「AIの回答を鵜呑みにするな」という記事でAIの回答を鵜呑みにしたら、さすがに完成度の高すぎるオチになってしまう。
今なら最初に何を見るか
今回みたいな金額計算を書くなら、今なら最初にこの辺を確認する。
まず、
その値は金額なのか、率なのか。
次に、
小数を本当に持つ必要があるのか。
率が小数1桁までなら、7.3%を73として扱える。
そして、
どの時点で端数処理するのか。
割引後金額を切り捨てるのか、値引額を切り捨てるのか。
消費税は何に対して計算するのか。
ここが曖昧なままコードを書き始めると、計算式だけ正しくても最終結果が仕様と合わなくなる。
最後に、
具体的な数字を入れて自分で検算する。
今回の* 10なんて、7.3%を実際に入れてみればすぐ分かる。
AIにコードを書かせる場合、この最後の工程はむしろ前より重要になった気がする。
AIにコードを書かせるな、ではない
今回の件で、
「AIにコードを書かせるのは危険だからやめよう」
とは思ってない。
むしろ便利なのでこれからも使う。
ただ、
AIがコードを書けることと、自分がそのコードを理解しなくていいことは全然つながってない。
AIが10分かかるコードを1分で出してくれたとしても、その1分後に、
「これ何してる?」
と聞かれて説明できないなら、まだ採用する段階じゃない。
今回も浮動小数点に気づかなければ、最初のコードをそのまま使っていたかもしれない。
浮動小数点に気づいたあとも、* 10に気づかなければ、今度はもっと派手に間違ったコードを使っていたかもしれない。
AIにレビューを頼んで安心したら、そのレビュー結果にもレビューが必要だった。
なんかもうレビューのマトリョーシカやな。
便利になったはずなのに、最後に必要なのは結局、
これ、ほんまに合ってる?と一回疑うことだった。
金額計算では特に、その一回をサボらん方がよさそうだ。