👻

QuineであそんでまなぶGo言語

2022/12/02に公開約3,700字

対象読者

この記事は次のような人を対象としています。

  • Go言語の基本的な文法を知っている
  • パズルとしてプログラミングを楽しむことに興味がある

Quine(クワイン) - 自分自身を出力するプログラム

Quine(クワイン)というものをご存知でしょうか?Quineとは、文字列を出力するプログラムであって、その出力される文字列がそのプログラム自身を表す文字列と同一であるもののことを言います。

https://ja.wikipedia.org/wiki/クワイン_(プログラミング)

ある日、「Go言語のクワインであって、できるだけコードの長さが短いものを募集してみよう」と思い立ちました。そこでGitHubリポジトリを用意し、クワインの判定器をGitHub Actionに書き込んでQuineを募集したところ、何名かの方から投稿をいただけました。

これは完全に遊びだったのですが、思いのほか「学び」もありました。そこでこの記事では、投稿されたQuineを紹介しながらそうした「学び」をシェアしたいと思います。

ルール

一応次のようなルールで募集していました。

  • main.goで完結していること
  • go run main.goを実行したとき、自分自身とmain.go同一の文字列を標準出力すること
  • gofmtで整形済みであること(gofmtにかけても変化しないこと)
  • os.Openなどで外部からデータを入力しないこと

このルールが一番面白いルールだと思ったわけではなく、単にCIを作るときにそれに合わせて定義しておいただけです。実際、このルールは満たさないが面白いQuineも投稿していただいたので、この記事ではルールを満たすものもそうでないものも紹介していきます。

筆者(nobishii)が最初に書いたクワイン(200 Bytes)

まず、筆者が最初に書いたQuineです。大きさは200 Bytesです。

package main

import "fmt"

func main() {
	fmt.Println(q + fmt.Sprintf("%q", q))
}

var q = "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(q + fmt.Sprintf(\"%q\", q))\n}\n\nvar q = "

https://go.dev/play/p/ruPgcxLx1kr

これを書くにあたり、WikipediaにあったHaskellのQuineを参考にしました。

main = putStrLn $ q ++ show q where q = "main = putStrLn $ q ++ show q where q = "

自分が知っていた別な言語のQuineでは、引用符"""に置換するなどの複雑なテクニックが必要とされていましたが、このHaskellのコードはとても簡潔ですね。この簡潔さの秘密は文字列であるqをプログラムの末尾で定義できていることにあると思いました。そこで、Go言語のパッケージ変数をつかって、文字列をプログラムの最後に置くことにしました。

パッケージ変数はそれが宣言されたり使われている行番号に関係なく、依存関係を勝手に解決して初期化してくれるので、このように書くことが出来ました。

https://go.dev/ref/spec#Package_initialization

.によるimport(151 Bytes) by @tenntenn

次は@tenntennさんに投稿いただいたQuineです。

package main

import . "fmt"

var s = "package main\n\nimport . \"fmt\"\n\nvar s = %q\n\nfunc main() { Printf(s, s) }\n"

func main() { Printf(s, s) }

https://go.dev/play/p/Ht7aRBmaLWt

.によるimportによりパッケージ名を指定せずにfmt.Printfを呼べるようになりました。
その他にも、フォーマット文字列に自分自身を渡すPrintf(s,s)形になっていてスマートです。

サイズは151 Bytesとかなり短いですね。

asciiコードの技巧(137 Bytes) by @cia-rana

次に紹介するのは @cia-ranaさんによる異なるアプローチのQuineです。

package main

func main() { a += "\x60"; println(a + a) }

var a = `package main

func main() { a += "\x60"; println(a + a) }

var a = `

https://go.dev/play/p/UGAQqcNJk5d

何をしているかわかるでしょうか?"\x60"はasciiコードの60番、つまりバッククオーテーションを表します。println(a + a)がこのmain.goと一致するということは、この main.goは前半と後半で全く同じ文字列を2回繰り返す文字列になっているわけです。

最後の行で定義したa自体にはバッククオーテーションが含まれていないことに気をつけます。そこにa += "\x60"でバッククオーテーションを1つだけ後ろにくっつけます。それを2回出力すると、ひとつめのaの末尾にあるバッククオーテーションは5行目のバッククオーテーションとなり、ふたつめのaの末尾にあるバッククオーテーションは最後のバッククオーテーションになってくれるというわけです。おもしろいですね!

printlnは標準エラー出力なのでルールを満たしていませんが、それでも非常に面白いし、長さは137 Bytesとかなり短くなりました。標準パッケージを使っていないのもかっこいいですね。

脇道: 標準パッケージを使わずに標準出力するには

さて、当初「標準パッケージを使わずにルール通りのQuineを書ける」と思い込んでいたのですが、よく考えてみると標準パッケージを使わないとシステムコールができないので、printlnのような組み込み関数が別にない限り標準出力自体が一切できないことに気づきました。

ルールの設定をミスったなあと反省していたところ、DQNEOさんから次の情報を頂きました。

https://twitter.com/DQNEO/status/1594939354874798082

.sファイルにアセンブリを書いてビルドすれば、標準パッケージなしでも標準出力ができるということですね。main.sもありというルールにしておけばよかったかもしれません。

禁じ手のようで禁じ手でない - embedパッケージ(108 Bytes) by @tenntenn

最後に禁じ手っぽいものを紹介します(?)。@tenntennさんからいただきました。

package main

import (
	_ "embed"
	. "fmt"
)

//go:embed main.go
var src string

func main() { Print(src) }

https://go.dev/play/p/A2iC2R_VSxA

main.goをembedしてsrcに格納しているので、それを出力すればQuineになります。
反則のように見えますが、embedは外部入力ではなく、あくまでビルド時にバイナリに含める機能ですから反則ではありません。

長さは 108 Bytes です!

おわりに

遊びではじめたリポジトリでしたが、投稿いただいたコードから学びがありました。
もっと短いQuineが書けるぞという方は、次のリポジトリにPRを出してみてください。

https://github.com/nobishino/goquine

GitHubで編集を提案

Discussion

ログインするとコメントできます