📚

Ruby でプロンプトエンジニアリングを試してみた

2023/04/25に公開約7,500字

ChatGPTに与える呪文

コーティングでプロンプトエンジニアリングをする際に、重要なのは要件と制約です。
制約を変えると、どのようにアウトプットが変わるか試してみます。

コードレビュー呪文①

以下の呪文(プロンプト)をChatGPTに与えてレビューさせてみます。

あなたは優秀なプログラマーです。
以下の要件と制約に従いコードレビューをしてください。

# 要件
- ruby 3.1.2 で記述している
- フレームワークに Rails 7.0.4 を使用している
- テスティングフレームワークに rspec を使用している

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

class Tag < Firestore::FirestoreRecord
  COLLECTION_PATH = 'tag'.freeze
  @collection_path = COLLECTION_PATH

  def self.find_row(id)
    firestore.col(self::COLLECTION_PATH).doc(id).get.fields
  end

  def self.find_to_create(tags)
    return false if tags.blank?

    tags = tags.uniq

    tags.each do |tag|
      next if tag.blank?
      ret = find_row(tag)
      next unless ret.blank?

      data = {}
      data[:name] = tag
      data[:created_at] ||= Time.current
      data[:updated_at] ||= Time.current

      ref = firestore.doc "#{self::COLLECTION_PATH}/#{tag}"
      ref.set(data)
    end

    true
  end
end

コード部分には以下のリポジトリのコードを与えています。
https://github.com/chikugoy/regex_backend

ChatGPTからのフィードバックは以下になります。

以下のレビューは、提供されたコードを見直し、要件と制約に従った変更を提案しています。

class Tag < Firestore::FirestoreRecord
  COLLECTION_PATH = 'tag'.freeze
  @collection_path = COLLECTION_PATH

  def self.find_row(id)
    firestore.col(self::COLLECTION_PATH).doc(id).get.fields
  end

  def self.find_or_create(tags)
    return false if tags.blank?

    unique_tags = tags.uniq

    unique_tags.each do |tag|
      next if tag.blank?
      existing_tag = find_row(tag)
      create(tag) if existing_tag.blank?
    end

    true
  end

  private

  def self.create(tag)
    data = {
      name: tag,
      created_at: Time.current,
      updated_at: Time.current
    }

    ref = firestore.doc("#{self::COLLECTION_PATH}/#{tag}")
    ref.set(data)
  end
end

変更点:

1. メソッド名を find_to_create から find_or_create に変更し、意味を明確にしました。
2. find_or_create メソッド内で、tags.uniq を unique_tags という変数に代入しました。これにより、タグの一意性がより明確になります。
3. タグの作成処理を create というプライベートメソッドに分割しました。これにより、メソッドの責務が小さくなり、単一責任の原則に従います。

このコードは、Ruby 3.1.2、Rails 7.0.4、およびRSpecの要件に準拠しています。また、制約に基づいた最適化が適用されています。

確かによりリーダブル、テスタブルになり、レビューの結果に文句はありません。
次は制約を少し増やしてみます。

コードレビュー呪文②

増やす制約は以下です。

  • 開放閉鎖の原則を守ること
  • リスコフの置換原則を守ること
  • インタフェース分離の原則を守ること
  • 依存性逆転の原則を守ること
  • 疎結合なコードを書くこと
あなたは優秀なプログラマーです。
以下の要件と制約に従いコードレビューをしてください。

# 要件
- ruby 3.1.2 で記述している
- フレームワークに Rails 7.0.4 を使用している
- テスティングフレームワークに rspec を使用している

# 制約
- モダンでシンプルな記述にすること
- KISSの原則を守ること
- 複雑な処理を避けるために高階関数は使用できることとする
- リーダブルなコードにすること
- メソッドは動詞始まりにすること
- メソッドの責務は小さく分割すること
- 単一責任の原則を守ること
- 開放閉鎖の原則を守ること
- リスコフの置換原則を守ること
- インタフェース分離の原則を守ること
- 依存性逆転の原則を守ること
- 疎結合なコードを書くこと
- 命名は言語に則り適切に行うこと
- 条件分岐は早期returnなどを使ってネストを浅くすること
- 多重ループ処理を極力避けるロジックにすること
- テスタビリティであること
- マジックナンバーは使わないようにすること

class Tag < Firestore::FirestoreRecord
  COLLECTION_PATH = 'tag'.freeze
  @collection_path = COLLECTION_PATH

  def self.find_row(id)
    firestore.col(self::COLLECTION_PATH).doc(id).get.fields
  end

  def self.find_to_create(tags)
    return false if tags.blank?

    tags = tags.uniq

    tags.each do |tag|
      next if tag.blank?
      ret = find_row(tag)
      next unless ret.blank?

      data = {}
      data[:name] = tag
      data[:created_at] ||= Time.current
      data[:updated_at] ||= Time.current

      ref = firestore.doc "#{self::COLLECTION_PATH}/#{tag}"
      ref.set(data)
    end

    true
  end
end

ChatGPTからのフィードバックは以下になります。

以下のレビューは、提供されたコードを見直し、要件と制約に従った変更を提案しています。

