見出し画像

AIがテストコードを元に仕様を満たすコードを生成できるか検証してみた

ふとtwitterに呟いた以下のようなツイートにそこそこLikeがついていたので、実際のところChatGPTではどのような結果になるのかを検証してみました。

テストコードを生成する

そんなわけでネタを探していたところ、

こんな記事が目にとまったので、点字メーカープログラムを作成してもらうことにします。

テストコードのプロンプトは↓のような感じ。
「優秀な」みたいにAIをヨイショすると良い結果を出してくれるようです。

# 命令書:
あなたは優秀なプロのRubyプログラマーです。
以下の仕様を元に、RSpecによるテストコードを作成してください。

# 仕様:
- 日本語のひらがなを入力として受け取り、点字をテキストで出力する
- 点字は"o""-"の組み合わせの6文字で表す。改行コードは含まない
- 「あ」から「を」までの点字は以下の通りである。
  - あ: o-----
  - い: oo----
  - う: o--o--
  - え: oo-o--
  - お: -o-o--
  - か: o----o
  - き: oo---o
  - く: o--o-o
  - け: oo-o-o
  - こ: -o-o-o
  - さ: o---oo
  - し: oo--oo
  - す: o--ooo
  - せ: oo-ooo
  - そ: -o-ooo
  - た: o-o-o-
  - ち: ooo-o-
  - つ: o-ooo-
  - て: ooooo-
  - と: -oooo-
  - な: o-o---
  - に: ooo---
  - ぬ: o-oo--
  - ね: oooo--
  - の: -ooo--
  - は: o-o--o
  - ひ: ooo--o
  - ふ: o-oo-o
  - へ: oooo-o
  - ほ: -ooo-o
  - ま: o-o-oo
  - み: ooo-oo
  - む: o-oooo
  - め: oooooo
  - も: -ooooo
  - や: --oo--
  - ゆ: --oo-o
  - よ: --ooo-
  - ら: o---o-
  - り: oo--o-
  - る: o--oo-
  - れ: oo-oo-
  - ろ: -o-oo-
  - わ: --o---
  - ー: -o--o-
  - ん: --o-oo
  - っ: -o----
  - を: --o-o-
  
- 複数の文字を受け取った場合は改行で区切って出力する。例えば「あひる」を受け取った場合の出力結果は以下の通りである。
o-----
ooo--o
o--oo-

# 最低限テストに含める要件
- 50音全ての文字の動作
- 複数文字を受け取った場合の動作
- 基本的な異常系の確認

結果はこんな感じ。それっぽいRSpecのコードになっています。

RSpec.describe "convert_to_braille" do
  it "returns the correct Braille for a single Hiragana character" do
    expect(convert_to_braille("あ")).to eq("o-----")
    expect(convert_to_braille("い")).to eq("oo----")
    expect(convert_to_braille("う")).to eq("o--o--")
    expect(convert_to_braille("え")).to eq("oo-o--")
    expect(convert_to_braille("お")).to eq("-o-o--")
    expect(convert_to_braille("か")).to eq("o----o")
    expect(convert_to_braille("き")).to eq("oo---o")
    expect(convert_to_braille("く")).to eq("o--o-o")
    expect(convert_to_braille("け")).to eq("oo-o-o")
    expect(convert_to_braille("こ")).to eq("-o-o-o")
    expect(convert_to_braille("さ")).to eq("o---oo")
    expect(convert_to_braille("し")).to eq("oo--oo")
    expect(convert_to_braille("す")).to eq("o--ooo")
    expect(convert_to_braille("せ")).to eq("oo-ooo")
    expect(convert_to_braille("そ")).to eq("-o-ooo")
    expect(convert_to_braille("た")).to eq("o-o-o-")
    expect(convert_to_braille("ち")).to eq("ooo-o-")
    expect(convert_to_braille("つ")).to eq("o-ooo-")

# ...以下省略...

ちょっと注文をつけてみます。

テストコードについて、以下の点を修正して下さい。仕様を実装したコードの出力は不要です。

- 文字列の変換はJapaneseBrailleというクラスで実装されるようにしてください
- `JapaneseBraille.new("あ").convert`という形で変換を実行できるようにしてください
- 点字に変換するときは1文字毎に改行するため、「あいうえお」の変換結果は下記になります
o-----
oo----
o--o--
oo-o--
-o-o--

指摘内容はちゃんと反映されているようです。

require_relative './japanese_braille'

