しんめ.rb は 2026年7月で定期開催を終了します

2年ほど開催を続けてきたしんめ.rb ですが、私がエンジニアにキャリアチェンジして丸3年を迎える2026年7月をもって、定期開催を終了することにしました。 RubyKaigi ハイ?で周囲にはこれからも続けます!という話をしたりしていたのですが、少し時間が経って落ち着いてきたところで考えを整理して、そろそろ「しんめ」を名乗る時期は卒業かなぁと薄々感じていたこともあって、一区切りをつけることにしました。

約2年の間にたくさんの方に参加いただき、現時点で通算66回開催をすることができました。自分が一人で運営していたコミュニティだったため、準備不足だったり自分の理解不足だったりでうまく進行できなかった回もあったりしました。ただ、毎回雑談の時間だったり勉強テーマについての質問を通じてだったり、しんめ.rb をやっていたからこその楽しい会話や気づきがありました。そして、仕事や自分が行ける範囲のオフラインのイベントやカンファレンスのみでは知り合うことができなかったであろう Rubyist のみなさんと出会えたことが何よりも自分でコミュニティを主催してよかったことです。

残り2か月ほどありますが、6月と7月は各1回ずつミートアップを開催しようと思っています。

  • 6月22日(月)19時半〜21時※時間が違うのでお気をつけください!
  • 7月27日(月)21時〜22時半

6月はまつもとゆきひろさんが開発されている Splinel を知る回、7月は座談会をやろうと考えています。

ご都合が合う方はぜひご参加いただけると嬉しいです!

しんめ.rb としてはコミュニティの開催に区切りをつけますが、私はみんなでワイワイしながらプログラミングしたり技術について雑談するのがやっぱり好きなので、形を変えて?なにかできたらいいなという思いはありつつ、今年は最近参加できていない他のコミュニティや地域 Ruby 会議に参加していこうと思っています。

これまでしんめ.rb に関わってくれた方々に感謝を込めて。 2年間ありがとうございました!!

Require Hooks に興味津々

RubyKaigi 2026 で palkan さんが発表されていた Require Hooks が自分の中ですごく印象的でした。

speakerdeck.com

Ruby には現在 require を拡張するための機能が存在しないため、require の瞬間に何かしたいみたいな場合に、monkey patch で実装依存になってしまいます。 この課題に対して Require Hooks が require 拡張ポイントを提供することで共通のしくみとして使えるようにしたという発表内容でした。

require を拡張するしくみを Ruby のレイヤーから実現しているという点も、そういうアプローチができるのか!と強く印象に残った理由の一つになっています。

というわけで、Require Hooks に興味津々なため実際に Rails アプリに入れて試したことを簡単にまとめてみます。

準備

maimux2x.hatenablog.com

こちらの記事で触れた自分用の Rails アプリで試してみました。

まず、README にある通りに導入準備をします。

github.com

# Gemfile
gem 'require-hooks'

bundle install を実行して、config/ 以下にファイルを作成します。

# config/require_hooks.rb

require 'require-hooks/setup'

作成したファイルを application.rb で読み込みます。

# config/application.rb


require_relative 'boot'

require 'rails/all'

Bundler.require(*Rails.groups)

require_relative 'require_hooks' # 追記

直接 application.rb で require 'require-hooks/setup' を追加しても良かったのですが、検証用にコードを追加したりするため、分かりやすいように分けました。

Require Hooks には3種類のフックがあります。

フック 内容
around_load ファイルのロード前後に処理を挟む
source_transform ソースコードを書き換えてからロードする
hijack_load ISeq(命令列)を自分でコンパイルして渡す

これをそれぞれ試していきたいと思います。

around_load

まずは around_load から。 app/ 以下のファイルがロードされるタイミングでログにパスを出力するようにします。

先ほど作成した require_hooks.rb に以下を追記します。

app_pattern = File.expand_path("../app/**/*.rb", __dir__)


RequireHooks.around_load(patterns: [app_pattern]) do |path, &block|
  puts "[require-hooks] Loading: #{path}"
  result = block.call
  puts "[require-hooks] Loaded:  #{path}"
  result
end

変更を保存してサーバーを起動して localhost:3000 にアクセスすると以下の出力が得られます。

[require-hooks] Loading: .../app/controllers/homes_controller.rb
[require-hooks] Loading: .../app/controllers/application_controller.rb
[require-hooks] Loading: .../app/helpers/application_helper.rb
[require-hooks] Loaded:  .../app/helpers/application_helper.rb
[require-hooks] Loading: .../app/controllers/concerns/authentication.rb
[require-hooks] Loaded:  .../app/controllers/concerns/authentication.rb
[require-hooks] Loaded:  .../app/controllers/application_controller.rb
[require-hooks] Loaded:  .../app/controllers/homes_controller.rb

Processing by HomesController#show as HTML

