なぜ足し算より掛け算を先に計算するのか?
1+2×3
- 突然ではあるが、上記数式の答えは、7である。(2×3=6、1+6=7)
- 左から順に計算すると9になるのだが、9と答えてはいけない。(1+2=3、3×3=9)
- 四則演算(しそくえんざん)には、加減算よりも乗除算を先に計算する、というルールがあるのだ。
- ルールと言われると、決まり事だから守らなければならない、と思って今まで何の疑問も抱かず計算してきた。
ところで一体、どうしてそんなルールになったのだろう?
ルールだから?
- 自分が少年の頃はルールだから、規則だから、と言われると渋々納得していた。
- 周りの皆もそうするし、乗除算を先に計算しないとテストでも○が貰えないので、言われたとおりに計算してきた。
- そんなことを繰り返すうちに、それはいつしか当然の常識となってしまい、何の疑問も感じなくなっていた。
- では、自分が大人の今、少年から同じ質問をされたら「ルールなんだよ」と答えるべきなのか?
- いや、その一言で済ませてしまうのは、あまりにも怠惰である。
- ルールとしか言えないのは自分が知らないことの言い訳なのだ。
- ルールとは、皆でそうしようと決めて守る約束事である。
- 皆でそうしようと決めたことには、どこかに合理的な理由があってそうしたはず。
- その合理的な理由とは何なのか、それを探って、教えてあげるべきなのだ。
掛算の定義から考える
(いずれも 0 でない)自然数 m と n に対して、m を n 個分加えた数
乗法 - Wikipedia
を
m × n, m · n, mn
などのように書いて m に n を掛けた数とか m と n の積、m 掛ける n などという。
つまり...
0 = 2×0 2 = 2×1 2+2 = 2×2 2+2+2 = 2×3 2+2+2+2 = 2×4 ...
なのだ。
- 掛け算とは、足し算によって定義される概念なのだ。
- 足し算と掛け算では、概念の次元が違っている。
- 計算する時には同じ次元の概念にしておくべき。
1+2×3 = 1+2+2+2
- 同じ次元の概念なら、先に計算しようと、後に計算しようと、答えは常に7となる。
- ところで毎回、掛け算を足し算の概念に展開して、計算するのは面倒である。
- だから、掛け算九九を覚えて、足し算の概念に展開した後の計算結果のみ想像して、数式全体を計算しているのだ。
- 掛け算を先に計算するというよりは、より基本的な概念である足し算に展開していたのだ。
- 足し算に展開する過程で、より合理的に計算するために、掛け算九九を利用して、その計算結果のみ利用していた。
掛け算九九が存在しない世界を想像してみる
- 大人になると、掛け算九九のような数表を無意識のうちに利用して、掛け算を計算している気になっている。
- しかし、その計算の実体は記憶から導いているだけで、純粋な計算とは言えないのである。
- ドラえもんの秘密道具に、かの有名な「もしもボックス」という電話ボックスがある。
- もしも、掛け算九九が存在しない世界だったら、掛け算を計算するためには、足し算に展開して、地道に足し算するしかないのである。
- 掛け算の混じった数式は、より基本的な概念の足し算に統一することで、はじめて計算できるようになるのだ。
日常的な合理性から考える
以上のように考えたとしても、ルールとしては、左から順番に計算するというルールにしても悪くない気がする。
1+2×3 = 9
- 上記のようなルールとならなかったのは、なぜなのか?
- 日常生活の中で計算する状況を考えてみる。
- すると、圧倒的に掛け算してから足し算する状況が多いことに気付いた。
- レジでの代金精算は、商品単価×数量を足し算していくことの繰り返し。
- お金の計算も、100円が3枚、10円が2枚...のように確認しながら、暗黙の掛け算をしながら、足し算している。
- 回転寿しの代金計算も、お皿の種類ごとに枚数を掛け算した結果を足し算している。
- 掛け算した結果を合計するという手順は、日常に即したルールだったのだ。
- もちろん、足し算した結果に掛け算するという状況もないわけではない。
- 回転寿しに3人で行って、本日のお勧め5品を3皿ずつ握ってもらう場合など。
- 5品の金額がそれぞれ100円、200円、300円、400円、500円だったとして...
- 一人の代金を求めて、3人分する方法なら、簡潔に計算できる。
(100+200+300+400+500)×3 = 4500円
-
- 一方、お皿ごとに掛け算すると、掛け算の回数が5回に増えてしまい、煩雑である。
100×3+200×3+300×3+400×3+500×3 = 4500円
- しかし、寿司屋に3人で行って、まったく同じものを食べて、帰ってくるという状況は極めて稀である。
- 一般的には、3人がそれぞれ自分の好みの物を、好きなだけ食べる、という状況がありふれているだろう。
- つまり、1の状況よりも、2の状況の方がありふれているのだ。
- 100+200+300+400+500×3 = (100+200+300+400+500)×3
- 100×3+200×3+300×3+400×3+500×3 = (100×3)+(200×3)+(300×3)+(400×3)+(500×3)
- ならば、よくある状況である掛け算を先に計算して、括弧を省略可能なルールとした方が、合理的なのだ。
因数分解と展開
- 1は数式を因数分解した形、2は数式を展開した形。
- 100+200+300+400+500×3 = (100+200+300+400+500)×3
- 100×3+200×3+300×3+400×3+500×3 = (100×3)+(200×3)+(300×3)+(400×3)+(500×3)
- 因数分解は、状況の中に共通因数を見つけられなければ数式にできないが、(特別な状況)
- 展開は、どのような状況であっても必ず数式にできるのだ。(必ず可能)
- 日常に、掛け算した結果を足し算する状況が多いのも頷ける。(どのような状況でも使える計算方法だから)
必ず括弧で明示するルールにならなかったのは?
暗黙の優先順位なんて作らずに、明示的に括弧で括れば良いという考え方もある。
- ところで、数式は、可能な限りシンプルに表現できることが好まれる。
- 小学時代は四則演算で十分だが、中学、高校と数学の奥深くに進むほどに、新たな概念が登場する。
- ベキ乗、ルート、対数...など、様々な演算子が入り乱れて、複雑な数式に出会う機会もある。
- その時、演算子の優先順位が左から順に計算するルールのみだと、複雑な数式の中に多数の括弧が含まれて、さらに複雑な様相となってしまう可能性がある。
- シンプルな表現を目指して、プラス・マイナス・代数の概念を学んだ中学以降は、四則演算の記号は姿を消してしまう。
- 加減算については、数値のプラスかマイナスかを明示して並べるだけになる。(減算はマイナスの加算という概念になる)
- 掛け算については、代数の掛け算記号は省略される。(割り算は逆数の掛け算に変換される)
- そのような暗黙のルールがあるからこそ、かの有名な公式も美しいと言われるのだ。
E=mc^2
-
-
- ^は、ベキ乗の意味。
-
- 仮に、左から順に計算するルールのみだと、とんでもない誤解の公式になってしまう。
- ベキ乗は、掛け算より優先順位が高いのだ。
- mc^2 = m(c^2)である。(くれぐれも(mc)^2ではないので誤解のないように)
- 暗黙の優先順位は、数式をシンプルで美しくするのである。シンプルで美しい数式は、分かりやすい。
- よって、加減算より乗除算を優先することで余分な括弧を省略するルールは、より多くの状況で数式をシンプルにする、理に適ったルールなのである。
優先順位を考慮して計算するアルゴリズム
- 今どきのスクリプト言語は、当然のように1+2*3=7と計算してくれる。(AppleScriptでもそうなる)
- ところが、数式をテキストとして受け取り、構文解析して、優先順位を考慮した計算を自力で処理するのは、かなり手強い。
- そのような必要性はほとんどないのだが、手法の一つとしてちゃんと理解しておきたかったので、試しにやってみた。
- 調べてみると、それを実現する定石のような方法があって、逆ポーランド記法というそうだ。
1+2 =+12 ...... ポーランド記法 =12+ ...... 逆ポーランド記法
- 逆ポーランド記法の素晴らしいところは、一般的な数式を、括弧不要で優先順位を考慮した数式に変換できるところである。
1+2*3 → 123*+ → 「2と3を掛け算して、その結果と1を足し算する」 2*3+4*5 → 23*45*+ → 「2と3を掛け算して、4と5を掛け算して、その結果同士を足し算する」 1+2*3+4 → 123*+4+ → 「2と3を掛け算して、その結果と1を足し算して、その結果と4を足し算する」
- 数字の順番は変わらず、演算子の順番だけ変化している。
- そして、一旦逆ポーランド記法に変換してしまえば、とても簡単に目的の計算が達成できてしまうのだ。
- その方法は、演算子を見つけたら手前の2要素を演算子に従って計算する、という非常にシンプルなもの。
- 難関は、一般的な数式を逆ポーランド記法に変換する処理の部分。やってみた。(AppleScript)
- 実験的なサンプルコードなので、数値は1桁、使える記号は+-*/()のみ、数式のエラーチェックもなし。
global re_poland
global temp
--calc(revrse_polish_notation("5+8*(4+5)"))
--calc(revrse_polish_notation("(5+8)*(4+5)"))
--calc(revrse_polish_notation("1+2*3+4*5"))
calc(revrse_polish_notation("1+(2*3+4)*5"))
on calc(formula_elements) set temp to {} repeat with e in formula_elements
if e is in "01234456789" then
push_temp(e) else
set num1 to pop_temp() set num2 to pop_temp() push_temp(run script (num2 & e & num1)) end if
end repeat
{formula_elements, temp} end calc
on revrse_polish_notation(formula) set re_poland to {} set temp to {""} repeat with e in (formula's text items) if e is in "01234456789" then
push_re_poland(e) else if e is in "(" then
push_temp(e) else if e is in ")" then
bracket_repeat() else
operator_repeat(e) end if
end repeat
operator_repeat(")") re_poland
end revrse_polish_notation
on bracket_repeat() repeat
set last_op to pop_temp() if last_op = "(" then
exit repeat
else
push_re_poland(last_op) end if
end repeat
end bracket_repeat
on operator_repeat(op) repeat
set last_op to pop_temp() if priority(last_op) < priority(op) then
push_temp({last_op, op}) exit repeat
else
push_re_poland(last_op) end if
end repeat
end operator_repeat
on push_re_poland(elements) repeat with e in (elements as list) set re_poland's end to e as text
end repeat
end push_re_poland
on push_temp(elements) repeat with e in (elements as list) set temp's end to e as text
end repeat
end push_temp
on pop_temp() set i to temp's item -1
set temp to temp's reverse's rest's reverse
i
end pop_temp
on priority(i) if i is in "()" then return 0
if i is in "+-" then return 1
if i is in "*/" then return 2
-1
end priority
- AppleScriptには、もともとpush・popの概念がないから、まずはその処理をするハンドラ(メソッド)から作らなければならなかった。
- 逆ポーランド記法にして計算する方法は定石とされる方法だけど、アセンブラやC言語のスタック処理と素晴らしく相性の良い方法だと思った。
- 今どきのスクリプト言語なら、その高度な機能を利用して、より効率的な処理方法があるのかもしれない。