RSpec.describe JapaneseBraille do
  describe "#convert" do
    context "when given a valid input" do
      it "returns the correct Braille for a single Hiragana character" do
        expect(JapaneseBraille.new("あ").convert).to eq("o-----")
        expect(JapaneseBraille.new("い").convert).to eq("oo----")
        expect(JapaneseBraille.new("う").convert).to eq("o--o--")
        expect(JapaneseBraille.new("え").convert).to eq("oo-o--")
        expect(JapaneseBraille.new("お").convert).to eq("-o-o--")
        expect(JapaneseBraille.new("か").convert).to eq("o----o")
        expect(JapaneseBraille.new("き").convert).to eq("oo---o")
        expect(JapaneseBraille.new("く").convert).to eq("o--o-o")
        expect(JapaneseBraille.new("け").convert).to eq("oo-o-o")
        expect(JapaneseBraille.new("こ").convert).to eq("-o-o-o")

# ...以下省略...

冗長なので、もう少し注文をつけてみます。

テストコードをもっと簡潔にしてください。

すると簡潔なコードにまとめてくれました。

require_relative './japanese_braille'

RSpec.describe JapaneseBraille do
  describe "#convert" do
	let(:valid_input) { "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわーんっを" }
	
    context "when given a valid input" do
       it "returns the correct Braille for each Hiragana character" do
        expected_output = [
          "o-----", "oo----", "o--o--", "oo-o--", "-o-o--",
          "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o",
          "o---oo", "oo--oo", "o--ooo", "oo-ooo", "-o-ooo",
          "o-o-o-", "ooo-o-", "o-ooo-", "ooooo-", "-oooo-",
          "o-o---", "ooo---", "o-oo--", "oooo--", "-ooo--",
          "o-o--o", "ooo--o", "o-oo-o", "ooooo-o", "-ooo-o",
          "o-o-oo", "ooo-oo", "o-oooo", "oooooo", "-ooooo",
          "--oo--", "--oo-o", "--ooo-",
          "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-",
          "--o---", "-o--o-", "--o-oo", "-o----", "--o-o-"
        ]

        expect(JapaneseBraille.new(valid_input).convert.split("\n")).to eq(expected_output)
      end
    end

    context "when given an invalid input" do
      it "raises an ArgumentError when given a non-Hiragana character" do
        expect { JapaneseBraille.new("a").convert }.to raise_error(ArgumentError)
        expect { JapaneseBraille.new("1").convert }.to raise_error(ArgumentError)
        expect { JapaneseBraille.new("ア").convert }.to raise_error(ArgumentError)
      end

      it "raises an ArgumentError when given an empty string" do
        expect { JapaneseBraille.new("").convert }.to raise_error(ArgumentError)
      end
    end
  end
end

冗長なままの方がRSpecとしては読みやすいので、実際には冗長な方が採用される気もしますが、長くてコピペする気にならないのでnote的にはこちらのコードを採用することにします。

ちゃんと指摘事項を反映してくれるのは嬉しいですね。人間だとなかなかこうもいかない。

テストコードを満たす実装を生成する

それでは早速テストコードを満たす実装を生成してもらいましょう。
以下のプロンプトをChatGPTに与えてみます。

あなたは優秀なプロのRubyプログラマーです。
以下のテストコードの仕様を満たすRubyのコードを作成してください。

require_relative './japanese_braille'

RSpec.describe JapaneseBraille do
  describe "#convert" do
	let(:valid_input) { "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわーんっを" }
	
    context "when given a valid input" do
       it "returns the correct Braille for each Hiragana character" do
        expected_output = [
          "o-----", "oo----", "o--o--", "oo-o--", "-o-o--",
          "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o",
          "o---oo", "oo--oo", "o--ooo", "oo-ooo", "-o-ooo",
          "o-o-o-", "ooo-o-", "o-ooo-", "ooooo-", "-oooo-",
          "o-o---", "ooo---", "o-oo--", "oooo--", "-ooo--",
          "o-o--o", "ooo--o", "o-oo-o", "ooooo-o", "-ooo-o",
          "o-o-oo", "ooo-oo", "o-oooo", "oooooo", "-ooooo",
          "--oo--", "--oo-o", "--ooo-",
          "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-",
          "--o---", "-o--o-", "--o-oo", "-o----", "--o-o-"
        ]

        expect(JapaneseBraille.new(valid_input).convert.split("\n")).to eq(expected_output)
      end
    end

    context "when given an invalid input" do
      it "raises an ArgumentError when given a non-Hiragana character" do
        expect { JapaneseBraille.new("a").convert }.to raise_error(ArgumentError)
        expect { JapaneseBraille.new("1").convert }.to raise_error(ArgumentError)
        expect { JapaneseBraille.new("ア").convert }.to raise_error(ArgumentError)
      end

      it "raises an ArgumentError when given an empty string" do
        expect { JapaneseBraille.new("").convert }.to raise_error(ArgumentError)
      end
    end
  end
end

すると、こんなコードを生成してくれました。

