ChatGPTとCopilotに要件だけ伝えて自動コーディングしてもらう(AIペアプロ)

***※この記事は個人的な実験の記事です※***
***※実験内容は随時記事を更新していきます※***

24時間いつでも手助けをしてくれるGitHub Copilotさん。
24時間文句も言わずにいつでも親切に回答してくれるChatGPTさん。

そんな両者に要件だけ伝えて自動コーディングしてもらおう(AIペアプロ)、という実験記事です。

# Copilot自動コーディング動画
以下の動画は、最初に要件だけをコメント入力して、それ以降はCopilotの提案を受け入れているだけになります。

コメントの入力と提案の受け入れ以外は、**全くタイピングをしていません。**

コメントの内容は `Next.jsでボタンをクリックしたら画像を選択してブラウザに表示する` です。

![Untitled2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399610/f7adfa65-08db-9031-9f73-83c67d89367a.gif)

# 完成品
要件のコメントと受け入れだけをしたコードを実際に動かしてみました。

![11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399610/6f6279a6-d2dc-3758-689e-526d9e97ee98.gif)


自動コーディングしてもらったコードは以下の通りです。

```tsx
// Next.jsでボタンをクリックしたら画像を選択してブラウザに表示する

import { NextPage } from 'next';
import React, { useState } from 'react';

const IndexPage: NextPage = () => {
  const [image, setImage] = useState<string | undefined>(undefined);

  const onChangeImage = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = () => {
      setImage(reader.result as string);
    };
    reader.readAsDataURL(file);
  };

  return (
    <div>
      <input type="file" onChange={onChangeImage} />
      {image && <img src={image} />}
    </div>
  );
};

export default IndexPage;
```

ちなみに、Copilotは提案中に ctrl + Enter を押すと、
提案の一覧を表示してくれます。

![Screenshot 2023-04-20 at 3.20.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399610/3ff11a47-3108-17c9-cba3-16d5ba7218f7.png)

# ChatGPTの用途
続いては、ChatGPTの用途についてです。
ChatGPTと併用することで理解を深めたり、コードを洗練させていくことができます。

