はじめに
この記事は、株式会社ゆめみサーバサイドコーディング試験模試(2023年度版)の回答例とフィードバック例に対応する記事となっています。
上記記事中で出題された模擬試験をChatGPT(GPT-3.5)を使って回答させたときの解説記事です。
コードレビューの対象とするため、この記事ではあえて、プログラムとしてリファクタリングを完了させていませんのでご了承下さい。
この記事では、指示したプロンプトとその解答の流れをPDCAのサイクルで解説していきます。
ChatGPTに回答させる模擬試験
こちらの、サーバサイドコーディング試験模試(2023年度版)の試験内容が対象となります。
PDCAサイクル1
Plan
とにかく仕様書をそのままChatGPTに渡してみる
Do
プロンプト
あなたは、あるe-sports大会で集められたゲームの`エントリーファイル`と`プレイログファイル`をもとに、ランキング上位10位までを算出することになりました。
このランキングを算出するphpのCLIプログラムの開発をしてください。
~以下、仕様を記載~
回答
An error occurd. ChatGPT 400: yourequested 8210 tokens(6162 in your prompt; 2048 for completion)
Check
Token使用量オーバーのため実行不能
Action
プロンプトとして仕様をそのまま全部渡すとGPT-3で利用可能なTokenの量を大きく超えることがわかったので、仕様を削る必要がある
PDCAサイクル2
Plan
Token使用量を減らすため、以下の仕様を削って簡潔にしたプロンプトを渡す。
- CSVファイルのフォーマットに関する記載を削る
- 以上系の挙動に関する定義を消す
- 各種の冗長な表現やプログラム実行例などを消す
Do
プロンプト
ゲームの`エントリーファイル`と`プレイログファイル`をもとに、ランキング上位10位までを算出するphpのCLIプログラムの開発をしてください。
このプログラムはエントリーファイルのファイル名とプレイログのファイル名を引数として受け取ります
~以下、簡潔化した仕様を記載~
回答
以下のPHPコードが生成された
<?php
// Get the entry file and play log file names from command line arguments
$entryFileName = $argv[1];
$playLogFileName = $argv[2];
// Read the entry file into an array
$entryData = array();
$entryFile = fopen($entryFileName, "r");
while (($data = fgetcsv($entryFile)) !== FALSE) {
$entryData[$data[0]] = $data[1];
}
fclose($entryFile);
// Read the play log file into an array
$playLogData = array();
$playLogFile = fopen($playLogFileName, "r");
while (($data = fgetcsv($playLogFile)) !== FALSE) {
if (array_key_exists($data[1], $entryData)) {
$playLogData[$data[1]] = $data[2];
}
}
fclose($playLogFile);
// Sort the play log data by score in descending order
arsort($playLogData);
// Output the ranking data
echo "rank,player_id,handle_name,score\n";
$rank = 1;
foreach ($playLogData as $playerId => $score) {
echo "$rank,$playerId," . $entryData[$playerId] . ",$score\n";
$rank++;
if ($rank > 10) {
break;
}
}
?>
Check
全体的にそれらしいコードが得られたので、
予め準備したテンプレートと照らし合わせたチェックを実施
できていたこと
観点 | 項目 | 評価 | ChatGPT(GPT-3)メモ |
---|---|---|---|
全体的に可読性が高いコードか | 適切な量のコメントが残されているか | OK | 必要十分なコメント量にみえる |
問題の分解について | 入力→処理→出力のブロック化ができているか | OK | 処理の流れは概ねそうなっている |
できていなかったこと
観点 | 項目 | 評価 | ChatGPT(GPT-3)メモ |
---|---|---|---|
テストの通過率 | 正常系の公開テストの通過率 | NG | そもそも仕様を落としたものでコードを記述させた |
異常系やValidationエラーの扱い | 公開テスト(引数の数チェック) | NG | そもそも仕様を落としたものでコードを記述させた |
異常系やValidationエラーの扱い | 公開テスト(ファイル存在チェック) | NG | そもそも仕様を落としたものでコードを記述させた |
全体的に可読性が高いコードか | main()関数が簡潔で見通しが良いか | NG | main()関数が存在しない |
全体的に可読性が高いコードか | 関数名や変数名が適切か | NG | 関数を利用していない |
全体的に可読性が高いコードか | 適切な変数を利用できているか | 一部NG | ループ中で$data[1]などを直接参照している |
問題の分解について | 入力→処理→出力がそれぞれ関数化できているか | NG | 関数が存在しない |
問題の分解について | 関数の責務として、複数の責務が混ざっていないか | NG | |
型の利用について | 型を意識したコードか | NG | |
定数や設定値の扱いが適切か | 仕様書中の設定値を定数的に扱うことができているか | NG | |
定数や設定値の扱いが適切か | 上記の数字は仕様書中にあらわれているか | NG |
その他、仕様として満たしていないこと
- [バグ] scoreが最高得点になっていない
- [バグ] 最高得点が同点のときに、同じrankが割り当てられていない
- [バグ] 対応するentryが存在しないときに未参照エラーとなる
- [仕様から落とした] 異常系が定義されていない
- [仕様から落とした] Validatonが入っていない
Action
この後、できていなかった部分を一つずつ潰していきます。
PDCAサイクル3
Plan
このままではコーディング試験を通過させることはできませんので、最初はそのまま、仕様として落としていた部分を追加実装させていくことも考えたのですが、このコードをそのままリファクタリングさせていくことは、修正部分の指定などが難しく困難でした。
結論から言うと次に各処理を関数化させる必要がありました。
Do
プロンプト
以下のプログラムを処理のブロックごとに関数化して、メインのコードはmain()関数の中に入れてください。
main()関数はできるだけ短い行数で書いてください。
~プログラム部分は省略~
応答
<?php
function getEntryData($entryFileName) {
$entryData = array();
$entryFile = fopen($entryFileName, "r");
while (($data = fgetcsv($entryFile)) !== FALSE) {
$entryData[$data[0]] = $data[1];
}
fclose($entryFile);
return $entryData;
}
function getPlayLogData($playLogFileName, $entryData) {
$playLogData = array();
$playLogFile = fopen($playLogFileName, "r");
while (($data = fgetcsv($playLogFile)) !== FALSE) {
if (array_key_exists($data[1], $entryData)) {
$playLogData[$data[1]] = $data[2];
}
}
fclose($playLogFile);
return $playLogData;
}
function sortPlayLogData($playLogData) {
arsort($playLogData);
return $playLogData;
}
function outputRankingData($playLogData, $entryData) {
echo "rank,player_id,handle_name,score\n";
$rank = 1;
foreach ($playLogData as $playerId => $score) {
echo "$rank,$playerId," . $entryData[$playerId] . ",$score\n";
$rank++;
if ($rank > 10) {
break;
}
}
}
function main() {
// Get the entry file and play log file names from command line arguments
$entryFileName = $argv[1];
$playLogFileName = $argv[2];
// Read the entry file into an array
$entryData = getEntryData($entryFileName);
// Read the play log file into an array
$playLogData = getPlayLogData($playLogFileName, $entryData);
// Sort the play log data by score in descending order
$playLogData = sortPlayLogData($playLogData);
// Output the ranking data
outputRankingData($playLogData, $entryData);
}
main();
?>
Check
概ねそれらしいコードに変更されました。
使用されている関数名も非常にわかりやすくて良いです。
できるようになったこと
観点 | 項目 | 評価 | ChatGPT(GPT-3)メモ |
---|---|---|---|
全体的に可読性が高いコードか | main()関数が簡潔で見通しが良いか | OK | 以下のプログラムを処理のブロックごとに関数化して、メインのコードはmain()関数の中に入れてください。main()関数はできるだけ短い行数で書いてください。 |
全体的に可読性が高いコードか | 関数名や変数名が適切か | OK | 直しの必要はありませんでした。 |
Action
次は、バグを潰していきます。
PDCAサイクル4
Plan
まずは以下のバグを潰します。
- [バグ] scoreが最高得点になっていない
Do
プロンプト
以下の、getPlayLogData関数は、$data[1]の値がユニークであるという前提で書かれていますが、実際にはユニークではありません。$data[1]の値ごとの最高得点を取得するように変更してください。
function getPlayLogData($playLogFileName, $entryData) {
$playLogData = array();
$playLogFile = fopen($playLogFileName, "r");
while (($data = fgetcsv($playLogFile)) !== FALSE) {
if (array_key_exists($data[1], $entryData)) {
$playLogData[$data[1]] = $data[2];
}
}
fclose($playLogFile);
return $playLogData;
}
応答
function getPlayLogData($playLogFileName, $entryData) {
$playLogData = array();
$playLogFile = fopen($playLogFileName, "r");
while (($data = fgetcsv($playLogFile)) !== FALSE) {
if (array_key_exists($data[1], $entryData)) {
if (!isset($playLogData[$data[1]]) || $playLogData[$data[1]] < $data[2]) {
$playLogData[$data[1]] = $data[2];
}
}
}
fclose($playLogFile);
return $playLogData;
}
Check
一発で正しい修正がなされました。
Action
引き続き、バグ修正を進めます。
PDCAサイクル5~n
この後も、以下のプロンプトを与えながら、自動修正されるものはChatGPTに修正を任せ、自動修正できなかったものや失敗した箇所は手動でコードを修正していき、最終的に準備したテストケースを全て通過させるまで修正を繰り返しました。
目的 | プロンプト | 結果 | メモ |
---|---|---|---|
バグフィックス | 以下のoutputRankingData関数は、scoreが同点だったときも表示されるrankが変更されてしまいます。 scoreが同点だったときに表示されるrankは前のrankと同じになるように修正して下さい。 |
コードは出力されましたが、今度は同点が複数あったときに次のrankがスキップされなくなりました。 | この修正は最終的に手動修正が必要でした。 |
関数リファクタリング | outputRankingData()の責務が、データの抽出と出力が混ざっています。データの抽出の責務は、extractRankingData()に切り出してください。 | 概ね正しく修正された。 | 一部、手動で修正したコードの部分が先祖返りをしていました。注意が必要です。 |
バグフィックス | 以下のコード中の、extractRankingData()関数中の、entryDataに対応するplayerIdが存在しないときには、抽出対象から外すように変更してください。 | 概ね正しく修正された。 | 一部、手動で修正したコードの部分が先祖返りをしていました。注意が必要です。 |
異常系の仕様追加 | 以下のコードは異常系の考慮が足らないです。 ファイルの存在確認や、引数の数のチェックなどを行い、異常時にはメッセージを標準エラーに出力し、終了コードを0以外でプログラムを終了するように変更してください。 |
正しく修正された。 | |
型の導入 | 以下のコードに、タイプヒンティングを導入してください。 | タイプヒンティングが追加された。 | |
バグフィックス | 同順位のときのソートがされていない。 | ChatGPTを使わずに手動修正した。 | |
バグフィックス | scoreの項目が出力されていなかった。 | ChatGPTを使わずに手動修正した。 | |
バグフィックス | 出力内容が異なっていた。 | ChatGPTを使わずに手動修正した。 |
最終的に得られたコードがこちら
コードレビューの余地を残すために、敢えて不完全な状態で完成としていますが、最終的に得られたコードはこちらです。
<?php
function getEntryData(string $entryFileName): array {
$entryData = array();
$entryFile = fopen($entryFileName, "r");
// header行を読み飛ばす
fgetcsv($entryFile);
while (($data = fgetcsv($entryFile)) !== FALSE) {
$entryData[$data[0]] = $data[1];
}
fclose($entryFile);
return $entryData;
}
function getPlayLogData(string $playLogFileName, array $entryData): array {
$playLogData = array();
$playLogFile = fopen($playLogFileName, "r");
while (($data = fgetcsv($playLogFile)) !== FALSE) {
if (array_key_exists($data[1], $entryData)) {
if (!isset($playLogData[$data[1]]) || $playLogData[$data[1]] < $data[2]) {
$playLogData[$data[1]] = $data[2];
}
}
}
fclose($playLogFile);
return $playLogData;
}
function sortPlayLogData(array $playLogData): array {
ksort($playLogData);
arsort($playLogData);
return $playLogData;
}
function extractRankingData(array $playLogData, array $entryData): array {
$rankingData = array();
$rank = 0;
$prevScore = 0;
foreach ($playLogData as $playerId => $score) {
if (array_key_exists($playerId, $entryData)) {
$rank++;
if ($score !== $prevScore) {
$printRank = $rank;
}
if ($printRank > 10) {
break;
}
$rankingData[$rank] = array($printRank,$playerId, $entryData[$playerId], $score);
$prevScore = $score;
} else {
// If $playerId does not exist in $entryData, skip it.
continue;
}
}
return $rankingData;
}
function outputRankingData(array $rankingData): void {
echo "rank,player_id,handle_name,score\n";
foreach ($rankingData as $rank => $data) {
echo "$data[0],$data[1],$data[2],$data[3]\n";
}
}
function main(string $entryFileName, string $playLogFileName): void {
// Check if the entry file and play log file exist
if (!file_exists($entryFileName) || !file_exists($playLogFileName)) {
fwrite(STDERR, "Error: File does not exist.\n");
exit(1);
}
// Read the entry file into an array
$entryData = getEntryData($entryFileName);
// Read the play log file into an array
$playLogData = getPlayLogData($playLogFileName, $entryData);
// Sort the play log data by score in descending order
$playLogData = sortPlayLogData($playLogData);
// extract ranking data
$rankingData = extractRankingData($playLogData, $entryData);
// Output the ranking data
outputRankingData($rankingData);
}
// Check if the number of command line arguments is correct
if ($argc != 3) {
fwrite(STDERR, "Error: Invalid number of arguments.\n");
exit(1);
}
// Get the entry file and play log file names from command line arguments
$entryFileName = $argv[1];
$playLogFileName = $argv[2];
main($entryFileName, $playLogFileName);
最終的なコードの評価と総評
コードに対する人間のレビュー例は、こちらに記載しています。コツこそ必要ではあるものの、ChatGPTの書いたコードに適切なフィードバックをを繰り返すことができればコーディング試験は通過してしまいそうです。
今後のコーディング試験はそちらの能力を観る流れになっていくかもしれません。
ただし、現時点で注意が必要なのは、2023年4月現在、ChatGPT(GPT-3)の書くコードは、ChatGPTが過去に学習してきた元のコード、仕様に大きく引きずられるケースがあるということです。ChatGPTの知らない仕様を日本語で指示してもうまく回答できなかったり、せっかく手動で修正したコードに対してもChatGPTが知っている別の、仕様を満たさないコードに先祖返りをしてしまうケースが何回か見受けられました。この点には重大な注意が必要です。
また、今回ChatGPTに修正依頼を指示していてわかったのは、人間に読みやすい形で構造化、リファクタリングされたコードでないと、ChatGPTに対しても適切な指示が出せないことを実感しました。『○○のコードを修正してください。』という曖昧な指示では修正してほしい部分とは別の部分を修正することがあるので、具体的に修正対象を関数名などで指示してあげることが必要です。
可読性を重視したコードというのは、今後AIによるコーディングが主流になったとしてもより重要となっていきそうです。