class JapaneseBraille
  BRAILLE_MAPPING = {
    "あ"=>"o-----", "い"=>"oo----", "う"=>"o--o--", "え"=>"oo-o--", "お"=>"-o-o--",
    "か"=>"o----o", "き"=>"oo---o", "く"=>"o--o-o", "け"=>"oo-o-o", "こ"=>"-o-o-o",
    "さ"=>"o---oo", "し"=>"oo--oo", "す"=>"o--ooo", "せ"=>"oo-ooo", "そ"=>"-o-ooo",
    "た"=>"o-o-o-", "ち"=>"ooo-o-", "つ"=>"o-ooo-", "て"=>"ooooo-", "と"=>"-oooo-",
    "な"=>"o-o---", "に"=>"ooo---", "ぬ"=>"o-oo--", "ね"=>"oooo--", "の"=>"-ooo--",
    "は"=>"o-o--o", "ひ"=>"ooo--o", "ふ"=>"o-oo-o", "へ"=>"ooooo-o", "ほ"=>"-ooo-o",
    "ま"=>"o-o-oo", "み"=>"ooo-oo", "む"=>"o-oooo", "め"=>"oooooo", "も"=>"-ooooo",
    "や"=>"--oo--", "ゆ"=>"--oo-o", "よ"=>"--ooo-",
    "ら"=>"o---o-", "り"=>"oo--o-", "る"=>"o--oo-", "れ"=>"oo-oo-", "ろ"=>"-o-oo-",
    "わ"=>"--o---", "ー"=>"-o---o", "ん"=>"--o-oo"
  }

  def initialize(input)
    raise ArgumentError, "Input must not be empty" if input.empty?
    raise ArgumentError, "Input must only contain Hiragana characters" unless input.match?(/\A[\p{hiragana}ー]+\z/)

    @input = input
  end

  def convert
    @input.chars.map { |char| BRAILLE_MAPPING[char] }.join("\n")
  end
end

以下のような注釈もつけてくれています。親切ですね。

このコードでは、 JapaneseBraille クラスを定義し、次の機能を提供します。

- 入力文字列に対してブライユ点字表に基づいた変換を実行する convert メソッド
- 入力が不正な場合に ArgumentError を発生させることでエラー処理を提供する

JapaneseBraille クラスでは、ブライユ点字表をハッシュで定義し、 BRAILLE_MAPPING 定数に格納しています。これにより、 convert メソッドでは、各入力文字をマッピングに従ってブライユ点字表に変換して、結果を join メソッドで連結しています。

また、不正な入力がある場合は、 initialize メソッドで ArgumentError を発生させることにより、エラー処理を行っています。

というわけで早速RSpecに通してみると、以下のような結果に。

$ rspec japanese_braille_spec.rb
F..

Failures:

  1) JapaneseBraille#convert when given a valid input returns the correct Braille for each Hiragana character
     Failure/Error: expect(JapaneseBraille.new(valid_input).convert.split("\n")).to eq(expected_output)
     
       expected: ["o-----", "oo----", "o--o--", "oo-o--", "-o-o--", "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o", ... "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-", "--o---", "-o--o-", "--o-oo", "-o----", "--o-o-"]
            got: ["o-----", "oo----", "o--o--", "oo-o--", "-o-o--", "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o", ... "--oo-o", "--ooo-", "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-", "--o---", "-o---o", "--o-oo"]
     
       (compared using ==)
     # ./japanese_braille_spec.rb:22:in `block (4 levels) in <top (required)>'

Finished in 0.03096 seconds (files took 0.27083 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./japanese_braille_spec.rb:8 # JapaneseBraille#convert when given a valid input returns the correct Braille for each Hiragana character

最後の方の点字の値が食い違ってますね。
テストコードの実行結果を踏まえて自己修復していけるかどうかを検証するために、そのままのログをフィードバックとして与えてみましょう。

テストが通りませんでした。以下のエラーメッセージを参考に、コードを修正してください。

Failures:

  1) JapaneseBraille#convert when given a valid input returns the correct Braille for each Hiragana character
     Failure/Error: expect(JapaneseBraille.new(valid_input).convert.split("\n")).to eq(expected_output)
     
       expected: ["o-----", "oo----", "o--o--", "oo-o--", "-o-o--", "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o", ... "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-", "--o---", "-o--o-", "--o-oo", "-o----", "--o-o-"]
            got: ["o-----", "oo----", "o--o--", "oo-o--", "-o-o--", "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o", ... "--oo-o", "--ooo-", "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-", "--o---", "-o---o", "--o-oo"]
     
       (compared using ==)
     # ./japanese_braille_spec.rb:22:in `block (4 levels) in <top (required)>'

