Help us understand the problem. What is going on with this article?

Go で競技プログラミングをやるためのツール

Go で競技プログラミングをするときの、提出用コード自動生成ツールを作ったので紹介します。

別パッケージにあるコードを必要な部分だけ抽出して 1 つのファイルにまとめたり、出力コードにgo fmt をかける機能がついています。

リポジトリはこちら: https://github.com/murosan/gollect

ツールの目的

Go の標準ライブラリは競技プログラミングをするには、少し物足りないです。
そのため、皆さんは自分で実装したライブラリを予め用意していると思います。

では、そのライブラリはどのように管理しているでしょうか。また提出するときどうしていますか。
全部 main.go に書いて管理し、そのまま提出していますか?

それでも基本的に問題はないわけですが、提出したコードを振り返ってみたとき「汚い..」と感じたことはありませんか?
それに、1 つのファイルに全て書いてメンテナンスするのは大変ですね。

そこで作ったのがこのツールです。ざっくり以下の特徴があります。

  • 使用されているコードの中で、使われている部分のみを抽出する
  • 提出可能なコードを自動生成する
  • ファイルやクリップボードに出力できる
  • go fmt によってきれいに整形する

抽出には golang.org/x/tools を利用し、ちゃんと AST をパースし、依存関係を調べています。
おまけで github.com/atotto/clipboard を使ってクリップボードに提出用コードをコピーする機能も付けました。

宣言は別のパッケージに書いてあっても問題ない、というのが最大の売りで、要するに、自分で書いたライブラリは当然のこと、一般で公開されているライブラリを go get して使うこともできます。

これでライブラリの管理が楽になりますし、提出するコードも綺麗になりますし、エディターのコード補完機能もフルに活用することができます。

使い方

インストールします。

go get -u github.com/murosan/gollect/cmd/gollect

設定を YAML で書きます。

config.yml
# main パッケージのファイルパス
# 一応 glob でも指定可能です。 e.g, ./*.go
inputFile: ./main.go

# 出力先のリスト
# 標準出力、クリップボード、ファイルパスを指定できます。
outputPaths:
  - stdout
  - clipboard
  - ./out/main.go

ついでに Makefile に追加しておくと便利です。

Makefile
# mac の人は pbpaste | go run main.go としておくと便利
r:
    go run main.go

g:
    gollect -config ./config.yml

あとは通常通りコードを書いて実行します。

# テストする
make r
# 提出するコードを生成する
make g

例えばこのコードは

lib/scanner.go
type Scanner struct{ *bufio.Scanner }

func NewScanner(r io.Reader) *Scanner {
    s := bufio.NewScanner(r)
    s.Split(bufio.ScanWords)
    return &Scanner{Scanner: s}
}

func (s *Scanner) Bytes() []byte { s.Scan(); return s.Scanner.Bytes() }
func (s *Scanner) Text() string  { s.Scan(); return s.Scanner.Text() }
main.go
package main

import (
    "fmt"
    "os"

    "github.com/owner/repo/lib"
)

func main() {
    var n int
    fmt.Scan(&n)

    m := make(map[string]interface{})
    sc := lib.NewScanner(os.Stdin)

    for i := 0; i < n; i++ {
        key := string(sc.Bytes())
        m[key] = struct{}{}
    }
    fmt.Println(len(m))
}

こんな感じで出力されます

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

func main() {
    var n int
    fmt.Scan(&n)

    m := make(map[string]interface{})
    sc := NewScanner(os.Stdin)

    for i := 0; i < n; i++ {
        key := string(sc.Bytes())
        m[key] = struct{}{}
    }
    fmt.Println(len(m))
}

type Scanner struct{ *bufio.Scanner }

func NewScanner(r io.Reader) *Scanner {
    s := bufio.NewScanner(r)
    s.Split(bufio.ScanWords)
    return &Scanner{Scanner: s}
}

func (s *Scanner) Bytes() []byte { s.Scan(); return s.Scanner.Bytes() }

func (s *Scanner) Text() は使わなかったため消えました。
また、lib.NewScannerNewScanner になり、Go のビルトインパッケージのみがインポートされています。

注意点

  • コンパイルが通らないコードには使えません
  • 途中で panic を起こすコードにも使えないかもしれません
  • 宣言のリネームはしません
    • コードを一つにまとめた時に名前衝突が起こらないようにしてください
  • _(アンダースコア)のみで宣言されたパッケージレベルの変数は消えます
  • interface を満たすために宣言されているが、直接は使われてないメソッドは何もしないと消えます
    • Sort で使用する Less などは注意
    • コメントに // gollect: keep methods を追加することで回避できます
    • このコメントは提出時には消えます
    • README.md

最後に

Go で競技プログラミングするのが少しでも楽になればいいなと思います。

おまけ

以外とまともなツールになったので、
デバッグ中に新しく知った Go の書き方などを適当に書きます。

一応これら全ての書き方で、正常に動作するように調整しました。

type A struct{}
func (A) m()  {}

var a A
A.m(a)

こんな書き方できるんですね。驚きましたが、Go の AST パーサーは優秀で、これも少しの書き換えで対応できました。

  • interface を埋め込める
type T struct{ I }
type I interface {
    a()
    b()
}

var t T
t.a() // this causes panic.

func (T) a() が実装されていませんが、コンパイルできます。
ただし実行すると、panic を起こすようです。

競技プログラミングで使うことはないと思いましたが、一応正常に動くようにしました。

  • 複数の変数を同時に宣言するときは改行できる
a,
b,
c := 1, 2, 3

知らなかったです。

  • 色々な変数宣言方法
const (
    zero = iota // 0
    one         // 1
    two         // 2
)

const (
    numA = 100 // 100
    numB       // 100
    numC       // 100
    numD = 200 // 200
    numE       // 200
)

const (
    a = 10     // 10
    b = iota   // 1 (0 ではない。a も残す必要あり)
    c          // 2
    d = "d"    // d
    e          // d
)

const (
    f, g = iota, iota // 0, 0
    h, i              // 1, 1
)

const (
    _ = iota // 0
    j        // 1
    _        // 2
    l        // 3
)

type A int

const (
    _ A = iota // A(0)
    m          // A(1)
    n          // A(2)
)

iota を含む宣言方法は面白いですね。

gollect では、iota が含まれる場合、()内いずれかの宣言が使われていたらすべて残す。
iota が含まれない場合、使ってない宣言は消す。
というルールで実装しました。_(アンダースコア)の宣言も iota があれば残します。


一応 The Go Programming Language Specification を読んで確認したので、だいたいカバーできていると思うのですが、なにか不具合等見つけたら issue をいただければと思います。

murosan
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした