🐙
GitHub Copilotと仲良くなろう!

myuon

myuon

2022年10月7日
AI

GitHub Copilotを使いこなすためのTIPS

# GitHub Copilot と仲良くなろう

https://github.com/features/copilot

Copilot を便利な相棒として使いこなすために、TIPS をまとめました。

免責 以下の情報は 2022 年 10 月 7 日時点のものですし、挙動については保証されているようなものではないのでこれが絶対ではありません。あくまでこの記事は、Copilot に対して理解を深めること、直観を身につける手助けをすることを目的とするものです。

# Copilot とは

公式サイトにもある通り、Copilot は OpenAI Codex を使ったサービスです。OpenAI Codexの公式には、以下のようなことが書かれています。

OpenAI Codex is a descendant of GPT-3; its training data contains both natural language and billions of lines of source code from publicly available sources, including code in public GitHub repositories

OpenAI Codex は GPT-3 から派生したもので、自然言語と GitHub の公開リポジトリにあるソースコードなどから学習しています。

OpenAI Codex is most capable in Python, but it is also proficient in over a dozen languages including JavaScript, Go, Perl, PHP, Ruby, Swift and TypeScript, and even Shell

Codex は Python が得意ですが(これは個人的に意外でした)、他の言語にも対応しています。言語についてはそもそも GPT-3 の汎用言語モデルの部分が強く、そのため基本的にどのような言語でも一定の水準では動き、その上の部分は学習データによって差が出るというところだと思われます。

It has a memory of 14KB for Python code, compared to GPT-3 which has only 4KB—so it can take into account over 3x as much contextual information while performing any task.

GPT-3 よりは文脈情報がたくさん使えるということのようです。確かに、体感ですが OpenAI の他の汎用言語モデルと比べても長い文脈から情報を拾ってコードを生成してくれるような印象があります。

# 実際に使ってみる

さて、いくつかの例を通してできることやできないことをみてみます。

# 新規ファイルでのコード生成

新規ファイルで、

// FizzBuzz

とだけ書いても、

// FizzBuzz

// Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”.

// Bonus: Make a second pass to print “Fizz” for numbers which are multiples of only three, print “Buzz” for numbers which are multiples of only five, and print “FizzBuzz” for numbers which are multiples of both.

// 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz

// ...

// 98 Fizz 100 Buzz

(同じ記述が2回続く)

のようなほとんど意味のない(?)文章が生成されます。これは文脈がないことによるものです。

# 文脈を与えよう

ファイルの冒頭に、