Finished in 0.0278 seconds (files took 0.27784 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./japanese_braille_spec.rb:8 # JapaneseBraille#convert when given a valid input returns the correct Braille for each Hiragana character

すると以下のようなコードを提出してきました。
・・・何かキナくさいコードになってきました。

class JapaneseBraille
  BRAILLE_MAPPING = {
    "あ"=>"o-----", "い"=>"oo----", "う"=>"o--o--", "え"=>"oo-o--", "お"=>"-o-o--",
    "か"=>"o----o", "き"=>"oo---o", "く"=>"o--o-o", "け"=>"oo-o-o", "こ"=>"-o-o-o",
    "さ"=>"o---oo", "し"=>"oo--oo", "す"=>"o--ooo", "せ"=>"oo-ooo", "そ"=>"-o-ooo",
    "た"=>"o-o-o-", "ち"=>"ooo-o-", "つ"=>"o-ooo-", "て"=>"ooooo-", "と"=>"-oooo-",
    "な"=>"o-o---", "に"=>"ooo---", "ぬ"=>"o-oo--", "ね"=>"oooo--", "の"=>"-ooo--",
    "は"=>"o-o--o", "ひ"=>"ooo--o", "ふ"=>"o-oo-o", "へ"=>"ooooo-o", "ほ"=>"-ooo-o",
    "ま"=>"o-o-oo", "み"=>"ooo-oo", "む"=>"o-oooo", "め"=>"oooooo", "も"=>"-ooooo",
    "や"=>"--oo--", "ゆ"=>"--oo-o", "よ"=>"--ooo-",
    "ら"=>"o---o-", "り"=>"oo--o-", "る"=>"o--oo-", "れ"=>"oo-oo-", "ろ"=>"-o-oo-",
    "わ"=>"--o---", "ー"=>"-o---o", "ん"=>"--o-oo",
    "っ"=>"------"
  }

  def initialize(input)
    raise ArgumentError, "Input must not be empty" if input.empty?
    raise ArgumentError, "Input must only contain Hiragana characters" unless input.match?(/\A[\p{hiragana}ー]+\z/)

    @input = input
  end

  def convert
    braille = @input.chars.map { |char| BRAILLE_MAPPING[char] }
    braille[-1] = "o-----" if @input[-1] == "っ" && braille.size > 1
    braille.join("\n")
  end
end

ChatGPT曰く、

修正したコードでは、ブライユ点字表に「っ」を追加しています。また、 convert メソッドに以下の処理を追加しています。

- 入力文字列の最後の文字が「っ」の場合、最後の文字の前に「o-----」を追加する
- 入力文字列の長さが1文字の場合、ブライユ点字表に対応する文字がなく、 convert メソッドがエラーをスローするため、この場合は「o-----」を返す

これにより、テストが正常に通過するはずです。

エラーの内容を明らかに理解できていません。
当然再度テストを実行するとコケます。

$ rspec japanese_braille_spec.rb
F..

Failures:

  1) JapaneseBraille#convert when given a valid input returns the correct Braille for each Hiragana character
     Failure/Error: expect(JapaneseBraille.new(valid_input).convert.split("\n")).to eq(expected_output)
     
       expected: ["o-----", "oo----", "o--o--", "oo-o--", "-o-o--", "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o", ... "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-", "--o---", "-o--o-", "--o-oo", "-o----", "--o-o-"]
            got: ["o-----", "oo----", "o--o--", "oo-o--", "-o-o--", "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o", ... "--ooo-", "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-", "--o---", "-o---o", "--o-oo", "------"]
     
       (compared using ==)
     # ./japanese_braille_spec.rb:22:in `block (4 levels) in <top (required)>'

Finished in 0.02386 seconds (files took 0.1992 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./japanese_braille_spec.rb:8 # JapaneseBraille#convert when given a valid input returns the correct Braille for each Hiragana character

もう一度機械的にフィードバックしてみます。

テストが通りませんでした。以下のエラーメッセージを参考に、コードを修正してください。

Failures:

  1) JapaneseBraille#convert when given a valid input returns the correct Braille for each Hiragana character
     Failure/Error: expect(JapaneseBraille.new(valid_input).convert.split("\n")).to eq(expected_output)
     
       expected: ["o-----", "oo----", "o--o--", "oo-o--", "-o-o--", "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o", ... "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-", "--o---", "-o--o-", "--o-oo", "-o----", "--o-o-"]
            got: ["o-----", "oo----", "o--o--", "oo-o--", "-o-o--", "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o", ... "--ooo-", "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-", "--o---", "-o---o", "--o-oo", "------"]
     
       (compared using ==)
     # ./japanese_braille_spec.rb:22:in `block (4 levels) in <top (required)>'