[require-hooks] Loading: .../app/models/current.rb
[require-hooks] Loaded: .../app/models/current.rb
[require-hooks] Loading: .../app/models/session.rb
[require-hooks] Loading: .../app/models/application_record.rb
[require-hooks] Loaded:  .../app/models/application_record.rb
[require-hooks] Loaded:  .../app/models/session.rb
  Session Load (0.1ms)  SELECT "sessions".* FROM "sessions" WHERE "sessions"."id" = 2 LIMIT 1 /*action='show',application='Yondayo',controller='homes'*/
  ↳ app/controllers/concerns/authentication.rb:29:in 'Authentication#find_session_by_cookie'
[require-hooks] Loading: .../app/models/user.rb
[require-hooks] Loaded:  .../app/models/user.rb
  User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 /*action='show',application='Yondayo',controller='homes'*/
  ↳ app/models/current.rb:3:in 'Current#user'
[require-hooks] Loading: .../app/models/reading.rb
[require-hooks] Loaded:  .../app/models/reading.rb

development 環境で最初のリクエスト時に Zeitwerk によってオートロードされた app/ 以下のファイルが確認できます。 ここで面白かったのはログのフェーズが2段階に分かれていることで、Processing by HomesController#show as HTML を境に異なるファイルがロードされていることが分かります。

HomesController#show アクションの実行前は homes_controller.rb のロード中に application_controller.rb がロードされ、さらにその中で application_helper.rb → authentication.rb と連鎖しています。

アクション実行中は current.rb ロード実行と完了、session.rb のロード中に application_record.rb を内部でロードして session.rb ロード完了、user.rb と reading.rb のロード実行とロード完了という流れが読み取れます。

パスをログに出すことができたのでファイルのロード時間をログに出力してみます。

先程のコードを以下のように修正します。

RequireHooks.around_load(patterns: [app_pattern]) do |path, &block|
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  result = block.call
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
  Rails.logger.debug format("[require-hooks] %.4fs %s", elapsed, path)
  result
end

サーバーを再起動してブラウザのページをリロードします。

[require-hooks] 0.0006s .../app/helpers/application_helper.rb
[require-hooks] 0.0002s .../app/controllers/concerns/authentication.rb
[require-hooks] 0.0017s .../app/controllers/application_controller.rb
[require-hooks] 0.0022s .../app/controllers/homes_controller.rb
Processing by HomesController#show as HTML
[require-hooks] 0.0008s /.../app/models/current.rb
[require-hooks] 0.0033s ...app/models/application_record.rb
[require-hooks] 0.0047s .../app/models/session.rb
  Session Load (0.1ms)  SELECT "sessions".* FROM "sessions" WHERE "sessions"."id" = 2 LIMIT 1 /*action='show',application='Yondayo',controller='homes'*/
  ↳ app/controllers/concerns/authentication.rb:29:in 'Authentication#find_session_by_cookie'
[require-hooks] 0.0076s .../app/models/user.rb
  User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 /*action='show',application='Yondayo',controller='homes'*/
  ↳ app/models/current.rb:3:in 'Current#user'
[require-hooks] 0.0010s .../app/models/reading.rb

ファイルのパスをログに出力した際にあるファイルをロード中に別のファイルがロードされていたと思いますが、この計測時間はそのネストしたロードを含む累積時間を指しています。 そのため、session.rb 自体のコードの評価にかかった時間は application_record.rb の時間を差し引いた 0.0047 - 0.0033 = 0.0014s ほどということが分かります。

user.rb はネストしていないため、単体ファイルとして一番ロードに時間がかかったファイルということが読み取れます。関連やバリデーションの定義が多いからかな。

ここまでで around_load でできることのイメージが具体的になったのではないでしょうか?試したことはシンプルですが、ロードのタイミングを可視化できて面白かったです。

source_transform

次は source_transform です。

RubyKaigi 2026 の発表でも触れられていた frozen_string_literal を自動挿入してみようと思います。

モデルに定数を定義して source_transform を使用する前後の結果を比較してみます。

# app/models/reading.rb

class Reading < ApplicationRecord
  TEST_STR = "hello"
end

rails console で Reading::TEST_STR.frozen? の結果を確認します。

> Reading::TEST_STR.frozen?
=> false

続いて、require_hooks.rbを以下のように修正してみます。

RequireHooks.source_transform(
  patterns: [app_pattern],
  exclude_patterns: [File.expand_path("../app/views/**/*", __dir__)]
) do |path, source|
  source ||= File.read(path)
  next source if source.start_with?("# frozen_string_literal: true")

  "# frozen_string_literal: true\n#{source}"
end

もう一度 rails console を起動して結果を確認すると

> Reading::TEST_STR.frozen?
=> true

true になりました!!

別の使い方として、test.book というファイルに本の名前を列挙して保存しておき、それを source_transform を使って Ruby コードに変換することを試してみようと思います。

