TL; DR
- 関数に
//go:fix inlineディレクティブを付けると、呼び出し箇所がgo fix ./...でインライン展開される - 関数定義を循環させると、呼び出し箇所のインライン展開の置換がループする
- ループを利用してアニメーションが可能
はじめに
みなさん、プログラミングしてますか?AIに丸投げしてますか?
AIでも人間でも、書いたコードの仕上げにはフォーマッタやコード修正ツールの利用が欠かせません。
なんといっても、ソースコードがいっせいに動き出し整列するさまは見ていて壮観です。
この躍動感、もっと味わいたくないですか?
というわけで本記事では、go fixによるソースコード修正で アニメーションを作る 方法を紹介します。
go fixコマンドによるインライン展開
go fix コマンドを使うと、自動でコードのリファクタリングができます。go fmt のようなシンプルな整形ではなく、例えばfor文を最新版の書き方に置き換える1といった構文レベルでの修正を行います。
さらに、Go1.26で go fix コマンドにインライン展開の機能が追加されました。
以下のように //go:fix inline ディレクティブを付けると、関数呼び出しをインライン展開することができます。
// ReadFile reads the file named by filename…
// Deprecated: As of Go 1.16, this function simply calls [os.ReadFile].
//go:fix inline
func ReadFile(filename string) ([]byte, error) {
return os.ReadFile(filename)
}
以下の呼び出し箇所が
data, err := ReadFile("hello.txt")
このようにインライン展開されます。
// ReadFile関数の中身に置換される
data, err := os.ReadFile("hello.txt")
主なユースケースは古い関数の自動置き換えです。
上記のようにライブラリ提供者がdeprecated関数に //go:fix inline をつけておくと、利用者は go fix するだけで新しい関数に自動で置き換えることができます。
インライン展開をループさせてアニメーションを作る
インライン化は繰り返し適用可能です。入れ子上に関数定義されている場合、go fix するたびに、マトリョーシカのように1つずつインライン展開されます。
//go:fix inline
func A() {
B()
}
//go:fix inline
func B() {
fmt.Println("b")
}
A()
B()
fmt.Println("b")
さらに、関数を循環定義させればインライン展開が無限ループします。これがアニメーションの仕組みです。
//go:fix inline
func A0() {
A1()
}
//go:fix inline
func A1() {
A2()
}
//go:fix inline
func A2() {
A0()
}
ここで1点注意です。関数呼び出しの置換は
A0() → A1() → A2() → A0()...
ではなく
A0() → A1() → A0()...
となります。
というのも、go fix によって 関数定義内の関数呼び出しもインライン展開されるためです。
//go:fix inline
func A0() {
A1()
}
//go:fix inline
func A1() {
A2()
}
//go:fix inline
func A2() {
A0()
}
//go:fix inline
func A0() {
A2()
}
//go:fix inline
func A1() {
A0()
}
//go:fix inline
func A2() {
A1()
}
//go:fix inline
func A0() {
A1()
}
//go:fix inline
func A1() {
A2()
}
//go:fix inline
func A2() {
A0()
}
具体的には、gofix適用ごとに関数呼び出しの添え字の増え方が1, 2, 4...と倍々に増えていきます(A0 → A1 → A3 → A7)。
上記の例だと関数は3つしかないため、実際には3で割ったあまりの A0 → A1 → A0 → A1 となります。
同様の理由で、関数の個数を 2^n にするとどこかのタイミングで添え字の増加を 2^n で割ったあまりが0になってしまいアニメーションが止まってしまいます。
あとは、関数定義をアニメーションに使いたい行数分用意し、関数名をアスキーアートにすれば冒頭のアニメーションになります。
アニメーション生成CLI
とはいえ毎回手で書くのは面倒なので、各フレームのアスキーアートからアニメーションソースコードを自動生成するツールを作りました2。
以下のようなアスキーアートファイルからソースコードを生成します。
_OOO_
O____
O_OO_
O___O
_OOO_
_OOO_
O___O
O___O
O___O
_OOO_
(以下同様に10フレーム目まで作成)
完成したソースコードがこちらです。
生成結果ソースコード(クリックで展開)
// Source code generated by gofixanime.
// Apply go fix ./... to animate the ASCII art in main.
package main
//go:fix inline
func _OOO_() {
_OOO__()
}
//go:fix inline
func O_____() {
O___O_()
}
//go:fix inline
func O_OO___() {
O___O__()
}
//go:fix inline
func O___O___() {
O___O____()
}
//go:fix inline
func _OOO_____() {
_OOO______()
}
//go:fix inline
func _OOO__() {
O___O()
}
//go:fix inline
func O___O_() {
OO_OO_()
}
//go:fix inline
func O___O__() {
O_O_O__()
}
//go:fix inline
func O___O____() {
O___O_____()
}
//go:fix inline
func _OOO______() {
O___O______()
}
//go:fix inline
func O___O() {
OOOOO()
}
//go:fix inline
func OO_OO_() {
O______()
}
//go:fix inline
func O_O_O__() {
OOOO___()
}
//go:fix inline
func O___O_____() {
O_______()
}
//go:fix inline
func O___O______() {
O________()
}
//go:fix inline
func OOOOO() {
O___O_______()
}
//go:fix inline
func O______() {
_O_O__()
}
//go:fix inline
func OOOO___() {
__O____()
}
//go:fix inline
func O_______() {
_O_O____()
}
//go:fix inline
func O________() {
O___O________()
}
//go:fix inline
func O___O_______() {
OOOOO_()
}
//go:fix inline
func _O_O__() {
O_________()
}
//go:fix inline
func __O____() {
OOOO____()
}
//go:fix inline
func _O_O____() {
O__________()
}
//go:fix inline
func O___O________() {
OOOOO____()
}
//go:fix inline
func OOOOO_() {
OOOOO__()
}
//go:fix inline
func O_________() {
__O___()
}
//go:fix inline
func OOOO____() {
__O_____()
}
//go:fix inline
func O__________() {
__O______()
}
//go:fix inline
func OOOOO____() {
OOOOO_____()
}
//go:fix inline
func OOOOO__() {
OOOOO___()
}
//go:fix inline
func __O___() {
__O_______()
}
//go:fix inline
func __O_____() {
__O________()
}
//go:fix inline
func __O______() {
__O_________()
}
//go:fix inline
func OOOOO_____() {
OOOOO______()
}
//go:fix inline
func OOOOO___() {
O___O_________()
}
//go:fix inline
func __O_______() {
OO__O_()
}
//go:fix inline
func __O________() {
O_O_O___()
}
//go:fix inline
func __O_________() {
O__OO___()
}
//go:fix inline
func OOOOO______() {
O___O__________()
}
//go:fix inline
func O___O_________() {
_OOO___()
}
//go:fix inline
func OO__O_() {
O___O___________()
}
//go:fix inline
func O_O_O___() {
OOOOO_______()
}
//go:fix inline
func O__OO___() {
O___O____________()
}
//go:fix inline
func O___O__________() {
O___O_____________()
}
//go:fix inline
func _OOO___() {
_____()
}
//go:fix inline
func O___O___________() {
______()
}
//go:fix inline
func OOOOO_______() {
_______()
}
//go:fix inline
func O___O____________() {
________()
}
//go:fix inline
func O___O_____________() {
_________()
}
//go:fix inline
func _____() {
_OOO_()
}
//go:fix inline
func ______() {
O_____()
}
//go:fix inline
func _______() {
O_OO___()
}
//go:fix inline
func ________() {
O___O___()
}
//go:fix inline
func _________() {
_OOO_____()
}
func main() {
_OOO_()
O_____()
O_OO___()
O___O___()
_OOO_____()
}
CLIの実装
史上最高の頭脳Fableにブン投げることにより13分で実装完了しました。 Fableの無駄遣い
まるなげぷろんぷと(クリックで展開)
`//go:fix inline` を利用してソースコード上にアニメーションを描画する CLI `gofixanime` を実装してください。
Fableのクレジットが足りなくなりそうであれば、適宜他のモデルのサブエージェントを使用してください。
# `//go:fix inline` のアニメーションの原理
`//go:fix inline` を使用すると関数がインライン展開されます。そこで、関数を循環定義すると、`go fix ./...` を適用するたびに関数呼び出しの中身がインライン展開し、展開による置換がループします。このループを使ってアニメーションを作ります。
```go
package a
//go:fix inline
func A0() {
A1()
}
//go:fix inline
func A1() {
A2()
}
//go:fix inline
func A2() {
A3()
}
//go:fix inline
func A3() {
A4()
}
//go:fix inline
func A4() {
A0()
}
func main() {
// go fix ./... すると A0 -> A1 -> A3 -> A2 -> A0 とループ
A0()
}
```
関数定義がn個ある場合、`go fix` をi回適用すると 0番目の関数の関数呼び出し(上記の例だと`A0()`) は k番目(ただし `k \equiv 2^i (mod n)` の関数呼び出しに置換されます。
上記からもわかるように、アニメーションを作りたい場合iフレーム目の表示はi番目の関数定義とはならない点に注意が必要です。
また、nによっては kがある値から動かなくなってしまう場合があり(例: nが2の累乗)、そのようなnを選ばないようにする必要があります。
アスキーアートを行ごとに分解し上記のような循環定義の関数にすることで、より大規模なアニメーションを作成可能です。
今回作成するgofixanimeは、与えられた複数のアスキーアート文字列ファイルからgo fixでアニメーションができるmain.goファイルを生成するCLIです。
```go
main() {
// gのアスキーアート(以降 o, f, i, x のアスキーアートに置換されることでアニメーション)
// NOTE: 各行で名前が衝突しないよう、末尾に行数に応じた_を付け名前が一意になるようにする
__OOO_()
_O___O_()
_O___O__()
_OOOOO___()
_____O____()
__OOO______()
}
```
## 仕様
- `gofixanime ${アスキーアートテキストファイルを格納したディレクトリ名}` でソースコード生成
- アスキーアートテキストファイルは、ディレクトリ配下の連番のテキストファイル `0.txt`, `1.txt`...
- アスキーアートに関数定義で使用できない文字列が含まれる場合はエラー
- アスキーアートテキストファイルは全ファイル行数、列数が同じである必要がある(違った場合エラー)
- アスキーアートテキストファイルの個数がgo fixのアニメーションが成立しない個数(go fixを適用し続けるとアニメーションが止まってしまう)である場合はエラー
- ソースコード生成には "github.com/dave/jennifer/jen" を使用
- ソースコードはTDDで作成
ソースコードの生成にはJenniferを使用しています。ビルダーパターンを使用して構文の要素を組み合わせていきます。
for pos := range frames {
for r := range names[pos] {
// cyclic definition: the row function at pos calls the same row at the next position
f.Comment("//go:fix inline")
f.Func().Id(names[pos][r]).Params().Block(
jen.Id(names[(pos+1)%n][r]).Call(),
)
}
}
また、フレームを関数定義化する際に別行の定義と混ざってしまわないよう、行番号に応じた長さのsuffixを付けて区別しています。
func main() {
// 関数名の末尾に_を付けて、各行の関数定義が一意になるようにしている
_OOO_()
O_____()
O_OO___()
O___O___()
_OOO_____()
}
おわりに
以上、go fixのインライン展開でアニメーションを作る紹介でした。
go fixにはまだまだ可能性を感じているので、皆さまもぜひ置換で遊んでみてはいかがでしょうか?
-
Go1.21まではfor文のループ中で1つのループ変数を使いまわしていました。そのため、クロージャで変数をキャプチャし後で実行したり、ゴルーチン等で非同期処理したりすると、イテレーションが進んでおり想定と異なる値になってしまいます。これを回避するため、同名のローカル変数に再代入するという実装が主流でした(
for _, x := range s { x := x ... })。Go1.22から、ループ変数はイテレーションごとに作成されるようになりました。go fixでは不要になったこの代入x := xが削除されます。 ↩ -
前述の通りループ内には表示されないフレームもある(A0, A1, A2の3フレームだとA2は表示されない)ため、余分なダミーフレームを用意してあげる必要があります ↩