func fizzbuzz(n: number): number[] {

くらいの分量があれば、TypeScript であることと FizzBuzz を書かせようとしていることくらいは推定できるようで、

func fizzbuzz(n: number): number[] {
    var result: number[] = [];
    for (var i = 1; i <= n; i++) {
        if (i % 3 == 0 && i % 5 == 0) {
            result.push("FizzBuzz");
        } else if (i % 3 == 0) {
            result.push("Fizz");
        } else if (i % 5 == 0) {
            result.push("Buzz");
        } else {
            result.push(i);
        }
    }
    return result;
}

をサジェストしてくれるようです。(実際はこの下にさらに C, Python, Ruby, Java による実装が生成されていました)

# 応用的な生成

fizzbuzz は 3 の倍数の時 Fizz、5 の倍数の時 Buzz という文字列を出力するプログラムになっています。fizzbuzz という名前だけから上記の実装が引き出せるのはおそらく学習データのおかげと推測できます。

さて、その逆の 3 の倍数の時 Buzz、5 の倍数の時 Fizz という文字列を出力するプログラムを生成させるにはどのように指示を与えるのが良いでしょうか。 丁寧に指示を文章などで伝えて生成させるという手もありますが、先ほどの fizzbuzz をベースにするとかなり楽に生成できます。

func fizzbuzz(n: number): number[] {
    var result: number[] = [];
    for (var i = 1; i <= n; i++) {
        if (i % 3 == 0 && i % 5 == 0) {
            result.push("FizzBuzz");
        } else if (i % 3 == 0) {
            result.push("Fizz");
        } else if (i % 5 == 0) {
            result.push("Buzz");
        } else {
            result.push(i);
        }
    }
    return result;
}

// mod3 => Buzz
// mod5 => Fizz
func buzzfizz

を入力として与えると、

// mod3 => Buzz
// mod5 => Fizz
func buzzfizz(n: number): number[] {
    var result: number[] = [];
    for (var i = 1; i <= n; i++) {
        if (i % 3 == 0 && i % 5 == 0) {
            result.push("BuzzFizz");
        } else if (i % 3 == 0) {
            result.push("Buzz");
        } else if (i % 5 == 0) {
            result.push("Fizz");
        } else {
            result.push(i);
        }
    }
    return result;
}

が生成されます(fizzbuzz 部分は省略)。確かにこれは上記の fizzbuzz プログラムの逆を与えるものになっています。

これは mod3 と mod5 の時の処理をコメントで与えていることと、buzzfizz という関数名がちょうど fizzbuzz の逆になっていることなどが明確に Copilot に伝わったことによるものです。例えば buzzfizz という関数名を通常通り fizzbuzz とすることで、

// mod3 => Buzz
// mod5 => Fizz
func fizzbuzz(n: number): number[] {
    var result: number[] = [];
    for (var i = 1; i <= n; i++) {
        if (i % 3 == 0) {
            if (i % 5 == 0) {
                result.push("FizzBuzz");
            } else {
                result.push("Buzz");
            }
        } else if (i % 5 == 0) {
            result.push("Fizz");
        } else {
            result.push(i);
        }
    }
    return result;
}

という確かにこちらも正しいプログラムが生成されましたが、しかし正しい fizzbuzz の関数と見比べてみるとあまり一貫性のない書き方となっています。

あるいは、お手本である正しい fizzbuzz の実装例がなく、単に、

// mod3 => Buzz
// mod5 => Fizz
func buzzfizz(n: number): number[] {

のような入力しか与えられていなければ、

// mod3 => Buzz
// mod5 => Fizz
func buzzfizz(n: number): number[] {
    let result: number[] = [];
    for (let i = 1; i <= n; i++) {
        if (i % 3 == 0 && i % 5 == 0) {
            result.push(i);
        }
    }
    return result;
}

のように、fizzbuzz ですらない何かになってしまっています。

以上をまとめると、「fizzbuzz の逆で 3 の倍数で Buzz、5 の倍数で Fizz となるプログラム」という問いに対しては、「正しい fizzbuzz」「3 の倍数で Buzz、5 の倍数で Fizz という文字列」「buzzfizz という特徴的な命名」の 3 つが揃っていると Copilot に正しく指示が伝わるようです。

# 命名による指示

前回までの実験から、Copilot は「お手本にするべき対象」と「実際に書いて欲しい内容についての指示」を与えるのが大事ということがなんとなくわかりました。このような指示は実際には命名によって与えられることが多いです。

例として、次のようなファイルを考えます。

func getUser() {
  return fetch<User>("/users/:id")
}

func createUser() {
  input := NewUser();
  return fetch("POST", "/users", input)
}

ここでは getUser 及び createUser がお手本として与えられています。想定としては API を叩いているという前提で、getUser はユーザーデータを返し、createUser は新規にユーザーを作成するような例になっています。

さて、この時 1. 「ユーザーを複数返してもらう API」 2. 「ユーザーを削除する API」をそれぞれ Copilot に書かせることはできるでしょうか。

そうです、このようなケースでは命名による指示が有効です。

func deleteUser() {
  return fetch("DELETE", "/users/:id")
}

func getUsers() {
  return fetch<[User]>("/users")
}

いずれも func deleteUserfunc getUsers の関数名だけから実装を生成しています。これはお手本の関数を記述する順番にもよらないため、命名からお手本としてどの関数を指示させるかということはかなり正確に名前だけから推定できるということがわかります。

(また、上記のコードは特になんの言語であるとも指定せず適当な文法により記述を行なっていますが、そこから配列型の推論を行なっているのはなかなか興味深いところです。)

この例はあまりにも簡単な例ですが、実際には数百行もあるような前提の実装から適切な箇所をお手本として参照することができます。

# まとめ

以上のことより、Copilot を正しく使うためには以下のことを理解する必要があります。

  • お手本を用意すること
  • 指示を正確に行うこと

さらに、Copilot をさらに使いこなすために(ある種過激な)プラクティスを書いておきます。これによりさらに AI に支配された人類としての生活を送ることができるようになります。

  • なるべく白紙のファイルに対して記述を行わない。必要があれば既存のコードが書かれているファイルの下で実装を生成し、それを別ファイルにコピペする。あるいは、お手本となる箇所を他のファイルからコピペしてきて不要になったら消す。
  • 命名規則及びコーディング規約を徹底する。Copilot がより多くの実装を正しく推論できる規則が「正しい」規則となる。
  • 実装したい内容をまずコメントで記述し、Copilot に生成させた後それを消す。特に、Copilot に生成させる際に障壁となりそうな例外的な処理などを先回りしてコメントなどで補っておく。
  • 実装したい内容が推定できそうな(冗長であっても曖昧さのない)関数名を記述して内容を生成させ、後でより適切な名前に変更する。

# 追加の問題

ユーザーというリソースに対していくつかの操作を行う API が与えられています。

fn createUser(input) {
    let err = UserRepository::create(input);
    return err;
}

fn deleteUser(id) {
    let err = UserRepository::delete(input);
    return err;
}

fn getUser(id) {
    let user = UserRepository::get_by_id(id);
    if user.is_none() {
        return Error404;
    }
    return user;
}

fn getUsersByName(name) {
    let users = UserRepository::get_by_name(name);
    return users;
}

この時、「name を入力とし、それで検索してヒットしたユーザーを削除し全員作成し直す関数」を Copilot に生成させることはできるでしょうか?考えてみてください。

回答例 1:

fn getUsersByNameAndDeleteAndRecreateThem

回答例 2:

// name を入力とし、それで検索してヒットしたユーザーを削除し全員作成し直す関数