Finished in 0.02386 seconds (files took 0.1992 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./japanese_braille_spec.rb:8 # JapaneseBraille#convert when given a valid input returns the correct Braille for each Hiragana character

次はどうでしょう?

class JapaneseBraille
  BRAILLE_MAPPING = {
    "あ"=>"o-----", "い"=>"oo----", "う"=>"o--o--", "え"=>"oo-o--", "お"=>"-o-o--",
    "か"=>"o----o", "き"=>"oo---o", "く"=>"o--o-o", "け"=>"oo-o-o", "こ"=>"-o-o-o",
    "さ"=>"o---oo", "し"=>"oo--oo", "す"=>"o--ooo", "せ"=>"oo-ooo", "そ"=>"-o-ooo",
    "た"=>"o-o-o-", "ち"=>"ooo-o-", "つ"=>"o-ooo-", "て"=>"ooooo-", "と"=>"-oooo-",
    "な"=>"o-o---", "に"=>"ooo---", "ぬ"=>"o-oo--", "ね"=>"oooo--", "の"=>"-ooo--",
    "は"=>"o-o--o", "ひ"=>"ooo--o", "ふ"=>"o-oo-o", "へ"=>"ooooo-o", "ほ"=>"-ooo-o",
    "ま"=>"o-o-oo", "み"=>"ooo-oo", "む"=>"o-oooo", "め"=>"oooooo", "も"=>"-ooooo",
    "や"=>"--oo--", "ゆ"=>"--oo-o", "よ"=>"--ooo-",
    "ら"=>"o---o-", "り"=>"oo--o-", "る"=>"o--oo-", "れ"=>"oo-oo-", "ろ"=>"-o-oo-",
    "わ"=>"--o---", "ー"=>"-o---o", "ん"=>"--o-oo",
    "っ"=>"------"
  }

  def initialize(input)
    raise ArgumentError, "Input must not be empty" if input.empty?
    raise ArgumentError, "Input must only contain Hiragana characters" unless input.match?(/\A[\p{hiragana}ー]+\z/)

    @input = input
  end

  def convert
    braille = @input.chars.map { |char| BRAILLE_MAPPING[char] }
    if @input[-1] == "っ" && braille.size > 1
      braille[-2] += "------"
      braille.pop
    end
    braille.join("\n")
  end
end

何だか明らかにヤバイ方向へ改修が進んでいきます。

修正したコードでは、ブライユ点字表に「っ」を追加しています。また、 convert メソッドに以下の処理を追加しています。

- 入力文字列の最後の文字が「っ」の場合、前の文字のブライユ点字を最後の文字のブライユ点字に結合し、最後の文字のブライユ点字を削除する

これにより、テストが正常に通過するはずです。

完全にナナメ上ですね・・・。
もちろんテストは以下のように落ちてしまいます。

$ rspec japanese_braille_spec.rb
F..

Failures:

  1) JapaneseBraille#convert when given a valid input returns the correct Braille for each Hiragana character
     Failure/Error: expect(JapaneseBraille.new(valid_input).convert.split("\n")).to eq(expected_output)
     
       expected: ["o-----", "oo----", "o--o--", "oo-o--", "-o-o--", "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o", ... "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-", "--o---", "-o--o-", "--o-oo", "-o----", "--o-o-"]
            got: ["o-----", "oo----", "o--o--", "oo-o--", "-o-o--", "o----o", "oo---o", "o--o-o", "oo-o-o", "-o-o-o", ... "--ooo-", "o---o-", "oo--o-", "o--oo-", "oo-oo-", "-o-oo-", "--o---", "-o---o", "--o-oo", "------"]
     
       (compared using ==)
     # ./japanese_braille_spec.rb:22:in `block (4 levels) in <top (required)>'