# test.book
BOOK1
BOOK2
BOOK3

require_hooks.rb を以下のように修正します。

book_pattern = File.expand_path("../*.book", __dir__)

RequireHooks.source_transform(patterns: [book_pattern]) do |path, source|
  source ||= File.read(path)
  books = source.lines.map(&:chomp).reject(&:empty?)
  class_name = File.basename(path, ".book").capitalize
  books_literal = books.map { |b| "    \"#{b}\"" }.join(",\n")
  ruby_code = <<~RUBY
    class #{class_name}
      BOOKS = [
    #{books_literal}
      ]
    end
  RUBY
  ruby_code
end

load File.expand_path("../test.book", __dir__)

source_transform はロードのタイミングで発火するフックのため、変換処理を登録するだけでは駄目で load で実際にファイルを読み込む必要があります。 検証用のコードとして一時的に置いています。

保存して rails console で Test::BOOKS の結果を見てみます。

> Test::BOOKS
=> ["BOOK1", "BOOK2", "BOOK3"]

test.book ファイルの内容が BOOKS という定数に配列として格納されています!

ここで面白いのは test.book は Ruby として無効なファイルなのに load ができることです。

RequireHooks.source_transform のブロックを消して load の行だけ残して rails console を起動しようとするとエラーになります。

uninitialized constant BOOK1 (NameError)

BOOK1
^^^^^

source_transform が test.book が Ruby コードとして評価される前にソースを差し替えていることが分かる例で、ロードの前後にフックする around_load との違いも理解できます。

hijack_load

最後は hijack_load です。

hijack_load は MRI や JRuby などの実装に応じてコンパイル方法を切り替えることで、たとえば require-hooks の README では以下ような使い方が書かれています。

# Pattern can be a Proc. If it returns `true`, the hijacker is used.
RequireHooks.hijack_load(patterns: ["/my_project/*.rb"]) do |path, source|
  source ||= File.read(path)
  if defined?(RubyVM::InstructionSequence)
    RubyVM::InstructionSequence.compile(source)
  elsif defined?(JRUBY_VERSION)
    JRuby.compile(source)
  end
end

今回は学習用に ISeq を取得してみることを試しました。

require_hooks.rb を修正します。

RequireHooks.hijack_load(patterns: [app_pattern]) do |path, source|
  source ||= File.read(path)
  iseq = RubyVM::InstructionSequence.compile(source, path, path, 1)
  puts "[hijack] Compiled #{File.basename(path)}#{iseq.inspect}"
  iseq
end

サーバーを再起動してブラウザのページをリロードします。

[hijack] Compiled homes_controller.rb → <RubyVM::InstructionSequence:<compiled>@.../app/controllers/homes_controller.rb:1>
[hijack] Compiled application_controller.rb → <RubyVM::InstructionSequence:<compiled>@.../app/controllers/application_controller.rb:1>
[hijack] Compiled application_helper.rb → <RubyVM::InstructionSequence:<compiled>@.../app/helpers/application_helper.rb:1>
[hijack] Compiled authentication.rb → <RubyVM::InstructionSequence:<compiled>@.../app/controllers/concerns/authentication.rb:1>
Processing by HomesController#show as HTML
[hijack] Compiled current.rb → <RubyVM::InstructionSequence:<compiled>@.../app/models/current.rb:1>
[hijack] Compiled session.rb → <RubyVM::InstructionSequence:<compiled>@.../app/models/session.rb:1>
[hijack] Compiled application_record.rb → <RubyVM::InstructionSequence:<compiled>@.../app/models/application_record.rb:1>

ISeq を取得してログに出せたわけですが、正直よくわからない・・・。 Claude に質問してみたところ、逆アセンブルしてみると面白いかもしれないとのことで試してみました。

require_hooks.rb を少し修正します。

RequireHooks.hijack_load(patterns: [app_pattern]) do |path, source|
  source ||= File.read(path)
  iseq = RubyVM::InstructionSequence.compile(source, path, path, 1)
  puts iseq.disasm if File.basename(path) == "book.rb"
  iseq
end

もう一度サーバーを再起動すると、逆アセンブルした結果をログに出力できました!

== disasm: #<ISeq:<compiled>@.../app/models/book.rb:1 (1,0)-(5,3)>
0000 putspecialobject                       3                         (   1)[Li]
0002 opt_getconstant_path                   <ic:0 ApplicationRecord>
0004 defineclass                            :Book, <class:Book>, 16
0008 leave

== disasm: #<ISeq:<class:Book>@.../app/models/book.rb:1 (1,0)-(5,3)>
0000 putself                                                          (   2)[LiCl]
0001 putobject                              :readings
0003 putobject                              :destroy
0005 opt_send_without_block                 <calldata!mid:has_many, argc:2, kw:[dependent], FCALL|KWARG>
0007 pop
0008 putself                                                          (   4)[Li]
0009 putobject                              :title
0011 putobject                              true
0013 opt_send_without_block                 <calldata!mid:validates, argc:2, kw:[presence], FCALL|KWARG>
0015 leave                                                            (   5)[En]

