競技プログラミングAtCoderを快適に解くためのPHPの環境を構築する
(2020/6/4追記: AtCoderのPHPのバージョンが7.4.4に更新されたので、サンプルコードに型をつけるなどGitHubで公開しているコード同様にアップデートしました)
tl;dt
- プログラミングコンテストで時間オーバーとなり、解けそうな問題に回答ができなかった
- コードを実行して結果を確認するプロセスを効率化すれば、ロジックの考察により多くの時間を割くことができる
- そもそも競技プログラミングでは、所与の入出力を満たすロジックの記述に専念すれば良い
- ロジックをデバッグするためなら、テストツール(PHPUnit)とデバッガ(Xdebug)を手軽に使える環境があれば良い
AtCoderのコンテストは時間との戦い
AtCoderが開催している競技プログラミングのコンテストに参加しています。
コンテスト本番と過去問での練習は、アルゴリズムを使って問題を解く点では同じです。
両者の違いは、コンテストでは制限時間があることです。
通常のコンテストでは問題は6問出題されるため、制限時間内に解けるだけ問題を解かなければなりません。
さらに、回答を提出するスピードが他の人より早いと、自分のレートが高くなります。
**このため、自分の書いたロジックが正解なのか、間違っているのか、フィードバックを得るスピードが成績に直結します。**間違っているなら、間違っている箇所を特定するスピードが早ければ早いほど高レート獲得に有利です。
PHPUnitとXdebugを導入してデータの処理過程と結果を確認する
プログラミングコンテストの問題形式は、与えられた入力に対する出力が正しいことを確認するものです。
入力と出力をチェックするなら、テストを書いて実行すればいいのです。出力が正しいなら、ロジックはどのようなものでも問われません(もちろんパフォーマンスの良し悪しは問われます)。出力が想定通りではない場合は、ロジックが間違っているということです。
その場合、コードの実行過程を素早くチェックできれば、デバッグは容易になります。
**つまり、PHPUnitとXdebugを使えば正解を出すためのフィードバックループを高速で回すことができるのです。**そこで、Dockerを使ってこの環境を構築することにしました。
また、この記事を書いた後、AtCoderの問題を題材にしてTDDを解説する記事を執筆しました。
関連記事: テスト駆動開発(TDD)とは何か。コードで実践方法を解説します
PHPUnitの便利な使い方
ジェネレータ関数とデータプロバイダーで複数パターンの入出力をシンプルに記述する
PHPUnitには@dataProvider
というアノテーションがあります。
データプロバイダに指定した関数の返り値を、@dataProvider メソッド名
というアノテーションをつけたメソッドの引数として扱う機能です。
<?php
use PHPUnit\Framework\TestCase;
class SampleTest extends TestCase
{
/**
* @group 100A
* @dataProvider DataA
*/
public function testA($expected, $a, $b)
{
$result = $this->solveA($a, $b);
$this->assertSame($expected, $result);
}
/**
* @return Generator
*/
public function DataA(): Generator
{
// yield "0" => ["出力", "入力1", "入力2"];
yield "1" => [7, "at", "coder"];
yield "2" => [11, "php", "language"];
}
/**
* 提出するロジック
*/
private function solveA($a, $b): int
{
return strlen($a . $b);
}
}
このコードでは、メソッドDataA()
の返り値をテストメソッドtestA()
の引数として扱っています。
データプロバイダには結果の値を$expected
として記述しておきます。
こうすることで、データプロバイダDataA()
に入出力値を記述し、テストメソッドtestA()
にアサーションを記述し、solveA
にはロジックを記述できます。
テストにおける役割をメソッドごとに分離することが可能になります。
結果、ロジックのコードを競技プログラミングの回答として提出すればいいことになります。
本来、ロジックはアプリケーションコードとして記述するものですが、簡便のためテストクラスにプライベートメソッドして記述しています。
テストクラスの中にロジックが入っていることに違和感がある方は、/src
ディレクトリを作って/src
配下のクラスでロジック記述し、TDDで開発できます。
groupアノテーションで実行したいテストを指定する
今回作成した環境では、PHPUnitは下記のコマンドで実行できます。
$ docker run --rm -v (pwd):/home atcoder/php
PHPUnit 9.0.0 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 00:00.489, Memory: 4.00 MB
OK (2 tests, 2 assertions)
これは、DockerfileにENTRYPOINT ["vendor/bin/phpunit", "tests"]
と記述しているため、コンテナを実行するとPHPUnitを実行する仕組みになっているためです。
上記のサンプルのテストケースでは、メソッドtestA()
に@group
アノテーションを付与しています。
/**
* @group 100A
* @dataProvider DataA
*/
public function testA($expected, $a, $b){...}
このため、dockerの実行コマンドに--group=100A
を加え、testAメソッドのみを指定して以下のコマンドを実行しましょう。
$ docker run --rm -v (pwd):/home atcoder/php --group=100A
$ docker run --rm -v $(pwd)/tests:/home/tests atcoder/php --group=100A
PHPUnit 9.0.0 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 00:00.489, Memory: 4.00 MB
OK (2 tests, 2 assertions)
競技プログラミングのコンテストでは、問題ごとに回答を提出するため、@group
アノテーションを使ってテストメソッドを指定することで、自分が今解いている問題のロジックをテストすることに集中できます。
ジェネレータ関数でイテレータを実装する
データプロバイダにジェネレータ関数を利用しています。
/**
* @return Generator
*/
public function DataA(): Generator
{
// yield "0" => ["出力", "入力1", "入力2"];
yield "1" => [7, "at", "coder"];
yield "2" => [11, "php", "language"];
}
ジェネレータを利用することで、複数の入出力のパターンをシンプルに記述できます。
AtCoderでは入力・出力のサンプルとして2~3パターンが提示されるため、サンプルの数だけyield
でイテレーションのアイテムを記述しておけば、ロジックの実装に集中できます。
PhpStormのRemote Debugを設定する
Dockerfileとphp.iniの設定と、PhpStormの設定の記事を参考にしました。
AtCoderのPHP環境を構築するためのコードをGitHubで公開しています
PHP7 + PHPUnit + XDebugの環境をDockerで作成できるようにGitHubでコードを公開しています。
~~AtCoderのPHP7系のバージョンは7.0.15のみです。~~
(追記)2020年にAtCoderの言語のアップデートがあり、現在はバージョン7.4.4を使えるようになりました。
これに対応するため、Dockerのコンテナイメージとしてphp:7.4.4-alpine3.11を利用しています。
併せて、PHPUnitのバージョンを9系に更新しました。
なお、Dockerfileはローカル向けなので、GitHubで公開しているDockerfileは決して本番環境で使わないようにお願いします。
READMEを読みながら、ぜひトライしてみてくださいね。
Happy Coding 🎉