Finished in 0.02639 seconds (files took 0.26334 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./japanese_braille_spec.rb:8 # JapaneseBraille#convert when given a valid input returns the correct Braille for each Hiragana character

このままだとどんどんナナメ上のコードになっていきそうなので、この方針での実験は中止しようと思います。

失敗した理由を考察する

前半のテストコードでは一体何がいけなかったのでしょうか?

恐らくテストコードからひらがなと点字の対応が読み解けなかったために、テスト結果のフィードバックを与えてもどんどんナナメ上の方向に解釈が進んでしまったのだと思われます。

実際に生成されたコードを読んでみると「ら」以降の点字対応が食い違っているために誤ったコードになってしまっています。

そこで次は考察を踏まえて冗長なテストコードをインプットに与えてみることにします。

冗長なテストコードを与えて実装を生成する

以下のようにひらがなと点字の生成結果が1対1対応するテストコードを与えてみます。

あなたは優秀なプロのRubyプログラマーです。
以下のテストコードの仕様を満たすRubyのコードを作成してください。

require_relative './japanese_braille'

RSpec.describe JapaneseBraille do
  describe "#convert" do
    context "when given a valid input" do
      it "returns the correct Braille for a single Hiragana character" do
        expect(JapaneseBraille.new("あ").convert).to eq("o-----")
        expect(JapaneseBraille.new("い").convert).to eq("oo----")
        expect(JapaneseBraille.new("う").convert).to eq("o--o--")
        expect(JapaneseBraille.new("え").convert).to eq("oo-o--")
        expect(JapaneseBraille.new("お").convert).to eq("-o-o--")
        expect(JapaneseBraille.new("か").convert).to eq("o----o")
        expect(JapaneseBraille.new("き").convert).to eq("oo---o")
        expect(JapaneseBraille.new("く").convert).to eq("o--o-o")
        expect(JapaneseBraille.new("け").convert).to eq("oo-o-o")
        expect(JapaneseBraille.new("こ").convert).to eq("-o-o-o")

# ...以下省略...

すると今度は素直にテストコードに従ったコードを生成してきました。

class JapaneseBraille
  HIRAGANA_TO_BRAILLE = {
    "あ" => "o-----",
    "い" => "oo----",
    "う" => "o--o--",
    "え" => "oo-o--",
    "お" => "-o-o--",
    "か" => "o----o",
    "き" => "oo---o",
    "く" => "o--o-o",
    "け" => "oo-o-o",
    "こ" => "-o-o-o",
    "さ" => "o---oo",
    "し" => "oo--oo",
    "す" => "o--ooo",
    "せ" => "oo-ooo",
    "そ" => "-o-ooo",
    "た" => "o-o-o-",
    "ち" => "ooo-o-",
    "つ" => "o-ooo-",
    "て" => "ooooo-",
    "と" => "-oooo-",
    "な" => "o-o---",
    "に" => "ooo---",
    "ぬ" => "o-oo--",
    "ね" => "oooo--",
    "の" => "-ooo--",
    "は" => "o-o--o",
    "ひ" => "ooo--o",
    "ふ" => "o-oo-o",
    "へ" => "oooo-o",
    "ほ" => "-ooo-o",
    "ま" => "o-o-oo",
    "み" => "ooo-oo",
    "む" => "o-oooo",
    "め" => "oooooo",
    "も" => "-ooooo",
    "や" => "--oo--",
    "ゆ" => "--oo-o",
    "よ" => "--ooo-",
    "ら" => "o---o-",
    "り" => "oo--o-",
    "る" => "o--oo-",
    "れ" => "oo-oo-",
    "ろ" => "-o-oo-",
    "わ" => "--o---",
    "ん" => "--o-oo",
    "っ" => "-o----",
    "を" => "--o-o-",
  }

  def initialize(input)
    raise ArgumentError, "Input cannot be empty" if input.empty?
    raise ArgumentError, "Input must contain only Hiragana characters" unless input.match?(/\A[\p{Hiragana}]+\z/)
    @input = input
  end

  def convert
    @input.chars.map { |char| HIRAGANA_TO_BRAILLE[char] }.join("\n")
  end
end

これをRSpecにかけてみます。

$ rspec japanese_braille_spec.rb
FF..

Failures:

  1) JapaneseBraille#convert when given a valid input returns the correct Braille for a single Hiragana character
     Failure/Error: expect(JapaneseBraille.new("ー").convert).to eq("-o--o-")
     
     ArgumentError:
       Input must contain only Hiragana characters
     # ./japanese_braille.rb:54:in `initialize'
     # ./japanese_braille_spec.rb:51:in `new'
     # ./japanese_braille_spec.rb:51:in `block (4 levels) in <top (required)>'

  2) JapaneseBraille#convert when given a valid input returns the correct Braille for multiple Hiragana characters
     Failure/Error: expect(JapaneseBraille.new("わーんっを").convert).to eq("--o---\n-o--o-\n--o-oo\n-o----\n--o-o-")
     
     ArgumentError:
       Input must contain only Hiragana characters
     # ./japanese_braille.rb:54:in `initialize'
     # ./japanese_braille_spec.rb:67:in `new'
     # ./japanese_braille_spec.rb:67:in `block (4 levels) in <top (required)>'

Finished in 0.00699 seconds (files took 0.20485 seconds to load)
4 examples, 2 failures

Failed examples:

rspec ./japanese_braille_spec.rb:6 # JapaneseBraille#convert when given a valid input returns the correct Braille for a single Hiragana character
rspec ./japanese_braille_spec.rb:57 # JapaneseBraille#convert when given a valid input returns the correct Braille for multiple Hiragana characters

どうやら長音記号が「ひらがな以外受け付けない」ガードの実装に引っかかってしまっているようです。

このフィードバックを与えたときに適切にコードの修正は行われるのでしょうか?

エラーメッセージが示す通り、JapaneseBrailleクラスが「ー」や「わーんっを」といった入力を受け取った場合、正規表現がこれらの文字にマッチしないため、ArgumentError例外が発生しています。

この問題を修正するには、正規表現を修正して、長音記号「ー」やその他の結合文字に対応するようにします。具体的には、\p{Hiragana}だけでなく、\p{Katakana}とーも含めるように正規表現を変更することで、以下のように実装することができます。

ガードの部分が悪さをしていることは認識できているようです。が、問題認識はおかしい・・・以下のコードを提案されました。

class JapaneseBraille
  HIRAGANA_TO_BRAILLE = {
    # ... same as before ...
  }

  def initialize(input)
    raise ArgumentError, "Input cannot be empty" if input.empty?
    raise ArgumentError, "Input must contain only Hiragana characters" unless input.match?(/\A[\p{Hiragana}\p{Katakana}ー]+\z/)
    @input = input
  end

  def convert
    # ... same as before ...
  end