内容は全然わからないため、Claude に解説してもらいました。

== disasm: #<ISeq:<compiled>@<compiled>:2 (2,0)-(6,3)>
0000 putspecialobject  3                  # クラス定義に必要な特殊オブジェクトをスタックに積む
0002 opt_getconstant_path  <ic:0 ApplicationRecord>  # ApplicationRecord 定数を取得
0004 defineclass  :Book, <class:Book>, 16  # Book クラスを定義(内側の ISeq を使う)
0008 leave                                 # 終了
== disasm: #<ISeq:<class:Book>@<compiled>:2 (2,0)-(6,3)>
0000 putself                              # self(Book クラス)をスタックに積む
0001 putobject  :readings                 # :readings をスタックに積む
0003 putobject  :destroy                  # :destroy をスタックに積む
0005 opt_send_without_block  has_many, argc:2, kw:[dependent]  # has_many :readings, dependent: :destroy を呼ぶ
0007 pop                                  # 戻り値を捨てる
0008 putself                              # self をスタックに積む
0009 putobject  :title                    # :title をスタックに積む
0011 putobject  true                      # true をスタックに積む
0013 opt_send_without_block  validates, argc:2, kw:[presence]  # validates :title, presence: true を呼ぶ
0015 leave                                # 終了

ちなみに app/models/book.rb の内容はこうなっています。

class Book < ApplicationRecord
  has_many :readings, dependent: :destroy
  validates :title, presence: true
end

本来の用途ではないですが、hijack_load で ISeq を逆アセンブルすると ISeq の命令列を見ることができるため、JIT の基礎を知ることに繋がったりするかな?と思ったりしました。

まとめ

Require Hooks の3つのフックについてそれぞれ試して何ができるのかを整理してみました。 試しながら結果を調べる過程で私はだいぶワクワク楽しく過ごせました。

もう少し実践的に活用できるシーンも考えてみたいと思います。

車輪の再発明だけど、最近よく使っている自作アプリ

RubyKaigi 2026に参加してまつもとゆきひろさんのキーノートを聞いた影響で、私もいろいろ作るぞという気持ちが高まっている。

一方で最近、自分の中でアウトプットのハードルがどんどん上がってしまっていて、全くブログを書いたりできていなかった。今日はアウトプットのハードルを下げるために、最近作った車輪の再発明だけどよく使っている読書記録アプリについて書いておこうと思う。

github.com

私は読書が好きで、いろいろな本を読むため「よんだよ」という自分が読んだ本を記録しておくためのWebアプリを作った。基本的には自分が使うためを想定していて、ログインしないと内容は見れないようになっている。 できることは、Google Books API で書籍を検索して読んでいる本を選択し、読書の進捗状況、読書中のメモ、読み終わった後の感想を記録できるようになっている。 作っている途中で元々自分が使っていた別の読書記録アプリがSNSで読書記録をシェアするときにOGPが表示されないことに気がついて、自分が作っているアプリの方でOGPありでシェアできたらいいなぁと思って公開用のリンクを別途取得できるようにした。

本の検索は、タイトル・著者・ISBN でできるようにしてある。

結果は複数ヒットするため、該当の本を選択する。

メモを取りつつ読み進めて読み終わったら感想を入れて更新して使っている。

公開用のリンクは読書記録の詳細画面から取得できるようになっていて、ログインが必要な詳細画面とは分けて違う画面として用意した。

公開用のリンクからは書籍のタイトルと進捗状況、感想を見れるようになっている。

yondayo.maimux2x.com

これだけではあるけど、デプロイした後も何気に使い続けていて、本の記録が増えてくる前に検索機能を追加しなきゃとかやっぱり書影を入れたい・・・とか改善したい部分がまだまだある。自分の趣味だったりに関係するちょっとしたお役立ちツールを自作するとちゃんと使うんだなということを実感している。

元々は書影を保存して表示させていたのだけれど、公開用のページを設けたり OGP を出す際に著作権的にグレーだなとなって、書影は保持しない形に修正して使っている。あったほうがリッチな感じにはなるんだけど、個人利用のアプリなのでなくても困らないという点でどうしようかな〜と迷い中。

作る過程でのあれやこれやは気が向いたら別のブログに書く!

このくらいのゆるさで作ったことだったり学んだことだったりの記録を気軽に続けていくぞ・・・!

RubyKaigi 2026 に参加した

函館で開催された RubyKaigi 2026 に参加してきました。

rubykaigi.org

RubyKaigi 2023, 2025 に続いて3回目の参加でした。

今回は Day0 の移動日に風邪で体調を崩してしまいドリンクアップの類は全部キャンセル、観光は一切せず帰宅・・・という心残りな参加回になってしまいました。とはいえ、本編には参加することができ、RubyKaigi の現地の熱気を感じることができたのはとても良かったです。

