今日とある場所で虫が入り込む瞬間を見た。虫といってもバグの方。それはプログラマ向けの Q&A サイトで始まった。質問の内容はこうだ。
文字列には0または4がだけが含まれる。文字列は 4 から始まり、例えば 440, 44, 40, 4400, 4440 など、これらは正しいとするが 404 は正しくない。今のところ、私は 0 の直後に 4 が現れるかどうかでチェックしている。これは果たして効率的だろうか。
始め僕はこの質問文をちゃんと読んでおらず、正規表現を使ってこれを実装した。
package main
import (
"regexp"
)
func check(s string) bool {
return regexp.MustCompile(`^4+0*$`).MatchString(s)
}
func main() {
for _, tt := range []string{"444", "44", "40", "4400", "4440"} {
if !check(tt) {
panic("want true: " + tt)
}
}
for _, tt := range []string{"404", "040"} {
if check(tt) {
panic("want false: " + tt)
}
}
}
でも質問をよく見たら彼は効率的かどうかを気にしていた。確かにこのお題で正規表現は無い。僕は慌てて以下のコードを付け添えた。
package main
func check(s string) bool {
i := 0
r := []rune(s)
for i = 0; i < len(r); i++ {
if r[i] != '4' {
break
}
}
if i == 0 {
return false
}
for ; i < len(r); i++ {
if r[i] != '0' {
return false
}
}
return true
}
func main() {
for _, tt := range []string{"444", "44", "40", "4400", "4440"} {
if !check(tt) {
panic("want true: " + tt)
}
}
for _, tt := range []string{"404", "040"} {
if check(tt) {
panic("want false: " + tt)
}
}
}
いずれのパッケージにも依存しておらく、おそらくちゃんと動くコードだろう。
その後、周りのオーディエンスが質問した彼に「どんなケースか良く分からないな、コードを見せてくれる?」と言った。そして彼は以下のコードを見せてくれた。
package main
import (
"fmt"
"strings"
)
func validate(str string) bool {
if strings.HasPrefix(str, "4") {
for i := 0; i < len(str)-1; i++ {
if (str[i] == '0') && (str[i+1] == '4') {
return false
}
}
} else {
return false
}
return true
}
func main() {
data := []string{"4", "44", "4400", "4440", "404", "004"}
for _, val := range data {
fmt.Println(validate(val))
}
}
なるほど彼が最初に言ってた通り「0 の直後に 4 が現れる事でチェック」している。一見このコードは正しそうに見える。でもこのコードは「406」の様な文字列で正しく機能しない。彼にそれを伝えたところ、彼は「@mattn -1 最初に 0 と 4 しか無いっていったじゃん」と返してきた。
僕はここで「あー、バグが混入するタイミングはここなんだ」と思った。例えばこの関数が「4 から始まり 0 が 0 個以上続く文字列をチェックする」関数だとして他のユーザに配られるとする。それを譲り受けた開発者はそれを使ってテストする。この時点でその開発者は「もちろん 406 みたいな文字列も弾いてくれる」と信じてしまうだろう。こうやってバグってのは混入するんだ、と思った。まぁ、もしかしたら彼のキーボードには 0 と 4 とリターンキーしか付いていなかったのかもしれない。