end

やっぱりナナメ上だなぁ。テストも落ちてしまいます。

$ rspec japanese_braille_spec.rb
FFF.

Failures:

  1) JapaneseBraille#convert when given a valid input returns the correct Braille for a single Hiragana character
     Failure/Error: expect(JapaneseBraille.new("ー").convert).to eq("-o--o-")
     
       expected: "-o--o-"
            got: ""
     
       (compared using ==)
     # ./japanese_braille_spec.rb:51:in `block (4 levels) in <top (required)>'

  2) JapaneseBraille#convert when given a valid input returns the correct Braille for multiple Hiragana characters
     Failure/Error: expect(JapaneseBraille.new("わーんっを").convert).to eq("--o---\n-o--o-\n--o-oo\n-o----\n--o-o-")
     
       expected: "--o---\n-o--o-\n--o-oo\n-o----\n--o-o-"
            got: "--o---\n\n--o-oo\n-o----\n--o-o-"
     
       (compared using ==)
     
       Diff:
       @@ -1,5 +1,5 @@
        --o---
       --o--o-
       +
        --o-oo
        -o----
        --o-o-
       
     # ./japanese_braille_spec.rb:67:in `block (4 levels) in <top (required)>'

  3) JapaneseBraille#convert when given an invalid input raises an ArgumentError when given a non-Hiragana character
     Failure/Error: expect { JapaneseBraille.new("ア").convert }.to raise_error(ArgumentError)
       expected ArgumentError but nothing was raised
     # ./japanese_braille_spec.rb:75:in `block (4 levels) in <top (required)>'

Finished in 0.02874 seconds (files took 0.28127 seconds to load)
4 examples, 3 failures

Failed examples:

rspec ./japanese_braille_spec.rb:6 # JapaneseBraille#convert when given a valid input returns the correct Braille for a single Hiragana character
rspec ./japanese_braille_spec.rb:57 # JapaneseBraille#convert when given a valid input returns the correct Braille for multiple Hiragana characters
rspec ./japanese_braille_spec.rb:72 # JapaneseBraille#convert when given an invalid input raises an ArgumentError when given a non-Hiragana character

またこのエラーメッセージをそのままフィードバックとして与えてみましょう。どうなるか?

Hiragana文字列をブライユ文字列に変換する際、長音記号「ー」に対応するブライユ点列を追加するようにします。具体的には、HIRAGANA_TO_BRAILLEハッシュテーブルに、長音記号「ー」をキーとして、値として"-o--o-"のブライユ点列を追加することで、次のように実装できます。

お、次はまともな修正になりそうな気がする!

class JapaneseBraille
  HIRAGANA_TO_BRAILLE = {
    "あ" => "o-----",
    "い" => "oo----",
    "う" => "o--o--",
    "え" => "oo-o--",
    "お" => "-o-o--",
    "か" => "o----o",
    "き" => "oo---o",
    "く" => "o--o-o",
    "け" => "oo-o-o",
    "こ" => "-o-o-o",
    "さ" => "o---oo",
    "し" => "oo--oo",
    "す" => "o--ooo",
    "せ" => "oo-ooo",
    "そ" => "-o-ooo",
    "た" => "o-o-o-",
    "ち" => "ooo-o-",
    "つ" => "o-ooo-",
    "て" => "ooooo-",
    "と" => "-oooo-",
    "な" => "o-o---",
    "に" => "ooo---",
    "ぬ" => "o-oo--",
    "ね" => "oooo--",
    "の" => "-ooo--",
    "は" => "o-o--o",
    "ひ" => "ooo--o",
    "ふ" => "o-oo-o",
    "へ" => "ooooo-o",
    "ほ" => "-ooo-o",
    "ま" => "o-o-oo",
    "み" => "ooo-oo",
    "む" => "o-oooo",
    "め" => "oooooo",
    "も" => "-ooooo",
    "や" => "--oo--",
    "ゆ" => "--oo-o",
    "よ" => "--ooo-",
    "ら" => "o---o-",
    "り" => "oo--o-",
    "る" => "o--oo-",
    "れ" => "oo-oo-",
    "ろ" => "-o-oo-",
    "わ" => "--o---",
    "ー" => "-o--o-",
    "ん" => "--o-oo",
    "っ" => "-o----",
    "を" => "--o-o-",
  }

  def initialize(input)
    raise ArgumentError, "Input cannot be empty" if input.empty?
    raise ArgumentError, "Input must contain only Hiragana characters" unless input.match?(/\A[\p{Hiragana}ー]+\z/)
    @input = input
  end

  def convert
    @input.chars.map { |c| HIRAGANA_TO_BRAILLE[c] }.join("\n")
  end
end

一部、ビミョーに惜しい。やっぱりテストには失敗してしまいます。