本編では AI に関連する発表が増えていたり、Ruby Committers and the World でも AI を使うことについてで盛り上がっていましたが、私は RubyKaigi のトークの予習を Claude と一緒に取り組んで臨みました。Claude に自分が聞きたい発表の詳細ページの URL を渡して、発表前に押さえておくと良い用語や関連資料・過去の発表動画などを整理してもらって可能な範囲で自分でも調べたり資料に目を通しておいた感じです。ZJIT 関連の発表はそれでも難しくて分からないことだらけでしたが、Claude にまとめてもらった内容や事前に読んだり聞いておいた資料のおかげでこれまで参加した RubyKaigi の中で一番自分の中に引っかかる部分を感じながら聞くことができたと実感しています。

今回聞いた発表はこちら。

Day1

Day2

Day3

今回は発表メインでたくさん聞きました! たくさん聞いたこともあって、個々の発表の感想は書ききれないのですが Oku Kazuho さんの 「Rapid Start: Faster Internet Connections, with Ruby's Help」、Vladimir Dementyev さんの「Require Hooks: Filling the Gap in Ruby's Extensibility」、Marco Roth さんの「HTML-Aware ERB: The Path to Reactive Rendering 」はもっと深ぼってみたい点や紹介されていたライブラリを試してみたかったり、特に印象に残った発表でした。

また、英語の発表を今年は積極的に聞きに行きました。昨年は英語の発表を聞くのは内容についていくことができずかなり厳しかったのですが、ゆるゆると英語の学習を1年続けた成果?と Claude との予習の効果もあってか、理解できる部分も増えていたのが嬉しかったです。一方で会話の方は全くダメだったので、次の1年は英会話にもちょっとだけ力を入れたい。一番力を入れたいのは プログラミングなので、英語はほどほどにやっていこうと思います。

最後に

まつもとゆきひろさんが AI を使って自分が欲しかったものをたくさん作って、実際に使って日々の生活で活用されていたり、spinel の開発についてを発表で聞いて、私もこれから1年間色々なものを作って試していきたいという気持ちが強くなっています。私はまだレベル感的に AI をバリバリ使いこなして何かを作るというより、自分で四苦八苦・試行錯誤しながら手を動かして開発したい気持ちの方が強いため、しばらくはその気持ちを大切にしつつ楽しく Ruby でプログラミングをしていきたいと思います!

発表者の皆様、オーガナイザー、スタッフの皆様、今年も素敵な RubyKaigi をありがとうございました! また、体調を気遣って声をかけてくれた Rubyist の方々も本当にありがとうございます。

RubyKaigi 2026 、体調や移動のトラブルなど色々なことがありましたが、参加することができて良かった!!

振り返ると良い1年だった

2025年は熱を出して寝込む回数が多かったり、自宅の鍵紛失未遂だったり、人見知りを拗らせまくったり、思い出すと色々あったのですが、それでも1年間たくさんの人と関われて、たくさんコードを書いて、いろいろな場所に出かけて・・・振り返ると充実した良い1年だったなと思います。

今年も1年、ありがとうございました!

トピックごとにざーっと振り返りをしようと思います。

カンファレンス参加

気がついたらたくさんのカンファレンスに参加していました。 2026年はどうなるかな。

  • 東京Ruby会議 12
  • TokyoWomen.rb
  • RubyKaigi 2025
  • 関西Ruby会議 08
  • PicoRuby overflow
  • Kaigi on Rails 2025
  • VimConf small 2025
  • TokyuRuby会議 16
  • 北陸Ruby会議01

しんめ.rb

途中お休み期間をいただきましたが、12月に通算60回目まで開催を続けることができました。今年もたくさんの方が参加してくださり、ありがとうございました! 8月から開催を再開したのですが、開催頻度を減らしたのと Discord や esa を使うなどツールを見直してみたのが結果的には良かったです。

地域 Ruby 会議などに参加すると、しんめに参加してくださっている方と直接会える機会も多く、自分がせっせとカンファレンスに行く動機の一つになっていたりします。

登壇

TokyoWomen.rb で「Rails 1.0 のコードで学ぶ method_missing と find_by_* 」というタイトルで発表しました。 あと、LTだと TokyuRuby会議 16 で「Gentoo 1年生 ビルドは終わらない」というタイトルで発表しました。

Gentoo Linux を使い始める

2025年の一番大きな変化はこれだと思う。 Gentoo Linux を使い始めて、Web サービスを作るために RubyRails を学んで成長したい!に1点集中だった自分の気持ちに新しい領域が加わりました。 ここはまだふわふわしていて具体的に表現できないのですが、OS や CLI 周りへの興味がとても強くなっています。Linux デスクトップを使うようになって、ターミナルや Zsh の設定も色々試して自分はどう使っていきたいのかを前よりも考えるようになりました。

