Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20210510前編)属性メソッドをキャッシュして最適化、Railsのガバナンスに関する声明、bundle install高速化ほか

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下のコミットリストのChangelogのうち、セキュリティ修正以外のものから見繕いました。

🔗 stylesheet_link_tagextnameオプションが追加


つっつきボイス:「javascript_include_tagにもextnameオプションがあるのでstylesheet_link_tagにも付けようという感じのようです」「extnameはextension nameつまり拡張子名を指定するのね」

CSSのパスにデフォルト.cssを追加するのをスキップするextname:オプションをstylesheet_link_tagに追加。

# 変更前:
stylesheet_link_tag "style.less"
# <link href="/stylesheets/style.less.scss" rel="stylesheet">
# 変更後
stylesheet_link_tag "style.less", extname: false, skip_pipeline: true, rel: "stylesheet/less"
# <link href="/stylesheets/style.less" rel="stylesheet/less">

Abhay Nikam
同Changelogより大意

「CSSの拡張子を.lessとかにしたい場合にデフォルトで.css拡張子を付けないようにするオプションか、たしかにできないと困る」「ところでlessって使ったことないんですけど、どのぐらい使われているのかな?」「言われてみれば自分の身の回りでは見かけないかも」「MIMEタイプも"stylesheet/less"になるのか」「使っている人がいる以上欲しいオプションですね👍」

参考: Getting started | Less.js

🔗 生成された属性メソッドをキャッシュおよび再利用する最適化


つっつきボイス:「attributes系メソッドをキャッシュする、なるほど」「キャッシュしたことでメモリがだいぶ節約できたみたいですね」「METHOD_CACHESを追加してこれを参照するようにしたのか↓」「define_method_attributeというprivateメソッドがgemで使われていると動かなくなる可能性があるとプルリクメッセージに書かれてました」

# activemodel/lib/active_model/attribute_methods.rb#L360
      private
-       class CodeGenerator
+       class CodeGenerator # :nodoc:
+         class MethodSet
+           METHOD_CACHES = Hash.new { |h, k| h[k] = Module.new }
+
+           def initialize(namespace)
+             @cache = METHOD_CACHES[namespace]
+             @sources = []
+             @methods = {}
+           end
+
+           def define_cached_method(name, as: name)
+             name = name.to_sym
+             as = as.to_sym
+             @methods.fetch(name) do
+               unless @cache.method_defined?(as)
+                 yield @sources
+               end
+               @methods[name] = as
+             end
+           end

「このキャッシュが効くシチュエーションは多そう: Railsワーカーのメモリを減らしてくれるいい高速化だと思います👍」


同PRより

🔗 追いかけボイス

「同issueのコメントが興味深いですね: memory_profilerというgemはIMEMO領域の分は見てくれないのでこれで計測するとむしろ劣化したように見えるけど、heap-profilerを使うとちゃんとIMEMOを含んだallocated sizeが出るのでメモリが削減されたことが分かるそうです」

SamSaffron/memory_profiler - GitHub

Shopify/heap-profiler - GitHub

参考: Reducing Memory Usage in Ruby | Tenderlovemaking — IMEMOの解説あり

「heap-profilerのREADMEにmemory_profilerとの違いも解説してある:ObjectSpace.each_objectでは拾いきれないものがObjectSpace.dump_allだと取れるらしい(以下の記事にもObjectSpace.dump_allの解説あり↓)」

Rubyのヒープをビジュアル表示する(翻訳)

🔗 ActiveSupport::Safebufferのstringへの暗黙の型強制を非推奨化

ActiveSupport::SafeBufferでオブジェクトからstringへの正しくない暗黙の型強制を非推奨化。

オブジェクトを文字列操作でStringに暗黙的に変換するためには#to_strを実装しなければならない(String#%など一部のメソッドを除く)。ActiveSupport::SafeBufferは、特定の状況でそうしたオブジェクトに対して誤って明示的な変換メソッド (#to_s) を呼び出していた。この動作が非推奨化された。
Jean Boussier
Changelogより大意


つっつきボイス:「implicit coercionはいわゆる暗黙の型強制」「coercionってそういう意味だったんですか」「MySQLなどでもcoercionという用語を見かけますね」

coercion {名-1} : 強制、無理強い

参考: Type coercion (型強制) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
参考: c# - EF Core to Mysql table binding - Stack Overflow

「ソースを見るとSafeBufferはStringを継承している↓: Ruby 3でStringのサブクラスを継承したときの振る舞いが変わったことに関連しているかなと思ったけど(ウォッチ20210427)、ここでは関係なさそうかな」

# activesupport/lib/active_support/core_ext/string/output_safety.rb#L133-L134
module ActiveSupport #:nodoc:
  class SafeBuffer < String
  ...

「issue #22947を見るとp("".html_safe + Object.new)のようなケースで暗黙の型強制による問題があったんですね↓」「html_safeを呼んで+で結合すると何でもstringに変換されちゃってたのか」「to_sだとどんなオブジェクトでもtype conversionできてしまうのでエラーになるべき(to_strが実装されていないなら文字列として結合されるべきではない)という意図かなと思いました」「なるほど」「現在動いているコードはたぶん影響を受けなさそうに見えますね」「よかった〜」

# #22947より
gem "activesupport", "4.2.5"
require "active_support/core_ext/string/output_safety"

p("".html_safe + nil) #=> ""
p("".html_safe + 123) #=> "{"
p("".html_safe + Object.new) #=> "#<Object:0x007fe664939778>"

🔗 ActiveSupport::Cacheでキャッシュエントリのシリアライザをカスタマイズ可能に


つっつきボイス:「2つのプルリクのうち上が本体で、下がRailsガイドの更新でした」「キャッシュのシリアライズにたしかデフォルトでMarshal.dumpを使っていますね」「シリアライザとデシリアライザはここではCoderっていう名前なのか: 単にシリアライズ・デシリアライズするだけならSerializerでしょうけど、gzipすると書かれているので圧縮・解凍も行うという意味でCoderなのかも」「なるほど、圧縮解凍もやってるんですね」

  • :coderオプションはキャッシュエントリのデフォルトシリアライズメカニズムをカスタムのものに置き換えられる。coderdumploadに応答する必要があり、カスタムコーダーを渡すと自動圧縮は無効になる。
    guides/source/caching_with_rails.md更新部分より

config.active_support.cache_format_versionというコンフィグが互換用に追加されてますね↓」

ActiveSupport::Cacheシリアライズにより高速でコンパクトなフォーマットが追加された。

これはconfig.active_support.cache_format_version = 7.0またはconfig.load_defaults(7.0)で有効にできる。Active Support 7.0は設定に関わらずActive Support 6.1でシリアライズされたキャッシュエントリを読み込めるので、キャッシュを無効にすることなくアップグレード可能。ただしRails 6.1は新しいフォーマットを読み込めないので、新しいフォーマットを有効にする前にすべてのリーダーをアップグレードする必要がある。
Jean Boussier
同Changelogより大意

🔗Rails

🔗 Railsのガバナンスに関する声明


つっつきボイス:「上は連休前に持ち上がったBasecampの一連の騒ぎの後で出された声明ですね」「Railsの運営は(Basecampのような)特定メンバーや特定企業の一存だけで決まるものではないということを改めて確認する内容になっていました」「実際以前からそうなっていますが、Railsの今後についての不安を解消するためにも改めて声明を出したという流れですね」

「経緯についてはdiscuss.rubyonrails.orgの書き込みの冒頭にひととおりまとまっているようです↓」「もう落ち着いたのかな?」「そのうち詳しいまとめ記事が出ると思うのでそちらに期待しましょう」

「今回Basecampから退社した人々の中にはRailsの一部のコアライブラリを業務としてメンテナンスしていた人たちもいましたが、声明にもあるようにRailsはこれまでも今後もBasecampだけのものではありませんし、多くの企業がRailsを使い続けているのも確かなので、Railsがこれで終わるというものではないと考えてよいと思います」「BasecampはBasecamp、RailsはRailsですよね」

参考: プロジェクト管理の老舗Basecampで「社員の政治的意見表明禁止」により社員3分の1が退社 | TechCrunch Japan


つっつきの後で、BasecampのCEOが一連の件について謝罪を表明したという記事が出ました↓。

参考: Basecamp CEO apologizes to staff in new post: ‘We have a lot to learn’  - The VergeRuby Weeklyより)

🔗 Railsでビューコンポーネントのライブラリを構築する(Ruby Weeklyより)


つっつきボイス:「Railsのビューコンポーネントの記事を久しぶりに見たので取り上げてみました」「この記事で使われているStorybookというJavaScriptライブラリはときどき目にしますね↓」「iOSにもStorybookってあったかも」「そうそう、紛らわしい」

参考: Storybook: UI component explorer for frontend developers

「記事はステップバイステップで進めている感じ」「以下のgemでビューコンポーネントとStorybookをつないでいるそうです↓」

jonspalmer/view_component_storybook - GitHub

「ビューコンポーネントは、こういうふうにRubyのコードでCSSを記述するスタイル↓を採用する決心がつくかどうかがポイントでしょうね」「あ、こういうふうに書くんですか」「クラスの行数が増えるとRuboCopに怒られそう」

# 同記事より: app/components/button_component.rb
 frozen_string_literal: true

class ButtonComponent < ViewComponent::Base
  attr_accessor :type

  PRIMARY_CLASSES = %w[
    disabled:bg-purple-300
    focus:bg-purple-600
    hover:bg-purple-600
    bg-purple-500
    text-white
  ].freeze
  OUTLINE_CLASSES = %w[
    hover:bg-gray-200
    focus:bg-gray-200
    disabled:bg-gray-100
    bg-white
    border
    border-purple-600
    text-purple-600
  ].freeze
  DANGER_CLASSES = %w[
    hover:bg-red-600
    focus:bg-red-600
    disabled:bg-red-300
    bg-red-500
    text-white
  ].freeze
  BASE_CLASSES = %w[
    cursor-pointer
    rounded
    transition
    duration-200
    text-center
    p-4
    whitespace-nowrap
    font-bold
  ].freeze

  BUTTON_TYPE_MAPPINGS = {
    primary: PRIMARY_CLASSES,
    danger: DANGER_CLASSES,
    outline: OUTLINE_CLASSES
  }.freeze

  def initialize(type: :primary)
    @type = type
  end

  def classes
    (BUTTON_TYPE_MAPPINGS[@type] + BASE_CLASSES).join(' ')
  end

end

「この記事でやっているのはサーバーサイドのビューコンポーネントということになりますね」「たしかに」「Rails側からは扱いやすいでしょうけど、フロントエンドエンジニアがどう思うかかな: この書き方にしないといけない理由をフロント側から聞かれたときに答えにくそう」「それですよね」

🔗 Value Objectをクラスで定義してプリミティブな値と戦う


つっつきボイス:「Value Objectを使いたくなる気持ちはわかる」「Value Objectをイミュータブルにする、なるほど」

# 同記事より
class AnswerScore
  def initialize(skill_id, score)
    @skill_id = skill_id
    @score = BigDecimal(score.to_s)
  end

  attr_reader :skill_id, :score

  def +(other)
    raise ArgumentError unless self.class === other
    raise ArgumentError if self.skill_id != other.skill_id

    ScoreSum.new(skill_id: skill_id, sum: score + other.score, n: 2)
  end

  def average_score
    score.round(2)
  end

  def ==(other)
    other.class === self &&
      other.hash == hash
  end

  alias eql? ==

  def hash
    [skill_id, score].join.hash
  end
end

「この記事のようにValue ObjectをStructとかではなくクラスとしてちゃんと定義しているのはいいですね👍」「なるほど、Structではなくクラスですか」「RailsでValue ObjectというとStructで手軽に作って使い捨てるのを割と見かけますが、それだとハッシュで定義するのとあまり変わらないかなという気持ちがありますね: こういうふうにクラスとして定義できるValue Objectなら値に意味を持たせられるので好ましいと思います」「ふむふむ」

「既存にない概念ならこうやってValue Objectのクラスにする意味があると思いますし、Rubyだと比較的やりやすいんですが、後はValue Objectにする意義がどのぐらいの頻度で生じるかかな」「メンテナンスのしやすさとのバランスでしょうね」

🔗 その他Rails


つっつきボイス:「bundle install--jobsオプションでパラレルダウンロードできるとは知らなかった」「パラレルダウンロードの依存関係はちゃんと解決されるのかな?」「依存関係が衝突したら捨てるしかないでしょうね」

「記事によると、やってみたけど思ったほど速くならなくて、ネイティブgemのmake--jobsオプションを付ける方が効いたそうです↓」「ああたしかに、ネイティブ系gemのコンパイルをパラレルにする方が効くでしょうし副作用もなさそう👍」「言われてみれば」

# 同記事より
$ time MAKE="make --jobs 8" bundle install
<SNIP>
bundle install
133.25s user 35.47s system 468% cpu 36.004 total

前編は以上です。

バックナンバー(2021年度第1四半期)

週刊Railsウォッチ(20210427後編)RactorでUDPサーバーを作る、JSONシリアライザalba gem、AppleのAirTagほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby on Rails Discussions

Ruby Weekly


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。