$ rspec japanese_braille_spec.rb
FF..

Failures:

  1) JapaneseBraille#convert when given a valid input returns the correct Braille for a single Hiragana character
     Failure/Error: expect(JapaneseBraille.new("へ").convert).to eq("oooo-o")
     
       expected: "oooo-o"
            got: "ooooo-o"
     
       (compared using ==)
     # ./japanese_braille_spec.rb:35:in `block (4 levels) in <top (required)>'

  2) JapaneseBraille#convert when given a valid input returns the correct Braille for multiple Hiragana characters
     Failure/Error: expect(JapaneseBraille.new("はひふへほ").convert).to eq("o-o--o\nooo--o\no-oo-o\noooo-o\n-ooo-o")
     
       expected: "o-o--o\nooo--o\no-oo-o\noooo-o\n-ooo-o"
            got: "o-o--o\nooo--o\no-oo-o\nooooo-o\n-ooo-o"
     
       (compared using ==)
     
       Diff:
       @@ -1,6 +1,6 @@
        o-o--o
        ooo--o
        o-oo-o
       -oooo-o
       +ooooo-o
        -ooo-o
       
     # ./japanese_braille_spec.rb:63:in `block (4 levels) in <top (required)>'

Finished in 0.02718 seconds (files took 0.27411 seconds to load)
4 examples, 2 failures

Failed examples:

rspec ./japanese_braille_spec.rb:6 # JapaneseBraille#convert when given a valid input returns the correct Braille for a single Hiragana character
rspec ./japanese_braille_spec.rb:57 # JapaneseBraille#convert when given a valid input returns the correct Braille for multiple Hiragana characters

しかしこれをまたフィードバックしてみたら、どうでしょう。

エラーメッセージを見る限り、文字 "へ" の Braille が間違っているようです。また、複数の Hiragana 文字からなる入力の場合も、最後の文字が余計に並んでいるようです。これらを修正するために、次のようなコードを試してみてください。

class JapaneseBraille
  BRAILLE_TABLE = {
    "あ" => "o-----",
    "い" => "oo----",
    "う" => "o--o--",
    "え" => "oo-o--",
    "お" => "-o-o--",
    "か" => "o----o",
    "き" => "oo---o",
    "く" => "o--o-o",
    "け" => "oo-o-o",
    "こ" => "-o-o-o",
    "さ" => "o---oo",
    "し" => "oo--oo",
    "す" => "o--ooo",
    "せ" => "oo-ooo",
    "そ" => "-o-ooo",
    "た" => "o-o-o-",
    "ち" => "ooo-o-",
    "つ" => "o-ooo-",
    "て" => "ooooo-",
    "と" => "-oooo-",
    "な" => "o-o---",
    "に" => "ooo---",
    "ぬ" => "o-oo--",
    "ね" => "oooo--",
    "の" => "-ooo--",
    "は" => "o-o--o",
    "ひ" => "ooo--o",
    "ふ" => "o-oo-o",
    "へ" => "oooo-o",
    "ほ" => "-ooo-o",
    "ま" => "o-o-oo",
    "み" => "ooo-oo",
    "む" => "o-oooo",
    "め" => "oooooo",
    "も" => "-ooooo",
    "や" => "--oo--",
    "ゆ" => "--oo-o",
    "よ" => "--ooo-",
    "ら" => "o---o-",
    "り" => "oo--o-",
    "る" => "o--oo-",
    "れ" => "oo-oo-",
    "ろ" => "-o-oo-",
    "わ" => "--o---",
    "ー" => "-o--o-",
    "ん" => "--o-oo",
    "っ" => "-o----",
    "を" => "--o-o-",
  }

  def initialize(input)
    raise ArgumentError, "Input must contain only Hiragana characters" unless input.match?(/\A[\p{hiragana}\p{space}]*\z/)
    @input = input
  end

  def convert
    @input.chars.map { |c| BRAILLE_TABLE[c] }.join("\n")
  end
end

なぜガード節のコードも変更してしまうんや・・・orz

更なる修正を求めてエラーメッセージをフィードバックしたところ、以下のメッセージが返ってきて打ち止めとなりました。

問題が解決するまで、何かお力になれることはありますか?

まとめ

  • 与えられた仕様からテストコードを生成することはできた

  • 簡略化されたテストコードから仕様を満たすコードを生成することは難しかった

    • エラーメッセージをフィードバックとして与えると、どんどんナナメ上のコードが生成されてしまった

  • LLMが解釈しやすいテストコードを与えると、簡略化されたテストコードを与えたときよりも品質は向上したが、与えたテストコードを満たす実装は生成できなかった

    • 冗長なテストコードを与えても、テストのエラーメッセージを元にコードを修正し切ることは困難だった

次世代のテスト駆動開発まではもう少しかかりそうです。


この記事が気に入ったらサポートをしてみませんか?