仕事

一番頑張った。まだまだ自信はないし未熟者ですが、2025年に頑張ることとして挙げた「Webサービスの開発に必要な基礎体力を鍛える」は周りの方々にたくさん助けていただきながら概ね達成できたと思います。

maimux2x.hatenablog.com

私はPM出身で、良い Web サービスとは?みたいな問いがずっと自分の中にあるので、来年もこの問いを自分なりに突き詰めていきたい。今年は技術を身につければ良いサービスが作れるという自分の頭の片隅にあった思い込みをかち割ることができたので、「何を作るのか・なぜ作るのか」という部分もバランスをとりながら考えられるようになっていきたいと思っています。

趣味

推しのライブにたくさんいきました!生のパフォーマンスを観ることが自分にとって一番元気をもらえてリフレッシュできる時間なので、来年も可能な範囲で足を運びたい。 あとは、相変わらず陶芸でお皿とかマグカップを作ったり、エンジニアに転職して以降やっていなかったパン作りとかお菓子作りを再開することができました。

終わりに

早いものでエンジニアにキャリアチェンジして丸2年が経っていました。フィヨルドブートキャンプに通っていた頃を含めると早4年近くプログラミングに触れているわけですが、そこで費やしてきた時間に対して今の自分は成長が鈍化してないか・・・?みたいな不安が今年は強く、それが何となく自分の外側にも出てしまった1年だったなぁと思っています。

振り返ってみると自分なりに登壇や新しいことにトライして、技術面での課題も逃げそうになりながらも何とか踏ん張って向き合えたんじゃないか?!と思います。

来年はそんな自分を認めてもう少し自信を持っていきたい。

2026年もよろしくお願いします!

私の好きな Ruby の変数代入 Tips

Ruby/Rails Advent Calendar 2025」の23日目の記事です!

qiita.com

先日、Ruby リリース30周年パーティに参加してきたのですが、歴代リリースマネージャの方の座談会、Ruby の未来の話など Ruby に関わるたくさんの方の話を伺うことができてとても充実した時間を過ごすことができました。

Ruby というプログラミング言語も、そのコミュニティも大好きな自分ですが、今回の記事では「私の好きな Ruby の変数代入 Tips」と題して、3つの方法に触れたいと思います。

partition で「意味のある2つ」に分ける

一つ目は Enumerable#partition メソッドです。

docs.ruby-lang.org

プログラムを書いていると、配列をある条件で2つに分けて扱いたくなることがよくありませんか。

partition は、ブロックで定義した条件の結果を2つの配列としてそのまま変数に受け取ることができる便利なメソッドです。

EC サイトのクーポンの有効期限を例に使い方を見てみたいと思います。

today = Date.new(2025, 12, 23) # Date.today だと日付が変動してしまうため、例として固定

coupons = [
  { code: 'COUPON_1', expires_on: Date.new(2025, 12, 10) },
  { code: 'COUPON_2', expires_on: Date.new(2025, 12, 25) },
  { code: 'COUPON_3', expires_on: Date.new(2025, 11, 15) },
  { code: 'COUPON_4', expires_on: Date.new(2026, 1, 1) }
]

active_coupons, expired_coupons = coupons.partition { it[:expires_on] >= today }

# expires_on は Date オブジェクトですが、分かりやすいように ISO 8601 形式で結果を載せています
pp active_coupons
# [{code: "COUPON_2", expires_on: "2025-12-25"},
# {code: "COUPON_4", expires_on: "2026-01-01"}]

pp expired_coupons
# [{code: "COUPON_1", expires_on: "2025-12-10"},
# {code: "COUPON_3", expires_on: "2025-11-15"}]

こんな感じで「何を基準に分けているのか」がコードから自然に読み取れるところが好きです。

values_at でハッシュや配列から必要な値だけを受け取る

2つ目は Hash#values_at です。

docs.ruby-lang.org

docs.ruby-lang.org

Rails の params から複数の値を取り出す際にとてもよく使います。

values_at は、引数で指定したキーに対応する値の配列を返します。そのため、分割代入を使って結果を個々の変数として受け取ることができます。

params = {variant: "りんご", origin: "青森県", producer: "Ruby 農園", price: 230}

variant, origin, price = params.values_at(:variant, :origin, :price)

variant #=> "りんご"
origin #=> "青森県"
price #=> 230

また、配列のインデックスを指定して値を取り出せるため、分割代入で不要な値を _ のように書かずに済みます。

row = ["001", "maimu", "maimu@example.com", "東京都", "03-1234-5678"]

id, name, tel = row.values_at(0, 1, 4)

id #=> "001"
name #=> "maimu"
tel #=> "03-1234-5678" 

パターンマッチで右代入風に値を取り出す

3つ目はパターンマッチの => を使って右代入風に値を取り出す方法です。

この記事では、変数代入の Tips としてパターンマッチを紹介しています。パターンマッチ自体は代入に限らず、条件分岐などでも使える汎用的な機能です。

docs.ruby-lang.org

Ruby/Rails Advent Calendar 2025の17日目の記事で sanfrecce_osaka もパターンマッチについて書かれています!

zenn.dev

そもそも、右代入とは?右代入風とは?という点についてまず触れたいと思います。

hash = { foo: 1, bar: 2, baz: 3 }

このようなハッシュがあって、一つ目の値を変数に入れたいとします。 普通に書くとこうなると思います。

foo = hash[:foo]

foo #=> 1 

これをパターンマッチの => を使って書き換えることができます。

hash = { foo: 1, bar: 2, baz: 3 }

hash => { foo: }

foo #=> 1

右代入のイメージが持てたでしょうか? 次に「右代入風」と書いていることについてです。上の例のように見た目は「右代入」のように使えるのですが、内部的にはパターンマッチの照合が行われています。そのため、マッチしない場合は例外が発生します。代入ではなくマッチングであるため、「右代入風」と表現しています。

hash = { foo: 1, bar: 2, baz: 3 }

hash => { piyo: }
#  {foo: 1, bar: 2, baz: 3}: key not found: :piyo (NoMatchingPatternKeyError)

パターンマッチではなく通常の代入を使うと例外ではなく、値が nil になります。

piyo = hash[:piyo]

piyo #=> nil

パターンマッチによる代入はハッシュのネストが深くなった時により便利さを実感できます。

user = {
  name: 'Alice',
  profile: {
    age: 10,
    address: 'Wonderland'
  }
}

パターンマッチを使わない場合はこうですね。

address = user[:profile][:address]

address #=> 'Wonderland'

パターンマッチを使う場合は以下のように書くことができます。

user => { name:, profile: { address: } }

address #=>  'Wonderland'

初めて見ると一瞬「なんだこれは・・・?」と思ってしまいそうですが、動きがわかると普段の実装にも活用できるシーンは色々ありそうです。 私はパターンマッチを使うことで期待している構造を前提にシンプルにコードを書ける点が気に入っています。

まとめ

私の好きな Ruby の変数代入 Tips として、partitionvalues_at、パターンマッチの => を使う方法について書いてみました。

何をしたいかやデータの構造に応じて使い分けて、これからもたくさん Ruby でプログラムを実装していきたいと思います!

【フロントエンド学習記録】マークダウンで任意の箇所に画像を挿入できるようにする

maimux2x.hatenablog.com

最後にフロントエンド学習の記事を書いてからだいぶ期間が空いてしまいました・・・。

今回は書きたいと思っていたマークダウンで任意の箇所に画像を挿入する実装を見ていきたいと思います。

バックエンドが Rails で、フロントエンドが Ember.js の組み合わせで開発していますが、フレームワーク固有な内容以外も含んでいるため、別のフレームワークで実装している場合でもある程度参考になる部分はあるのではないかと思います。

Ember.js 側の実装

maimux2x.hatenablog.com

こちらの記事で画像を複数枚登録できるようにした実装を修正していきます。 まず、現在は画像が固定箇所に挿入されているのでその部分のコードは削除します。

<!-- web/app/components/post.hbs -->
    </h2>
    <div class="card-text">{{this.renderMarkdown @post.body}}</div>