[こちらのサイトでは](https://simonwillison.net/2022/Dec/5/rust-chatgpt-copilot/)以下のように書かれています。

>ChatGPTは教授、教育助手、学習パートナーとして使っています。Rustの使い方について質問すると、回答が返ってきます（たいていはコードサンプルもあります）。また、エラーメッセージの理解を助けるために使っていますが、これは非常に効果的です。

## エラーの解消方法を教えてもらう
Copilotに自動生成してもらったコードも一発で動かない時もあります。

そんな時は、エラー内容をそのままChatGPTに貼り付けて解消方法を教えてもらいます。

### 質問例
```text:質問内容
next.jsで以下のエラーが出ました。
解消方法を教えて下さい。

Error: The default export is not a React Component in page: "/"

This error happened while generating the page. Any console logs will be displayed in the terminal window.
```

### 回答内容
***-----ここから回答内容***

このエラーは、Next.jsが`/`ページに関連するデフォルトエクスポートがReactコンポーネントではないことを示しています。つまり、エクスポートされるデフォルトのモジュールがReactコンポーネントでなければならないのに、そうでないものがエクスポートされているということです。

解決方法は、`pages`ディレクトリの`index.js`または`index.tsx`ファイルを開いて、適切なReactコンポーネントをデフォルトエクスポートするように修正することです。

例えば、以下のような形式でファイルを修正してください：

```tsx
import React from 'react';

const HomePage = () => {
  return (
    <div>
      <h1>Welcome to the home page!</h1>
    </div>
  );
}

export default HomePage;
```

この例では、`HomePage`という名前のReactコンポーネントを作成し、デフォルトエクスポートとしています。これにより、エラーが解決されるはずです。

ファイルを修正したら、開発サーバーを再起動して、エラーが解消されたことを確認してください。

***-----ここまで回答内容***

## ChatGPTにリファクタ/レビューしてもらう
続いて、Copilotに自動生成してもらったコードをChatGPTに貼り付けて、
リファクタ/レビューしてもらいます。

### 質問例
```text:質問内容
あなたは優秀なプログラマーです。
以下の制約に従いレビューしてください。

# 要件
- Next.js v13 / TypeScript v5 で記述している
- Next.jsでボタンをクリックしたら画像を選択してブラウザに表示する

# 制約
- モダンでシンプルな記述にすること
- KISSの原則を守ること
- 複雑な処理を避けるために高階関数は使用できることとする
- リーダブルなコードにすること
- メソッドは動詞始まりにすること
- メソッドの責務は小さく分割すること
- 単一責任の原則を守ること
- 命名は言語に則り適切に行うこと
- 条件分岐は早期returnなどを使ってネストを浅くすること
- 多重ループ処理を極力避けるロジックにすること
- オニオンアーキテクチャに則り各レイヤーの責務を適切にすること
- 知識はdomainレイヤーへ集約すること
- テスタビリティであること
- マジックナンバーは使わないようにすること
- letなどのミュータブルな変数は使用しないこと
  - ミュータブルな変数を避けるために即時関数は使用可能とする

# コード
import { NextPage } from 'next';
import React, { useState } from 'react';

const IndexPage: NextPage = () => {
  const [image, setImage] = useState<string | undefined>(undefined);

  const onChangeImage = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = () => {
      setImage(reader.result as string);
    };
    reader.readAsDataURL(file);
  };

  return (
    <div>
      <input type="file" onChange={onChangeImage} />
      {image && <img src={image} />}
    </div>
  );
};

export default IndexPage;
```

### 回答内容
***-----ここから回答内容***

このコードは、次の要件に従って、モダンでシンプルで読みやすい、機能的にテスタブルで、単一責任の原則に沿った実装です。

```tsx
import { NextPage } from 'next';
import React, { useState } from 'react';

type ImageProps = {
  dataUrl: string;
};

type InputProps = {
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
};

const Image: React.FC<ImageProps> = ({ dataUrl }) => (
  <img src={dataUrl} alt="selected file" />
);

const Input: React.FC<InputProps> = ({ onChange }) => (
  <input type="file" onChange={onChange} />
);

const handleFileInputChange = (
  callback: (dataUrl: string) => void
): ((event: React.ChangeEvent<HTMLInputElement>) => void) => (event) => {
  const file = event.target.files?.[0];
  if (!file) return;

  const reader = new FileReader();
  reader.onload = () => {
    callback(reader.result as string);
  };
  reader.readAsDataURL(file);
};

const HomePage: NextPage = () => {
  const [selectedFile, setSelectedFile] = useState<string>();

  return (
    <div>
      <Input onChange={handleFileInputChange(setSelectedFile)} />
      {selectedFile && <Image dataUrl={selectedFile} />}
    </div>
  );
};

export default HomePage;
```

主な変更点は次のとおりです。

1. `IndexPage` は `HomePage` に名前を変更しています。
2. `onChangeImage` メソッドは、高階関数 `handleFileInputChange` に置き換えられ、コールバック関数 `callback` を受け取ります。
3. `Image` と `Input` は、再利用可能なコンポーネントとして切り出され、プロパティを受け取るようになりました。
4. 関数コンポーネントでは、関数の引数に型を定義することができます。このため、`onChangeImage` の型を定義する代わりに、`Input` に渡す `onChange` の型を定義しました。
5. `useState` で使用される状態の型が `string | undefined` から単純な `string` に変更されました。
6. マジックナンバーが削除され、可読性が向上しました。
7. `let` を使用しないように修正され、不変性を保つために即時関数が使用されています。

これらの変更により、コードの可読性と保守性が向上し、同様の機能を持つ他のコンポーネントにも適用できる再利用可能なコンポーネントが得られました。また、単一責任の原則とオニオンアーキテクチャの概念に従い、コンポーネントと関数を小さく分割し、各コンポーネントや関数の責務を明確にすることができました。また、handleFileInputChange 関数のような高階関数を使用することで、コードの可読性と保守性を高めながら、複雑な処理を避けることができました。

さらに、条件分岐は早期returnを使用することで、ネストを浅くし、可読性を向上させました。また、多重ループ処理を避けるため、関数コンポーネントを使用しているため、よりシンプルで効率的なコードが実現できました。

最後に、言語に則った適切な命名規則が使用されているため、コードの可読性が向上しました。このように、KISS原則と単一責任の原則を守りながら、オニオンアーキテクチャに基づいて各レイヤーの責務を適切に分割することで、可読性が高く、保守性が高いコードを実現することができます。

***-----ここまで回答内容***

## 不明箇所があれば解説してもらう
自動コーディングしてもらったけど、ここの箇所を詳しく知りたい、となった場合に、ChatGPTに聞いてみます。

[GitHub Copilot Labs](https://qiita.com/tak001/items/9ef12a924469b0370c01)にも説明してくれる機能がありますが、どちらでも好きな方で良いと思います。

今回はChatGPTに聞いてみます。

### 質問例
```text:質問内容
以下のコードを解説して下さい。

const onChangeImage = (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];
  if (!file) return;

  const reader = new FileReader();
  reader.onload = () => {
    setImage(reader.result as string);
  };
  reader.readAsDataURL(file);
};
```

### 回答内容
***-----ここから回答内容***

このコードは、ファイルの選択イベントが発生した際に、選択されたファイルを読み込み、その内容を画像として表示するためのものです。

ファイル選択イベントが発生すると、まず、選択されたファイルを `const file` に格納します。その後、 `if (!file) return;` を使用して、ファイルが選択されていない場合は、何もしないで処理を終了します。

次に、 `const reader = new FileReader();` を使用して、 `FileReader` オブジェクトを作成します。このオブジェクトは、ブラウザでファイルを読み込むために使用されます。

次に、 `reader.onload` メソッドを定義します。このメソッドは、ファイルが正常に読み込まれたときに呼び出されます。このメソッドでは、 `reader.result` を使用して、読み込んだファイルの内容を文字列として取得し、 `setImage` 関数を使用して、画像として表示します。

最後に、 `reader.readAsDataURL(file);` を使用して、選択されたファイルを読み込みます。このメソッドは、ファイルを非同期で読み込み、ファイルの読み込みが完了したら、`reader.onload` メソッドが呼び出されます。

このように、 `onChangeImage` 関数は、非常にシンプルで、ファイルの読み込みと表示に必要な最小限の処理しか行いません。また、早期returnを使用することで、可読性が向上しています。

***-----ここまで回答内容***

## テストコードを書いてもらう
続いては、ChatGTPにテストコードも書いてもらいます。

こちらも、コメントを書いてCopilotに自動コーディングしてもらうこともできます。

今回はChatGPTに聞いてみます。

### コード例
仮に以下のコードがあったとします。
(このコードもコメントだけ書いて、GitHub Copilotに自動コーディングしてもらいました)

```typescript
// number型の引数を受け取って、小数点以下を切り捨てた整数を2倍した値を返す関数
export const double = (num: number): number => {
  return Math.floor(num) * 2;
};
```

### 質問例
```text:質問内容
以下TypeScriptで書かれたコードのテストコードをjestで記述して下さい。


// number型の引数を受け取って、小数点以下を切り捨てた整数を2倍にした値を返す関数
export const double = (num: number): number => {
  return Math.floor(num) * 2;
};
```

### 回答内容
***-----ここから回答内容***

以下は、Jestを使用したTypeScriptコードの単体テストの例です。

```typescript
import { double } from './double';

describe('double', () => {
  it('should return double of the integer obtained by flooring the input number', () => {
    expect(double(3.7)).toEqual(6);
    expect(double(4.2)).toEqual(8);
    expect(double(-3.7)).toEqual(-6);
    expect(double(-4.2)).toEqual(-8);
  });

  it('should return NaN for non-numeric input', () => {
    expect(double('')).toEqual(NaN);
    expect(double(null)).toEqual(NaN);
    expect(double(undefined)).toEqual(NaN);
    expect(double({})).toEqual(NaN);
  });
});
```

これは、double関数が、与えられた数値の小数点以下を切り捨てた整数を2倍にして返すことを確認するテストです。最初のテストは、いくつかの入力値で正しい出力を返すことを確認します。次のテストは、非数値入力に対してNaNを返すことを確認します。これらのテストを実行するには、Jestをインストールして、上記のテストコードを保存し、 `jest` コマンドを実行してください。

***-----ここまで回答内容***

## Copilotではなく、ChatGPTに要件を伝えて自動コーディングしてもらう
今までは、Copilotで自動コーディング -> ChatGTPで深掘りしていく、という流れでしたが、ChatGPTだけでも要件を伝えれば回答してくれます。

### 質問例
```text:質問内容
number型の引数を受け取って、小数点以下を切り捨てた整数を
2倍にした値を返す関数をTypeScriptで記述して下さい。
```

### 回答内容
***-----ここから回答内容***

以下は、指定された要件を満たすTypeScript関数の例です。

```typescript
function truncateDouble(input: number): number {
  return Math.floor(input) * 2;
}
```

この関数は、与えられた引数 `input` を `Math.floor()` 関数を使って小数点以下を切り捨てた後、2倍して整数値を返します。 `Math.floor()` 関数は、与えられた数値以下の最大の整数値を返します。

例えば、以下のように使用できます。

```typescript
console.log(truncateDouble(3.14159)); // Output: 6
console.log(truncateDouble(4.5)); // Output: 8
console.log(truncateDouble(10)); // Output: 20
```

注意：この関数は、引数が `NaN` または `Infinity` の場合には正しく動作しません。必要に応じて、引数のバリデーションを実装してください。

***-----ここまで回答内容***

# まとめ
- [Copilot](https://qiita.com/tak001/items/9ef12a924469b0370c01)にコメントした要件をコーディングしてもらう
  - 主にタイピングアシスタントとしてやりたいことをコメントで書いて、Copilotがそのコメントにマッチするコードを提案してくれたものを参考にする
  - ChatGPTに要件を伝えて自動コーディングしてもらっても良い
- 提案コードでは一発で動かない時がある
  - もしも原因がわからなければChatGPTに質問し続ける
  - もしくは、Copilotへコメントの内容を変えてみる
- 提案コードでリファクタをしたければ、GitHub Copilot Labsか、ChatGPTにリファクタリングしてもらう
- 実務的な大きなコードでは未知数
- ***最終的な良し悪しは、現場と即しているかや自分自身の判断が必要***


# 筆者の関連記事
https://qiita.com/tak001/items/7605f0be7b424118e5a5

https://qiita.com/tak001/items/748f648900dc88419434

https://qiita.com/tak001/items/9ef12a924469b0370c01

https://qiita.com/tak001/items/c6bca65d67843c24478f

https://qiita.com/tak001/items/73d04d57bdcc57e81dd3