間違い探し
July 16, 2021
問題
以下のGoのコードでコンパイルエラーになる行はどこか? なお、コンパイルエラーは1つとは限らない。
package main
import ("fmt"; _ `fmt`; _ `fmt`)
func main() {;;;;;;;;;;
_ = fmt.Stringer.String
panic()
type(n int)
_:_=":"[:]
var _ struct{string } = struct{ string `` }{string(((((((0_0)))))))}
}
var init = func() {}
var _ = recover()
問題を作ろうと思ったわけ
いきなり解説を書くと目の端に写ったりするので、間に問題を作ろうと思った理由を書きますね。 以前からGoに関する難解なクイズを作ることが趣味なんですが、最近は4択以外のクイズの制作に取り組んでいます。
例えば、Go Conference 2021 SpringのAfter Partyでは、以下のような問題を出題しました。
以下のコードの出すじゃんけんの手に勝ってください(R: グー, S: チョキ, P: パー)。なお、ファイル名はmain.goとし、コンパイラはGo 1.16.3のgo
コマンドを用いたものとします。また、ソースコードはgofmt
によってフォーマットされています。
// Go Conference 2021 Spring!!
package main
import (
_ "embed"
"fmt"
)
var (
// go:embed main.go
src string
n = len(src) % 3
m = f()
)
func main() {
fmt.Printf("%c", "RSP"[m])
}
func f() int {
return n + map[bool]int{false: 1}[g() == nil]
}
func g() (err error) {
defer func() (err error) {
if err = recover().(error); err != nil {
return nil
}
return
}()
var e *struct{ error }
err = e
panic(err)
}
この問題、非常に悪質でGo 1.16でリリースされたばかりのembed
パッケージを用いることで、まんまと変数src
にソースコードが入ると思わせています。実際は//go:embed
ではなく、// go:embed
となっており、ソースコードは埋め込まれません。単にコメントとして扱われ、変数src
は空文字のままです。流石に辛いのでgo vet
コマンドで指摘してほしいところです。issueも出ているのでそのうち修正されるでしょう。
当日、After Partyの参加者のみなさんが頑張ってソースコードのバイト数を数えているのをニヤニヤしながらみていました。後日、著名なGoエンジニアのDaveにも解いてもらいましたが、“hardest quiz”だったらしいです。
この問題の特徴は、1つずつ変数の値を追っていき、最終的に何が出力されるのか考える、『ピタゴラスイッチ』のような問題になっています。実際に、『ピタゴラじゃんけん装置』というNHKの教育番組の1つのコーナーからアイデアを思いつきました。
さて、ブログの先頭に書いた問題の話に戻ります。今回の問題は、いわゆる『間違い探し』です。私はサイゼリヤの間違い探しやスカイマークの機内雑誌に掲載している間違い探しに挑戦するのが好きです。とても子供向けとは思えない難しさがあります。特にスカイマークの間違い探しはめちゃっくちゃむずいので、ぜひチャレンジしてみてください。
私もGoクイズで間違い探しを作ってみたくなり、プログラミングで間違い探しといえば、バグ探しになるかなと思います。単にバグだと作問が難しいため、今回はコンパイルエラーになる行を探す問題にしました。日頃はコンパイラさんが指摘くれてるのを「指摘するなら直せよー」や「はいはい、そこは今やろうと思ってました!」と思ってた人にはコンパイラさんのありがたみが分かる問題となっています。
だいぶ文字数稼ぎましたね。ふぅ。
解説
さて、解説です。問題をもう一度掲載しておきます。見やすいように先頭に行番号をつけておきましょう。
1: package main
2: import ("fmt"; _ `fmt`; _ `fmt`)
3: func main() {;;;;;;;;;;
4: _ = fmt.Stringer.String
5: panic()
6: type(n int)
7: _:_=":"[:]
8: var _ struct{string } = struct{ string `` }{string(((((((0_0)))))))}
9: }
10: var init = func() {}
11: var _ = recover()
では、各行について解説していきましょう。
1行目
package main
ここは特に問題ありません。私はpackage
をよくタイポするけど流石に問題に入れるのはやめました。
2行目
import ("fmt"; _ `fmt`; _ `fmt`)
同じインポートパスのimport文は書けます。インポートパスは文字列リテラル"
だけじゃなくてバッククウォートも使えますが、式などは使えません。ブランク識別子(_
)を使った定義は何度でもできます。改行区切りの代わりに;
が使える。文法上は文などの末尾に;
があるが、コンパイラが挿入してくれるので通常は書かなくて良いです。
3行目
func main() {;;;;;;;;;;
何もない文は許されます。文の区切りにセミコロンを用いることができます。
4行目
_ = fmt.Stringer.String
インタフェースのメソッドでもメソッド式を記述できます。
5行目
panic()
間違いその1。panic
の引数は1つです。
6行目
type(n int)
これは関数呼び出しに見えますが、フォーマットをかけると以下のようになります。
type (
n int
)
つまり、以下のように書いた場合と同じです。
type n int
単なる型定義でした。
7行目
_:_=":"[:]
_:
は識別子にブランク識別子が用いられたラベルです。_=":"[:]
はわかりやすく書くと_ = ":"[:]
のような代入文です。。ブランク変数に代入しています。
":"[:]
は文字列をバイトスライスとして扱って、その先頭から末尾までを参照する新しいスライスをスライス演算で取得しています。
8行目
var _ struct{string } = struct{ string `` }{string(((((((0_0)))))))}
構造体のタグがある場合と無い場合の代入可能性を問うところです。タグがある場合は通常はキャストなしでは代入できません。 しかし、タグに空文字を指定した場合はタグを省略した場合と同じ扱いになるため代入可能です。
0_0
は8進数の00
(つまり0o0
でゼロ)にGo1.13で入った数値リテラルに_
を入れる方法で記述したものです。
式は()
でたくさんくくっても大丈夫です。Operand -> Expression -> UnaryExpr -> PrimaryExpr -> Operand という感じで展開されます。
9行目
}
特に注意点なしです。
10行目
var init = func() {}
間違いその2。パッケージ変数としてinit
という名前の変数は定義できません。関数のみ定義できます。
11行目
var _ = recover()
recover
関数はパッケージ変数の初期化であっても呼び出せます。パニックが起きてるわけではないのでnil
が返されます。
まとめ
コンパイラエラーになるのは、5行目と10行目でした!普段書かない記法ばっかりだったかなと思いますが、パーサーやコンパイラ、静的解析などを作るにはこういったエッジケースを考える必要もあるため、エッジケースに思いを巡らすのも悪くありません。
また何かのイベントで時事ネタを含んだ難しいクイズを出しますのでお楽しみに!