LEFログ

:smile

Mugaという名前のプログラミング言語を作っています

Muga という新しいプログラミング言語 作っています。まだ初期の実験段階ですが、仕様と実装を GitHub に公開しました。

https://github.com/lef237/muga

Rust製です。

cargoが使えれば、READMEのQuickstartを読んですぐにプログラムを実行できると思います。

動画

百聞は一見に如かず!ということで動画を用意しました。実際に動いているところが分かります。

youtu.be

https://youtu.be/ndEshgkMVgU

動画内では正常系と異常系の両方を試しています。テキストで貼り付けるとこんな感じです。

$ cargo run -- samples/inferred_types.muga
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/muga samples/inferred_types.muga`
10
10

正常系の場合はちゃんと出力されています。

うまくいかない場合はtype mismatchのエラーもちゃんと出力されます。

$ cargo run -- samples/inferred_types.muga
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/muga samples/inferred_types.muga`
10:12: T002 type mismatch: expected Int, found String

設計判断

Muga の設計にあたっていくつか決めたことがあります。今のところは次の2つ。

基本的に immutable

すべての束縛はデフォルトで immutable。変更したいときだけ mut を明示的に書きます。

x = 1          # immutable
mut total = 0  # mutable
total = total + x

letにするか迷ったのですが、letは言語ごとに意味的なばらつきがあるので、mutableを推測しやすいmutにしました。

shadowing を禁止

同じスコープで同じ名前を再束縛することはできません。これはElmの影響を受けています。

すごく悩んだのですが、決め手となったのはRedditのこのコメントかも。

https://www.reddit.com/r/rust/comments/1e4p5hb/comment/ldgjpp4/?tl=ja&utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

引用は長いのでdetailsに囲みます。

A lot of people are talking about why they think shadowing is fine, but in order to attempt to answer the question:

People don't like shadowing because, in the enterprise context, you often deal with code that is 1) not written by you and 2) a little messier (often longer) than you'd personally like. These two things combine to make shadowing a source of confusion and mental overhead.

Let's take the example you used from another comment:

for rr in sol.sln.rr.iter() {
    for rr in rr.iter() {
        if !rr.is_finite() {
             println!("ERROR: Solution diverged at timestep: {}:{}", iter, m);
             break 'outer;
        }
    }
 }

That's all well and good --- it's fairly easy to see that the rr in the if statement is not the same rr from the initial for loop. However, if this function evolves from what it is now to something like:

for rr in sol.sln.rr.iter() {
    rr.doStuff();
    some_other_function();
    maybe_this_one_takes_rr();
    // ... 10 more lines

    for rr in rr.iter() {
        some_other_calls_unrelated_to_rr();
        // ... 10 more lines, maybe a loop or conditional

        if !rr.is_finite() { // Now --- what is rr and where did it come from? What's its type?
             println!("ERROR: Solution diverged at timestep: {}:{}", iter, m);
             break 'outer;
        }
    }
 }

