💡

ChatGPT を使って go-infinity-channel パッケージを作成してみた

2023/04/15に公開

go-infinity-channel は無限にキューイングできるチャネルを提供するライブラリです。このパッケージでは、標準の channel の容量制限を克服し、大量のデータを効率的に処理できるように設計されています。

美味しいポイントとして、ジェネリクスを利用して型安全性も確保しています。またサードパーティのパッケージ併存をしていないため軽量です。

https://github.com/Code-Hex/go-infinity-channel

Go playgroundでも試せます。使い方はめっちゃシンプルです。

main.go
package main

import (
	"fmt"
	"time"

	infinity "github.com/Code-Hex/go-infinity-channel"
)

func main() {
	ch := infinity.NewChannel[int]()

	go func() {
		for i := 0; i < 10; i++ {
			ch.In() <- i
			time.Sleep(100 * time.Millisecond)
		}
		ch.Close()
	}()

	for i := range ch.Out() {
		fmt.Println("Received:", i)
	}
}

リアルユースケース

channel の容量を想定できない、あらかじめ大きくキャパシティを取っておきたくない場合に利用します。(他にもいい方法ありそう)

例えば僕が欲しかった例だとこの部分です。

https://github.com/Code-Hex/vz/blob/7f648b6fb9205d6f11792263d79876e3042c33ec/virtualization.go#L164

これは Objective-C で作成したあるプロパティの値が変化したことを検知し、それ cgo 内でチャネルを経由で Go へ通知しています。channel の特性としてキャパシティが超えたことでキューイングができず、できるようになるまでブロッキングされます。それを回避するためだけに Go routine を使っていました。

この記事を書いている時点ではまだ置き換えられてませんが、おそらくこのように置き換えられると思います。

v.stateNotify.In() <- newState

ChatGPT と作る

さて、本題ですがこれは ChatGPT (GPT-4) と一緒に開発しました。とはいえこのロジックを任せたわけではなく、テストコード、GoDoc、README の作成をお願いしました。今までいくつか Go のパッケージを作成していましたがこの 3 つは一番時間がかかってました。

プロンプトを紹介します。

テストコードを書いてもらう

テストコードを書いてもらうためには、メインロジックが書かれたコードを渡す必要があります。今回の場合だと channel.go に書かれたコメントなしの Go コードを渡しました。

これは無限にキューイングできる Go の package です。
これのテストコードを書いてください

// ここにコードを貼り付ける。
生成された内容

以下は、infinity パッケージに対するテストコードです。_test.goファイルを作成し、このコードを貼り付けてください。以下の例では、いくつかの基本的なテストケースが含まれています。

package infinity

import (
	"sync"
	"testing"
	"time"
)

func TestNewChannel(t *testing.T) {
	ch := NewChannel[int]()
	if ch == nil {
		t.Fatal("NewChannel() should return a non-nil value")
	}
}

func TestInAndOut(t *testing.T) {
	ch := NewChannel[int]()

	go func() {
		for i := 0; i < 10; i++ {
			ch.In() <- i
		}
		ch.Close()
	}()

	var values []int
	for v := range ch.Out() {
		values = append(values, v)
	}

	for i, v := range values {
		if v != i {
			t.Errorf("Expected %d, got %d", i, v)
		}
	}
}

func TestLen(t *testing.T) {
	ch := NewChannel[int]()

	go func() {
		for i := 0; i < 10; i++ {
			ch.In() <- i
		}
		ch.Close()
	}()

	time.Sleep(100 * time.Millisecond) // Give time for the goroutine to run

	if ch.Len() != 10 {
		t.Fatalf("Expected length of 10, got %d", ch.Len())
	}
}

func TestConcurrency(t *testing.T) {
	ch := NewChannel[int]()
	wg := sync.WaitGroup{}

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			ch.In() <- n
		}(i)
	}

	go func() {
		wg.Wait()
		ch.Close()
	}()

	var count int
	for range ch.Out() {
		count++
	}

	if count != 10 {
		t.Fatalf("Expected count of 10, got %d", count)
	}
}

これらのテストケースは、infinity パッケージの基本的な機能をテストします。新しいチャンネルの作成、入力チャンネルへの書き込み、出力チャンネルからの読み取り、長さの取得、および同時実行のサポートをテストしています。

GoDoc を書いてもらう

これは無限にキューイングできる Go の package です。
これに godoc に反映できるようにコメントを書いてください

// ここにコードを貼り付ける。
生成された内容

以下は、Go の infinity パッケージに追加するための GoDoc に対応したコメントです。

// Package infinity provides an unbounded buffered channel implementation.
package infinity

// Channel represents an unbounded buffered channel for values of type T.
type Channel[T any] struct {
	input, output chan T
	length        chan int
	buffer        []T
}

// NewChannel creates a new unbounded buffered channel for values of type T.
func NewChannel[T any]() *Channel[T] {
	ch := &Channel[T]{
		input:  make(chan T),
		output: make(chan T),
		length: make(chan int),
		buffer: []T{},
	}
	go ch.infiniteBuffer()
	return ch
}

// In returns a send-only channel for writing values to the Channel.
func (ch *Channel[T]) In() chan<- T {
	return ch.input
}

// Out returns a receive-only channel for reading values from the Channel.
func (ch *Channel[T]) Out() <-chan T {
	return ch.output
}

// Len returns the current number of elements in the Channel buffer.
func (ch *Channel[T]) Len() int {
	return <-ch.length
}