<!-- each ブロックの3行を削除する -->
    {{#each @post.image_urls as |url|}}
      <img src={{url}} alt="" class="img-fluid">
    {{/each}}
  </div>
</div>

続いて post-form.js に処理を追加していきます。

Ember.jsが提供する modifier を使って、ブログ記事を入力する textareaのDOM要素をコンポーネントインスタンスとして保持してアクセスできるようにしています。

// web/app/components/post-form.js

import Component from '@glimmer/component';
import { DirectUpload } from '@rails/activestorage/src/direct_upload';
import { action } from '@ember/object';
// ↓追加する
import { modifier } from 'ember-modifier';

export default class PostFormComponent extends Component {
// ↓3行を追加する
  setTextarea = modifier((textarea) => {
    this.textarea = textarea;
  });

// ...
}

modifier は DOM を操作したい場合に利用します。

guides.emberjs.com

DOM を操作する目的はマークダウン形式の画像リンクをテキストエリアの任意の箇所に挿入するために、現在のカーソル位置を取得→リンクを挿入→カーソル位置をリンクの最後に持ってくるなどの制御をするためです。 具体的な実装を見ていくと以下の手順に従って処理が行われています。

  1. カーソル位置の取得: selectionStartとselectionEndでテキストエリア内のカーソル位置を取得
  2. テキストの分割 : カーソル位置を基準に前後のテキストを分割
  3. 画像リンクの生成 : Active Storageのsigned_idを使用してMarkdown形式の画像リンク![filename](url)を作成
  4. テキストの結合と更新 : 分割したテキストの間に画像リンクを挿入してpostのbodyを更新
  5. UI更新 :
    • autosize.updateでテキストエリアのサイズを自動調整
    • カーソル位置を挿入したテキストの直後に設定
    • フォーカスを戻す
  6. 入力フィールドのクリア: ファイル選択をリセット
// web/app/components/post-form.js

export default class PostFormComponent extends Component {
  // ...
  uploadImage(e: Event) {
    
      upload.create((error, blob) => {
  // ...
        if (error) {
          console.error(error.message);
        } else if (blob) {
          // ↓1行削除
          this.args.post.images.push(blob.signed_id);
          // ↓追加するコード
          const textarea = this.textarea!;

          const startPos = textarea.selectionStart; // 手順1
          const endPos = textarea.selectionEnd;
          const before = textarea.value.substring(0, startPos); // 手順2
          const after = textarea.value.substring(endPos); 
          const text = `![${blob.filename}](${ENV.appURL}/rails/active_storage/blobs/redirect/${blob.signed_id}/${blob.filename})`; // 手順3

          this.args.post.body = before + text + after; // 手順4

          // 手順5
          runTask(this, () => {
            autosize.update(textarea);

            textarea.selectionStart = textarea.selectionEnd =
              startPos + text.length;
            textarea.focus();
          });
        }
      });
    }

    target.value = ''; // 手順6
 // ここまでが追加するコード
  }
}

続いて、web/app/components/post-form.js で DOM を操作するために追加した modifier を呼び出す修正をしていきます。

<!-- web/app/components/post-form.hbs -->
<!-- ... -->
<!-- 1行削除 -->
<Textarea @value={{@post.body}} id="body" class="form-control {{if @post.errors.body "is-invalid"}}" />
<!-- 1行追加 -->
<Textarea {{this.setTextarea}} @value={{@post.body}} id="body" class="form-control {{if @post.errors.body "is-invalid"}}" />

最後にマークダウンから画像タグ(<img>)を安全に表示できるように、HTMLのサニタイズ処理で画像タグを許可する修正をします。

// web/app/components/post.js

return htmlSafe(
  sanitizeHtml(marked.parse(body), {
    // img タグを許可する
    allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
  }),
);

続いて、Rails側の実装を見ていきます。

Rails 側の実装

フロントエンド側ではすでにマークダウンでブログの記事を書けるようになっていますが、記事のマークダウンを Rails 側でも扱えるようにする必要があります。そのために commonmarker というgem を使います。

github.com

データベースにブログ記事を保存する際に commonmarker で解析をして、画像情報の取得と保存をする処理を実装していきます。

  1. Markdownの解析: Commonmarker.parse(body)でpost本文をパースし、ASTを生成
  2. ノードの走査: walkメソッドで全ノードを巡回し、:linkと:imageタイプのノードを検出
  3. Active Storage URLの抽出: 正規表現で以下のパターンをマッチング:
    • /rails/active_storage/blobs/redirect/{signed_id}/
    • /rails/active_storage/blobs/proxy/{signed_id}/
    • 名前付きキャプチャ(?<signed_id>\S+)でsigned_idを抽出
  4. signed_idの収集: マッチしたsigned_idを配列に格納
  5. imagesカラムの更新: 収集したsigned_id配列をimagesカラムに保存
# app/models/post.rb
class Post < ApplicationRecord

# ...

  before_save :set images 

# ...

  private

  def set_images
    signed_ids = []
    # 手順1
    Commonmarker.parse(body).walk do |node|
      case node.type
      # 手順2
      when :image
        # 手順3
        if %r{/rails/active_storage/blobs/(?:redirect/|proxy/)?(?<signed_id>\S+)/} =~ node.url
          # 手順4
          signed_ids << signed_id
        end
      end
    end
   # 手順5
    self.images = signed_ids
  end
end

set_images メソッドは before_savePost が保存される直前にコールバックとして実行されるようにします。

また、今回はActiveStorage のダイレクトアップロードを使用しているため、クライアント側で画像を選択した時点で データベースに blob が作成されています。ブログ記事を保存前に画像を差し替えたりした場合、未使用の blob が残ってしまうため、それを削除するメソッドも用意します。

# app/models/post.rb

class Post < ApplicationRecord

# ...

  after_commit :purge_unattached_blobs

  private
  # ...
  def purge_unattached_blobs
    ActiveStorage::Blob.unattached.each(&:purge)
  end
end

purge_unattached_blobs メソッドは after_dommit でデータベースへの保存が確定した後に実行されるようにします。

まとめ

以上がマークダウンで任意の場所に画像リンクを挿入するための実装です。 DOM の操作や 画像リンクの組み立てなどとても勉強になる実装内容でした。

なるべく間が開かないように他の機能の実装についてもまとめていきたいと思います。 次は tag 検索について書きたいと思います!