Now, by the time you get to the if statement inside the nested loop, in order to properly understand which rr you're talking about, you have to carefully read (and keep in your working memory) the entire function context from the start; you can't just skim from the top for the declaration of the variable. Heck, if you do skim from the top, you might end up thinking rr is a totally different type than it actually is! This is particularly awful for immutable values, since you might see an immutable declaration at the start of a function and assume you know to what data that variable name refers, but then it changes over the course of the function (bc now it's shadowed).

You might counter --- "but this is because of bad coding practice; if you kept your functions small and only carefully used shadowing, you wouldn't have this issue!" You're right. But, the same is true about memory errors in unsafe languages, or type errors in languages like Python or Javascript.

Even though careful programmer discipline can prevent many problems, in the long-term, multiple-maintainer context, relying on careful programmer discipline just isn't sound. Instead, people like language features to forcibly reduce mental overhead and the required program context you have to keep "in [human] memory."

コード例

https://github.com/lef237/muga/tree/main/samples

このリンクに色々置いていますが、一番分かりやすいものを貼ってみます。*1

record Counter {
  value: Int
}

fn start(n: Int): Counter {
  Counter {
    value: n
  }
}

fn inc(counter: Counter): Counter {
  counter.with(value: counter.value + 1)
}

fn double(x: Int): Int {
  x * 2
}

fn main(): Int {
  # This chain is equivalent to:
  # double(inc(inc(start(10))).value)
  10.start().inc().inc().value.double()
}

関数をネストすることもできるし、ドットを使ってチェーンもできます。好きな方を自由に選べるようにしています。

recordはC#から影響を受けました。書き換え不可を基本にしたかったので、StructではなくてRecordにしています。

レコード - C# reference | Microsoft Learn

ファイルは.mugaという拡張子にしました。

名前の由来

"muga" は日本語の「無我」から取りました。無我夢中でプログラミングに取り組めるような言語を目指しています。

キャッチフレーズは A quiet programming language.

簡単な現状紹介

だいたいこんな感じです。

  • Rust 実装 (parse / name resolution / type check / HIR / bytecode VM) が動作
  • 18 件のテストをpass
  • ライセンスは MIT
  • 未実装: receiver-style 関数の解決規則、関数型の型注釈 A -> B、など

仕様ドキュメントの方が実装より少し先行していて、spec/ ディレクトリに章ごとの草案があります。

なんでつくりはじめたの?

好きな言語をいいどこ取りした最強のプログラミング言語を作りたくてェ…

これから

まだシンプルな機能しか実装されていませんが……動きます!

(あとでmuga foo.mugaで実行できるようにする予定です)

もしかしたらフィードバックしてくれる方がいらっしゃるかもしれないので早い段階で公開してみました。

設計も実装も徐々に育てていこうと思っています。

気になった点、疑問、設計へのツッコミ、Issue でも pull-req でもなんでも歓迎します。

リポジトリはこちら

github.com

https://github.com/lef237/muga

*1:シンタックスハイライトはいったんRustのものを使っています

macOSでサクッとローカルLLMを動かしてOpenCodeやCodexで使ってみる

サクッとローカルLLMをmacOSで動かしたい方向けの記事です。macbook neoが流行っているので書いてみました。

ノートPCでも動きやすいモデル選びについても自分なりにまとめました。

ちなみに、タイトルにはOpenCodeと書いていますが、Claude CodeやCodexからもローカルLLMを使えます。

Homebrew

Homebrewが入っている前提です。homebrewを知らない方のために念のため説明すると、様々なツールを簡単に管理するためのものです。インストール方法はネット上にたくさん記事があります。

Ollamaを入れて動かすまで

まずはOllamaをインストールします。OllamaはCLIで操作できるローカルLLMツールです。

brew install ollama

インストールできたらサーバーを起動します。

ollama serve

次にモデルを指定して走らせます。

モデルはここからPopularなものを探せます。

ollama.com

BというのはBillionのことで、パラメータの数。パラメータが少ないものは小型のモデルなのでノートPCでも動きやすいです。

サイズはOllamaからページを開くとこのような感じで載っています。

今回はministralを選んでみます。

ministral-3のモデル表

https://ollama.com/library/ministral-3

ministral-3:3bはサイズが小さそうですね。(ざっくりした目安ですが、3b以下だとロースペックなPCでも動きやすいと思います。)

ということで、動かしてみます。

ollama run ministral-3:3b

初回だけダウンロードに時間がかかりますが、そのあとは普通に動かせます。

実際に動かした図

普通に使うぶんにはこれで終わりです。

翻訳などのちょっとしたプロンプトにはこれで充分だと思います。

コーディングエージェントを使う

せっかくローカルLLMを入れたのだから、コーディングエージェントを使いたいですよね。

ということでやっていきます。

まずはOpenCodeを使います。

opencode.ai

homebrewでインストールできます。

brew install anomalyco/tap/opencode

OpenCodeを使えるようになれば後は簡単です。

先ほどのollamaのministral-3のページに次のように表示されていました。

Applicationsの項目

ここに書いてあるとおり、次のコマンドを実行しましょう。

ollama launch opencode --model ministral-3

先ほどと同様、小さいモデルを使いたい場合は、ministral-3:3bを指定してください。

ollama launch opencode --model ministral-3:3b

あとはこのように、OpenCode上でministral-3:3bが使えます。

opencodeでministral-3:3bが表示される

さあ、使ってみましょう。

opencode経由で使ってみた

お、ちゃんとREADMEの内容を読んでくれてそう。

次はコードを書いてくれるかな?

コードを書いて その1

コードを書いて その2

実際に書けてるのか、別のウィンドウを開いて確認してみます。

❯ ruby hello_world.rb
hello world!

OKそう。

Claude CodeとCodexでも動かしてみる

ちなみにClaude CodeやCodexも、先ほどのOllamaのモデル説明のページに書かれたコマンドを実行するだけで使えます。(3bを指定するので少し書き換えます)

ollama launch claude --model ministral-3:3b
ollama launch codex --model ministral-3:3b

参考に、実行した様子の画像も添付しちゃいます。

claude codeで実行した様子

codexで実行した様子

ここまでやって、PCがめちゃくちゃ熱くなってきたので中断しました。

動かした感想

手元のPCで動くのは感動。だけどやっぱり指示を伝えてもうまくいかないことも多くて、モデル次第だなぁ、という印象。

このあとちょっと複雑めのプログラムをお願いしたところ(ちょっとしたunixコマンド)、すぐにハルシネーションを起こしてしまいました。

軽いモデルだと、実用性はあまりないかもしれません。使えるとしても、コードを読み解くのになんとか使える、という感じです。(Readにはギリギリ使えるけど、Writeは厳しそうな印象)

あと、結構頻繁にハルシネーションするし、嘘もつきます。嘘を嘘だと(ry…間違いを間違いだと見抜ける力がないと、扱いきれないかもしれないです。

おもちゃとしては最適です。みなさんもぜひ使ってみてください。

オチ

最速の人間、ハル・マーシャル

地球上の陸上記録: もし「地球上の陸上競技で最速の記録」を指すのであれば、それは陸上競技の100m走で、日本のハル・ マーシャルが持つ 9.58秒 の記録が最速です。

ハル・マーシャルって誰ぞ?

送信前の下書きに使えるエディタ One-Time Editor をリリースしました

One-Time Editorというアプリを作りました。

One-Time Editorの見た目

上のはダークモード。ライトモードだとこんな感じ。

ライトモードの見た目

どんなアプリかというと、チャット送信前の下書きに使えるテキストエディタです。

このアプリを使うことで、Enterで誤って送信を誤爆する心配から逃れられます。

どんな使い勝手かについては動画を見ると速いと思うので、撮影したものを下に添付します。

youtu.be

https://youtu.be/qwj9fr77vQg

インストール方法

macOSの場合はhomebrewを使います。ターミナルで次のコマンドを打つだけですぐに使い始められます。

brew tap lef237/tap
brew install --cask one-time-editor

Electron製のアプリなので、一応クロスプラットフォームにも対応しています。

WindowやLinuxの方は、ビルド済みのアプリをここからインストールできます。

github.com

https://github.com/lef237/one-time-editor/releases

Windowsの方はexe、Linuxの方はAppImageを選択すればたぶんインストールできるはず。*1

特徴

一番のポイントは、ショートカットキーでウィンドウをトグル(表示/非表示)できることです。

しかも、トグルと同時に入力した内容をコピーしてくれるので、一瞬でペーストできます。

  1. ショートカットキーでOne-Time Editorを開く
  2. 送信したい内容を入力する
  3. ショートカットキーで閉じる
    • 自動で内容もコピーされます
    • macOSの場合は、自動で直前に操作していたウィンドウにフォーカスが戻ります
  4. ペーストして、後は送信するだけ

ライトモードとダークモードの切り替えにも対応しています。デザインもお洒落だと思います。

もちろん、ショートカットキーもカスタマイズできます! デフォルトはCtrl+jですが、好きなショートカットに割り当ててください。

なぜ作ったのか

チャットなどの送信で、EnterかShift+Enterか、はたまたCtrl+Enterなのかに悩んだ経験がある方は多いと思います。

そんでもって、文章の作成途中で誤爆してしまったり……

何かしらのエディタを使って下書きを書くのも悪くはないのですが、毎回ファイルを作成するかを聞かれたり、ウィンドウをどう管理するかがやや面倒だったりします。

このアプリの場合は書き捨ての用途で使えるし、1つのショートカットで表示/非表示&内容のコピーをしてくれるので、かなりスピーディかなと。

開発で苦労したところ

いくつかありました。

が、一番難しかったのは、コード署名と公証をどうするか問題でしょう。

Electronでアプリを作ると、手元で動かす分には問題ないのですが、ネットに上げてダウンロードするとアプリが壊れてしまいます。

Dockerなどのダウンロード時に見たことある方もいらっしゃるのではないでしょうか?

このアプリは壊れているため開けません、の表示

なので、xattrコマンドを使う必要があります。

qiita.com

https://qiita.com/quattro_4/items/f5b56c1897c0cc235c0f

つまりインストールしたあとに、このコマンドxattr -rc "/Applications/hoge.app"を打つ必要があります。これはやや面倒です。

シンプルにできないか検討したところ、postflightを使えば、homebrew-tap経由なら自動化できることに気づきました。

Electronアプリを配布したい方は参考にしてみてください!

GitHub

GitHubも公開しています。

追加の機能の要望があればIssueなどお待ちしています。🐘

github.com

https://github.com/lef237/one-time-editor

*1:もしWindows等で動かなかったらGitHubにコメントください!

Rails の find はどう動く? Active Recordの内部実装を見てみよう!

この記事はフィヨルドブートキャンプ Advent Calendar 2025の22日目の記事です。

昨日はodentakashiさんのgem の PR から学んだ ActiveRecord と lease_connection の話でした!

自分もおでんくんに引き続き、ActiveRecordの話題、いっちゃいます🍢

はじめに

皆さんはRuby on Rails使っていますか? Ruby on RailsにはActive RecordというORMがあります。

RailsのActive Recordを使う上で欠かせないのが、『Ruby on Railsガイド(以下Railsガイド)』です。この公式ドキュメントを読むことで、全体像を理解しながら体系的に学ぶことができます。

railsguides.jp

Ruby on Rails ガイド:体系的に Rails を学ぼう

Railsガイドには載っていないような細かい挙動を調べたいときもあります。そんなときは、『Ruby on Rails API』を調べるのが鉄板です。

api.rubyonrails.org

Ruby on Rails API

この2つのドキュメントを参考にするだけで、基本的には困らないのですが……もっと先へ進んでみたくないですか?

そうです。Ruby on Railsそのもののソースコードです。今回は find を取り上げて、このメソッドを読み解いていきましょう。

find ってそもそもなに?

まずはおさらいから。

ActiveRecord の find は 主キー検索 を行うためのメソッドです。

User.find(1)        # id=1 の User を取得
User.find([1, 2, 3]) # 複数レコードも取得可能

シンプルではありますが、内部ではキャッシュの利用、例外の扱い、SQL の組み立てなど様々な処理が行われます。

どこで定義されている?

では、findメソッドはどこで定義されているでしょうか?

GitHubにある rails/rails で調べてみましょう。

github.com

find メソッドは ActiveRecord の FinderMethods モジュールで定義されています。

具体的には次のファイルです。

それでもって、中のコードを読んでいくと、findが定義されています。

def find(*args)
  return super if block_given?
  find_with_ids(*args)
end

おっ、findは find_with_ids を呼んでいますね。今度はこのメソッドを見てみましょう。

  def find_with_ids(*ids)

    # 中略

    case ids.size
    when 0
      error_message = "Couldn't find #{model_name} without an ID"
      raise RecordNotFound.new(error_message, model_name, primary_key)
    when 1
      result = find_one(ids.first)
      expects_array ? [ result ] : result
    else
      find_some(ids)
    end
  end

このメソッドの最後の方を見ると、idsのサイズが0のときはエラー、1のときはfind_one、それ以外のときはfind_someを呼んでいます。

つまり、findfind_oneかfind_someを呼んでいる!ということです。*1

両方とも追いかけるとややこしいので、今回はidを一つしか渡さないと仮定して、find_oneを潜っていきましょう。

find_oneはこのように定義されています。

  def find_one(id)

    # 中略

    relation = if model.composite_primary_key?
      where(primary_key.zip(id).to_h)
    else
      where(primary_key => id)
    end

    record = relation.take

    raise_record_not_found_exception!(id, 0, 1) unless record

    record
  end

複合キーではない場合と仮定して、ものすごくざっくりと解説してみますが、つまり、whereメソッドで条件に合うものを検索して、そのなかから一つだけを take する。これがfind_oneがやっていることです。

whereはどうなってる?

つまりfindは内部的にwhereを呼んでいることが、Railsの実装を見ていくとわかります。

次にwhereを紹介していきます。*2

whereメソッドはこのファイルで定義されています。

具体的なコードだとここになります。

def where(*args)
  if args.empty?
    WhereChain.new(spawn)
  elsif args.length == 1 && args.first.blank?
    self
  else
    spawn.where!(*args)
  end
end

引数はempty?でもblank?でもないはずなので、とりあえずwhere!を見てみましょう。

def where!(opts, *rest) # :nodoc:
  self.where_clause += build_where_clause(opts, rest)
  self
end

どうやら、build_where_clauseっていうメソッドの結果を、どんどんオブジェクトに追加していっているらしいということがわかります。

build_where_clauseへ移動します。

  def build_where_clause(opts, rest = []) # :nodoc:
    opts = sanitize_forbidden_attributes(opts)

    if opts.is_a?(Array)
      opts, *rest = opts
    end

    case opts
    when String
      if rest.empty?
        parts = [Arel.sql(opts)]
      elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
        parts = [build_named_bound_sql_literal(opts, rest.first)]
      elsif opts.include?("?")
        parts = [build_bound_sql_literal(opts, rest)]
      else
        parts = [Arel.sql(model.sanitize_sql([opts, *rest]))]
      end
    when Hash
      opts = opts.transform_keys do |key|
        if key.is_a?(Array)
          key.map { |k| model.attribute_aliases[k.to_s] || k.to_s }
        else
          key = key.to_s
          model.attribute_aliases[key] || key
        end
      end
      references = PredicateBuilder.references(opts)
      self.references_values |= references unless references.empty?

      parts = predicate_builder.build_from_hash(opts) do |table_name|
        lookup_table_klass_from_join_dependencies(table_name)
      end
    when Arel::Nodes::Node
      parts = [opts]
    else
      raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
    end

    Relation::WhereClause.new(parts)
  end
  alias :build_having_clause :build_where_clause

一気に難しくなってきました。

この引数に、例えば文字列を渡すとします(User.where("age > 20")みたいなやつ)。すると、when Stringの条件に入ります。

    when String
      if rest.empty?
        parts = [Arel.sql(opts)]
      elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
        parts = [build_named_bound_sql_literal(opts, rest.first)]
      elsif opts.include?("?")
        parts = [build_bound_sql_literal(opts, rest)]
      else
        parts = [Arel.sql(model.sanitize_sql([opts, *rest]))]
      end

するとparts = [Arel.sql(opts)]が呼ばれるはず。そして、[Arel.sql("age > 20")]がpartsの中に入ります。*3

最後にRelation::WhereClause.new(parts)が呼び出され、WhereClause オブジェクトが生成されます。

この WhereClause オブジェクトが、Relation オブジェクトの where_clause フィールドに+=でどんどん貯まっていき、最終的にはSQLに変換されて、実行されます。*4

SQLに変換される仕組みや、実行される仕組みまで解説すると、かなり長くなってしまうので、このあたりで止めたいと思います。

おわりに

いかがでしたでしょうか? 後半はちょっとむずかしくなってしまいましたが、前半は分かりやすかったと思います。

RailsRubyのコードでできています。なので、Rubyに慣れて親しんでいれば、Railsのコードは理解できるようになっています。

もし、公式ドキュメントなどを読んでも分からない挙動に遭遇した場合は、Railsそのもののソースコードを読んでみると、原因を特定できるかもしれません。*5

コードを読み解くことで、勉強になる面は多いですし、貢献できる可能性もあります。みなさんもぜひ、トライしてみてください!


明日のAdvent Calendarは ゆーかさんと mh-mobile さんです。🎄

*1:ブロック無しで ID を渡した一般的なケースにおいて

*2:コードが難しいので、自信がない箇所もあります。間違いがあったらごめんなさい。🐧

*3:Arelについては、この記事が詳しく解説していてオススメです。→https://www.timedia.co.jp/tech/activerecord-arel-5-1/

*4:自分の理解が間違っていなければ、WhereClause が積み重なった Relation が、最終的に Arel を通して SQL に変換されて、take や to_a などで DB に対して実行されるはずです。

*5:実際の業務でも、理解を深めるために元のソースコードを読む場面はたびたびありました。

RustでCSVをJSONに変換するツールをリリースしました

概要

最近、Rustを使って、CSVJSONに変換するツールをリリースしました。

ctj - crates.io: Rust Package Registry

cargo install ctjですぐインストールして使うことができます。

2025-07-17現在のバージョンは0.1.8。ちょこちょこ機能を追加しています

このツールを作った理由について、簡単に書いてみたいと思います。

作った理由

一言でいうと、ヘッダ行がないCSVファイルにも対応をしたかったのが一番大きな理由です。他のツールを使うと情報が欠落することがありました。

また、CSVの真偽値や数値について、ツール側で文字列に変換されるのが困っていました。

具体的には…?

,b,
,,FALSE
,55.5,

このようなファイルがあったとき、別のツールを使うと次のような結果になります。

[
  {
    "": "FALSE",
    "b": ""
  },
  {
    "": "",
    "b": "55.5"
  }
]

大文字のFALSEがそのまま文字列として扱われています。また、55.5という数値も文字列になってしまっています。

真偽値と数値はそのままJSONで扱えるので、できればその型情報を識別し、同じように展開したいです。

それでは自分が新たに作成したツールを紹介しましょう。このctjを使うと、次のような結果になります。

[
  {
    "": false,
    "b": ""
  },
  {
    "": "",
    "b": 55.5
  }
]

FALSEfalseに、55.5は数値(float)として扱われます(小数点が無ければintegerとして処理されます)。

また、今回のCSVファイルはヘッダ行がありません。このような状況は実際にあり得ると思います。ヘッダ行がないと、情報の欠落が生じるリスクが高まります。

そのユースケースに対応するため、オプションを用意しました--no-headerをつけると、ヘッダ行がないことを ctj に伝えられるため、このように出力できます。

[
  {
    "column_0": "",
    "column_1": "b",
    "column_2": ""
  },
  {
    "column_0": "",
    "column_1": "",
    "column_2": false
  },
  {
    "column_0": "",
    "column_1": 55.5,
    "column_2": ""
  }
]

何列目にどの情報が入力されていたのか、すぐに把握できます。

無事に変換時の情報の欠落を防ぐという目的を達成することができました!

おわりに

あえて機能はシンプルにしつつ、実務に活かせるように設計してみました。すでにあるCSVファイルだけでなく、パイプで渡した文字列を処理することも可能です。

CSVJSONに変換するツールを探している方は、ぜひ使ってみてください。コントリビュートも歓迎です!

github.com

開発生産性Conference 2025に行きました!

グッドハートの法則を説明するKent Beck

開発生産性Conference2025に行きました!

何よりも嬉しかったのが、やはりKent Beckさん御本人にお会いできたことです。自分がプログラミングの学習をし始めてから、アジャイルソフトウェア開発宣言をはじめとして、色んなところで名前を聞いていた伝説的な人物の講演を、肉眼で見ることができたのは僥倖でした。

発表はほとんどスライドを使わず、身振り手振りを用いながら行われており、壇上の左から右へと歩きながら喋っていくスタイルでした。

内容については、jgeemさんやginkounoさんがまとめてくださった、こちらの資料に詳しく書いてあります。

scrapbox.io

note.com

「プレッシャーではなく、気づきを促す」。自分も将来教育する立場に回ったときは、このことを強く意識したいと思いました。

講演のあとにはサイン会がありました。拙い英語でしたが、直接Kent Beckさんに感謝を伝えることができて嬉しかったです。timer.teamのカードも直接お渡しできました。

(サイン会のときにはできなかったのですが、翌日たまたまKent Beckさんが会場の廊下で佇んでいて、握手してもらいました!🤝)

また、お一人お一人を挙げると長くなってしまうので省略しますが、他のイベントや仕事でもお世話になっているRubyistの方々にお会いすることもできました。RubyAgileの結びつきについて再確認する機会になりました。

今回、このような貴重な機会に参加することができて、本当に良かったです。イベントを主催してくださった皆様、お話ししてくださった皆様、ありがとうございました。

講演中のKent Beck

サイン会。timer.teamのプレゼントカードを受け取ってもらえて感無量です

Railsで別ファイルに切り出さずに、Viewのプライベートなpartialを実装する

結論

captureメソッドとprocを使おう!

経緯

部分テンプレート(partial)はとても便利な機能ですが、使いすぎるとコードが少し散らかってしまいます。

たった1つのViewファイルで使うためだけの場合でも、_foo.html.erb_bar.html.erbといったファイルが増えてしまいます。それにもかかわらず、これらのファイルはどのViewからも呼び出せてしまいます(スコープが広いです)。

また、複数のファイルを行ったり来たりする手間もかかります。

例えばReactの場合では、単一ファイルの中でコンポーネントを分割することができます。このような機能をRailsのView(erb, haml, slim)でも実装する方法があるので共有します。

実装方法

結論に書いたように、captureメソッドとprocを使います。

captureメソッドについてはRailsガイドのこちらに説明があります。

railsguides.jp

captureメソッドを使うと、以下のようにテンプレートの一部を抽出して変数にキャプチャできます。

このcaputreメソッドを、procと組み合わせます。

docs.ruby-lang.org

2つを組み合わせて、次のようなコードにします。

<%# --- 定義:UIの断片を Proc オブジェクトに束ねる --- %>
<% row = proc do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= user.email %></td>
  </tr>
<% end %>

<%# --- 呼び出し: capture ヘルパーで Proc を評価・描画する --- %>
<table>
  <% @users.each do |user| %>
    <%= capture(user, &row) %>
  <% end %>
</table>

この方法によって、UIの断片を別ファイルに切り出すことなく、プライベートなpartialを実現できます。

もちろん、次のように複数の引数を渡すことも可能です。

<% row = proc do |user, book| %>
  <tr>
    <td><%= user.name  %></td>
    <td><%= book.title %></td>
  </tr>
<% end %>

<%= capture(current_user, featured_book, &row) %>

具体例

例えばこのようなerbを書きます。

<h1>Products</h1>

<%# --- 定義:テンプレート断片を Proc に束ねる --- %>
<% render_product = proc do |product| %>
  <div class="product">
    <h2><%= product.name %></h2>
    <p><%= product.description %></p>
    <span>¥<%= product.price %></span>
  </div>
<% end %>

<%# --- 1回目の呼び出し:通常の一覧表示 --- %>
<div id="products">
  <% @products.each do |product| %>
    <%= capture(product, &render_product) %>
  <% end %>
</div>

<%# --- 2回目の呼び出し:おすすめ商品セクション --- %>
<h2>Recommended Products</h2>
<div id="recommended">
  <% @products.select(&:recommended?).each do |product| %>
    <%= capture(product, &render_product) %>
  <% end %>
</div>

データを用意してCSSをつけると、次の画像のように表示されます。

プライベートなpartialが動いている画面

おわりに

この方法が一般的かは分かりませんが、一つのファイル内でプライベートなpartialを実装したいときに便利です。外部のGemや、複雑な実装をおこなうことなく、RubyRailsの基本的な機能のみで実現できます。

(もしかしたらもっと良い方法があるかもしれないので、お気軽にコメントください!)

えにしテック内のSlackでtmaedaさんとdarashiさんに頂いたコメントが、この記事のアイデアの元でした。この場を借りて感謝いたします🙏