間違い探し

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行目でした!普段書かない記法ばっかりだったかなと思いますが、パーサーやコンパイラ、静的解析などを作るにはこういったエッジケースを考える必要もあるため、エッジケースに思いを巡らすのも悪くありません。

また何かのイベントで時事ネタを含んだ難しいクイズを出しますのでお楽しみに!