class FirestoreTagRepository
  COLLECTION_PATH = 'tag'.freeze

  def initialize(firestore)
    @firestore = firestore
  end

  def find_row(id)
    @firestore.col(COLLECTION_PATH).doc(id).get.fields
  end

  def find_or_create(tags)
    return false if tags.blank?

    unique_tags = tags.uniq

    unique_tags.each do |tag|
      next if tag.blank?
      existing_tag = find_row(tag)
      create(tag) if existing_tag.blank?
    end

    true
  end

  private

  def create(tag)
    data = {
      name: tag,
      created_at: Time.current,
      updated_at: Time.current
    }

    ref = @firestore.doc("#{COLLECTION_PATH}/#{tag}")
    ref.set(data)
  end
end

変更点:

1. クラス名を Tag から FirestoreTagRepository に変更しました。これにより、このクラスがFirestoreに依存していることが明確になり、依存性逆転の原則に従います。
2. Firestoreの依存性をコンストラクタインジェクションを通じて注入するようにしました。これにより、テスタビリティが向上し、疎結合なコードが実現されます。
3. クラスメソッドからインスタンスメソッドに変更しました。これにより、オブジェクト指向プログラミングの原則に従います。
4. メソッド名を find_to_create から find_or_create に変更し、意味を明確にしました。

このコードは、Ruby 3.1.2、Rails 7.0.4、およびRSpecの要件に準拠しています。また、制約に基づいた最適化が適用されています。

変更点の1から3のフィードバックが追加されています。

1からソースコードを生成するにはまだ難はあるけど、リファクタ、または、レビューでの使用は全然使える印象です。

次にテストコードを生成してみます。

テストコード生成呪文

あなたは優秀なプログラマーです。
以下の要件と制約に従いコードからテストコードを生成してください。

# 要件
- ruby 3.1.2 で記述している
- フレームワークに Rails 7.0.4 を使用している
- テスティングフレームワークに minitest を使用している

# 制約
- モダンでシンプルな記述にすること
- KISSの原則を守ること
- 複雑な処理を避けるために高階関数は使用できることとする
- リーダブルなコードにすること
- メソッドは動詞始まりにすること
- メソッドの責務は小さく分割すること
- 単一責任の原則を守ること
- 開放閉鎖の原則を守ること
- リスコフの置換原則を守ること
- インタフェース分離の原則を守ること
- 依存性逆転の原則を守ること
- 疎結合なコードを書くこと
- 命名は言語に則り適切に行うこと
- 条件分岐は早期returnなどを使ってネストを浅くすること
- 多重ループ処理を極力避けるロジックにすること
- テスタビリティであること
- マジックナンバーは使わないようにすること

class Tag < Firestore::FirestoreRecord
  COLLECTION_PATH = 'tag'.freeze
  @collection_path = COLLECTION_PATH

  def self.find_row(id)
    firestore.col(self::COLLECTION_PATH).doc(id).get.fields
  end

  def self.find_to_create(tags)
    return false if tags.blank?

    tags = tags.uniq

    tags.each do |tag|
      next if tag.blank?
      ret = find_row(tag)
      next unless ret.blank?

      data = {}
      data[:name] = tag
      data[:created_at] ||= Time.current
      data[:updated_at] ||= Time.current

      ref = firestore.doc "#{self::COLLECTION_PATH}/#{tag}"
      ref.set(data)
    end

    true
  end
end

ChatGPTからのフィードバックは以下になります。

以下は、提供されたコードに基づいてMinitestテストケースを作成しました。要件と制約に従って記述されています。

require 'test_helper'

class TagTest < ActiveSupport::TestCase
  class FirestoreMock
    def initialize(path)
      @path = path
    end

    def col(path)
      self
    end

    def doc(id)
      self
    end

    def get
      self
    end

    def fields
      {name: 'tag1', created_at: Time.current, updated_at: Time.current}
    end

    def set(data)
      true
    end
  end

  def setup
    @firestore_mock = FirestoreMock.new('tag')
    @tag_id = 'tag1'
    @tags = %w[tag1 tag2]
    Tag.instance_variable_set(:@firestore, @firestore_mock)
  end

  def test_find_row
    result = Tag.find_row(@tag_id)
    assert_equal({name: 'tag1', created_at: result[:created_at], updated_at: result[:updated_at]}, result)
  end

  def test_find_to_create
    assert_not_predicate [], :present?
    assert_equal false, Tag.find_to_create([])

    assert_predicate @tags, :present?
    assert_equal true, Tag.find_to_create(@tags)
  end
end

このテストケースは、Tagクラスの.find_rowおよび.find_to_createメソッドの振る舞いを検証しています。
また、FirestoreMockクラスを使って、Firestoreとの通信を模倣しています。
このテストケースは、Ruby 3.1.2、Rails 7.0.4、およびMinitestを使用して記述されています。

動かしてまで試してはいませんが、パッと見良さそうです。
少なくともここからブラッシュアップして、更にリファクタリングする土台としては十分そうです。

まとめ

ここまでの結果でChatGPT、レビューやテストコード生成などでは十分な戦力になる印象です。
正直私はここまでとは思っておらず舐めてました。衝撃の結果です。

現状の用途としてはサポート的なペアプロパートナーのような位置づけになるかと思います。ただ非常に優秀なパートナーです。シニアレベルのプログラミングスキルはあるでしょう。

部分的に変なコードを出力することはありますので、全面的に使えるという訳ではないですが、ペアプロ用途では十分です。

凄まじい技術革新が起こったみたいなのでしばらくは注視が必要ですね。
でないとあっという間に置いていかれそうです。

Discussion

ログインするとコメントできます