// Close closes the input channel, signaling no more values will be sent.
func (ch *Channel[T]) Close() {
	close(ch.input)
}

// queue appends a value to the end of the Channel buffer.
func (ch *Channel[T]) queue(v T) { ch.buffer = append(ch.buffer, v) }

// dequeue removes the first value from the Channel buffer.
func (ch *Channel[T]) dequeue()  { ch.buffer = ch.buffer[1:] }

// peek returns the first value in the Channel buffer without removing it.
func (ch *Channel[T]) peek() T {
	return ch.buffer[0]
}

// infiniteBuffer is the internal buffer management goroutine for the Channel.
func (ch *Channel[T]) infiniteBuffer() {
	var input, output chan T
	var next T
	input = ch.input

	var zero T

	for input != nil || output != nil {
		select {
		case elem, open := <-input:
			if open {
				ch.queue(elem)
			} else {
				input = nil
			}
		case output <- next:
			ch.dequeue()
		case ch.length <- len(ch.buffer):
		}

		if len(ch.buffer) > 0 {
			output = ch.output
			next = ch.peek()
		} else {
			output = nil
			next = zero
		}
	}

	close(ch.output)
	close(ch.length)
}

コメントは、パッケージ、型、関数に関連する説明を提供し、コードの意図や使用方法を明確にします。これにより、GoDoc によって生成されたドキュメントが、他の開発者にとって役立つ情報を提供します。

ここで変化球として書かせた後に、panic になるコードの書き方を聞いてみました。

ユーザーがこのライブラリを利用したときに panic になるような使われ方を提案してください
生成された内容

閉じたチャンネルにデータを送信すると、panicを起こします。これを避けるには、データを送信する前にチャンネルが閉じていないことを確認するか、sync.Once を使用してチャンネルが一度だけ閉じられることを確認します。

sync.Once を使用して安全にチャンネルを閉じる例を紹介します:

package main

import (
	"fmt"
	"sync"
	"time"

	"github.com/yourusername/infinity"
)

func main() {
	ch := infinity.NewChannel[int]()
	var once sync.Once

	go func() {
		for i := 0; i < 10; i++ {
			ch.In() <- i
			time.Sleep(100 * time.Millisecond)
		}
		once.Do(func() {
			ch.Close()
		})
	}()

	for i := range ch.Out() {
		fmt.Println("Received:", i)
	}
}

ここで僕は sync.Once を使った実装に直してみました。その後に記述したプロンプトです。OpenAI の公式でプロンプトエンジニアリングに関するベストプラクティスも公開しています。これに従ってコードのセクションであることがわかるように """ (本当はバッククォート 3 つ) で囲んでいます。

Channel struct に `once sync.Once` フィールドを追加して Close メソッドを次のように修正しました。

"""
func (ch *Channel[T]) Close() {
	ch.once.Do(func() {
		close(ch.input)
	})
}
"""

これを踏まえて Close メソッドのコメントを修正してください
生成された内容

Channel 構造体の Close メソッドに変更を加えことを理解しました。以下は、追加された sync.Once フィールドを考慮した、 Close メソッドの修正コメントです:

// Close safely closes the input channel, ensuring that it is closed only once.
// It uses the sync.Once field in the Channel struct to guarantee a single execution.
func (ch *Channel[T]) Close() {
	ch.once.Do(func() {
		close(ch.input)
	})
}

このコメントでは、Closeメソッドの新しい動作について説明しています。このメソッドでは、sync.Onceを利用して、入力チャンネルを安全に一度だけ閉じます。

臨機応変に対応してくれて凄いですね!

README を書いてもらう

AI も人も良ければ褒めることが大事です。

いいですね。これまでの内容を含めた README.md を作成してください。
この README.md には `Synopsys` セクション(ここには最小限にしたこのパッケージを使ったコードの例のみを書く)、Description セクション(ここにはこのパッケージが何かを説明する)、先ほどの `panic` について注意を促すセクションを書いてください
生成された内容(コピペがむずかったのでスクショ)


README は英語で書いて欲しかったのでその指示をします。それと panic が発生するようなコードも一緒に提示してもらいます。

これを英語で書いてください

また Caution に少しだけコード例があるといいと思います
生成された内容(Synopsis は同じなので省く)



提示されたコードはほぼあり得ないので書き方について指示をします。

その問題を避けるなら注意としてはインクリメンタルな for ループの代わりに for range ch.Out() を使うことを書くべきだと思います。

Caution のセクションを修正してください
生成された内容(スクショ)


良い感じに作成してもらえました!

ChatGPT について思うこと

ChatGPT の協力により、今まで一番時間を費やしていたドキュメントはあっという間に補完できるようになったのではないかなと思いました。そのおかげでメインロジック部分の開発に殆どの時間を割くことがでるようになったと思います。

職が失われるなどの悲観的な意見も度々目にしますが、ソフトウェアエンジニアリングに関していうと間違いなく開発者の助けとなる強力なツールであると思います。

全部やってくれるからもう勉強しなくても良いでしょ!!とはまだならないのかなと感じています。上記でもこんな書き方はしないけどなーといったコードを指示を出さなければ普通に提案してきます。なのでこれを見極めるためにはやはり私たちもまだまだ努力をしていく必要がありそうです。

https://twitter.com/kelseyhightower/status/1646875384531779586?s=20

それでもこれからの進化にも期待できますね!

最後に

ライブラリも使って欲しいので PR や issues も大歓迎です!

Discussion