はじめに
@koyo-miyamura と申します。以下の Elixir 合宿でワイワイ自由研究してみました。
AI をコードリーディングに使う。
みなさん AI をどのように使っていますか?コードを書いてもらっている!という方も多いでしょう。
でも実務ではだれかが書いたコードを読む機会も多いです。このような時にも AI は役立ちます。
とはいえ Elixir は他言語に比べて若干マイナーです。うまく読めるのでしょうか?やってみましょう!
実際にやってみる
使用する AI
VsCode 上の GitHub Copilot Chat の Agent モード。Claude Sonnet 4 モデル。
コードリーディングの対象
Phoenix でも使われているデータマッピングライブラリの Ecto を読んでみます。
「Repo.all してから MySQL とやりとりする箇所までを特定して読んでみる」ことを目的にしてみます。
やってみる
まず以下のように clone してみます。
$ git clone git@github.com:elixir-ecto/ecto
まず AI に概要を説明してもらいましょう。
Q. この Ecto というライブラリのコードリーディングをしたいです。まず概要を理解したいです。
A. GitHub Copilot: Ectoライブラリのコードリーディングをお手伝いします。まず、このライブラリの概要を理解するために重要なファイルを調べてみましょう。
Ectoライブラリの概要を理解しました。以下にまとめます:
Ecto とは
Ectoは、Elixir言語のためのデータマッピングと言語統合クエリのためのツールキットです。主にデータベースとの連携で使用されますが、任意のデータソースをElixirの構造体にマッピングすることも可能です。
4つの主要コンポーネント
Ectoは以下の4つの核となるコンポーネントで構成されています:
1. Ecto.Repo
- データがどこにあるか
- データストアのラッパー
- データの作成、更新、削除、クエリを実行
- アダプターと認証情報が必要
2. Ecto.Schema
- データが何であるか
- 外部データをElixir構造体にマッピング
- データベーステーブルの定義によく使用される
3. Ecto.Query
- データをどう読むか
- Elixir構文で書かれたクエリ
- 安全で組み合わせ可能なクエリシステム
4. Ecto.Changeset
- データをどう変更するか
- データに適用する前の変更の追跡とバリデーション
サポートするデータベース
データベース | Ectoアダプター | 依存関係 |
---|---|---|
PostgreSQL | Ecto.Adapters.Postgres | ecto_sql + postgrex |
MySQL | Ecto.Adapters.MyXQL | ecto_sql + myxql |
MSSQL | Ecto.Adapters.Tds | ecto_sql + tds |
SQLite3 | Ecto.Adapters.SQLite3 | ecto_sqlite3 |
ClickHouse | Ecto.Adapters.ClickHouse | ecto_ch |
ETS | Etso | etso |
基本的な使用例
# スキーマ定義
defmodule Weather do
use Ecto.Schema
schema "weather" do
field :city, :string
field :temp_lo, :integer
field :temp_hi, :integer
field :prcp, :float, default: 0.0
end
end
# クエリ例
query = from w in Weather,
where: w.prcp > 0 or is_nil(w.prcp),
select: w
# パイプ演算子を使ったクエリ
Weather
|> where(city: "Kraków")
|> order_by(:temp_lo)
|> limit(10)
|> Repo.all
プロジェクト構造の特徴
- 現在のバージョン: 3.13.3
- Elixir要件: ~> 1.14
-
主要な機能:
- 型システム(type.ex)
- アソシエーション(association.ex)
- 埋め込みスキーマ(embedded.ex)
- マルチ操作(multi.ex)
- JSON サポート(json.ex)
Ectoは非常に体系的に設計されており、データベース操作を安全で効率的に行うための包括的なソリューションを提供しています。さらに詳しく調べたい特定のコンポーネントはありますか?
...のような形でかなり十分にコードリーディングができるようです。
しかも疑問に応じて対話的に進められます!
例えばこのようなプロンプトを投げました。
- どこから読めばいいかな?
- ecto.ex について教えて
- put_meta の説明をして
寄り道して知らない文法を聞いてみる
また、以下のように自分の知らない文法が出てきた時に、スッと聞くことができます。
該当の行にフォーカスを当てます。
Q. %_{} って何?
A. 選択されている行の %_{}
について説明します。
%_{}
の説明
%_{}
は Elixirの構造体のパターンマッチングで使用される特殊な記法です。
意味
-
%_{}
は「どんな構造体でもマッチする」という意味 -
_
はElixirのワイルドカードパターンで、任意の値にマッチし、その値を無視する
この文脈での役割
def assoc_loaded?(%Ecto.Association.NotLoaded{}), do: false # NotLoadedの場合はfalse
def assoc_loaded?(list) when is_list(list), do: true # リストの場合はtrue
def assoc_loaded?(%_{}), do: true # 任意の構造体の場合はtrue
def assoc_loaded?(nil), do: true # nilの場合はtrue
この関数は関連データ(association)が読み込まれているかどうかをチェックしています:
-
%Ecto.Association.NotLoaded{}
- 明示的に「未読み込み」を表す特別な構造体 →false
-
list when is_list(list)
- リスト(複数の関連データ) →true
-
%_{}
- 任意の構造体(User、Post、Commentなどの実際のデータ) →true
-
nil
- nilの場合 →true
なぜ %_{}
を使うのか?
関連データが読み込まれている場合、以下のような様々な構造体が返される可能性があります:
# 単一の関連データ
%Comment{id: 1, text: "Hello"}
%User{id: 2, name: "Alice"}
%Post{id: 3, title: "My Post"}
# これらすべてに対して true を返したい
%_{}
を使うことで、具体的な構造体の型を指定せずに、「何らかの構造体である」ことだけを確認できます。
他の例
# 具体的な構造体のみマッチ
def process(%User{} = user), do: "処理するユーザー: #{user.name}"
# 任意の構造体にマッチ
def process(%_{} = struct), do: "何らかの構造体: #{struct.__struct__}"
# マップにマッチ(構造体ではない)
def process(%{} = map), do: "普通のマップ"
つまり、%_{}
は「どんな種類の構造体でも受け入れるが、構造体でなければならない」という条件を表現する便利な記法です。
知らない文法を知ることができました!
読み進めてみる
読み進めてみると Ecto の具体的な実行部分は Ecto.SQL になっているようで、さらに MySQL の処理は MyXQL ドライバに移譲されているようです。
AI の解答は以下です。
...
EctoとMySQLの関係は階層的な構造になっています。詳しく説明します。
- 全体のアーキテクチャ
Ecto Core
↓
Ecto.SQL (別パッケージ)
↓
Ecto.Adapters.MyXQL
↓
MyXQL (MySQL ドライバー)
↓
MySQL データベース
実際に読んでみると execute
関数を使っていそうでした。
execute
関数の実体は Ecto.Adapters.MyXQL の実装にあるのでクローンします。
$ git clone git@github.com:elixir-ecto/ecto_sql.git
同じように ecto_sql リポジトリで AI とコードリーディングしていきます。
実装はこのあたりですね。
またここから先を AI と読み解くと、結果的に sql_call
関数に行きつきます。コメントで実際のモジュールを教えてくれるのはうれしいですね。
defp sql_call(adapter_meta, callback, args, params, opts) do
%{
pid: pool,
telemetry: telemetry,
sql: sql, # MyXQL.Connectionモジュール
opts: default_opts,
log_stacktrace_mfa: log_stacktrace_mfa
} = adapter_meta
conn = get_conn_or_pool(pool, adapter_meta)
opts = with_log(telemetry, log_stacktrace_mfa, params, opts ++ default_opts)
args = args ++ [params, opts]
apply(sql, callback, [conn | args]) # 実際のDBドライバー関数呼び出し
end
ということで最終的にはここに行きつきます。MyXQL
モジュールという MySQL を Elixir から操作するライブラリを使って操作しているようですね。
これで「Repo.all してから MySQL とやりとりする箇所までを特定して読んでみる」が達成できました。
やってみて
実際にやってみると Elixir でも十分 AI と一緒にコードを読むことができました。
対話的に進められるので、疑問に対してすぐにフィードバックが生まれ、非常に学習が捗りますね。
学習効率が大幅に上がるので、個人的におすすめです。
良き Elixir ライフを。