見出し画像

バグが私に教えてくれたもの

失敗は成功のもとという言葉があります。プログラミングにおいて「バグ」こそが失敗そのものです。そこから教わることは多い。学ばない手はありません。

コピーしてシンボルを変更するのを忘れたとか、== を = としてしまったとか、そういったケアレスミス的なものも少なくはありませんが、もっと設計の根本に関わるようなものもあります。「設計の根本」といっても必ずしも複雑なものとは限りません。関数内のアルゴリズムや、あるいは関数設計においても同じです。ほんの些細なことでバグが入り込む余地を作ってしまう。そういうことが少なからずあるのです。バグに遭遇してからようよう「ああ、これはいけない」と思い至ることになります。

先日書いたこの記事。
「Prime numbers sieve C言語化編」
これがまさにそうでした。


先日、書いた記事はこちら。

最後にこう呟きました。

(「2」を特別扱いしたその歪さが後々に不具合の元となる。だが、この時の私には知るよしもなかった・・・)

記事に書いたプログラムは小さなものです。

1000 より小さい素数を全て列挙して標準出力に出力する

数値が素数かどうかをチェックするのは、
その数値より小さい値で割りきれるものがあるかどうか
でチェックしました。自分の半分を超えたら割りきれるはずはないので、正確には半分までの数値でチェックしています。

例えば、53 が素数かどうかをチェックするには、2 ~ 26 までの数値で割りきれるかどうかをチェックするわけです。

このチェックで間違いではないのですが、数学的には自分より小さい素数で割りきれるものがなければ素数とも言えます。

53 が素数かどうかをチェックしたい場合
2 ~ 26 までの数値で割りきれなければ素数
ですが、
53 より小さい素数で割りきれなければ素数
という判定も可能です。

そして後者の方がチェックする回数が少ないのです。数値が大きいほど、チェック回数は少なくて済みます。なので、先のプログラムもそれに書き換えようと思い立ったわけです。


では書き換えてみます。

プログラムを再掲します。

#include <stdio.h>
#include <stdbool.h>

bool is_prime(int number)
{
    int i;
    for (i = 2; i <= (number/2); i++)
    {
        if ((number % i) == 0) {return false;}
    }
    return true;
}

int iter_primes(int prime)
{
    prime++;
    while (!is_prime(prime))
    {
        prime++;
    }
    
    return prime;
}

int main()
{
    int prime = 2;
    while (prime < 1000)
    {
        printf("%d\n", prime);
        prime = iter_primes(prime);
    }
    
    return 0;
}

関数「is_prime」だけを書き換えればいいでしょう。

こんな風に書き換えてみました。

#define MAX_PRIMES 1000
bool is_prime(int number)
{
    static int primes[MAX_PRIMES] = {0};
    static int prime_cnt = 0;
    int i;

    for (i = 0; i < prime_cnt; i++)
    {
        if ((number % primes[i]) == 0) {return false;}
    }

    primes[prime_cnt] = number;
    prime_cnt++;
    return true;
}

ちょっといろいろアレだけど、今回のように
数値が昇順に呼び出されるという使い方限定
でよしとします。
とりあえず、改修は完了。
動かしてみました。

すると、なんと!
「4」が素数リストに入ってくる!

そう、これが
『「2」を特別扱いしたその歪さが後々に不具合の元となった』
のです。

このプログラムでは「is_prime(2)」を実行していません。このため「is_prime」では「2」を素数として認識できず、「4」が素数と判断されてしまいました。

今回の「is_prime」の改修はいささか特殊で、もともとこの関数の使用方法になかった制約を加えてしまっていることも理由ではあるのですが、それでも使う側も特殊なことをしなければ、「2」の素数判定だけ自分でやっちゃうみたいなことをしなければ、こういう事態にはなりませんでした。

しかも、私はこれを公開してから指摘された。
1000より小さい素数判定で間違ったのは「4」だけだったので気付きにくかった。こういうこともよくあることです。

素数は昨今暗号などにも使われます。たとえば、そういうようなセキュリティでこんな間違いを犯したら目も当てられない。そういうようなことも起こりかねません。

ソースコードの見た目の美しさは、考えている以上に品質に影響する。他の人にも分かりやすくなる、それだけではないのです。

今回のケースでは、初版の時点で「is_prime」を経ずに「2」を素数と判定していることには気付いていました。バランスが悪いなぁ、とも思った。なのに「ステップ数が少ない」というそれだけの理由でこのコードを採用した。「バランスが悪い」コードというのは得てして後からツケを払わされます。「is_prime」がするべき役割を簡単だからという理由だけでやってはいけない。一つの役割を複数の関数に分散させるのは混乱を招くだけで得るものはほとんどありません。


繰り返しますが、バグから学ぶことはたくさんあります。
間違った、そして直したというだけではなくて
「どうすればよかったんかなぁ」
そう考えてみる。
そうして改善していくことは、必ずプログラミング技術を向上させます。


この記事が気に入ったらサポートをしてみませんか?