18
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

シーエー・アドバンス Advent Calendar 2022Advent Calendar 2022

Day 23

AIペアプログラマーGithub Copilotが書くコードは実際どのくらい使えるのか

Last updated at Posted at 2022-12-22

はじめに

Github CopilotはOpenAIのCodexを使ってGithub上のコードを学習させたAIが補助的にコードを補完してくれるサービスです。
使ってある程度たったので利用するか迷ってる方向けにどのくらい使えるのか所感をまとめてみようと思います。

料金

60日間の無料試使用期間があり、10\$/月もしくは 100\$/年 です。
学生、教師、主要なOSSメンテナは無料です。

対応エディタ

  • Visual Studio Code
  • Visual Studio
  • Neovim
  • JetBrains

対応言語

ざまざまな言語とフレームワークに対応する候補を提示してくれますが、特に以下言語が有用とのことです。

  • Python
  • JavaScript
  • TypeScript
  • Ruby
  • Go
  • C#
  • C++

操作方法

基本的に自動で提案が出てきますが、以下操作も可能です。

  • 受け入れるには Tab
  • 拒否 Esc
  • 次の提案 Option (⌥) or Alt + ]
  • 前の提案 Option (⌥) or Alt + [
  • 提案の一覧 Ctrl + Enter

いろいろ試してみる

いろいろな条件で生成されたコードの一例を見てみます。
毎回結果は変わるし、候補も複数出てきます。

コメントから生成

日付の差分をyyyy:mm:dd形式で返す関数 というコメントをヒントに生成されたtypescript、ruby、python、goのコードです。

typescript
// 日付の差分をyyyy:mm:dd形式で返す関数
function diffDate(date1: Date, date2: Date): string {
  const diff = date1.getTime() - date2.getTime();
  const diffDay = diff / (1000 * 60 * 60 * 24);
  const diffYear = Math.floor(diffDay / 365);
  const diffMonth = Math.floor((diffDay % 365) / 30);
  const diffDay = Math.floor(diffDay % 365 % 30);
  return `${diffYear}:${diffMonth}:${diffDay}`;
}
ruby
# 日付の差分をyyyy:mm:dd形式で返す関数
def date_diff(date1, date2)
  diff = (date2 - date1).to_i
  year = diff / 365
  month = (diff % 365) / 30
  day = (diff % 365) % 30
  return "#{year}:#{month}:#{day}"
end
python
# 日付の差分をyyyy:mm:dd形式で返す関数
def date_diff(date1, date2):
    date1 = datetime.strptime(date1, '%Y-%m-%d')
    date2 = datetime.strptime(date2, '%Y-%m-%d')
    diff = date2 - date1
    return str(diff).split()[0]
go
// 日付の差分をyyyy:mm:dd形式で返す関数
func diffDate(date1, date2 time.Time) string {
    var diff time.Duration
    if date1.After(date2) {
        diff = date1.Sub(date2)
    } else {
        diff = date2.Sub(date1)
    }
    return diff.String()
}

単純な関数ならコメントからでもある程度それらしいコードを生成することができます。
コメントを 日付の差分をyyyy年mmヶ月dd日の形式で返す関数 のように変えると指示に従って生成されるコードも変わります。
ただし、コメントという少ない情報から生成しているので、ロジックはもとより望んだ引数や戻り値になっていないことも多いです。

関数名から生成

diffDate という関数名をヒントに生成されたtypescript、ruby、python、goのコードです。

typescript
const diffDate = (date1: Date, date2: Date) => {
  const diff = Math.abs(date1.getTime() - date2.getTime());
  const diffDays = Math.ceil(diff / (1000 * 3600 * 24));
  return diffDays;
};
ruby
def date_diff(start_date, end_date)
  start_date = Date.parse(start_date)
  end_date = Date.parse(end_date)
  return (end_date - start_date).to_i
end
python
def date_diff(date1, date2):
    date1 = datetime.strptime(date1, "%Y-%m-%d")
    date2 = datetime.strptime(date2, "%Y-%m-%d")
    return abs((date2 - date1).days)
go
func diffDate(date string) (int, error) {
    t, err := time.Parse("2006-01-02", date)
    if err != nil {
        return 0, err
    }
    return int(time.Now().Sub(t).Hours() / 24), nil
}

関数名でもある程度生成することができます。

また import moment from 'moment' など日付処理のライブラリをインポートした状態で補完すると生成されるコードも変わりよしなに利用して作ってくれます。(周りのコードを見て合わせてくれます)
こちらもまだまだ必要な情報が不足した状態で生成しているので、ロジックはもとより望んだ引数や戻り値になっていないことも多いです。

関数名と型から生成

関数名と引数や戻り値の型を指定することでもっと具体的な指示をすることができます。
渡された開始日付と終了日付の期間中、30分毎の日付とその表示用ラベルを返すという少し複雑な関数を以下のように途中まで書いてそれ以降を生成させてみました。

interface TimeSelectValue {
  label: string // HH:mm (h時間m分後)
  value: Date
}

const build30minutesSplitTimeSelectValues = (start: Date, end: Date): TimeSelectValue[] => {

以下は生成されたコードです。

interface TimeSelectValue {
  label: string // HH:mm (h時間m分後)
  value: Date
}

const build30minutesSplitTimeSelectValues = (start: Date, end: Date): TimeSelectValue[] => {
  const startTime = start.getTime();
  const endTime = end.getTime();
  const diffMinutes = (endTime - startTime) / (1000 * 60);
  const timeSelectValues: TimeSelectValue[] = [];
  for (let i = 0; i < diffMinutes; i += 30) {
    const time = new Date(startTime + (i * 1000 * 60));
    const label = `${time.getHours()}時間${time.getMinutes()}分後`;
    const value = new Date(startTime + (i * 1000 * 60));
    timeSelectValues.push({ label, value });
  }
  return timeSelectValues;
}

実際に結果を確認してみるとこんな感じになります。

build30minutesSplitTimeSelectValue(
  new Date('2000-01-01 10:00:00'),
  new Date('2000-01-01 19:00:00'),
);
[
  { label: '10時間0分後', value: 2000-01-01T01:00:00.000Z },
  { label: '10時間30分後', value: 2000-01-01T01:30:00.000Z },
  { label: '11時間0分後', value: 2000-01-01T02:00:00.000Z },
  { label: '11時間30分後', value: 2000-01-01T02:30:00.000Z },
  { label: '12時間0分後', value: 2000-01-01T03:00:00.000Z },
  { label: '12時間30分後', value: 2000-01-01T03:30:00.000Z },
  { label: '13時間0分後', value: 2000-01-01T04:00:00.000Z },
  { label: '13時間30分後', value: 2000-01-01T04:30:00.000Z },
  { label: '14時間0分後', value: 2000-01-01T05:00:00.000Z },
  { label: '14時間30分後', value: 2000-01-01T05:30:00.000Z },
  { label: '15時間0分後', value: 2000-01-01T06:00:00.000Z },
  { label: '15時間30分後', value: 2000-01-01T06:30:00.000Z },
  { label: '16時間0分後', value: 2000-01-01T07:00:00.000Z },
  { label: '16時間30分後', value: 2000-01-01T07:30:00.000Z },
  { label: '17時間0分後', value: 2000-01-01T08:00:00.000Z },
  { label: '17時間30分後', value: 2000-01-01T08:30:00.000Z },
  { label: '18時間0分後', value: 2000-01-01T09:00:00.000Z },
  { label: '18時間30分後', value: 2000-01-01T09:30:00.000Z }
]

少なくとも(感覚的には100%)入出力の型には従ってくれます。
30分毎という指示も関数名だけで理解してくれる場合が多いです。また、コメントもつけるともう少し精度が上がります。
ただし、labelは 0分後 からスタートして欲しかったが 10時間0分後 から開始している、valueは 10時 から開始して欲しかったが 1時 から開始しているなど、込み入ったものは厳しいですが、それらしいコードは生成されます。

コード途中での行の生成

関数全体ではなく行などの小さい範囲であれば欲しいコードを精度高く補完してくれます。
以下変数定義の途中まで書いてそれ以降を生成させてみました。

const startTime
const startTime = start.getTime();

行などの小さい範囲でより具体的になってくると変数名や周りに定義されてる変数などから判断して正解率も上がってきます。
繰り返しが比較的多いテストなどでは特に相性がよく、結構重宝します。

複数ファイルに跨ったコードの生成

提案の一覧を確認できるショートカット Ctrl + Enter で複数ファイルに跨ったコードの生成も可能でした。
ドキュメントにはフレームワーク対応と書いており、コメントだけで簡単なサーバーを立てるコードを生成してみます。
以下は // Express server on port 3000 で生成したコードの例です。

import * as express from 'express';
import * as path from 'path';
import * as bodyParser from 'body-parser';
import * as cors from 'cors';
import * as logger from 'morgan';

const app = express();

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cors());

app.get('/', (req, res) => {
  res.send('Hello world!');
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

// Path: tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist",
    "target": "es5",
    "module": "commonjs",
    "sourceMap": true,
    "noImplicitAny": true,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "preserveConstEnums": true
  }
}

// Path: package.json
{
  "name": "copilot",
  "version": "1.0.0",
  "description": "",
  "main": "dist/server.js",
  "scripts": {
    "start": "tsc && node dist/server.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.18.2",
    "cors": "^2.8.4",
    "express": "^4.16.2",
    "morgan": "^1.9.0",
    "ts-node": "^5.0.1",
    "typescript": "^2.8.1"
  }
}

// Path: package-lock.json
{
  "name": "copilot",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "@types/body-parser": {
      "version": "1.17.0",
      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz",
      "integrity": "sha512-5n5r5r5k5Z5vF8DnW6lFJ6wBd6Q1U6

メインコードに加え、 package.jsontsconfig.json なども提案されます。
もちろん細かい設定はや内容確認する必要があるものの、サンプルコード的にだいたいの流れをイメージすることはできます。
同様にruby sinatora、python flask、go ginなども同様に生成することができました。

別ファイルのコードから雰囲気読み取って生成

ドキュメントの言及は探せませんでしたが、別ファイルやネームスペース内、プロジェクト全てのコードから生成するコードを判定してくれているようです。
検証のためfizzbuzzに7の倍数も出力を変える特殊ルールを加えたテストを書いてみました。

fizzbuzz.test.ts
import { fizzbuzz } from './fizzbuzz';

describe('fizzbuzz', () => {
  it('3の倍数の場合Buzzを返す', () => {
    expect(fizzbuzz(3)).toEqual('Fizz');
    expect(fizzbuzz(6)).toEqual('Fizz');
  });

  it('5の倍数の場合Buzzを返す', () => {
    expect(fizzbuzz(5)).toEqual('Buzz');
    expect(fizzbuzz(10)).toEqual('Buzz');
  });

  it('7の倍数の場合渡された数値に!をつけて文字列で返す', () => {
    expect(fizzbuzz(7)).toEqual('7!');
    expect(fizzbuzz(14)).toEqual('14!');
  });

  it('15の倍数の場合FizzBuzzを返す', () => {
    expect(fizzbuzz(15)).toEqual('FizzBuzz');
    expect(fizzbuzz(30)).toEqual('FizzBuzz');
  });

  it('それ以外の場合渡された数値を文字列で返す', () => {
    expect(fizzbuzz(1)).toEqual('1');
    expect(fizzbuzz(2)).toEqual('2');
    expect(fizzbuzz(4)).toEqual('4');
  });
});

テスト作成後、生成された関数が以下です。

fizzbuzz.ts
export const fizzbuzz = (num: number): string => {
  if (num % 15 === 0) {
    return 'FizzBuzz';
  }
  if (num % 3 === 0) {
    return 'Fizz';
  }
  if (num % 5 === 0) {
    return 'Buzz';
  }
  if (num % 7 === 0) {
    return `${num}!`;
  }
  return num.toString();
}

これは結構驚いたのですが、テストコードを見て7の倍数を考慮した出力がされている、周りのファイルやコードも見て補完してくれていることがわかります。
ただし、簡単かつうまく行った例だという事、必ずテストに通る正しいロジックで正解を出すとは限らない事には引き続き注意が必要です。

所感

実際使いながらコードを書いていて、ロジカルなコードかテストのような反復的なコードかにもよりますが、感覚的にはコードの1割くらいはCopilotに頼ってるような気がしていて、1割でもサポートしてもらえるのは十分有用に感じます。(意識せず補完しててもっと使ってるかも?)

学生は無料なのもあり、経験の少ないプログラマが生成されたコードを理解せず使うケースが多くならないかという懸念も感じてましたが、参考コードを見て学べるという側面もあるかもしれません。

ただ、簡単な例ではうまくいく場合も多いものの、実際のプロジェクトで正解が生成される事は少なく感じるので、やはり生成されたコードは基本的に疑ってかかる、部分的に使うなど、あくまでCopilot(副操縦士)として利用した方が良いです。

プラグイン自体重くなる事もなく快適に書けていて、Githubのコードから学習されてるだけあって変数や関数の名前付けなどが悪いと思う事もあまりなく、使い続けたいツールになっています。

18
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?