Quantcast
Channel: Rubyの記事一覧|TechRacho by BPS株式会社
Viewing all 1080 articles
Browse latest View live

週刊Railsウォッチ: カウンタキャッシュをスレッドセーフに更新、Journey::Ast追加、GitLabをAWS Graviton2で動かすほか(20210818前編)

$
0
0

こんにちは、hachi8833です。RubyKaigi Takeout 2021のスケジュール/講演タイトル/スピーカーが発表されました。

週刊Railsウォッチについて

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

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

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

今回は以下の公式更新情報からです。既に次の更新情報もいくつか出ています。

🔗 /favicon.icoへの内部ルーティングを追加


つっつきボイス:「今までrails newするといつも/favicon.icoでエラーになっていたのでエラー抑制用のコンフィグをいつも足していましたけど、ついに修正されたんですね」「お〜マジで、このエラーいつも目にしていました」「よかった😋

# railties/lib/rails/templates/rails/welcome/index.html.erb
<% ruby_on_rails_logo_favicon_data_uri = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIwcHgiIGhlaWdodD0iMzIwcHgiIHZpZXdCb3g9IjAgMCAzMjAgMzIwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPHRpdGxlPkljb248L3RpdGxlPgogICAgPGcgaWQ9Ikljb24iIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJSdWJ5LU9uLVJhaWxzLUxvZ28iIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUuMDAwMDAwLCAyMC4wMDAwMDApIiBmaWxsPSIjRDgxRTAwIj4KICAgICAgICAgICAgPHBhdGggZD0iTTIxLjkzMzA3MDYsMjg1IEwxNjguMTc1NDI2LDI4NSBDMTQ3Ljk0MDg1OCwxNTAuNjkxNzA2IDE5Ni44ODIzODMsNjYuMzE0MjkwMiAzMTUsMzEuODY3NzUzNiBDMzE1LDIxLjc4NTEyODQgMzE1LDMxLjg2Nzc1MzYgMzE1LDIxLjc4NTEyODQgQzE2MS4yNTI4MywyNC45MTE2Mjc4IDYzLjU2Mzg1MzMsMTEyLjY0OTkxOCAyMS45MzMwNzA2LDI4NSBaIiBpZD0iUGF0aCIvPgogICAgICAgICAgICA8cG9seWdvbiBpZD0iUGF0aCIgcG9pbnRzPSI0MC40MDgyNjQzIDE4NS45MTU3MSAxMi43MzI2NDk0IDE3NC40MDE2NjMgLTEuNDIxMDg1NDdlLTE0IDIwNS40NDI3MSAyOS41NzExOTY2IDIxNi42NDcxMTUiLz4KICAgICAgICAgICAgPHBvbHlnb24gaWQ9IlBhdGgiIHBvaW50cz0iMTgwLjQ3MzEwMiAyNjguMDczNjQzIDIwNC45MzYwMTYgMjc2LjQxMDk2NiAyMDQuOTM2MDE2IDI1NC41MDg2NzMgMTgwLjQ4OTY0NCAyNDQuMzM4MTA3Ii8+CiAgICAgICAgICAgIDxwb2x5Z29uIGlkPSJQYXRoIiBwb2ludHM9IjEwMC41ODk1MTkgOTcuMjI4NzYwNiA3Ni42ODQwMTU2IDc5LjE2ODcwMjEgNTUuMjQ0MDc5MSAxMDAuMTgzMTA1IDgxLjUxNDMyMzkgMTE3LjQzMzcyNSIvPgogICAgICAgICAgICA8cG9seWdvbiBpZD0iUGF0aCIgcG9pbnRzPSIxODQuNTc1Njc5IDE4NC44OTYyOTUgMjA3Ljk1MjAzIDIwMC4yNDY2MSAyMTEuNzI3NzI5IDE4MS4yMDUyNjYgMTg5Ljg2MzY1MyAxNjQuNjg3NDYiLz4KICAgICAgICAgICAgPHBvbHlnb24gaWQ9IlBhdGgiIHBvaW50cz0iMjYxLjczNDAxIDY1Ljg5NTk0NDYgMjY5LjM1NDIzNCA4Mi4zMjk1NCAyODUuMzE4MjU2IDcyLjcwNDg1OTYgMjc4LjMxMDEyOSA1Ni45ODI5ODk1Ii8+CiAgICAgICAgICAgIDxwb2x5Z29uIGlkPSJQYXRoIiBwb2ludHM9IjI2MS45MTM3IDE2LjE4NDU0NzMgMjU1LjU5ODQ3OSA3LjEwNTQyNzM2ZS0xNSAyMzIuNzk2MDIgMy40ODg5NjMyMyAyNDAuNDYzODczIDIwLjAyNTI3MjUiLz4KICAgICAgICAgICAgPHBvbHlnb24gaWQ9IlBhdGgiIHBvaW50cz0iMjExLjkzNDExOSAxMTEuNTgwNjc1IDIyNi43MjI1NDcgMTI3Ljc5MjYwMSAyMzguMDIzOTI1IDExMy45MDM3MTUgMjIzLjQ2ODM3NSA5Ni41NDY4Njg1Ii8+CiAgICAgICAgICAgIDxwb2x5Z29uIGlkPSJQYXRoIiBwb2ludHM9IjE3OS42ODY4NTggMzguNDk1MjIxOCAxNjQuNjQxOTMxIDIwLjAyNTI3MjUgMTM5Ljg0NzYyIDMyLjU1NTI5OTIgMTU2LjYwMTMyNCA1MC45MjE2NzUzIi8+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4=" %>
<!DOCTYPE html>
<html>
<head>
  <title>Ruby on Rails</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <link rel="shortcut icon" href="<%= ruby_on_rails_logo_favicon_data_uri %>" />

「よく見るとRailsのファビコンをsvgで追加しているんですね」「これっていいんだろうか?」「このファビコンをデフォルトのまま使い続けると、そのWebアプリがRails製ということがそこから推測できるので、個人的にはRailsのファビコンよりはゼロバイトの画像を返すなどの方が、推測の手がかりを増やさないという点で好ましいかもしれないと思いました」「それもそうですね」「アプリがどんな言語やフレームワークでできているかというシグネチャ情報の収集は、攻撃の予備動作にもなりえます」

🔗 Journey::Astを追加


つっつきボイス:「ASTは抽象構文木ですね」「JourneyはRailsのAction Dispatchのルーティング周りに関連するモジュールだったかな」「Journeyって今言われるまで全然知りませんでした」「Railsのルーティングになぞらえて旅(journey)という言葉をかけたのかも」

参考: 抽象構文木 - Wikipedia
参考: rails/actionpack/lib/action_dispatch/journey at main · rails/rails

「今見つけた記事↓にあるこの図がASTをNFA(非決定性有限オートマトン)で表したものですね」「こういうふうになるのか〜」

参考: Railsのルーティングを支える技術 - Journeyについて - Qiita


『Railsのルーティングを支える技術』より

参考: 非決定性有限オートマトン - Wikipedia

「このJourney::Astはルーティング周りのパフォーマンス改善のために追加されたみたい」「不要なルーティング探索をJourney::Astで削減したんですね」「RailsのJourneyを触れる人はなかなかいないという話を聞いたことがありますけど、久しぶりにJourneyが改修された👍

# 同PRより
master:
  TOTAL    (pct)     SAMPLES    (pct)     FRAME
    52   (0.5%)          52   (0.5%)     ActionDispatch::Journey::Nodes::Node#symbol?
    58   (0.5%)          45   (0.4%)     ActionDispatch::Journey::Scanner#scan
    45   (0.4%)          45   (0.4%)     ActionDispatch::Journey::Nodes::Cat#type
    43   (0.4%)          43   (0.4%)     ActionDispatch::Journey::Visitors::FunctionalVisitor#terminal
    303  (2.7%)          43   (0.4%)     ActionDispatch::Journey::Visitors::Each#visit
    69   (0.6%)          40   (0.4%)     ActionDispatch::Routing::Mapper::Scope#each

this commit:
  TOTAL    (pct)     SAMPLES    (pct)     FRAME
    82   (0.6%)          42   (0.3%)     ActionDispatch::Journey::Scanner#next_token
    31   (0.2%)          31   (0.2%)     ActionDispatch::Journey::Nodes::Node#symbol?
    30   (0.2%)          30   (0.2%)     ActionDispatch::Journey::Nodes::Node#initialize

🔗 メッセージ改善2件


つっつきボイス:「1件目はActionController::InvalidAuthenticityTokenをraiseするときにwarningも表示するようになった」「Ruby on Rails Discussionsで提案した人がプルリク投げてるんですね↓」

参考: Proposal for improving InvalidAuthenticityToken error when invalid same origin - rubyonrails-core - Ruby on Rails Discussions

「Railsを始めて間もない人にとっては何が起こっているかわかりにくいと思うので、warningも欲しいのはワカル」「今でもエラーを見てググれば調べられますけどね」「Webのセキュリティ関連要素も昔より増えてきましたし、ググって見つけた情報が古い可能性もあるので、warningも出力する方が親切でしょうね👍

# actionpack/lib/action_controller/metal/request_forgery_protection.rb#L225
      class Exception
+       attr_accessor :warning_message
+
        def initialize(controller)
          @controller = controller
        end

        def handle_unverified_request
-         raise ActionController::InvalidAuthenticityToken
+         raise ActionController::InvalidAuthenticityToken, warning_message
        end
# actionpack/test/controller/request_forgery_protection_test.rb#L710
+ def test_raised_exception_message_explains_why_it_occurred
+   forgery_protection_origin_check do
+     session[:_csrf_token] = @token
+     @controller.stub :form_authenticity_token, @token do
+       exception = assert_raises(ActionController::InvalidAuthenticityToken) do
+         @request.set_header "HTTP_ORIGIN", "http://bad.host"
+         post :index, params: { custom_authenticity_token: @token }
+       end
+       assert_match(
+         "HTTP Origin header (http://bad.host) didn't match request.base_url (http://test.host)",
+         exception.message
+       )
+     end
+   end
+ end

「2件目は、今までだとbin/rails db:migrate -hでrakeの一般的なヘルプが表示されていたのを、db:migrateのヘルプを出せるようになったみたい」「ヘルプの量が増えるとrakeのヘルプが邪魔になりがちでしたね」「これでググらずに調べられる👍

🔗 config_accessorアクセサでデフォルト値を定義できるようになった


つっつきボイス:「ActiveSupport::Configurableのアクセサにdefault:オプションを渡せるようになったのか」「イニシャライザで設定しなくてもよくなった🎉」「これはある方がよいでしょうね👍: ActiveSupport::Configurableはあまり使ったことはありませんが、Active Supportにはこういう地味に便利な機能がまだまだあります」

# activesupport/test/configurable_test.rb#L65
  test "configuration accessors can take a default value as an option" do
    parent = Class.new do
      include ActiveSupport::Configurable
      config_accessor :foo, default: :bar
    end

    assert_equal :bar, parent.foo
  end

参考: ActiveSupport::Configurable の話 - scramble cadenza

🔗 Middleware#removeMiddleware#delete!にリネーム


つっつきボイス:「前回マージされたMiddleware#removeウォッチ20210810)がさらにリネームされてMiddleware#delete!になったそうです」「また変わった😆」「早!」

「元々Middleware#deleteという前からあった機能がエラーをraiseするように変更されていたんですが、挙動を変えると互換性に問題があることがわかったので前回Middleware#deleteを元に戻してMiddleware#removeを追加したいう流れでした: でもdeleteremoveが両方存在して機能が違うのはたしかにわかりにくそうなので、今回Middleware#removeMiddleware#delete!にリネームしたということみたい」

「たしかに!でエラーをraiseする方がRubyっぽくてわかりやすいかも」「!を付けたらエラーをraiseするというのはActive Recordのfind_byfind_by!createcreate!savesave!などの使われ方に沿った挙動でしょうね: 一方”!があるとより破壊的になる”と考えれば、!を付けると無言で削除する方がより破壊的とも言えるので、どちらの命名がよいかは悩ましいかも」「あ、そうか」「この#42867の場合は既存のdeleteを変えたくないという事情があったので、!を付けたらエラーをraiseする方に倒すしかなさそうかな」「たしかに」

🔗Rails

🔗 カウンタキャッシュをスレッドセーフに更新する(Ruby Weeklyより)


つっつきボイス:「カウンタキャッシュ更新の競合状態を防ぐ記事ですね: これは昔からよく問題になっています」「そうそう」

「以下のサンプルコードのようにマルチスレッドを絡めてみると割とすぐ競合が発生する」

# 同記事より
class UnsafeTransaction
  def self.run
    account = Account.find(1)
    account.update!(balance: 0)

    threads = []
    4.times do
      threads << Thread.new do
        balance = account.reload.balance
        account.update!(balance: balance + 100)

        balance = account.reload.balance
        account.update!(balance: balance - 100)
      end
    end

    threads.map(&:join)

    account.reload.balance
  end
end

参考: class Thread::Mutex (Ruby 3.0.0 リファレンスマニュアル)

「記事ではミューテックスやActive Recordのlock!で回避する方法のほかに、Active Recordのupdate_countersメソッドのアトミックな性質を使って競合を回避する方法も紹介されている↓」「へ〜!」「面白いけど、Rubyのようなスクリプト言語でアトミックとか意識したくない気持ちもちょっとあるかな: C言語などでは普通の発想なんですが」

# 同記事より
class CounterTransaction
  def self.run
    account = Account.find(1)
    account.update!(balance: 0)

    threads = []
    4.times do
      threads << Thread.new do
        Account.update_counters(account.id, balance: 100)

        Account.update_counters(account.id, balance: -100)
      end
    end

    threads.map(&:join)

    account.reload.balance
  end
end

参考: ミューテックス - Wikipedia
参考: lockActiveRecord::Locking::Pessimistic

「お、concurrent-rubyにはAtomicFixnumというクラスがあるのか↓: 実際の内部実装ではミューテックスあたりを使っていそうに見える」

ruby-concurrency/concurrent-ruby - GitHub

「トランザクションを張ったうえで別途カウンタキャッシュを更新するコードを書くと、たまに競合が発生するという問題は実は昔からあって、真面目に回避しようとすると複雑になりがち」「ふむふむ」「カウンタキャッシュはRailsの機能などを使えば簡単に実現できるんですが、複雑なトランザクションが絡んでくるとデッドロックしたりする: そこに引っかかるようなコードを書かなければたいてい問題にならないので、経験している人もいれば経験せずに済む人もいたりします」

「発生の可能性がつきまとうのはカウンタキャッシュの仕組み上仕方ないんですが、カウンタキャッシュで競合が起きる可能性があるということだけでも知っておくと損はないと思います: よさそうな記事👍

Rails向け高機能カウンタキャッシュ gem「counter_culture」README(翻訳)

「ところで、記事の末尾にあるリンクをたどるとハイゼンバグ(Heisenbug)という知らない用語があったんですが、やはり『ハイゼンベルグの不確定性原理』のもじりでした」「調査しようとすると競合状態が変わって再現が難しいマルチスレッド系バグとかが、ちょうどそういう感じでしょうね」

参考: 特異なバグ - Wikipedia
参考: 不確定性原理 - Wikipedia

🔗 Railsアプリでコードをdeprecateする(Ruby Weeklyより)


つっつきボイス:「アプリでのdeprecationの書き方の記事」「ウォッチの『先週の改修』ではよく見かけますけど、考えてみたらアプリのコードでもRailsのActiveSupport::Deprecationを使っていいんですね」「もちろん使っていいんですよ😆」「オープンソースのRailsアプリなどで普通に有効な書き方ですね」

def process_widget
  ActiveSupport::Deprecation.warn(
    "#process_widget is deprecated. " \
    "Use #send_widget_to_processor instead."
  )
  # other code ...
end

ActiveSupport::Deprecationで書いておくと、テストコードを回したときにもdeprecation warningが表示されるのが便利です」「なるほど!」

🔗 NokogiriがHTML5の機能をサポート(Ruby Weeklyより)


つっつきボイス:「CRuby限定でNokogiriがHTML5をサポートしたそうです」「Nokogiriにマージされたnokogumboって何だろう↓」「初めて見ました」

rubys/nokogumbo - GitHub

「お〜、nokogumboはHTML5のfragmentや内部フェッチやカスタム属性とかも使えるのか↓: これまでNokogiriがHTML5を読み込めなかったのかと思ったら、HTML5のこうした機能がNokogiriで使えるようになったということのようですね」「なるほど」

# rubys/nokogumboより: fragmentのパース
require 'nokogumbo'
doc = Nokogiri::HTML5.fragment(string)
# rubys/nokogumboより
require 'nokogumbo'
doc = Nokogiri::HTML5.get(uri)

参考: Links - The complete HTML5 tutorial
参考: data-* - HTML: HyperText Markup Language | MDN

「今回はNokogiriのマイナーバージョンアップで大きな改修ではなさそうなので、従来どおりにも使えそうかな」「nokogumboがCで書かれているのでJRubyとかでは動かないのはしょうがない」

🔗 GitLabのArmベースAWS Graviton2記事


つっつきボイス:「へ〜、GitLabをArmベースのAWS Graviton2インスタンスに置くと23%安くなって36%パフォーマンスが向上するという記事」「この図はGitLabがベンチマークに使った環境なのね↓」


同記事より

「AWS Gravitonがわかってなかった😅」「AWSが作っているArmプロセッサのインスタンスですね: 出たのは最近ですが、もう割と使われていると思いますよ」「なるほど、それの新しいのがGraviton2ですか」

参考: AWS Graviton (EC2 に最良の料金とパフォーマンスを提供 | AWS
参考: AWS Graviton2 を搭載した新しい EC2 M6g インスタンス | Amazon Web Services ブログ

「RDBMSなら今でもArmプロセッサでまったく問題なく動かせますが、GitLabのような大規模Railsアプリで使われているgemをArmプロセッサ上でビルドして実行できたということは、BPSが今メインで使っているGitLabサーバーも、原理的にはArmベースのAWS Graviton2インスタンスに引っ越し可能ということになりますね」「お〜」「社内でも検討してみようかな😋


前編は以上です。

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

週刊Railsウォッチ: システムテスト用headlessドライバにCupriteが追加、rails-mini-profiler、Jeremy Evansインタビューほか(20210810)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: カウンタキャッシュをスレッドセーフに更新、Journey::Ast追加、GitLabをAWS Graviton2で動かすほか(20210818前編) first appeared on TechRacho.


週刊Railsウォッチ: SorbetのRuby AOTコンパイラが公開、「Compiler Explorer」にRubyが追加、Ractorで非同期通信ほか(20210823後編)

$
0
0

こんにちは、hachi8833です。もろもろの事情で後編の公開が遅れました🙇

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 SorbetのRuby AOTコンパイラ(Ruby Weeklyより)


つっつきボイス:「まだ実験段階だそうです」「SorbetのRuby用コンパイラはJITではなくAhead-Of-Time(事前)方式で、これまでStripe社内のみで使っていたのが公開されたみたい」「今度のRubyKaigi Takeout 2021でこれの話が聞けそうな気がしますね」「あ、そうかも」

参考: 事前コンパイラ - Wikipedia


同記事より

その後発表されたRubyKaigi Takeout 2021のスケジュールを確認してみましたが、Sorbetを手掛けているStripe社のスピーカーは惜しくもいませんでした。

🔗 Compiler ExplorerにRubyも追加(Ruby Weeklyより)


つっつきボイス:「Compiler Explorer?」「なるほど、こういうふうにソースコードをコンパイルできるのか↓」「このサイトのサポート言語にRubyも加わったそうです」「お〜、たとえば左にRubyのコードを書くと右にコンパイル結果が出力されるんですね」


godbolt.orgより

「Rubyのバージョンも2.5.9〜3.0.2から選べる、やだこれスゴい」「バージョンによってコンパイル結果が変わる可能性もあるので、選べるのはありがたい」「コンパイルボタンはどこかなと思ったら、書いた途端にコンパイルされるみたい」「Rubyの場合はVMの命令シーケンス(ISeq)をdisasmした結果が出るのか」「これは面白い!」「C言語にするとCPUとコンパイラのさまざまな組み合わせを選べました」

# godbolt.orgより
== disasm: #<ISeq:<compiled>@example.rb:1 (1,0)-(4,3)> (catch: FALSE)
0000 definemethod                           :square, square           (   2)[Li]
0003 putobject                              :square
0005 leave

== disasm: #<ISeq:square@example.rb:2 (2,0)-(4,3)> (catch: FALSE)
local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] num@0<Arg>
0000 putstring                              ""                        (   3)[LiCa]
0002 leave

参考: RubyVM::InstructionSequence#disasm (Ruby 3.0.0 リファレンスマニュアル)

「ちょっとしたコード最適化で部分的なコンパイル結果をチェックしたり、結果の比較をプルリクに添えたりするのに使えるかも👍: 自分でここまでカリカリにチューニングするかどうかはわからないけど」「なるほど!」「コードの最適化では、なぜかこう書く方が速いということもあったりしますけど、そういうときに納得したり説得したりするときの材料に使えそう」「何より見てて楽しい」「見てて楽しいですね」

🔗 Ractorで安全な非同期通信の実験(Ruby Weeklyより)


同記事より


つっつきボイス:「Ractorを初めてやってみた記事だそうです」「Ractorでノンブロッキングに書く方法とブロッキングに書く方法が紹介されていますね: ここまでできればほぼスレッド並かも」「お〜」「このブログには他にもいろんなRuby記事があってよさそう👍

# 同記事より
receiver_ractor = Ractor.new do
  loop do
    message = Ractor.receive
    sleep 1
    puts "Processed #{message}"
  end
end

counter = 0
while true
  counter += 1
  receiver_ractor.send(counter)
end

参考: ruby/ractor.md at master · ruby/ruby
参考: class Thread (Ruby 3.0.0 リファレンスマニュアル)

🔗 その他Ruby

つっつきボイス:「bundle install --without defaultとは?」「おそらくですが、Rubyのbundlerを普通に実行するとGemfile.lockにあるgemを全部インストールしますけど、そうではなくて特定のgemだけをインストールしたいということでしょうね」「あ〜なるほど」「Gemfile.lockに書かれている特定のgemだけインストールしたい、他のはインストールしたくないという状況かなと思いました」「こうやるとできるんですね」

参考: rubygems/bundler at master · rubygems/rubygems

🔗クラウド/コンテナ/インフラ/Serverless

🔗 ElasticのクライアントライブラリとAWS OpenSearch(Publickeyより)


つっつきボイス:「ああ、ElasticとAWSのこのニュースは考えさせられましたね」

「Elasticがこうするまでのいきさつってどんなでしたっけ?」「以下の記事などにあるように以前からRedisやElasticなどがAWSに申し立てしています↓」

参考: AWSをElasticが名指しで非難。ElasticsearchとKibanaのライセンスを、AWSが勝手にマネージドサービスで提供できないように変更へ - Publickey

「ElasticがElastic Cloud上で運営しているElasticsearchのホスティングサービスがAWSのサービスと競合する点(ElasticのはAWSより後ですが)や、AWSがElasticsearchという商標を使ってサービスしている点などが問題視されているようです」「あ〜」

参考: Amazon Elasticsearch Service(Elasticsearchを簡単にデプロイ、保護、運用)| AWS
参考: Elastic Cloud:マネージドのElasticsearchと検索 | Elastic

「いちユーザーとしては、既に持っているAWSアカウントでELK Stackを立てられるのは便利かつ嬉しいし、Amazon ESもそれなりに息の長いサービスなので、既に使われているプロダクトも結構世の中にあるはず」「ふむふむ」「今回ElasticSearch公式がクライアントライブラリに手を加えたことで、こうした”善意の”ユーザーまで影響を受けるようになってしまうのはいろいろ残念」

参考: ELK Stack: Elasticsearch、Logstash、Kibana | Elastic

「もちろんElasticの言い分もとても理解できるんですが、こういう現場の前方互換が失われるような変更を入れられてしまうと、長く使われる可能性のあるエンタープライズ系のシステムで使うのをちょっとためらうかも」「気持ちわかります」

「もうひとつ気になるのが、Elasticと似たような形でAWSと対立しているRedisなどの動向」「Redisもですか」

参考: Redis、MongoDB、Kafkaらが相次いで商用サービスを制限するライセンス変更。AWSなどクラウドベンダによる「オープンソースのいいとこ取り」に反発 - Publickey

「Elasticは明らかに法人ですが、Redisはどうだったかな…Redis Labsがスポンサーになっているのか↓」

参考: Redis Labs - Wikipedia

「正直、ElasticsearchやRedisってできれば自前ではホスティングしたくないサービスの筆頭」「そうそう、自分たちで運用するとつらいヤツ」「Redisはスケールアップするときに一度止めないといけないとか、ノウハウが結構必要になるので、できればやっぱりクラウドのものを使いたい」

「以前NVIDIAのCUDAがEULAに記載された利用方法を後から変更したために、データセンターに導入できなくなってビットコインを掘ったりするのに使えなくなったことがありましたね」「ありゃ〜」「利用要件を回避するためにやむなく古いCUDAを使い続けたりしているそうです」「後から変えるとやっぱりもめますよね」「以前機械学習で遊んだときにCUDAを入れたことありますが、めちゃデカかった」

参考: CUDA - Wikipedia
参考: エヌビディアが消費者向けGPUのライセンスを変更、データセンターへの導入を禁止 | 日経クロステック(xTECH)

🔗 JavaScript

🔗 Deno 1.13にネイティブHTTPサーバーAPIが搭載

denoland/deno - GitHub


つっつきボイス:「Denoって今どのぐらい使われているのかな?」「追ってみたことはありませんが、かなり頻繁にアップデートされているようです」「今調べて知りましたが、Denoは次世代のNodeみたいな位置づけなんですね」「Nodeの作者がさまざまな反省を込めてDenoを作ったそうです」

参考: DenoとNode.jsの大きな違い - keroxpのScrapbox

「CloudFrontのLambda@EdgeのようなエッジコンピューティングとかだったらDenoは十分いけるかなという気持ちはありますね」「お〜」「いきなり大規模なものに使うのはまだためらいがありますが、小規模なプログラムならイレギュラーなことも起こりにくいでしょうし、マイクロサービス的なものと新しいプログラミング言語環境は割と相性がよさそうな印象もあります」

参考: CloudFront Lambda@Edge での AWS Lambda の使用 - AWS Lambda

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 ちいさなWebブラウザを作ってセキュリティを学ぶ


つっつきボイス:「お、今年のセキュリティ・キャンプ↓はどことなく大学の集中講義みたいな雰囲気」「スライドはアウトラインで、その下の資料に詳しく書かれているそうです」「セキュリティ・キャンプも今の御時世なのでオンライン開催か」「こういう資料を無料で参照できるのはいい👍

参考: セキュリティ・キャンプ:IPA 独立行政法人 情報処理推進機構

「セキュリティ・キャンプの対象はメインが高校生で次点が大学生、ときには中学生もいたりするんですよ」「こういう場で学べる今の若者は強いですね」「オンライン開催になったことで以前より参加しやすくなった反面、こういう独特の場のアツい空気みたいなものに触れる機会がなくなったのは残念」「そうなんですよね…」

🔗言語/ツール/OS/CPU

🔗 CO2センサー


つっつきボイス:「これは自分も見かけて、いい記事だと思いました👍」「そう言えば以前CO2センサーの記事を出してましたよね↓」「そうそう、自分が記事で使ったセンサーはCO2を直接測定していないので精度があまり高くないと言われています」

自宅作業環境の二酸化炭素濃度をM5Stack+CCS811で計測してみた

「こうした商用センサーの精度はいろいろ難しい面があって、製造側にしてみれば精度が高くなくても売れればいいというモチベーションがある一方、それを指摘する側は手間もかかるし訴訟リスクもあるしお金にもならないということになりやすい」「あ〜たしかに」

「元記事では大学の研究室がやっていますけど、これは果たして大学の仕事なのかという面もあるわけです: たとえば国や非営利の公的機関が大学の研究室に依頼してこうした精度を調査するしくみができれば、精度の低い商用センサーを使ったために論文のデータが信頼できなくなるということも減ってよいと思うんですけどね」「ふむふむ」「こういう調査は今後の新製品に対して継続的に実施する必要もありますし、ロットによって違いが生じたりもするので、本来は国や非営利の公的機関が定期的に研究室に調査を依頼するのが望ましいと思います」「なるほど」「製品として売られているセンサーの精度みたいなテーマは世の中でとても有用ですが、単体では論文になりにくいんですよ」


後編は以上です。

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

週刊Railsウォッチ: カウンタキャッシュをスレッドセーフに更新、Journey::Ast追加、GitLabをAWS Graviton2で動かすほか(20210818前編)

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

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

Ruby Weekly

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ: SorbetのRuby AOTコンパイラが公開、「Compiler Explorer」にRubyが追加、Ractorで非同期通信ほか(20210823後編) first appeared on TechRacho.

『Polished Ruby Programming』(Jeremy Evans著)を読みました

$
0
0

こんにちは、hachi8833です。夏休み課題図書というわけではありませんが『Polished Ruby Programming』を2回読み、Webチーム内でも発表しました。

既にjnchitoさんが書評記事を書いてくださっているので、なるべく違う切り口で、かつネタバレにならないように書いてみようと思います。

著者について

Jeremy EvansさんはRubyコミッターであり、Railsにもコントリビューションしていた時期もあります。OpenBSD版のRubyのメンテナーでもあります。

jeremyevans/openbsd-ruby-ports - GitHub

昨年末にRuby Award 2020を受賞した直後のインタビュー記事を以下でも読めます。また、RubyKaigi Takeout 2021にもスピーカーとして登壇が決まりました。

参考: Interview with Jeremy Evans, OpenBSD Ruby ports maintainer by Evrone

私が最初にJeremy Evansさんを知ったのは2019年のRubyKaigiでトリを務めたのを見たときでした(以下の動画)。特濃のキーノートスピーチが忘れられません。今チラ見すると本書との共通点があちこちにあるように思えました。

Jeremy Evansさんは以下のgemを手掛けていることで知られています(リポジトリには他にも多数のgemがあります)。Sequelは引き継ぎでメンテナンスを行っているそうです。本書でもSequelとrodaは頻繁に登場しています。

jeremyevans/sequel - GitHub

jeremyevans/roda - GitHub

jeremyevans/rodauth - GitHub

jeremyevans/ruby-warning - GitHub

jeremyevans/erubi - GitHub

読み方

ちなみにKindle版を買いました。

1回目はKindleでマーカーを付けながらざっと駆け抜け、2回目はサンプルコードを動かしながらDocbaseにmarkdownで自分用に17章をエイヤでまとめました。

これはあくまで自分用に作ったドッグフーディング的なまとめであり、翻訳ではありません。当然ながら非公開なのであしからず。

書籍のサンプルコードは以下のリポジトリで公開されています。当然ながら書籍がないとさっぱりわかりませんが、読み進めながら動かしてみたところすべて問題なく動きました。

PacktPublishing/Polished-Ruby-Programming - GitHub

同書の特徴

Rubyに特化した中小規模ライブラリ設計の全方位的な知見を網羅

週刊Railsウォッチでも言及されていたように、Rubyに特化した最適なコードの書き方や知見が盛りだくさんです。Railsアプリのユーザーコードよりは、ライブラリの作成やメンテナンスを対象とした中規模または小規模の設計が中心という印象です。

参考: 週刊Railsウォッチ(20210810)『Polished Ruby Programming』

プロのRuby開発者に既に知られている内容もそれなりにあるかと思いますが、Rubyのコアのパフォーマンスを知り尽くした人が網羅的かつ統一的に書いているのがありがたい点です。

情報が新しい

Ruby 3.0をベースにしているので情報が新しく、過去バージョンのRubyとの違いについても必要に応じて言及されています。

特に、Ruby 2.7からRuby 3.0にかけて行われたキーワード引数の改修は著者が大きく貢献している部分だけあって、Ruby 3.0がリリースされてから満を持して本書を執筆したのだろうと想像しています。

参考: プロと読み解く Ruby 3.0 NEWS - クックパッド開発者ブログ

Rubyの言語設計思想をベースにしている

同書の解説は、Ruby言語が「何を重視していて」「何を重視していないか」を出発点としていると感じました。

たとえばRubyではローカル変数が他のインスタンス変数やクラス変数などより重視されている理由や、Rubyのハッシュが[]のキーが無効の場合にnilを返す理由、シンボルと文字列の違いや適切な使い分けについての解説などのさまざまな知見を、Rubyの言語設計思想と特性を踏まえて詳しく説明しています。

最適化の知見が豊富

全般に、Rubyコードの高速化はいかにオブジェクトの生成を避けるかが大きなポイントと感じました。ただしその都度、最初は最適化よりもきれいな設計を目指すべきとも述べられています。「最適化を急ぐな」「今のRubyは最適化しなくてもたいてい速い」は再三強調されています。

スコープゲート

本書でスコープゲートという用語を今頃知りました。どうやら「メタプログラミングRuby」でも使われているようです。

参考: passingloop • Ruby の class_eval でスコープゲートを乗り越えるもの、乗り越えられないもの

メタプログラミングとDSL

Rubyは他の言語と異なり、通常のプログラミングとメタプログラミングの間に大きな差はないと説いています。

そしてメタプログラミングやDSLはどうしても抽象度が高くなり、それがよい抽象なのか悪い抽象なのかを見分けるのは簡単ではないとしています。本書の目的のひとつが、そうした抽象の良し悪しを見分けられるようになることだそうです。

プラグインシステム

プラグインシステムの書き方についても1章を割り当てていて、可能ならライブラリにプラグインシステムを導入することを推奨しています。著者のRodaやRodauth gemはプラグイン方式を取り入れているので、その経験が生かされているのでしょう。

テストとリファクタリング

テストとリファクタリングにも1章ずつ費やしていて、同じく力が入っています。プロファイリングとベンチマークを繰り返しながら達人がコードを次第にきれいにしていく様子が手に取るようにわかります。

「カバレッジ100%に意味はない」という記述も印象的でした。

コードのスタイル

コードのスタイルについても1章を割いています。プログラマーを大きく「詩人タイプ(いろんなスタイルで書きたい派)」と「哲学者タイプ(スタイルを揃えたい派)」に分けて論じているのが面白い点です。

SOLID原則やデザインパターン

よく言われているように、本書でもRubyではJava方面由来のSOLID原則やGoFのデザインパターンはそのまま適用できるとは限らないと説いています(オープン/クローズ原則はRubyのコアライブラリでも完全に無視されているなど)。

デザインパターンにも1章を費やしていますが、扱われているのはJavaでおなじみのGoFのデザインパターンよりはるかに少ない8個に絞り込まれています。Visitorパターンについても、訪問先にメソッドを追加しない修正版Visitorパターンを紹介しています。GoFにないObject Poolデザインパターンを本書で初めて知りました。

参考: デザインパターン (ソフトウェア) - Wikipedia
参考: Object Pool Design Pattern - GeeksforGeeks

「デザインパターンはRubyではさほど重要ではないが、それでも知っておくと役に立つ」という記述が印象的でした。なお本書には「継承vsコンポジション」のようなトピックはありません。

Web

Webについては、最後の3章でデータベース選びと設計、フレームワーク選びやORM選び、セキュリティなどについて解説しています。

フレームワークはRailsだけではなく、Sinatra、Grape、そしてRodaを扱っています。Rodaは著者自ら作ったフレームワークだけあって解説にも力が入っており、ルーティングツリーとアクションが一体化した独特の作りと高速性に興味を惹かれました。

読んでみて

『Polished Ruby Programming』は、Rubyらしい素直で読みやすい書き方が大半を占めている印象です。黒魔術やメタプロも少し登場しますが、その都度警告しています。

ベテランらしく、設計の解説ではトレードオフ(主にパフォーマンスときれいな設計のバランス)がしばしば強調されています。「どちらの設計がよいかは場合による」がほとんどで、「常にこう書くべき」「こう書いておきさえすればよい」という書き方は少なく、自分好みの設計を出しすぎないよう可能な限り配慮しているのが伝わってきます。それでも著者の設計の好みがほんのりと感じられるのも面白い点です。

RuboCopのコードサイズ制限については珍しく「悪影響しかない」とやや強めの口調でした。気持ちわかります。

英語についても、あいまいになりやすい代名詞を避けて読みやすく書かれている点に好感が持てました。その分1つの文が長めで係り結びも増えますが、おそらく機械翻訳でかなりスムーズに翻訳できると思います。著者のドキュメンテーション能力の高さを実感しました。個人的な趣味ですが、アメリカンジョークがほとんどない点も好きです。

Sandi Metz『オブジェクト指向設計実践ガイド』はRuby向けのオブジェクト指向設計を学ぶ本として有名ですが、『Polished Ruby Programming』はSandi Metz本とはだいぶ趣が異なるように思いました。

  • Sandi Metz本はオブジェクト指向ありきでパフォーマンスにはあまり言及せず、Rubyに限定しない印象
  • Jeremy Evans本はRuby言語の設計思想と内部パフォーマンスを土台にしていて、オブジェクト指向やデザパタは必ずしも主役ではない印象

『Polished Ruby Programming』にはC言語のコードはまったく登場していません。おそらくRuby内部のC言語にまで言及したらきりがなくなってしまうからだろうと想像しました。なお、以下の『Rubyのしくみ』はRuby内部のC言語コードがビシバシに登場します。本書を読んでおくと役に立ちそうです。

本のつくりとしては、Kindleで読んだせいなのか、地の文とコードサンプルがなぜか同じフォントになっている点がちょっと読みづらく感じました。また、章内の見出しはほとんどがh2h3レベルでしたが、h4レベルの見出しまでブレイクダウンされていたらさらに読みやすくなりそうです。


Polished Ruby Programmingより

以下のkakutaniさんのgistにもあるように、本書には類書がほとんどないのも特徴です。

参考 Polished Ruby Programming翻訳査読書(のようなもの)

jnchitoさんのチェリー本を卒業してRubyやRailsのコードを実務で書くようになった中級者以上におすすめだと思います。また、他の言語の経験者がRubyを詳しく知るにも向いていそうです。ライブラリ作者がどんなことを考えているかを知るのに絶好の本です。

私の血となり肉となるにはだいぶ時間がかかりそうですが、『Polished Ruby Programming』は私にとって「繰り返し読みたい本」となりました。Docbaseにまとめたおかげでいつでも自分用に検索できるのがうれしい😂


The post 『Polished Ruby Programming』(Jeremy Evans著)を読みました first appeared on TechRacho.

週刊Railsウォッチ: Rails 7でのimport maps導入、Steepで型を導入、KubernetesでRailsを動かすためのガイドほか(20210830前編)

$
0
0

こんにちは、hachi8833です。以下をお見逃しの方はTwitterの#ginzarailsタグである程度追いかけられると思います。

週刊Railsウォッチについて

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

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

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

今回は以下の公式更新情報からです。本家が2つ先まで進んでいます。

🔗 deprecation warningを一括オプトアウト可能に


つっつきボイス:「config.active_support.report_deprecations = falseを指定すればdeprecation warningをまとめてオプトアウトできるようになった: 乱用は避けたいけどやむを得ず一時的にそうしたくなることもあるヤツ」「以前は以下を両方指定しないといけなかったんですね」「オプトアウトできる機能はあってもいいと思います👍

# changelogより
config.active_support.deprecation = :silence
config.active_support.disallowed_deprecation = :silence

🔗 Action CableのJSをES Moduleとしてトランスパイルせずに出力

Railsの全JSを可能な限りES2017およびESM(ES Module)をターゲットとするよう更新しよう。これによってバンドルなしでこれらの出力をブラウザで直接利用できるようになり、かつES Moduleを使えるようになる。これはWebpackerなしのデフォルトへの布石となる。
ここでは後方互換性問題の考慮が必要。これが互換性を妨げるのであれば2つの異なる出力を生成してもよい。
同PRより

参考: ブラウザで覚えるES Modules入門 - JavaScriptでモジュールを使う時代 - ICS MEDIA


つっつきボイス:「Rails 7でWebpackerレスを目指す方向」「ESMへトランスパイルせずにJSを出力するようになるといえば、Rails 7のJSがimport mapsベースになる話も出ていますね」「ちょうどこの後でも取り上げています」「考えてみればAction CableのJSライブラリは元からオープンなコードなので、難読化のためにわざわざトランスパイルすることもないんですよね」「たしかに」

参考: トランスコンパイラ - Wikipedia

🔗 development環境のNoDatabaseError画面に[Create database]ボタンを追加


つっつきボイス:「お〜、ActiveRecord::NoDatabaseErrorエラー画面にCreate databaseボタンができた」「当然ながらdevelopmentモードにしか出ないようになっている」「UIがますます親切になった👍


同PRより

🔗 ActiveRecord::QueryMethods#in_order_ofを追加


つっつきボイス:「おぉ、ついにActive Recordにもin_order_ofが入った🎉」「別のものが前からあったんでしょうか?」「少し前にRailsにEnumerable#in_order_ofが入っていたんですが↓、今回それのActive Record版もできて適用範囲が広がってきた感じですね」「なるほど!」

参考: rails commit log流し読み(2021/08/05) - なるようになるブログ

概要
これにより、SQL式に基づいてレコードを返す際に明示的な順序を指定できるようになる。デフォルトでは次のようにCASEで実現される。

Post.in_order_of(:id, [3, 5, 1])

上は以下のSQLを生成する。

SELECT "posts".* FROM "posts" ORDER BY CASE "posts"."id" WHEN 3 THEN 1 WHEN 5 THEN 2 WHEN 1 THEN 3 ELSE 4 END ASC

しかしこの機能はMySQLではFIELD関数の形で組み込まれているので、コネクションアダプタは代わりに次のようなSQLを生成する。

SELECT "posts".* FROM "posts" ORDER BY FIELD("posts"."id", 1, 5, 3) DESC

この機能は#41333でEnumerableに追加された機能に強くインスパイアされている。cc:@dhh
同PRより

「なるほど、in_order_ofのSQLはCASE文に続いてWHEN 3 THEN 1 WHEN 5 THEN 2...みたいに展開されるのか: まあそうなるでしょうね」「改行なしだと一瞬考え込んでしまったけど今理解しました」「力技だけどその分わかりやすいかも」「MySQLだとFIELD関数があるのでこう書けるんですって↓」「in_order_ofはあっていい機能👍

-- Changelogより
SELECT "posts".* FROM "posts" ORDER BY FIELD("posts"."id", 1, 5, 3) DESC

🔗 ActiveRecord::Relation#structurally_compatible?が追加


つっつきボイス:「呼び出し元のActiveRecord::Relationと引数に渡したActiveRecord::Relationが構造的に互換かどうか、つまり両者が同じカラムをselectしていて互換であればtrueを返すということのようですね」「おぉ?」「たとえば以下の2つ目だとjoinsで両者のカラムが異なっているのでfalseが返る」

# activerecord/lib/active_record/relation/query_methods.rb#751
# 与えられたリレーションがこのリレーションと構造的に互換性があるかどうかをチェックし、
# エラーを出さずに`#and`や`#or`メソッドを使えるかどうかを判断する。
# 「構造的に互換性がある」は、両者が同じモデルをスコープしていて
# `#where`(`#group`が定義されていない場合)または#having(`#group`が存在する場合)
# によってのみ異なることと定義される。
Post.where("id = 1").structurally_compatible?(Post.where("author_id = 3"))
# => true

Post.joins(:comments).structurally_compatible?(Post.where("id = 1"))
# => false

「そして以下のように両者を#orでつなげる場合はcurrent.or(other)で、つなげない場合はModel.where(id: current)のようにラップしてから#orでつないでいる」「なるほど」「特にスコープでjoinsされた場合だと#and#orでつないでいいか考えることがちょくちょくあるので、これはあっていいメソッド👍」「名前はちょっと長いけど、知っていたら使うかも」

# 同PRより
relations = [...]
relations.drop(1).inject(relations.first) do |current, other|
  if current.structurally_compatible?(other)
    current.or(other)
  else
    Model.where(id: current).or(Model.where(id: other))
  end
end

🔗Rails

🔗 RailsプロジェクトにSteepで型を導入


つっつきボイス:「これは自分も読みました: RBSを自動生成するところから始めてひととおりやれる、具体的ないい記事👍」「RubyMineのRBS対応も進んでいますし、小さな新規プロジェクトでやってみてもよさそう」

「ちょうどこのツイートも見つけました」「RBSとSteepはRuby 2.6以降から使える、いいですね〜」

🔗 AWS LambdaでRails

aws/aws-lambda-ruby-runtime-interface-client - GitHub


つっつきボイス:「取り上げるのが遅れましたが、7月末の銀座Rails #35の@joker1007さんの発表です」「これもいい発表でした」

「元々Amazon ECSのタスク実行機能が使いにくいという問題があって、この問題は実際に踏んでみないとわかりにくいんですが、その部分をLambdaでやる方がいいという趣旨」

「AWSのコンテナをスケジューリングで動かす方法は現在2とおりあります: 1つはFargateやECSのscheduled taskと呼ばれるもので、コンテナをワンショットでバッチ的に実行する」「ふむふむ」「しかしECSの実行そのものがかなり重いのが不便: 具体的にはECSのイメージが置かれているリポジトリのパスがECSコンテナのタスク定義ファイルに書かれていて、起動のたびにそこからイメージをダウンロードしてECSの実行環境に展開することではじめて実行される」「聞くからに重そうですね…」「下手すると起動に数分かかることもあります」

参考: スケジュールされたタスク - Amazon ECS

「2つ目のLambdaは同じようにコンテナを起動しますが、ECSのタスク起動よりずっと速い」「お〜」「Lambdaにはコンテナサイズや実行時間に制限がありますが、実行時間が短くて何度も実行するようなものならLambdaでやる方がずっといい、というのがスライドのこのあたりの話↓」「なるほど」「ECSは常にECR(Elastic Container Registry)からDockerイメージをダウンロードするのと、タスクスケジューラが毎回コールドスタートするので遅いんですが、Lambdaは起動済みのDockerコンテナがあればウォームスタートできるので数秒おきや数分おきに起動するタスクだと特に速い: ちょうど自分もこのあたりにハマったことがありました」

参考: Amazon ECR(Docker イメージの保存と取得)| AWS

🔗 Rails 7でのimport maps導入

rails/importmap-rails - GitHub


つっつきボイス:「ついこの間話題になった、Rails 7のJSコードをWebpack経由でトランスパイラを通す代わりにimport mapsを使う話の動画について、DHHが記事も書いていたので取り上げました」「これについては既にTwitterにも書きましたけど、今後のRailsがこういうWebpackerレスな流れになることはほぼDHHが決めているようなので、このDHHの記事と解説動画、あとこれに関連する上の#42856の改修内容は一度見て押さえておくことをおすすめします: 技術的にもそれほど難しいものではありませんし、字幕をオンにすれば英語もそれほど大変ではないので」「なるほど」「RailsでTypeScript使いたい人たちはどうなるんだろう」

「ところで、DHHはReactの場合の動画も後追いで公開したんですよ↓」「これは知りませんでした」

「2本目のReact動画は、Railsがトランスパイラを使わなくなるとReactのJSXやTypeScriptなどが書けなくなるという問題を自分も含めていろんな人が指摘したので↓、DHHがJSXについては『こうすれば書けるよ』という回答として公開したものです」

参考: JSX の導入 – React

「ところが2本目の動画を見ると実際にはJSXそのものを書けるのではなくて、どうやらJSファイルにJSXライクなコードを書けるhというヘルパーを使うみたいなんですよ: import html from "htm"というコードをたどるとdevelopit/htmというライブラリがどうもそれみたい」「ありゃ」


Alpha preview: Using React with importmaps on Rails 7 – YouTubeより

「developit/htmのREADMEにも『プレーンなJavaScriptにトランスパイラなしでJSXライクな構文で書ける』とあるのでたぶんこれかなと思います」

developit/htm - GitHub

🔗 徳丸先生のRailsセキュリティ関連解説動画


つっつきボイス:「こちらの動画は、銀座Rails #34の徳丸先生のセッションを主催者承認のうえでYouTube動画で公開したそうです」「たしかに銀座Railsで見たヤツ」

「ところで徳丸先生のYouTubeチャンネルにもだいぶ動画が増えていますね」「動画だと見るのに時間がかかるのが大変」「そこですよね」「文章と動画とどちらを好むかは年齢層によって変わってきそうですけど」「せめて動画内でセリフや文字を検索できたらいいかも」「たしかに」「話し言葉と書き言葉はどうしても違ってくるので、自分は記事で読みたいかな」

🔗 KubernetesでRailsを動かす決定版ガイド


つっつきボイス:「KubernetesでRailsを動かす情報を、ドメインまで取って公開しているサイトです」「Kubernetesで単にRailsのDockerイメージを動かしてログを取るぐらいならさほど難しくありませんが、secretの扱いやrakeタスクやマイグレーションのようなRailsで最低限必要なことをひととおり網羅しているようですね: KubernetesでRailsを初めて動かす人向けの入り口として便利そう👍」「お〜」

🔗 その他Rails


つっつきボイス:「@yasaichiさんの発表が以下のスライド↓の続編になりそうなので期待しています」「9/15(水)開催だからRubyKaigi Takeout 2021の翌週か、とりあえず申し込んでおこう」


前編は以上です。

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

週刊Railsウォッチ: SorbetのRuby AOTコンパイラが公開、「Compiler Explorer」にRubyが追加、Ractorで非同期通信ほか(20210823後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: Rails 7でのimport maps導入、Steepで型を導入、KubernetesでRailsを動かすためのガイドほか(20210830前編) first appeared on TechRacho.

週刊Railsウォッチ:TruffleRubyでdig_fetchを実装、ruby/debug gem、AWSハンズオン教材ほか(20210901後編)

$
0
0

こんにちは、hachi8833です。遅ればせながら私もRubyKaigi Takeout 2021のTシャツを注文しました。

参考: Novelties - RubyKaigi Takeout 2021

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 TruffleRubyでdig_fetchを実装して高速化

# 同記事より
def self.dig_fetch(obj, idxs)
  idxs_size = idxs.size
  n = 0
  while n < idxs_size
    idx = idxs[n]

    obj = obj[idx]
    if obj.nil?
      raise KeyError.new("key not found: #{idx.inspect}", :receiver => self, :key => idx)
    end
    n += 1
  end

  obj
end

つっつきボイス:「dig_fetchというメソッド名がスゴい」「Rubyのdigfetchはデータ構造が見えていれば高速化の余地はあるかも」「そういえばruby-jp Slackで最近Rubyのdigfetch周りが話題になっていたのを見かけた気がします」「お、知りませんでした」

参考: instance method Hash#fetch (Ruby 2.3.0)
参考: Hash#dig (Ruby 3.0.0 リファレンスマニュアル)

「TruffleRubyはOracle Labsがこれだけ精力的に手を加え続けているんだから、どこかのproductionで使われていてもよさそう」「気になりますね」

oracle/truffleruby - GitHub

後で探すと、ShopifyがTruffleRubyのproduction利用に向けて実験を重ねているという記事を見かけました。またRubyKaigi Takeout 2021でもShopifyのメンバーがTruffleRubyの正規表現について発表することになっています。

参考: Optimizing Ruby Lazy Initialization in TruffleRuby with Deoptimization — Development (2021)
参考: Just-in-Time Compiling Ruby Regexps on TruffleRuby — Schedule - RubyKaigi Takeout 2021

🔗 ko1さんのdebug gemブログ


つっつきボイス:「ko1さんがruby/debugのブログを始めたそうです」「お〜、こういう情報が記事として記録されていくのは大事👍」「歴史大事ですね」「そうでないと端から失われていってしまうので」「relineを使ってdebugにREPL機能も追加されたのか」「記事でも紹介されているst0012さんのruby/debug紹介記事↓は近々TechRachoで翻訳を公開します」

参考: A Sneak Peek of Ruby’s New Debugger! - DEV Community
参考: REPL - Wikipedia

ruby/reline - GitHub

🔗 その他Ruby


つっつきボイス:「大倉さんがRubyConf 2021に登壇🎉」「英語で登壇つよい」「RubyConf 2021はオンラインとオフライン両方で開催とは頑張ってる」「開催地のデンバーは米国コロラド州で、州の布告に基づいてイベント開催してるようですね」

🔗クラウド/コンテナ/インフラ/Serverless

🔗 Amazon MemoryDB for Redis


つっつきボイス:「Amazon MemoryDB for RedisはRedisの互換実装によるサービスで、先々週ぐらいに発表されていましたね: 値段はお安くないですが、Redisを自前で立てると運用が大変なので、こういうサービスはやはり便利だと思います👍

🔗 東大のAWSハンズオン教材


つっつきボイス:「このAWSハンズオン教材は昨年公開されていたヤツかな」「ホントだ、2020年となってました」「東大クラスでないとなかなかこれだけの教材は用意できないでしょうね」「特に考えなくても上から順にやっていけば終わるので、東大の単位としては取りやすい方かも」「たまに考えないといけない場面で詰まるかもしれないので、TAやSAのサポートは必要かもしれませんね」

参考: 大学職員のTA・SAの仕事の違いは? | 大学職員の仕事・なり方・年収・資格を解説 | キャリアガーデン

「逆に東大のCPU実験授業はCPUつくってコンパイラつくって動作テストするところまでやるというエグさ」「そうそう、東大の名物授業」

参考: ほんとうのコンピュータ自作/CPU実験 — 東大 理学部情報科学科/大学院情報理工学系研究科|情報科学科NAVIgation

「東大のAWSハンズオン教材は、内容の賞味期限が切れないうちにやるのがいいと思います👍」「そうそう、今年なら十分いける」「こういうハンズオン授業は教材アップデートのためにも毎年やって欲しいですね」

🔗 その他クラウド

つっつきボイス:「Dockerfileでヒアドキュメントが書ける機能が正式にリリースされた🎉」「これで&& \を書かなくてよくなりますね」「変数展開もできる↓」「今までなかったのが不思議だったぐらい」

# moby/buildkit READMEより
# syntax = docker/dockerfile:1.3-labs
FROM alpine
ARG FOO=bar
COPY <<-eot /app/foo
    hello ${FOO}
eot

参考: Dockerで新しくサポートされるようになったヒアドキュメントを試してみました。 | オスースBlog

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 ブラウザのデザインモード


つっつきボイス:「これ今頃知りました」「ブラウザコンソールでdocument.designMode = "on"を入力するとブラウザが編集可能になるのね」「ずっと以前からHTMLElement.contentEditableという属性を使うとどの要素でも編集可能にできますけど、designModeは初めて見た」

参考: Document.designMode - Web API | MDN
参考: HTMLElement.contentEditable - Web API | MDN

「ちなみにブラウザ用WYSIWYGエディタによってはHTMLElement.contentEditableを使って実装しているものがあります」「お〜」「しかもHTMLElement.contentEditableを使うとHTMLのスタイルタグも自動的に付けられるのでHTML的に扱いやすいんですよ」

HTMLElement.contentEditableは相当昔からあった」「IE5の独自仕様だったのが取り入れられたという記事↓があったのでかなり古そう」「document.designModeもMDNにIE6に関する記述があるので同じぐらい古そうですね」

参考: HTMLのcontenteditable属性 - 備忘帳 - オレンジ工房
参考: “contentEditable” | Can I use... Support tables for HTML5, CSS3, etc

🔗言語/ツール/OS/CPU

🔗 クラック


つっつきボイス:「朝起きたら自宅の3Dプリンタからこんなのが出力されてビビったというReddit記事だそうです」「あぁクラックされたのね」「これはビビる」「普通のネットワークプリンタなんかもクラッキングされやすいという話は昔からありますけどね」

「ところで3Dプリンタも昔よりだいぶ使いやすくなりましたよね」「自宅に置くまではまだやれてないです」「よく使うなら買いたいけど、まだそこまではいかない」「コンビニで3Dプリントできたらいいんですけどね」「専有時間が長いから難しいんじゃないかな」「専門店で3Dプリントやったことならあります」「試行錯誤やトラブルシュートとかも考えると、最初はサポートのある場所でやってみるのがいいかもですね」


後編は以上です。

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

週刊Railsウォッチ: Rails 7でのimport maps導入、Steepで型を導入、KubernetesでRailsを動かすためのガイドほか(20210830前編)

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

The post 週刊Railsウォッチ:TruffleRubyでdig_fetchを実装、ruby/debug gem、AWSハンズオン教材ほか(20210901後編) first appeared on TechRacho.

Rubyの新しいデバッガの機能を先行紹介(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。なお翻訳記事公開時点のデバッガバージョンはv1.0.0rc2になりました。

Rubyの新しいデバッガの機能を先行紹介(翻訳)

ruby/debug - GitHub

debugはRubyの新しいデバッガで、Ruby 3.1に同梱される予定です。近頃はこのデバッガにコントリビューションしつつ自分でも使ってきたので、1.0が正式にリリースされる前にそろそろ皆さんにいち早く紹介するときが来たと感じています。

(本記事執筆時点のdebugはまだ正式リリースではないため、本記事で紹介する機能は正式リリースまでに変更または削除される可能性があります)

(追記: 本プロジェクトのリード開発者@ko1氏が以下のデバッガ記事連載を始めましたので、こちらもチェックよろしくお願いします😉

🔗 あらまし

上述のように、このデバッガはRuby 3.1の標準ライブラリになる予定です。現時点では以下のようにgemとしてインストールできます。

$ gem install debug --pre

または

# Gemfile
# 開発が活発なので、可能ならGitHubをソースとして指定するのがおすすめ
gem "debug", github: "ruby/debug"

debugは、機能面では有名なGDBデバッガやRubyのbyebug gemと似ています。豊富なデバッグコマンドの他に、独自の機能をいくつも備えています。

READMEより引用します。

debug.rbには以下のような多くの利点がある。

  • 高速性: ノンステップモードやノンブレークポイントでパフォーマンスを損なわない
  • リモートデバッグをネイティブでサポート
  • 拡張性: 以下のようにさまざまな方法でアプリケーションにデバッグを導入可能
    • rdbgコマンド
    • コマンドラインで-rオプションを指定
    • 明示的なRubyメソッド呼び出し
  • その他
    • スレッド(ほぼ完了)とRactor(ToDo)のサポート
    • ほぼいつでもCtrl-Cでコンソールデバッグのオンオフを切り替え可能
    • バックトレースコマンドでのパラメータ表示

以下は私が気に入っている機能です。

  • カラー表示

  • backtraceコマンドでバックトレースを表示するとメソッドの「引数」「ブロック引数」「戻り値」も表示される
=>#0    Foo#forth_call(num1=20, num2=10) at target.rb:20 #=> 30
  #1    block {|ten=10|} in second_call at target.rb:8
  • binding.breakでデバッグコマンドをスクリプト化して手動操作を軽減できる(後述の組み合わせセクションのサンプルを参照)
  • breakcatchwatchなどのコマンドを用いてブレークポイントをさまざまな条件で起動できる

🔗 binding.break(エイリアス: binding.b

私のようなpryのヘビーユーザーなら、pryで慣れ親しんだbinding.breakbinding.bだけでもよい)を用いてデバッグセッションを開始できます。

ただし、実際のbinding.breakには以下のようにコマンドを渡せるので、binding.pryよりも強力です。

  • binding.b(do: "catch CustomException"): デバッガーはcatch customExeptionコマンドを実行してプログラムを続行する
  • binding.b(pre: "catch CustomException"): デバッガーはcatch customExeptionコマンドを実行してその行で停止する

(コマンドを複数実行したい場合は"cmd1 ;; cmd2 ;; cmd3"のように;;で区切ります)

🔗 よく使うコマンド

新しいデバッガには強力なコマンドが多数搭載されています。その中でも私が最も多用しているものを以下に紹介します。

🔗 breakコマンド(エイリアス: b

class A
  def foo; end
  def self.bar; end
end

class B < A; end
class C < A; end

B.bar
C.bar

b1 = B.new
b2 = B.new
c = C.new

b1.foo
b2.foo
c.foo
基本の用法
  • b A#foo: b1.foob2.fooc.fooが呼び出されると停止する
  • b A.bar: B.barC.barが呼び出されると停止する

  • b B#foo: b1.foob2.fooが呼び出されると停止する

  • b B.bar: B.barが呼び出されると停止する

  • b b1.foo: b1.fooが呼び出されると停止する

コマンド
  • b b1.foo do: cmd: b1.fooが呼び出されるとcmdを実行する(ただし停止しない)

  • b b1.foo pre: cmd: b1.fooが呼び出されるとcmdを実行して停止する

🔗 catchコマンド

class FooException < StandardError; end
class BarException < StandardError; end

def raise_foo
  raise FooException
end

def raise_bar
  raise BarException
end

raise_foo
raise_bar
  • catch StandardError: StandardErrorFooExceptionBarExceptionも含む)のインスタンスがraiseされると停止

  • catch FooException: FooExceptionがraiseすると停止

🔗 backtraceコマンド(エイリアス: bt

以下は出力例です。

=>#0    Foo#forth_call(num1=20, num2=10) at target.rb:20 #=> 30
  #1    block {|ten=10|} in second_call at target.rb:8
  #2    Foo#third_call_with_block(block=#<Proc:0x00007f9283101568 target.rb:7>) at target.rb:15
  #3    Foo#second_call(num=20) at target.rb:7
  #4    Foo#first_call at target.rb:3
  #5    <main> at target.rb:23
  • bt: スタックの全フレームを表示
  • bt 10: スタックの最初の10フレームのみを表示

  • bt /my_lib/: my_libにマッチするパスのフレームのみを表示

🔗 outlineコマンド(エイリアス: ls

outlineコマンドは、irbやpryのlsコマンドと同様に使えます。

🔗 binding.bとコマンドの組み合わせ

🔗 binding.b(do: "b Foo#bar do: bt")

この機能を使うと、メソッド定義に手を加えたりコマンドを手動で入力したりせずにメソッド呼び出しのバックトレースをinspectできます。

以下はスクリプト例です。

binding.b(do: "b Foo#bar do: bt")

class Foo
  def bar
  end
end

def some_method
  Foo.new.bar
end

some_method

以下は出力です。

DEBUGGER: Session start (pid: 75555)
[1, 10] in target.rb
=>    1| binding.b(do: "b Foo#bar do: bt")
      2|
      3| class Foo
      4|   def bar
      5|   end
      6| end
      7|
      8| def some_method
      9|   Foo.new.bar
     10| end
=>#0    <main> at target.rb:1
(rdbg:binding.break) b Foo#bar do: bt
uninitialized constant Foo
#0  BP - Method (pending)  Foo#bar do: bt
DEBUGGER:  BP - Method  Foo#bar at target.rb:4 do: bt is activated.
[1, 10] in target.rb
      1| binding.b(do: "b Foo#bar do: bt")
      2|
      3| class Foo
=>    4|   def bar
      5|   end
      6| end
      7|
      8| def some_method
      9|   Foo.new.bar
     10| end
=>#0    Foo#bar at target.rb:4
  #1    Object#some_method at target.rb:9
  # and 1 frames (use `bt' command for all frames)

Stop by #0  BP - Method  Foo#bar at target.rb:4 do: bt
(rdbg:break) bt
=>#0    Foo#bar at target.rb:4
  #1    Object#some_method at target.rb:9
  #2    <main> at target.rb:12

🔗 binding.b(do: "b Foo#bar do: info")

メソッドが呼び出されたときのメソッドの環境(引数など)をinspectできます。

以下はスクリプト例です。

binding.b(do: "b Foo#bar do: info")

class Foo
  def bar(a)
    a
  end
end

def some_method
  Foo.new.bar(10)
end

some_method

以下は出力です。

DEBUGGER: Session start (pid: 75924)
[1, 10] in target.rb
=>    1| binding.b(do: "b Foo#bar do: info")
      2|
      3| class Foo
      4|   def bar(a)
      5|     a
      6|   end
      7| end
      8|
      9| def some_method
     10|   Foo.new.bar(10)
=>#0    <main> at target.rb:1
(rdbg:binding.break) b Foo#bar do: info
uninitialized constant Foo
#0  BP - Method (pending)  Foo#bar do: info
DEBUGGER:  BP - Method  Foo#bar at target.rb:4 do: info is activated.
[1, 10] in target.rb
      1| binding.b(do: "b Foo#bar do: info")
      2|
      3| class Foo
      4|   def bar(a)
=>    5|     a
      6|   end
      7| end
      8|
      9| def some_method
     10|   Foo.new.bar(10)
=>#0    Foo#bar(a=10) at target.rb:5
  #1    Object#some_method at target.rb:10
  # and 1 frames (use `bt' command for all frames)

Stop by #0  BP - Method  Foo#bar at target.rb:4 do: info
(rdbg:break) info
%self = #<Foo:0x00007fdac491c200>
a = 10

私はRails開発者なので、以下のようなコードをコントローラやアクションの冒頭に置いて使うことがよくあります。

class SomeController < ApplicationController
  def index
    binding.b(pre: "b User#buggy_method do: info")
    # 他のコード
  end
end

後はデバッガーでコマンドを実行するもよし、見たいメソッドでデバッガーを停止するもよしです。
おかげで、binding.pryputsを追加してファイルからファイルへジャンプする必要がなくなりました😎

🔗 小さな問題点(その後解消)

しかし新しいデバッガーは(まだ)完璧ではありません。byebugやpryのようにRubyの式を直接評価できません。

(rdbg) 1 + 1
unknown command: 1 + 1

式を評価するには、以下のようにpppを追加する必要があります。

(rdbg) p 1 + 1
=> 2

ただし本プロジェクトのメンテナーである@ko1コメントによると、1.0の正式リリースまでに式の評価機能をサポートするかもしれないとのことです。

続報

その後#227がマージされたおかげで、この問題は解消されました😉

最後に

新しいデバッガーはまだ公式にはリリースされていませんが、私は日々の業務で使い始めています。近い将来、すべてのRubyistのツールボックスに常備されると信じています。新しいデバッガーの機能に興味をお持ちでしたら、ぜひお試しください😉

関連記事

Ruby: 「オブジェクト指向トレース」とtapping_device gemで効率よくデバッグ(翻訳)

The post Rubyの新しいデバッガの機能を先行紹介(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: ActiveRecord::QueryLogs追加、spring gemがデフォルトから削除、fast_gettextほか(20210906前編)

$
0
0

こんにちは、hachi8833です。今週はいよいよRubyKaigi Takeout 2021ですね。

週刊Railsウォッチについて

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

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

お知らせ: 来週の週刊Railsウォッチはお休みいたします🙇

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

公式に追いつくべく今回は2回分取り上げます。

import-mapsは先週取り上げたので(ウォッチ20210831)、importmaps-railsリポジトリを貼ります。

rails/importmap-rails - GitHub


つっつきボイス:「DHHは今後このgemを推していくんでしょうね」「予想はしていたけどRails 7以上なのか〜」「今やってるプロジェクトでimport mapsが使えたらと思ったんですが残念」

🔗 Marginalia gemがActiveRecord::QueryLogsとして追加


つっつきボイス:「Marginalia?」「Basecampのgemだそうです」「そのgemがネイティブのActiveRecord::QueryLogsとして追加されたみたい」

basecamp/marginalia - GitHub

「MarginaliaはSQLクエリのログにどのアプリのどのコントローラのどのアクションからのクエリかというコメントをこういうふうに追加するのね↓: そういえばこんなgemがあった」「名前でぱっとわかりにくそう」「造語かと思ったら”傍注”という意味でした」「入っていたら使うかも👍

# basecamp/marginalia READMEより
Account Load (0.3ms)  SELECT `accounts`.* FROM `accounts` 
WHERE `accounts`.`queenbee_id` = 1234567890 
LIMIT 1 
/*application:BCX,controller:project_imports,action:show*/

ActiveRecord::QueryLogsを追加

Active Recordで生成されるすべてのSQLクエリにコンフィグ可能なタグが自動追加可能になった。

# config/application.rb
module MyApp
  class Application < Rails::Application
    config.active_record.query_log_tags_enabled = true
  end
end

デフォルトでは、クエリタグに「アプリケーション」「コントローラ」「アクション」の詳細が追加される。

class BooksController < ApplicationController
  def index
    @books = Book.all
  end
end
GET /books
# SELECT * FROM books /*application:MyApp;controller:books;action:index*/

静的な値やProcを含むカスタムタグをアプリケーション設定で定義可能。

config.active_record.query_log_tags = [
  :application,
  :controller,
  :action,
  {
    custom_static: "foo",
    custom_dynamic: -> { Time.now }
  }
]

Keeran Raj Hawoldar, Eileen M. Uchitelle, Kasper Timm Hansen
同Changelogより

以下は作成中の移行ガイドだそうです。

参考: upgrade.md

🔗 マルチDBのrails db:setuprails db:resetで特定のデータベースを指定できるようになった

これは、データベース固有のセットアップとリセットのタスクを可能にする試み。自分たちはマルチプルデータベースを広範囲に使っていて、すべてのデータベースを一度にリセットすることは避けたいと考えている。この変更により、名前空間ごとにデータベース固有のセットアップやリセットのタスクが追加される。デフォルトのセットアップタスクやおよびリセットタスクは、説明文を除いて変更していない。

ひとつ問題があるとすれば、db:seedタスクはデータベースに依存しないため、データベースに依存するタスクはすべてをseedすることになる点。特定のデータベースだけをseed可能だろうか?見たところ、seedファイルはどのActive Recordモデルでも参照できるので、できなさそうに思える。

たとえば、primaryとeventsという2つのデータベースがある場合、以下のタスクが利用できる。

rails db:reset                   # Drops and recreates all databases from their schema for the current environment and loads the seeds
rails db:reset:events            # Drops and recreates the events database from its schema for the current environment and loads the seeds
rails db:reset:primary           # Drops and recreates the primary database from its schema for the current environment and loads the seeds
rails db:setup                   # Creates all databases, loads all schemas, and initializes with the seed data (use db:reset to also drop all databases first)
rails db:setup:events            # Creates the events database, loads the schema, and initializes with the seed data (use db:reset:events to also drop the database first)
rails db:setup:primary           # Creates the primary database, loads the schema, and initializes with the seed data (use db:reset:primary to also drop the database first)

同PRより


つっつきボイス:「マルチプルDBで特定のデータベースだけを設定したりリセットしたりできるrakeタスクが追加されたんですね」「今までできなかったとは」「これは欲しい機能👍

🔗 spring gemをデフォルトインストールから削除


つっつきボイス:「development環境やtest環境でRailsをプリロードして起動を速くするspring gemがRailsのデフォルトから消えるそうです」「いいねがかなり多いですね」

rails/spring - GitHub

コンピュータが高速になったので、小〜中規模アプリでspringを使う大きなメリットがほぼなくなった。よって、たまに問題を起こすspringをデフォルトで入れてつらい思いをする必要はもうない。
同PRより

「自分もspringはいつも真っ先にオフにしてる」「私も」「テストが理由なく落ちるときにspringを無効にすると解消するということがちょくちょくありましたね」「マイグレーションがなぜか失敗したときもよくありました」「ネイティブ環境だとspringもそれなりに有用なのかもしれないけど、最近はDocker環境で使う人が増えていますし、それもあって外したのかもしれませんね」

🔗 classicモード廃止に伴う孤立メソッド削除


つっつきボイス:「ActiveSupport::Dependencyからorphanな(孤立した)メソッドが続々削除されたそうです」「privateなインターフェイスだし消しても大丈夫そう」

参考: 定数の自動読み込みと再読み込み (Classic) - Railsガイド

「もうひとつのプルリクはActiveSupport::Dependencyにあるsafe_constantizeが削除された」「constantizeは使っていたけどsafe_constantizeは知らなかったな〜」

参考: constantizeString

(これはclassicモードの削除に伴うActiveSupport::Dependenciesのお掃除の一環)

ActiveSupport::Dependenciesconstantizeメソッドとsafe_constantizeメソッドはprivateでオートローディングとは関係しておらず、単にinflectorに転送される。これらは歴史的な理由で残されていたが、今や既に不要。
publicなインターフェイスはStringクラスにある。

model_name.constantize

したがって以下の代わりに上のように書ける。

ActiveSupport::Dependencies.constantize(model_name)

既にフレームワークの大半がこのようになっており、残りはわずか。
#43058より

🔗 weekday_options_for_selectビューヘルパーが追加


つっつきボイス:「ブラウザで平日の曜日選択のプルダウンを表示するweekday_options_for_selectってありそうでなかったのか」「ビューのフォームヘルパーなんですね」「これはあっていいメソッド👍

自分はこれまで多くのRailsアプリで平日の曜日を選択するヘルパーを手作りしなければならなかった。Railsには優秀なヘルパーがほとんど揃っているので、このヘルパーがないことにいささか驚いていた。ちょうどヘルパーをまた実装しなければならなくなり、他の開発者もこのヘルパーがないことに驚いていたので、Railsにプルリクを投げてもいい頃合いだと思った。

このプルリクはFormOptionHelperFormBuilderに2つのメソッドを追加し、Tags::WeekdaySelectクラスを追加する。

weekday_options_for_select
# => "<option value=\"Sunday\">Sunday</option>\n<option value=\"Monday\">Monday</option>\n
# <option value=\"Tuesday\">Tuesday</option>\n<option value=\"Wednesday\">Wednesday</option>\n
# <option value=\"Thursday\">Thursday</option>\n<option value=\"Friday\">Friday</option>\n
# <option value=\"Saturday\">Saturday</option>"

weekday_options_for_selectではselected値を受け取るほかに:index_as_valueオプションと:day_formatオプションも受け取れる。

weekday_options_for_select(nil, day_format: :abbr_day_names)
# => "<option value=\"Sun\">Sun</option>\n<option value=\"Mon\">Mon</option>\n
# <option value=\"Tue\">Tue</option>\n<option value=\"Wed\">Wed</option>\n
# <option value=\"Thu\">Thu</option>\n<option value=\"Fri\">Fri</option>\n
# <option value=\"Sat\">Sat</option>"

weekday_options_for_select(nil, index_as_value: true)
# => "<option value=\"0\">Sunday</option>\n<option value=\"1\">Monday</option>\n
# <option value=\"2\">Tuesday</option>\n<option value=\"3\">Wednesday</option>\n
# <option value=\"4\">Thursday</option>\n<option value=\"5\">Friday</option>\n
# <option value=\"6\">Saturday</option>"

weekday_options_for_selectは以下のような場合で使うヘルパーメソッド。

weekday_select(:model, :weekday)
# => "<select name=\"model[weekday]\" id=\"model_weekday\"><option value=\"Sunday\">Sunday</option>\n
# <option value=\"Monday\">Monday</option>\n<option value=\"Tuesday\">Tuesday</option>\n
# <option value=\"Wednesday\">Wednesday</option>\n<option value=\"Thursday\">Thursday</option>\n
# <option value=\"Friday\">Friday</option>\n<option value=\"Saturday\">Saturday</option></select>"

weekday_selectメソッドはFormBuilderでも使われるので、以下のように書くと

<!-- 同PRより -->
<%= form_for @digest do |f| %>
  <%= f.weekday_select :weekday %>
  <%= f.submit %>
<% end %>

以下のようなHTMLが生成される。

<!-- 同PRより -->
<select name="digest[weekday]" id="digest_weekday">
  <option value="Sunday">Sunday</option>
  <option value="Monday">Monday</option>
  <option value="Tuesday">Tuesday</option>
  <option value="Wednesday">Wednesday</option>
  <option value="Thursday">Thursday</option>
  <option value="Friday">Friday</option>
  <option value="Saturday">Saturday</option>
</select>

同PRより

🔗 特定データベースのyaml設定にdatabase_tasks: falseオプションが追加


つっつきボイス:「通常はたとえばrails db:migrateを実行するとすべてのデータベースにコネクションを張るけど、database_tasks: falseを指定したデータベースにはコネクションを張らなくなるようですね」「ふむふむ」「以下のmy_animals_databaseでアクセスを一切行いたくない場合に使うということだと思います: コマンドラインでもいいような気はしますが、yamlで設定できるとより便利そう👍

データベース設定オプションdatabase_tasksを追加
「スキーマ管理」「マイグレーション」「seed」などの管理タスクがない外部データベースに接続したい場合、データベースごとにdatabase_tasks: falseオプションを設定できるようになった。

# config/database.yml
production:
  primary:
    database: my_database
    adapter: mysql2
  animals:
    database: my_animals_database
    adapter: mysql2
    database_tasks: false

Weston Ganger
同Changelogより

🔗 削除されたオプション


つっつきボイス:「どちらもDHHによるプルリクです」「#42996で--skip-gemfileオプションが削除されて、#42998で--skip-pumaオプションが削除されたんですね」「歴史的なオプションみたい」

いにしえの小競り合いの記念碑をいつまでも残しておく必要はない。
#42996より
必要な設定を削除するためのオプションには意味がない。
#42996より

🔗Rails

🔗 大量のActionMailジョブをSidekiqで一括処理する(Ruby Weeklyより)


つっつきボイス:「deliver_laterで個別のジョブを大量に投げて詰まるのはよくあるヤツ」「1万通のメールで40分待ちはつらそう…」「この記事ではジョブの登録の詰まりが問題になっているみたいですね」

「Sidekiqのpush_bulkを使うと複数ジョブをまとめて投げられるのか」「メールジョブごとにパラメータを渡したい場合はActionMailer::Parameterized::DeliveryJobを使う必要があるのね」「メールによって送り先や文面をパラメータで変えるのはよくありますね」「ジョブを用意する段階でパラメータを渡しておいてからpush_bulkでまとめてプッシュする方が負荷が小さくなる、たしかに」「アトミックな処理を行う大量のジョブを1個ずつ登録するのはやりたくないですね」

参考: Method: Sidekiq::Client#push_bulk — Documentation for mperham/sidekiq (master)

# 同記事より
def enqueue_many_parametrized_mails(mail_class, template, args_array)
  job = ActionMailer::Parameterized::DeliveryJob

  # convert template and args array into an array of arrays containing args
  # for ActionMailer::DeliveryJob objects
  mailer_job_args = args_array.map { |args|
    [job.new(
      mail_class.name,
      template.to_s,
      "deliver_now",
      {some_arg: "foo"},
      *args
    ).serialize]
  }

  Sidekiq::Client.push_bulk(
    "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper,
    "wrapped" => job,
    "queue" => MAILER_QUEUE,
    "args" => mailer_job_args
  )
end

参考: Rails6 のちょい足しな新機能を試す41(MailDeliveryJob 編) - Qiita

🔗 fast_gettext: 高速i18n gem(Ruby Weeklyより)

grosser/fast_gettext - GitHub

# 同リポジトリより
FastGettext.with_locale 'gsw_CH' do
  FastGettext._('Car was successfully created.')
end
# => "Z auto isch erfolgriich gspeicharat worda."

つっつきボイス:「i18n(国際化)のgemのようです」「gettextライブラリをrubyで再実装したみたい」

参考: gettext - Wikipedia

「fast_gettextはgettextより12倍高速でガベージが530分の1でスレッドセーフですって」「Active SupportのI18n::Simpleも比較されてる」「大量の国際化テキストを処理しないといけない場合に必要になってくるんでしょうね: 自分はあまりその必要に迫られたことはありませんが、海外のサイトだと国際化の言語数が多い分切実なのかも」

Hash FastGettext GetText ActiveSupport I18n::Simple
Speed* 0.08s 0.14s 1.75s 3.75s
Objects* 11K 15K 8017K 7107K
Included backends db, yml, mo, po, logger, chain mo yml (db/key-value/po/chain in other I18n backends)

同リポジトリより


前編は以上です。

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

週刊Railsウォッチ:TruffleRubyでdig_fetchを実装、ruby/debug gem、AWSハンズオン教材ほか(20210901後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: ActiveRecord::QueryLogs追加、spring gemがデフォルトから削除、fast_gettextほか(20210906前編) first appeared on TechRacho.

週刊Railsウォッチ: 責任あるモンキーパッチの当て方、gem脆弱性スキャンツール、Docker Desktop課金プラン改定ほか(20210907後編)

$
0
0

こんにちは、hachi8833です。来月出るんですね。

週刊Railsウォッチについて

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

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

お知らせ: 来週の週刊Railsウォッチはお休みいたします🙇

🔗Ruby

🔗 Twist: 出版原稿レビュープラットフォーム(Ruby Weeklyより)

radar/twist-v2 - GitHub


つっつきボイス:「ややこしいですが、本を執筆している著者が出版前に原稿をレビューしてもらうためのプラットフォームを作ったのがこのTwistだそうです」「自分でこのアプリを作ってみたさまざまな知見を記事にした感じかな: よさそうな記事👍

「twist-v2はバックエンドを切り離した今風の設計っぽい↓」「ディレクトリ構成はRailsっぽく見えるしZeitwerkもあるけど、RailsアプリでもSinatraアプリでもなくHanamiで、dry-rbrom-rbyなどもいろいろ使って作ってみたらしい、へ〜」「フロントエンドはReactで、GraphQL経由でサーバーを呼び出しているのね」

DB <-> Backend Repositories <-> Backend GraphQL endpoint <-> Frontend <-> Browser
radar/twist-v2 READMEより

「そういえば作者のRyan Biggさんは以下の記事を書いた人でした↓」「CurrentAttributesが有害というのはワカル」

Railsの`CurrentAttributes`は有害である(翻訳)

🔗 責任あるモンキーパッチの当て方(Ruby Weeklyより)


つっつきボイス:「ちょっと長い記事です」「2011年にRuby 1.8.7で巨大Railsアプリをやっていた話がじわじわ来ますね」「え、そのときにString#%にモンキーパッチを当てたのか!」「マジで?」「String#%は文字列フォーマット用メソッドですね: こういうどこでも使われそうなメソッドにモンキーパッチを当てるのは怖い…」

# 同記事より: String#%にモンキーパッチを当てたときの挙動
replacements = {
  horse_count: 3,
  horses: {
    one: "is 1 horse",
    other: "are %{horse_count} horses"
  }
}

# "there are 3 horses in the barn"が出力される
"there %{horse_count:horses} in the barn" % replacements

参考: String#% (Ruby 3.0.0 リファレンスマニュアル)

「モンキーパッチが失敗する主な原因↓とかいろいろ面白そう」

  • パッチそのものが壊れた場合: 上述のコードベースでは、同じメソッドで複数の実装が競合するのみならず、「勝った」メソッドも動いてくれなかった
  • 仮定が正しくなかった場合: ホストコードが更新されてパッチが期待どおりに当たらなくなっていた
    同記事より

「そういうときのためにRubyにはrefinementがあるのに、と思ったら、意外にもこの記事にはrefinementの話がまったくなかった」「あら、ホントだ」

RubyのRefinement(翻訳: 公式ドキュメントより)

「ざっと眺めた限りではRubyの経験が豊富な人という印象: ちゃんと読んだら学びがありそう👍」「翻訳してみたくなりました」

🔗 Snykのgem脆弱性スキャンツールとRails脆弱性データベース(Ruby Weeklyより)


つっつきボイス:「このSnykは自社でさまざまな言語を対象とする脆弱性スキャンCLIツールを出していて、それを使ってGemfileのセキュリティチェックを行う記事のようですね」「価格表を見た感じではこのツールでビジネスをやっているみたい」「今でもGitHubのDependabotとかを使えば脆弱性を通知できますけどね」「フリープランもあって自動テストでこういうツールを回せるならちょっと使ってみてもいいかも」

snyk/snyk - GitHub

dependabot/dependabot-core - GitHub

参考: Language support summary – Docs Library | Snyk

「Snykはこういうgemごとの脆弱性データベースも公開しているそうです↓」「お、これはなかなか便利そう」「npmやpipなどの脆弱性情報もあるんですね」

参考: rails vulnerabilities | Snyk

🔗 pnglitch: pngファイルを壊すgem(Ruby Weeklyより)

ucnv/pnglitch - GitHub


つっつきボイス:「以下のサイトにあるような感じでpngファイルをいい感じに壊すgemだそうです」「ああpngファイルってたしかにこういうふうに壊れますよね😆」「面白い😆」「pnglitchって文字どおりpngのglitch(故障)なのね」

参考: The Art of PNG Glitch


ucnv.github.ioより

「何のためのgemなんでしょうね?」「それがよくわからなくて、その割に★がたくさんあるのも謎です」「デスクトップやZoomの背景にしてビックリさせるとか?😆」「そういえば液晶に線が入ったような感じになるスクリーンセーバーもありましたね」

READMEによると、単に壊れたpngを眺めて楽しむためのgemのようです。後でart of pnglitchでググるとそれっぽいサイトがたくさん出てきました。

🔗 Unixのepoch時間をRubyオブジェクトに変換する(Ruby Weeklyより)

参考: UNIX時間 - Wikipedia


つっつきボイス:「Time#atin:オプションのドキュメントがなかったので、記事書いた人がプルリクを投げたのね↓」「マージされてよかった🎉

参考: Update docs for Time#at method by prathamesh-sonpatki · Pull Request #2929 · ruby/ruby
参考: class Time (Ruby 3.0.0 リファレンスマニュアル)

「お、これは例のmilitary time zone」「『タイムゾーン呪いの書』にも載っていた、1文字で表すタイムゾーンですね(ウォッチ20210713)」

# 同記事より
>> Time.at(ts, in: "E")
#=> 2020-03-02 09:13:24 +0500
>> Time.at(ts, in: "Z")
#=> 2020-03-02 04:13:24 UTC

参考: List of military time zones - Wikipedia

「このエラーメッセージを見ると、military time zoneにはA〜IとK〜ZはあるけどJがないのが面白い↓」

# 同記事より
>> Time.at(ts, in: "IST")
Traceback (most recent call last):
        2: from (irb):12
        1: from (irb):12:in `at'
ArgumentError ("+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset)

🔗DB

🔗 UUIDとULID


つっつきボイス:「はてブで見つけました」「記事にもあるように、UUID(バージョン1)はランダム部が桁の上位にあるために、データベースのプライマリキーに使うとクラスタインデックスのデータ分布が散ってしまう点がパフォーマンス上不利ですね」

「このあたりはデータのlocality(局所性)の話題で、一般にlocalityが高いほどキャッシュヒットしやすくなるのでパフォーマンス上有利な代わりに、障害に弱くなる傾向があるというトレードオフの関係がありますが、RDBMSのインデックスの話であれば一部だけが破損するようなケースは想定しにくいのでlocalityが高い方が有利です」「なるほど」「逆に分散システムだと、パフォーマンスを少々犠牲にしてでもlocalityを下げることもあります」

参考: 参照の局所性 - Wikipedia

「分散システムではデータが一様に分布する方が障害に強くなりますが、この記事のような単一のデータベースシステムの場合は基本的にlocalityが高い方がパフォーマンス上有利」「ふむふむ」「もともとUUIDの設計はパフォーマンスよりも分散システム上でIDが衝突しないことがメインなんですよ: RDBMSでlocalityが高い方が効率がよくなるならULIDを使うのがいいでしょうね」「なるほど!」

参考: ULID - shimojubox

「元記事は図も含めて丁寧に書かれているので、UUID周りを知らなかった人にはおすすめ👍: このサイトは他のデータベース記事もよいです」

🔗クラウド/コンテナ/インフラ/Serverless

🔗 Docker Desktopの課金プラン改定


つっつきボイス:「Docker Desktopの件は今だいぶ騒がれているので、そのうち方針が変わるんじゃないかなと思いたい」「乗り換えを考える記事もいろいろ出始めてますね」

参考: Docker Desktop有料化の影響 - Qiita

「今回発表された課金プランだとDocker Desktopがいろいろ使いにくくなってしまうのが残念」「ですよね」「課金されるかどうかは売上ベースか従業員数ベースで決まるんですが、Docker Desktopを使ってない他部署の売上も課金の閾値に影響することになりますし、大企業がこれからDocker Desktopを導入しようとしたときに売上が見込めないうちから課金対象になると話が通しにくくなりそう」「あ〜」「課金とは別の話ですが、アカウントを”人”に紐付けるのが必須なので、開発メンバーの引き継ぎとかがやりづらい」「誰か退職するとCIが止まりそうですよね」

「ツイートにも書きましたけど、自分たちはローカルでDocker Desktopをクライアントとして使えればよくて、それでいて有償のクラウド機能に欲しいものがないところに食い違いを感じるんですよ」「そこですよね」「課金そのものはあっていいと思うので、もっと有用感のある課金プランを検討して欲しい気持ち」「それこそお金を払ってでもPersonal版を使いたい企業もありそう」

参考: Docker Pricing & Monthly Plan Details | Docker


後編は以上です。

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

週刊Railsウォッチ:ActiveRecord::QueryLogs追加、spring gemがデフォルトから削除、fast_gettextほか(20210906前編)

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

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

Ruby Weekly

The post 週刊Railsウォッチ: 責任あるモンキーパッチの当て方、gem脆弱性スキャンツール、Docker Desktop課金プラン改定ほか(20210907後編) first appeared on TechRacho.


Ruby: 静的型付けで解決しない問題とは(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Ruby: 静的型付けで解決しない問題とは(翻訳)

Brandur Leach氏のブログ記事『Better typing in Ruby』は、Stripeの静的型付けチェッカーSorbetが誕生したときの状況と、Sorbetで得られたメリットについて解説しています。私はこれまで静的型付けのメリットについては割り引いて考えてきましたが、この記事を読むうちに、私がそう考えるに至った原因となる点がいくつも浮かび上がってきました。同記事では、Stripeのコードベースにある問題の中で、静的型チェックが作業の軽減に有用なケースについて言及しています。しかし静的型付けはそれらの問題を「解決」するものではなく、静的な型付けが存在し続けることで別のコストが発生する可能性もあります。

私は何も、Stripeの方法に間違いがあるとか、ほかの方法を採用すべきだったなどと申し上げたいのではありません。私は彼らほど大規模なRubyコードベースで仕事をしたことはなく、はるかに小さなコードベースでの仕事しか経験していないので、彼らの決定を評価する立場にはありません。彼らの場合は問題の発生が避けられず、静的型チェックが問題解決としてベストだった可能性も十分考えられます。

むしろ同記事をきっかけとして、そこで抽象的に説明されている問題について考察し、自分が関わるプロジェクトでその問題が発生したときに自分ならどうするかを考えてみたいと思います。私にとって静的型付けは、最初に手をつけるソリューションではありません。

以上を踏まえて、問題点を見ていくことにしましょう。

問題に適用される静的型付け

Rubyはモジュール化の促進が苦手なので、コードは巨大な不定形のblob(binary large object)のようになり、あらゆる場所からあらゆる場所が呼び出され、コードの境界は本質的に理論止まりになる。

上の文で述べられているのは実際には因果関係です。「Rubyはモジュール化の促進が苦手なので」が原因の部分で、その結果「コードは巨大な不定形のblobのようになり、あらゆる場所からあらゆる場所が呼び出され、コードの境界は本質的に理論止まりになる」という流れです。しかし不定形のblobが生じるのは本当に言語そのものが原因でしょうか?

コードを書くのはRuby言語ではなく開発者であり、Rubyにモジュール化を促進する機能がないとしても「あらゆる場所からあらゆる場所が呼び出されるコード」を書くかどうかを決めるのも開発者です。言語による効果とは、ある方向にどれだけ強く働きかけるかということです。「Rubyにはモジュール化を強力に促進する機能がないので、開発者はコードをモジュール化する形で書くべきなのに、そこに注意を払っていなかった」とも解釈できそうです。

(中略)エンジニアは新しいコードを書くよりも既存のコードを修正することがほとんどでした。コードのどの部分を精査するときでも、その型を知るために必要なのは名前だけで済み(中略)型がすぐにわからない場合でも、パスにpryを投げ込んでその部分を実行するテストケースを探し、実行時に変数を検査すれば十分調べられます(中略)

(中略)コードはテストの有無にかかわらず解析されるので、テストカバレッジに漏れがある場合の保護層にもなります。

コードのかなりの部分がテストされていないとしたら問題です。私のテストカバレッジは100%ではありませんし、外部システムやライブラリとの統合/アダプター層のテストを書くことはほとんどありませんが、アプリケーションを構成するコードについては徹底的にテストでカバーしたいと思っています。

これは「既存のコードを修正する」プロセスにおいて、コードをカバーするテストが存在するのかやテストがどこにあるかがわからない状況での話です。このケースに該当する場合、変数の型がわかるだけでは保証が不十分です。私は自分の変更によってビジネスロジックが壊れないことを確認したいので、そのコードがテストされている必要があります。

(中略)該当部分を実行するテストケースを探し出して、実行時に変数を検査するのは耐え難いほど時間のかかる作業でした(Rubyはインタプリタ言語なので、コードが増えると起動時のオーバーヘッドが増えます)(中略)
(中略) 静的解析の実行速度はテストスイート1件(つまり1つのファイル)よりも高速で、多くの場合テストケース1件よりも速くなりました(当社のテストは起動時のオーバーヘッドが大きい)。

Rubyでは、テストが遅いのはインタプリタそのものが遅いことよりもフレームワーク全体を起動してしまうことが原因です。フレームワークを起動するようなテストを書くよりも、フレームワークや他の外部オブジェクトに依存しないオブジェクトを実行する単体テストを書くのがベストです。そのようなテストではフレームワークが起動されず、インタプリタで実行するコードの量も最小限で済みます。

コード作者が当初意図した変数の型は1種類だったかもしれませんが、その後新たな用途が生まれ、可能な型を多数含む形でインターフェースが拡張される可能性もあります(中略)

(中略)元々は整数の利用が想定されていたのが、あるとき誰かが文字列でも動作することに気づき、文字列も渡すようになる可能性もあります。

コードベースの開発者は、そのコードがどう使われるかを理解しておくことが重要です。確かにコード片の使われ方はコードのライフタイムの間に移り変わるものですが、コード片を再利用できるというのは大きなメリットです。このような場合には、そのコードを使う他の人にもわかるように、コードに関する知識を周知することが重要です。コードベースが大規模になると、プルリクをひとつ残らずレビューすることも、アプリケーションのあらゆる細部を把握することも不可能になるので、コードの細かな使い方を周知するには他の方法が必要です。入力テストはそのための方法のひとつであり、しかも実行可能であるというメリットがあります。

同記事の著者は入力される型のテストについても言及していますが、その主張にはあまり同意していません。もちろん、メソッドを変更するときにその変更が入力型をすべて網羅しきれていないことがテストで判明したら、その点を考慮する必要があります。しかしこれではメソッドに型制約を与えるのと大差ありません。あるメソッドが複数の型で使われるなら、それらの型をサポートするように書く必要があります。静的型がなくても、そのときに必要な作業量は増えたりしません。

開発者が何かを変更すると、テストスイートはすべてパスしてひとつも失敗しなかったので問題は存在しないという誤った仮定を立ててしまいがちで、テストされなかったパスが本番で500回ほど実行されたときにやっと気づきます(なお「理論上は」テストをそこまで増やすべきではないという建前ですが、実際の私たちは大量のテストを書いています)。

テスト戦略がパスをろくにカバーしていないとしたら、静的型を使っているかどうかに関わらず非常にまずいことになります。重要なのは各テストのパスでビジネスロジックが正常に動くことを確認できることであり、静的型ではそうした点を確かめられません。

私はテストカバレッジを徹底するために、単体テストと受け入れテストを両方を用いることで、互いの強みを活かし弱みを補うという戦略を採用しています。

  • 単体テスト: 各クラスを徹底的にテストすることで、すべてのコードパスのカバーを含めて振舞いを十分規定できる
  • 受け入れテスト: アプリケーション全体を通してテストを実施することで、単体テストされた部分が協調動作していることを確認できる

受け入れテストであらゆるパスの組み合わせをカバーしようとすると、大変なコストがかかります。代わりに、単体テストで保証されている部分を考慮して、必要な部分を受け入れテストでカバーするように設計します。大規模アプリケーションでは、変更をかけるたびに受け入れテストをすべて実行するのは無理だとしても、リリース前なら確実に実行できますし、実行すべきです。私の経験では、これで潜在的なエラーを残らず検出とまではいかなくても、大半を検出できるはずです。

静的解析は確かに素晴らしいものですが、型シグネチャは人間のためのものでもあることを忘れてはいけません。コードを読むときに特定の変数やメソッドで期待される型がわかれば、理解を深めるうえで非常に有用です。もちろん優秀なIDEでも同じことはできますが、両方使える方がいいと思いませんか?Rubyの静的解析は無料です。

この記事では型シグネチャのメリットを説明するときにデメリットを伏せているのみならず、型シグネチャが「無料」である点を強調していて、あたかも「欠点は存在しない」かのように見えます。しかし多くの人々が動的言語での型シグネチャのメリットについて議論を重ね、「型シグネチャにはトレードオフもある」と述べています。どんなトレードオフでしょうか。

極端なケースでは、複雑な型シグネチャによってコードが乱雑になり、ロジック自体がかすんでしまうこともあります。書籍『Swift Style』に掲載されているSwift言語で書かれた例を見てみましょう。

Excerpt from *Swift Style* showing function with many more lines of type signatures than implementation

Swriftが作り出すコードのスタイルはひときわトップヘビーになります。このため、メソッドやイニシャライザの本体に到達する前にSwiftの処理が多数発生する可能性があります。ここで紹介するのは、双方向のコレクションのインスタンスを初期化するSwiftの標準ライブラリの例です。
元記事の引用画像より

わかりにくいと思いますが、アプリケーションコードと言えるのは末尾のself.base = baseの部分だけで、それ以外の部分は複雑極まる型シグネチャがびっしり並んでいます。

型アノテーションは、シンプルで典型的なケースでもコードが煩雑になってしまいます。私がRubyで気に入っている点のひとつは、クリアな構文によってビジネスロジックが前面にくっきりと浮かび上がってくることです。対照的に、型アノテーションを用いる言語のコードは、ほとんどの場合情報がてんこ盛りになってしまうことが私の目には明らかです。

MatzはRubyConf 2019の基調講演で、Rubyが型アノテーションを追加せずに別のファイルに置く理由を説明した際に同様の点に触れています(動画↓)。型シグネチャはDRYではないので、型アノテーションをRubyコードの外に追いやることでコードをDRYにできると述べています。

重複はコストを強います。コードを読むたびに脳が型の記号を処理しなければならなくなり、既にわかりきった型ではその労力に何のメリットもありません。たとえ労力はわずかでも、時間とともに降り積もっていきます。

静的型が有用な場合とは

以上の引用から、静的型付けのメリットを最大化できる場面が浮かび上がってきます。

静的型付けはモジュール性の低いアプリケーションを書くときに有用: しかし、そもそもモジュール性の低いアプリケーションを書かないのがベストであり、静的型付けがあってもモジュール性の低さは将来に渡って問題になります。私は大規模なチームで大規模なモノリスを書いて良好なモジュール性を失わずに済んだという経験がないので、実践は難しそうです。しかし少しでもその境地に近づけるなら、それに越したことはありません。新しい開発者をしっかり教育し、徹底したコードレビューを行うことで前進できると思います。

静的型付けはコードがテストで一貫してカバーされていない場合に有用:  テストされていないコードは何をしているのかわかりにくく、変更したときに安全かどうかも見当がつきません。静的型付けはそのような場合に有用です。しかし静的型付けを行う場合でもコードをテストすることは引き続き重要です。コードの単位を個別にテストすると同時に、アプリケーション全体のユーザーフローもテストする必要があります。動的型付け言語ではテストの重要性が他の言語より高くなるので、Rubyでもテストの重要性が強調されています。ほとんどの型システムでは、静的型付けではビジネスロジックが正しいことを保証できないので、やはりそのためのテストが必要です(使ったことはありませんがIdris言語のような例外もあるようです)。コードベースやチームの規模に関わらず、ほとんどのコードでテストを要求することは理にかなっています。徹底したコードレビュー(私の好みです)やコードカバレッジのメトリクス (これはこれで大きな問題がありますが、徹底したコードレビューが行われないなら何もしないよりマシでしょう) を使うかどうかにかかわらず、コードのほとんどの部分がテストされるようにすることは可能です。

静的型付けは書いたテストが遅いときに有用: ある時期にRubyコミュニティで高速なテストが求められてきた理由がこれです。結合テストを高速化する回避方法はいくつかありますが、最も信頼できる方法は、フレームワークを使わないPORO(plain old Ruby object)を書き、それらのオブジェクトを分離した状態でテストする「真の単体テスト」を書くことです。これを実践するには規律の徹底が必要で、しかも最初のうちは規律の必要性もはっきりしません。しかし静的型付けを用いるにしても、ビジネスロジックの動作確認は欠かせないので、高速なテストはどちらにしても重要です。

静的型付けはコードがどこでどう使われているかわからないときに有用: ある目的を持ったコードが目的外の形で使われると、理解できない形でコードが使われていることになります。しかし自分のコードがどこでどう使われているかを把握しておくことは重要です。型が一致したとしても、コードが自分のあずかり知らない方法で使われればバグやメンテナンスコストの増加につながります。コードを理解していれば、そのコードが何に使われているかをいつでも把握できます。あるコードの用途が変われば、それに対する理解も変える必要があります。新しいケースをカバーするテストを追加したり(#addを整数の他に文字列にも使うなど)、適用範囲を拡大するためにメソッド名やクラス名を変更したり、ドキュメントを更新したりすることで理解を深められます。これはアプリケーションを進化させるための規律あるアプローチです。静的型付けはこのアプローチを強制し、忘れた場合にキャッチする方法ですが、前述のように認知のオーバーヘッドを増やしますし、型は同じでもコードのビジネスユースケースが異なる場合はキャッチできません。

まとめると、静的型付けは「自分が把握していない」「モジュール性が低い」「テストが不完全」「テストが遅い」コードで特に有用です。そのような状況では静的型付けがおそらく役に立つでしょう。

よりよいソリューション

しかし静的型付けがあっても、モジュール性が低く、テストも不完全で遅い、自分の把握していないコードベースを相手にするのは困った状況です。やはり自分が把握しているモジュール性の高いコードベースで、徹底的なテストを高速に実行したいものです。そうなれば静的型付けの値打ちは大幅に下落し、代わりに可読性のコストが増加します。

最後にもう一度申し上げます。私は大規模なチームで大規模なRubyコードベースを相手に仕事をした経験がないので、コードベースを前述のような理想的な状態に保つことがどのくらい難しいかは見当がつきません。そうした状況では静的型付けが有用という業界常識も数多く見かけます。しかし、そう言う人たちが動的型付けのメリットについて合意が取れているかどうかについては、私には何とも言えません。「コードの把握」「モジュール化」「徹底した高速テスト」を追求するのであれば、おそらく静的型付けはある程度のガードレールとして有用でしょう。

しかし小規模プロジェクトや少人数チームであれば、コードベースの深い理解、モジュール化、徹底した高速テストの実施は可能であることを経験から知っています。静的型付けと動的型付けのどちらを使うにしても、そこを目指しましょう。これが実現され、Rubyコードの見た目や動作に惚れ込んでいるのであれば、静的型付けは使わなくてもよいかもしれません。

関連記事

2020年のRailsでブラウザテストを「正しく」行う方法(翻訳)

The post Ruby: 静的型付けで解決しない問題とは(翻訳) first appeared on TechRacho.

RubyKaigi Takeout 2021のスライド(Day1)

$
0
0

こんにちは、hachi8833です。RubyKaigi Takeout 2021 Day1が終了しました。主催者・スポンサーの方々疲れさまでした&ありがとうございます!参加者の皆さまもお疲れさまでした。

参考: #rubykaigi - Twitter検索 / Twitter

主に自分用に、現時点でわかっている発表スライドを取り急ぎまとめました。可能なものについてはいずれ公式サイトに資料が掲載されると思います。

なお、見出しの時間はリスケ後のものです。

Day1 10:00 – 10:55

#rubykaigiA 『TypeProf for IDE: Enrich Dev-Experience without Annotations@mametter)』

最後は焚き火(?)のクワインで締めくくられました。

来年はTRICKが復活しますように。

過去のTRICK: https://github.com/tric/

Day1 11:00 – 11:25

#rubykaigiA 『Why Ruby’s JIT was slow@k0kubun)』

参考: Ruby 3 JIT can make Rails faster. I’ve wondered Why Rails becomes slow… | by k0kubun | Medium

Day1 13:00 – 13:25

#rubykaigiA 『Optimizing Partial Backtraces in Ruby 3jeremyevans)』


Optimizing Partial Backtraces in Ruby 3より

字幕付きのスライドがありがたいです🙏

#rubykaigiB 『RuboCop in 2021: Stable and Beyond@koic)』

RuboCopのラージ-Aオプションは安全な安全でないオートコレクトと知りました。

Day1 14:00 – 14:25

#rubykaigiB 『The Art of Execution Control for Ruby’s Debugger@ko1)』

スライドはPDF形式です。


The Art of Execution Control for Ruby’s Debuggerより

ruby/debug - GitHub

なお、RailsのGemfileのbyebugがruby/debugに置き換わったそうです。

Day1 14:30 – 14:55

#rubykaigiB 『Toycol: Define your own application protocolMisaki Shioi)』

shioimm/toycol - GitHub

Day1 15:30 – 15:55

#rubykaigiB 『Story of Rucy – How to “compile” a BPF binary from Ruby@udzura)』

udzura/rucy - GitHub

以下は感想戦のお知らせです。

関連記事

BPS株式会社はRubyKaigi Takeout 2021 Gold Sponsorに登録しました

The post RubyKaigi Takeout 2021のスライド(Day1) first appeared on TechRacho.

RubyKaigi Takeout 2021のスライド(Day2)

$
0
0

こんにちは、hachi8833です。昨日に引き続き、RubyKaigi Takeout 2021 Day2のスライドや資料をわかった範囲でまとめました。

参考: #rubykaigi - Twitter検索 / Twitter

Shopifyの以下のイベントも開催されていたことに、終わってから気づきました。

Day2 10:00 – 10:55

#rubykaigiA『The Future Shape of Ruby ObjectsChris Seaton)』

Day2 11:00 – 11:25

#rubykaigiB『PRK Firmware: Keyboard is Essentially RubyHitoshi HASUMI)』

Day2 11:30 – 11:55

#rubykaigiA『YJIT – Building a new JIT Compiler inside CRubyMaxime Chevalier-Boisvert)』

RubyKaigi Takeout 2021の発表資料は見つかりませんでしたが、発表者が今年6月に公開した同じタイトルの記事と、これも今年3月に公開された同じタイトルの動画を見つけましたので参考までに貼っておきます。

#rubykaigiB『The newsletter of RBS updates@ pocke)』

Day2 13:00 – 13:25

#rubykaigiA『Parsing RubyKevin Newton)』

発表の内容は「Parsing Ruby」サイトに凝縮されています。

#rubykaigiB『iunclude/prepend in refinements should be prohibited

Day2 13:30 – 13:55

#rubykaigiA『Ractor’s speed is not light-speed)』

#rubykaigiB『Graphical Terminal User Interface of Ruby 3.1@aycabta)』

最後の方しか見られませんでしたが、発表内容は以下に凝縮されているとのことです。

Day2: 14:00 ———– 18:00 ———- …

#rubykaigiA『Ruby Committers vs the World

記事執筆時点でまだ終わっていません😳 濃厚すぎてとてもまとめられない…

Rubyアソシエーション開発助成金2021公募開始

Ruby Committers vs the World延長戦で以下の公募の話題に触れられていたのでツイートを貼っておきます。

The post RubyKaigi Takeout 2021のスライド(Day2) first appeared on TechRacho.

RubyKaigi Takeout 2021のスライド(Day3)

$
0
0

こんにちは、hachi8833です。RubyKaigi Takeout 2021がDay 3まで無事終了しました🎉

主催およびスポンサーの皆さま、お疲れさま&ありがとうございます!参加者の皆さまもお疲れさまでした。

ありがたいことに、何と土曜日のうちにセッションの動画アーカイブがYouTubeで公開されました🙏

既に公式サイトスケジュールの各セッションのページにも動画が埋め込まれていますので、そちらからたどることもできます。本シリーズではクロージングキーノートを除いて特に動画は埋め込みませんでした。

今年は正規表現の話題が多かったのが個人的に嬉しい点でした。毎度のことですが、たとえようもないチャットの盛り上がりとライブ感、そしてモチベの爆発的な高まりはやはりイベントに参加してこそ得られると改めて感じました。今回も全日参加して本当によかったとしみじみ思います😂

togetterでツイートがまとめられました。

なお、来年の開催について「開催場所・時期・形態については追ってお知らせします」「次回はTRICKを再開します」とクロージングでアナウンスされ、その後開催形態については以下が公開されました。

TRICKはRubyKaigiで何度か催されたコードコンテストです。RubyKaigi 2018で開催されたときの動画をご覧いただくと雰囲気が掴めるかと思います。

Day3 10:00 – 10:25

こちらでスライド・資料・関連リポジトリを見つけられたもののみ掲載しています。

#rubykaigiA 『Do regex dream of Turing Completeness?』(Daniel Magliola

以下のリポジトリが公開されています。

dmagliola/regex_game_of_life - GitHub

参考: ライフゲーム - Wikipedia

#rubykaigiB 『Use Macro all the time ~ マクロを使いまくろ ~』(@osyo

osyo-manga/gem-rensei - GitHub

Day3 10:30 – 10:55

#rubykaigiB『Charty: Statistical data visualization in Ruby』(@mrkn

red-data-tools/charty - GitHub

Chartyの開発に参加したい方は以下のイベントにどうぞ。

Day3 11:00 – 11:25

#rubykaigiA『Building Native Extensions. This Could Take A While…』(Mike Dalessio

以下はサンプルコードのリポジトリです。

flavorjones/ruby-c-extensions-explained - GitHub

なお、Sutouさんによる昨年のRubyKaigi発表も趣旨が近いそうです。

参考: Goodbye fat gem - Kouhei Sutou - Rabbit Slide Show

#rubykaigiB『Dive into Encoding』(Mari Imaizumi

ima1zumi/encoding_iroha - GitHub

Day3 11:30 – 11:55

#rubykaigiA『Beware the Dead End!!』(Richard Schneeman

zombocom/dead_end - GitHub

以下は関連が深いと思われる@schneemsさんの過去記事です。

参考: Squash Unexpected-End errors with syntax_search

#rubykaigiB『How to develop the Standard Libraries of Ruby?』(@hsbt

GitHubの以下のページでRubyの各種リポジトリを見られるそうです。

参考: The Ruby Programming Language

Day3 13:00 – 13:25

#rubykaigiA『It is time to build your mruby VM on the microcontroller?』(蒼時弦也

以下のリポジトリのようです。

elct9620/mruby-toy-vm - GitHub

#rubykaigiB『Ruby, Ractor, QUIC』(@unasuke

Day3 13:30 – 13:55

#rubykaigiB『Red Arrow – Ruby and Apache Arrow』(Sutou Kouhei

Red Arrowのリポジトリは現在Apache Arrowに移行しています↓。

apache/arrow - GitHub

Red Arrowの開発に参加したい方も以下のイベントにどうぞ。

Day3 14:00 – 15:00

#rubykaigiA『Matz Keynote』(@matz

Matzのクロージングキーノートは当日無料で配信されました。

スピーチの中で、Pythonも高速化プロジェクトを立ち上げたことを知りました。

YJITのベンチマークを以下で見られます。

スピーチの中で呼びかけられたRubyへの協賛先は以下です。

Day3後の感想戦

本編終了後の感想戦も長らく盛り上がりました。以下で質疑応答やメモを参照できます。

その中でハッシュのショートハンド記法について議論されていました。自分の体力が持たなかったので途中抜けしましたが、その後受理およびコミットされていることに気が付きました。

参考: オブジェクト初期化子 - JavaScript | MDN

その他感想戦などの情報

既に開催されたものも含め、以下からたどれます。

早くも感想動画をアップしている方もいらっしゃいました。

関連記事

RubyKaigi Takeout 2021のスライド(Day1)

RubyKaigi Takeout 2021のスライド(Day2)

The post RubyKaigi Takeout 2021のスライド(Day3) first appeared on TechRacho.

週刊Railsウォッチ: Ruby 7 Alpha 1と2が公開、Rubyハッシュのショートハンド記法、iCare Dev Meetupほか(20210921)

$
0
0

こんにちは、hachi8833です。RubyKaigi Takeout 2021が終わりましたね。

週刊Railsウォッチについて

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

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

今週の週刊Railsウォッチは祝日が多いので短縮版でお送りします。また、来週の週刊Railsウォッチはお休みいたします🙇

🔗 特集: Rails 7 Alpha 1と2がリリース

なおつっつきの後で、Alpha 1と同じ日にAlpha 2もリリースされていたことに気づきました。


つっつきボイス:「Alpha 1出た🎉」「ちょっと前からDHHのYouTube動画チャンネルにもRails 7 Alphaプレビュー情報が出ていたのでそろそろ出る頃かもと思っていたところ」「DHHの動画はコードも見えるので比較的イメージを掴みやすいと思います: おすすめ👍」「あとで見ようっと」

🔗 Rails 7は選択肢を複数用意する方向へ

「これまでのRailsはConvention over ConfigurationのようなRails wayに沿って進めるのが望ましいという立ち位置でしたが、今回のRails 7 Alpha 1リリースを眺めていると、Hotwireが入るけどHotwire一択ではないという点がこれまでと違うかなという気がします」「あ、そうかも」「これまでRailsを使い慣れている人はRails wayから適切に外れるノウハウを身につけていて、人によってはたとえばTurbolinksを速攻で外したりすることもありましたよね」

turbolinks/turbolinks - GitHub

「Railsも成熟したんだなという気持ち」「Webpackも使いたければ使っていいよというのはありがたい🙏」「そういえば最近はリリースノートでも言及されているesbuildがいいらしいという噂もちらほら見かけますね」

evanw/esbuild - GitHub

以下はつっつき後に見つけたツイートです。

「リリースノートにはこれまでチェックしてきたActive Record暗号化(ウォッチ20210412)やmarginalia-gem風のQueryLog追加(ウォッチ20210906)などがフィーチャーされていますね」「Active Record暗号化うれしい😂」「load_asyncってもう入っているような気持ちになっていたけどまだだったか(ウォッチ20210222)」「そういえば少し前の銀座Rails#33でもload_asyncを取り上げていましたね↓」

「Rails 7へのアップグレード手順は、JS環境を完全にHotwireベースに乗り換えるなら作業量が増えそうだけど、今の構成のままでよければそこまで増えなさそうかな」「アップグレード楽しみです😋」「デフォルトのvariant(サイズ違いの画像)生成がmini_magickからvipsに変わるとは知らなかった(#42744)」

libvips/ruby-vips - GitHub

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

今回は以下の公式更新情報を取り上げます。

🔗 sass-railsへのデフォルト依存を削除

最近のWebアプリケーションでは、Tailwind、Bootstrap、BulmaなどのCSSフレームワークを使うことが増えている。Railsは、まだすべてを手書きしているかのようなモデルごとのスタイルシート生成を行うべきではない。
また、Sassはdart-sassに注力することを選んだが、dart-sassはRailsがデフォルトで採用していないあらゆる依存関係を必要とする。そこで、Sassへの依存を減らしてオプションとして追加することにした。
同PRより


つっつきボイス:「sass-rails gemが消されたのかと思ったら、デフォルトではsass-ralisを使わないことにしたのね: Sassを引き続き使いたかったらSprocketsかWebpack-dev-serverかそれらに相当する何らかのアセットプリコンパイラが必要ということになるでしょうね」「たしかに」「もちろん必要なら自分でSassをインストールすればOK」「sass-ralisがデフォルトでなくなるのはそれはそれで嬉しいかも」

rails/sass-rails - GitHub

「アセットプリコンパイル、結局よくわからずじまいでした」「アセットプリコンパイルは果たしてサーバーサイド側の責務なのか、それともフロントエンド側なのかという問題は以前からありますね」「ありますあります」「Railsはこれまでサーバーサイドでアセットプリコンパイルするという考え方でしたね」「それが今回デフォルトからは消えたということか」「RailsコマンドにSassインストール用のオプションが入るといいですよね」「まだオプションは見当たらないけどあるとよさそう」

「ところでDHHはTailwind cssを推しているような気がしますね」「Tailwind、まだ使ったことないんですけど流行っているんでしょうか?」「いわゆるユーティリティ系CSSフレームワークでは世間的には有名ですね: フロントエンド界隈ではTailwindを使うことがデファクトというわけではないと思いますが、個人的にはユーティリティ系CSSフレームワークを使うならTailwindでもいいのではとは思います」「なるほど」

参考: Tailwind CSS - Rapidly build modern websites without ever leaving your HTML.

「TailwindはIDEがサポートする前提で人間が手書きする感じではないのが個人的に何となく合わないんですが、かといってSassならいいというほどでもないんですよね」「Tailwindが生成するCSSを見ていると、CSSのカスケーディングという仕様が現代のコンポーネント化されることを想定したWebページに合わなくなってきてつらいよなあ、という気持ちになります」

🔗 parse_floatvalid_float?をRuby 2.7以降向けに最適化

# actionview/lib/action_view/helpers/number_helper.rb#450
        def parse_float(number, raise_error)
          Float(number)
        rescue ArgumentError, TypeError
          raise InvalidNumberError, number if raise_error
          result = Float(number, exception: false)
          raise InvalidNumberError, number if result.nil? && raise_error
        end
# activesupport/lib/active_support/number_helper/number_converter.rb#176
        def valid_float?
          Float(number)
        rescue ArgumentError, TypeError
          false
          Float(number, exception: false)
        end

つっつきボイス:「ActiveSupport::NumberHelper#parse_floatActionView::Helpers::NumberHelper#valid_float?を、例外を投げ直さないようにする形で高速化したのね」「ここで使ったFloat(..., exception: false)はRuby 2.7以降のみと書かれている」「つまりRailsで必要な最小限のRubyバージョンがRails 7で2.7.0以上に変更されることで↓、Ruby 2.7の機能が使えるようになったということでしょうね」「たしかにif RUBY_VERSION的なコードがありませんね」「そういうコードは少ない方がみんなが幸せになれる」

Rails requires Ruby version 2.7.0 or later. It is preferred to use latest Ruby version. If the version number returned is less than that number (such as 2.3.7, or 1.8.7), you’ll need to install a fresh copy of Ruby.
edgeguides.rubyonrails.org Getting Started with Railsより

🔗 preload_link_tagを修正


つっつきボイス:「preload_link_tagって使ったことないかも」「ああ、リンクタグにrel="preload"を追加するビューヘルパーなのか」「一部の画像でas="image"が設定されなかったのが修正されたということのようですね👍

# #Lactionview/test/template/asset_tag_helper_test.rb#248
  PreloadLinkToTag = {
-   %(preload_link_tag '/sprite.svg') => %(<link rel="preload" href="/sprite.svg" as="image" type="image/svg+xml">)
+   %(preload_link_tag '/sprite.svg') => %(<link rel="preload" href="/sprite.svg" as="image" type="image/svg+xml">),
+   %(preload_link_tag '/mb-icon.png') => %(<link rel="preload" href="/mb-icon.png" as="image" type="image/png">)
  }

🔗 schema_cache_ignored_tables設定オプションが追加


つっつきボイス:「特定のテーブルをスキーマキャッシュにダンプしないようにできるオプションが追加されたんですね」「正規表現も使える」「これはどんなときに嬉しいんでしょうか?」「おそらく、Railsのマイグレーションで管理されていない外部データベースを読み込み専用で参照しているような状況で、スキーマがキャッシュされると困るときに欲しい機能でしょうね」「なるほど、ちょうどそういう感じの作業が降ってきたところです😅」「既存の方法でも全テーブル単位とかでダンプをオフにすることはできたと思いますが、コンフィグでより細かく設定できるようにしたということだと思います」「お〜」

  • スキーマキャッシュのダンプ時にテーブルを無視するコンフィグオプションを追加
    アプリケーションがスキーマキャッシュのダンプ時に特定のテーブルを無視するよう設定可能になった。
    この設定オプションは以下のようにテーブルの配列にすることも、正規表現にすることも可能。
config.active_record.schema_cache_ignored_tables = ["ignored_table", "another_ignored_table"]
config.active_record.schema_cache_ignored_tables = [/^_/]

Eileen M. Uchitelle
同Changelogより

🔗 ActiveModel::APIが追加


つっつきボイス:「Alpha 1のコミットの中からたまたま見つけました」「Action PackとAction Viewが参照するModelとして期待するAPI群をActiveModel::APIに切り出すことで、ActiveModel::Modelの見通しを良くしたリファクタリングのようですね」「ActiveModel::API、わかりやすそう」「persisted?はデフォルトでfalseになるのか、へ〜」

# activemodel/lib/active_model/api.rb(ドキュメントは略)
# frozen_string_literal: true

module ActiveModel
  module API
    extend ActiveSupport::Concern
    include ActiveModel::AttributeAssignment
    include ActiveModel::Validations
    include ActiveModel::Conversion

    included do
      extend ActiveModel::Naming
      extend ActiveModel::Translation
    end

    def initialize(attributes = {})
      assign_attributes(attributes) if attributes

      super()
    end

    def persisted?
      false
    end
  end
end

現在のActiveModel::Modelは、Action PackやAction Viewとやりとりするための最低限のAPIとして定義されている。
その名のとおりこれをincludeしてActive Recordモデルを作成できるが、モデル作成機能はごくわずかしかない。たとえばActiveModel::Attributesincludeすることが非常に多い。
ActiveModel::Modelの実装を新たなActiveModel::APIに移動することで、Action PackやAction Viewとやりとりするための最小限のAPI定義を維持できる。
これでActiveModel::ModelにはActiveModel::APIだけがincludeされるようになり、後方互換性を失わずにActiveModel::Modelに機能追加できるようになる。

なお、このプルリクは以前更新しそびれていた古い#42042と同じ。
同PRより

🔗Rails

🔗 iCare Dev Meetup #25開催


つっつきボイス:「昨日(2021/09/15水)開催されたiCare Dev Meetup #25、HotwireやBiTemporal Data Modelの話など、いろいろ充実したイベントでしたね」「イベントやってたの知らなかった…」「毎月開催されていますよ」

以下は#icare_meetup - Twitter検索 / Twitterより。

「早くも動画や資料が公開されていてありがたいです🙏」「いい内容でしたし動画もそれほど長くありませんので視聴をおすすめします👍

「BiTemporal Data Modelは、適用開始年月日と適用終了年月日を持つデータモデルで、昔からよく取沙汰される話題ですね」

「神速さんのRailsアップグレードの話もよかった: このアップグレード環境を構築するのは大変そうだけど、それを作れればしくみとして回せるようになるんだなと思えました」

「yasaichiさんの資料はしばらくお待ちくださいということですが、動画ではもう見られます」

つっつき後にyasaichiさんの資料が公開されました↓。

「そういえばTwitterもだいぶ前にCassandraを導入しようとして断念したことがありましたね: 新しい技術は一見よさそうに見えても、実際に使ってみるまでわからないところがあるから慎重にならざるを得ない」「ピクスタさんもこれが最終決定ではないそうなのでこれからですね」

参考: Apache Cassandra - Wikipedia
参考: Twitterが、Cassandraの本採用を断念。「いまは切り替えの時期ではない」 - Publickey

「ところで、yasaichiさんの発表はこれより前のバージョンを見た覚えがありますよ: たしか以前の銀座Railsかな」「お、例の『Ruby on Railsの正体と向き合い方』(ウォッチ20190401)とは別でしょうか?」「それではなくて、今回のと近いタイトルだったと思います: こういうふうに同じテーマを定期的に追い続けてくれる方がいると、状況の移り変わりなどもわかってとても助かります🙏

つっつき後に見つけました↓。

「yasaichiさんの発表を見ていて、必ずしも毎回新しいテーマでなくてもいいんだなって思ったので、機会があったらまたデータベースVIEWの話をしてもっと啓蒙してみようかな」「データベースVIEWは以前銀座Rails#10で発表していましたね↓」「3年経ったからそろそろ頃合いかも」

RDBMSのVIEWを使ってRailsのデータアクセスをいい感じにする【銀座Rails#10】

「ところで自分は聞いたことはありませんが、yasaichiさんの発表内容はこちらのtextafmポッドキャスト↓を普段から聞いていた人にはお馴染みだったようです」「そうそう、こうやっていろんな形で普及に努めているんだなって思いました」

🔗 その他Rails


つっつきボイス:「Railsクイズ、皆さんもぜひやってみるとよいと思います👍: 自分たちが普段やっていることなら見当がつくけど、2番のような、既にプロファイルが存在するのにuser.build_profileするという普段まずやらないような問題は難しかった」

🔗Ruby

🔗 ハッシュのショートハンド記法

参考: Shorthand hash syntax in Ruby - rubyonrails-talk - Ruby on Rails Discussions


つっつきボイス:「そうそう、RubyKaigi Takeout 2021最終日の感想戦でES2015風のハッシュのショートハンド記法をRuby 3.1に追加する話が出ていましたね」「結局マージされたんでしたっけ?」「c60dbcdでマージされていました」「お〜、割と前から要望があった構文ですけど、RubyKaigiの勢いに乗ってマージされたんでしょうね」

参考: Allow value omission in Hash literals · ruby/ruby@c60dbcd

参考: オブジェクト初期化子 - JavaScript | MDN

// developer.mozilla.orgより
// Shorthand property names (ES2015)
var a = 'foo', b = 42, c = {};
var o = {a, b, c};

RubyKaigi Takeout 2021のスライド(Day3)

「TechRacho翻訳記事でお世話になっているBrandon Weaverさんが早くもこの新構文で遊んでみた記事をアップしていました↓」


今回は以上です。

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

週刊Railsウォッチ: 責任あるモンキーパッチの当て方、gem脆弱性スキャンツール、Docker Desktop課金プラン改定ほか(20210907後編)

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

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

Rails公式ニュース

The post 週刊Railsウォッチ: Ruby 7 Alpha 1と2が公開、Rubyハッシュのショートハンド記法、iCare Dev Meetupほか(20210921) first appeared on TechRacho.

Rubyオブジェクトの未来をつくる「シェイプ」とは(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。本記事はRubyKaigi Takout 2021 Day2キーノートのスピーチ原稿につき、最終的な発表内容はこのとおりでない部分もあります。流れを把握するために必要と思われる部分については訳注で補足いたしましたが、わかりにくい場合は動画と合わせてご覧ください。

本記事ではshapeの仮訳として「シェイプ」を採用しています。

oracle/truffleruby - GitHub

Rubyオブジェクトの未来をつくる「シェイプ」とは(翻訳)

これはRubyKaigi 2021で行ったセッションの原稿につき、スライドやコードを見ながら話しているかのような口語体で書きました。

皆さんこんにちは、Chris Seatonです。ShopifyのシニアスタッフエンジニアとしてRubyのパフォーマンス研究に従事しており、非常に高度に最適化されたRuby実装であるTruffleRubyを立ち上げました。また、Rubyに関する研究のまとめサイト『The Ruby Bibliography(rubybib.org)』も運営しています。

本日は、TruffleRubyがRubyオブジェクトを実装するのに用いている既存のいくつかのアイデアのうち、MRIや他のRubyの実装への応用が見込めるものについて紹介したいと思います。これらのアイデアの由来や歴史の解説、TruffleRubyがオブジェクトをどのような形で実装しているか、それによって達成できたこと、どうすれば同じことをMRIで実現できるかについてお話しいたします。

それから将来も視野に入れ、こうしたアイデアの上にどんなものを構築できるかについてお話しいたします。

Rubyの「もしも〜だったら」とは

Rubyが他の言語に比べてときどき遅くなることがある理由を聞かれたら、私なら「もしも〜だったら」を筆頭の理由に挙げます。

Rubyの実装では、プログラムを実行するときに「もしもこうだったら、ああだったら」を考えるのに多くの時間を費やします。足し算のたびに「オーバーフローしたらどうするか」、メソッドを呼び出すたびに「メソッドがモンキーパッチされていたらどうするか」、コードの1行1行で「トレースが有効にされていたらどうするか」といったことを判断しなければなりません。

大量の「もしも〜だったら」のコストを削減するために、現在も多くの作業が進められており、MJITJRubySorbet CompilerYJIT、TruffleRubyなど、いずれも何らかの形でこの問題に取り組んでいます。しかしJRubyとTruffleRubyを除けば、Rubyオブジェクトの扱いはどれも同じであり、そこではRubyオブジェクトの「もしも〜だったら」は未だに解決していません。

Rubyのオブジェクトには「もしも〜だったら」が多数関連付けられています。オブジェクトがfrozenだったらどうするか、欲しいインスタンス変数がオブジェクトになかったらどうするか、オブジェクトが複数のRactorで共有されていたらどうするかなど、常に判断を下さなければなりません。今から説明する「オブジェクトシェイプ(object shape)」というアイデアは、可能な限りこうした「もしも〜だったら」の排除を試みる手法です。オブジェクトシェイプという次なる手段によって、MJITやSorbet CompilerやYJITなど、Rubyでその他のパフォーマンス向上に取り組むときの効果も高まると考えている方が何人もいます。

この後説明するように、オブジェクトシェイプは時間的なパフォーマンス向上のみならず、Rubyプログラムの実行に必要なメモリ空間上のメリットも得られる可能性があります。

Rubyオブジェクトとは

ここでお話しするRubyのオブジェクトは、特に「インスタンス変数を持つオブジェクト」を指します。また、インスタンス変数がオブジェクト内にどのように格納される方法についても解説します。オブジェクトには他にも興味深い点がたくさんあり、本セッションを通じてより詳しく説明しますが、「ビッグアイデア」の部分ではインスタンス変数に絞ってお話しいたします。

Rubyのオブジェクトは目的に応じたさまざまなキーバリューペアの入れ物になります。Rubyの Hashと異なり、これらのキーはすべてシンボルで、キーバリューペアはいかなる方法でも順序付けられません。

インスタンス変数は@構文で読み出したり設定したりできますが、instance_variable_getメソッドやinstance_variable_setメソッドでも同じことができます。また、instance_variables などのメソッドですべてのキーを取得することも、defined?などのキーワードでキーが設定されているかどうかを知ることもできます。こうしたインスタンス変数の取得や設定に使える同様のツールはデバッガにも装備されていることがあります。理想的には、これらの構文やメソッドの重要性や最適化の価値はどれも同じであると考えたいと思います。それによって、どの方法が速いかを気にすることなく、自分が解決しようとする問題に適したツールを使えるようになります。

ここでぜひとも強調しておきたいのですが、Rubyのインスタンス変数は「概念的にはオブジェクトの中に存在する」ことが重要なポイントです。つまり、インスタンス変数はオブジェクトのクラスやメタクラスの一部でもなければクラスの中で定義されるのでもなく、そのまま使われるということです。

Ruby実装でのオブジェクトの扱い

オブジェクトとインスタンス変数の動作について、いくつかのRubyの実装を見てみましょう。

MRI

(なお、このセッションでお見せするコードは、わかりやすくするために大幅に簡略化されており、クラスインスタンス変数、埋め込みインスタンス変数、ジェネリックインスタンス変数といった議論と無関係な多くのエッジケースや最適化を省略してあります。)

MRICRuby)は,C言語で実装されたRubyの標準的な実装です.最初にMRIのデータ構造を見てみましょう.

struct RObject {
  VALUE   klass;
  int     numiv;
  VALUE[] ivptr;
};

struct RClass {
  struct st_table *iv_index_tbl; // Hash of Symbol -> index
};

オブジェクトには、「クラスへの参照」「オブジェクト内にある多数のインスタンス変数への参照」「インスタンス変数の値の配列への参照」があります。

クラスはインスタンス変数名のハッシュを、インデックス(ivptr)へのシンボルの形でオブジェクト内に持ちます。

それでは、これらのデータ構造にアクセスするMRIのコードを見てみましょう。ここでは、すべてが順調な場合に実行される「高速パス(fast path)」すなわち「ハッピーパス」にのみ注目します。なお、Rubyらしく書かれたコードはほとんどがこのパスを通ります。それ以外のすべてを処理する「低速パス」すなわち「フォールバック」コードもありますが、ここでは扱いません。

このコードでは、class_serialが前回のアクセス時に記録された「期待されるclass_serial」と同じであることを確認していることがわかります。class_serialはクラスのバージョン番号で、クラスが変更されるとインクリメントされます。また、オブジェクトのサイズが同じクラスの他のインスタンスのサイズと異なる可能性もあるので、必要な数のインスタンス変数スロットがオブジェクトにあるかどうかもチェックする必要があります。

VALUE vm_getivar(VALUE obj, IVC inline_cache) {
  if (inline_cache && inline_cache->class_serial == obj->klass->class_serial) {
    int index = inline_cache->index;
    if (obj->type == T_OBJECT) && index < obj->numiv) {
      return obj->ivptr[index];
    } else {
      // 低速パス
    }
  } else {
    // 低速パス
  }
}

VALUE vm_setivar(VALUE obj, VALUE val, IVC inline_cache) {
  if (inline_cache && inline_cache->class_serial == obj->klass->class_serial) {
    int index = inline_cache->index;
    if (obj->type == T_OBJECT) && index < obj->numiv) {
      obj->ivptr[index] = val;
    } else {
      // 低速パス
    }
  } else {
    // 低速パス
  }
}

理解しやすさのために以下のような疑似Rubyコードで考えてもかまいません。

訳注

上のパラグラフは以下のように詳しく説明されています。

so to get an ivar we have a cache which is passed by the interpreter and we have the receiving object that contains instance variable we check that the cache’s serial number is the same as the object’s actual serial number at the moment
and we check that the expected number of instance variables is less than the number that has and then we can go ahead and read it
otherwise we use a slow path
and the code for setting is very similar
just instead of returning reading from ivptr we write into it
and again we have a slow path for writing
6:06字幕より(整形)

def vm_getivar(cache, obj)
  if cache.serial == obj.klass.serial && cache.index < obj.numiv
    obj.ivptr[cache.index]
  else
    # 低速パス
  end
end

def vm_setivar(cache, obj, val)
  if cache.serial == obj.klass.serial && cache.index < obj.numiv
    obj.ivptr[cache.index] = val
  else
    # 低速パス
  end
end

JRuby

JRubyは、RubyのJava再実装です。MRIのさまざまなJITと異なり、オブジェクトの振る舞いについても再実装されています。

JRubyは、@構文で参照される変数についてすべてのメソッド(継承されたメソッドを含む)を探索することで、クラスで使われる可能性のあるインスタンス変数を静的に推測します。そして実行時に、Rubyのインスタンス変数ごとに1つのJavaフィールドを含む新しいJavaクラスを生成します。生成されたこれらのJavaクラスは、同じ個数の変数を含むすべてのRubyクラスで共有されます。

続いて各Rubyクラスは、インスタンス変数名とそれに対応するJavaフィールドのマップを持ちます。

class TwoVariablesObject {
  Object var0;
  Object var1;
}

JRubyでは、コードがこれらの変数に効率的にアクセスするためにinvokedynamicと呼ばれるJVMのメカニズムを利用しています。ここではJavaのコードには触れずに、効果的な結果をRuby擬似コードでスケッチしてみましょう。

訳注

上のパラグラフに続いて以下も説明しています。

so for the java version of vm get ivar
we check that the caches id which is like a class serial is the same as the
meta classes
real classes id
and then again we can object we can index into the object
and again there’s a slow path
動画7:59字幕より(整形)

def vm_getivar(cache, obj)
  if cache.id == obj.getMetaClass().getRealClass().id
    obj[cache.index]
  else
    # 低速パス
  end
end

このアプローチのメリットは、インスタンス変数の個数が静的に割り当てられるため、オブジェクトのクラスが期待どおりであることだけを確認すればよく、MRIで行われていた「オブジェクトがインスタンス変数のための十分なメモリー領域が確保されているかどうかの確認」を削減できることです。オブジェクトのクラスidは、キャッシュされている値と比較されます。残念ながら、これはオブジェクトから3ホップ離れています。これが効率よく処理されるには、「オブジェクト」と「そのメタクラス」および「その論理クラス」がすべてCPUキャッシュに乗っていることを期待しなければなりません。

もうひとつの制限は静的な推測です。変数やクラスがメタプログラミングで設定されたために推測を誤ると、インスタンス変数を読み書きする低速パスにフォールバックしてしまいます。この振る舞いはRubiniusでも同じです。

JRubyによるこのトレードオフには賛否両論ありますが、私たちは「シェイプ(shape)」を用いることで両方の長所を最大限に活かせると考えています。

シェイプの歴史

オブジェクトシェイプの歴史は、プログラミング言語「Smalltalk」と「Self」までさかのぼります。この2つの言語は言語設計や歴史の話によく登場すると思いますが、シェイプはこれらの言語を研究していた研究者によって生みだされた技術の好例です。

訳注

動画では上のパラグラフの途中で以下も言及されています。

people are always very keen to say their language is inspired by self and smalltalk in the same way they’re keen to say it’s inspired by haskell
動画09:41字幕より(整形)

私たちの分野の歴史について多くを語るつもりはありませんが、この機会にこのアイデアの元となった論文を少し見てみましょう。

そもそもの出発点はSmalltalkでした。

今でこそSmalltalkはシンプルでエレガントな言語と考えられていますが、当時は複雑すぎるという意見もありました。その反動で、開発者の表現力や生産性の向上(今で言う「開発者の幸せ」)を目的としたSelfというよりシンプルな言語が開発されました。

Selfでシンプルさを追求するために行われた改善のひとつが、クラスの代わりにプロトタイプを追加することでした(プロトタイプはJavaScriptのオブジェクトと同じように振る舞います)。Rubyにもクラスはありますが、個別のインスタンスメソッドやインスタンス変数の依存先は論理的なクラスだけではありませんので、(訳注↓)

訳注

上のパラグラフ1文目は実際には以下のように話しています。

one of the improvements for simplicity they added was prototypes instead of classes
and that’s prototypes as in the way javascript objects work if you have experience with javascript as well
so javascript doesn’t have classes it simply has objects which have properties
and to inherit you point to another object as your fallback for reading properties if they’re not in the object you directly access
動画10:39字幕より(整形)

また、同パラグラフの末尾では以下が補足されています。

so ruby is somewhat more like prototypes than pure objects
動画11:04字幕より(整形)

当初のSelfは、開発者の幸せを優先したことと引き換えに低効率でした。開発者の幸せを優先する言語は、残念ながらパフォーマンスで多少の犠牲を払うことになりますが、その点はRubyと似ているかもしれません。しかしこれが研究者たちにとって、Selfの効率を高め、導入されたコストを克服して自分たちの望む設計を実現する方法を見出そうとするモチベーションにつながりました。

この研究が、現在も影響の大きい「ポリモーフィックインラインキャッシング(polymorphic inline caching)」などの主要な開発をリードしました。ここではこの技術について説明しませんが、現代のJavaやJavaScriptの高パフォーマンスの鍵となっています。TruffleRubyでは、Rubyのメタプログラミングを最適化するために「ディスパッチチェイン(dispatch chain)」と呼ばれる新しい形のポリモーフィックインラインキャッシングを採用しています。

私たちの興味をそそった主要な開発は「シェイプ」です。Selfでは「マップ(map)」と呼ばれていますが、現代のJavaScriptの文脈では「隠しクラス(hidden class)」と呼ばれることもあります(「マップ」だとハッシュと混同される可能性があるので私たちは「シェイプ」という用語を使っています)。

ここで用いたのは「インスタンス変数のキーと値の分離」というアイデアでした。キーは「マップ」に保存され、これによってオブジェクト内の値のインデックスを取得できます。オブジェクトはマップを参照しているので、ある変数にどの値を読み込めばいいのかはマップを用いて調べられます。

なお、元々このアイデアが導入された動機の一部は、実際にはスピードではなくメモリ空間でした。各オブジェクトがキーのコピーを保持する必要がなくなるからです。

訳注

上のパラグラフ後半は実際には以下のように話していました。

because when they switched to prototypes they realized all objects had a full set of keys as well as the values
and they realized that by separating them out they no longer needed to keep all those copies because of objects with the same keys could share them
動画13:10字幕より(整形)

ビッグアイデア

以上が、元の研究者たちによって記述された歴史です。ここからは、もっと具体的にRubyと関連する用語や図を用いてこのビッグアイデアをひととおり説明していきます。

ここでは「シェイプ」と呼ばれるものを使います。ひとつのシェイプには、インスタンス変数名から、そのシェイプを持つすべてのオブジェクトにおけるインデックスへのマップが含まれます。上の図ではシェイプを破線で、オブジェクトを実線で表しています。このオブジェクトはクラス(ここでは重要ではありません)とシェイプを参照します。

訳注

上のパラグラフ後半は動画でもう少し詳しく説明されています。

so there’s an arrow from the object to the shape
and the the class and the shape pointers together represent the header of the object with same the object then contains just the values
and the shape contains this mapping from names to indexes so it doesn’t contain a value of instance variables
it just contains an index saying where to find them
動画14:12字幕より(整形)

各オブジェクトには必ず「シェイプ」があります1。オブジェクトにシェイプが与えられると、そのシェイプで必要となる適切な量のメモリ空間も与えられます。空間がどのように使われるかはシェイプによって記述されます。

訳注

上のパラグラフは動画でもう少し詳しく説明されています。

every object has a shape
so objects initially created with a special empty shape for example
when an object is given a shape is also given the right amount of space that shape requires
so it may require resizing the object or using a separate data structure such as an object array to store if the shape needs to be if the object needs to be increased in size
how the space in the object is used is described by the shape so you think of the shape as metadata something that describes the the data and how it can be used or you can think of it as a schema from a database context
動画14:39字幕より(整形)

あるインスタンス変数を取得するには、概念上はシェイプのインデックスを探索してからそのスロットをオブジェクトから読み取ります。

index = obj.shape[:name]
obj[index]

以前に設定されたことのあるインスタンス変数を設定する場合、概念上は同じことを行いますが、値を書き込む点が異なります。

index = obj.shape[:name]
obj[index] = value

オブジェクト内で未設定のインスタンス変数を設定した場合の挙動については後述します。

これはMRIの振る舞いとどう違うのでしょうか?

訳注

動画では上と下のパラグラフのつながりを以下のように話しています。

so how is this different to what mri is doing because it seems sort of similar
well the key differences start with that shapes are immutable and we know that immutability is often a good way to do design software
動画16:26字幕より(整形)

主な違いは、シェイプはイミュータブル(不変)という点です。つまり、過去のある時点でどんなシェイプだったかがわかれば、以後そのシェイプに関するすべてを確実に把握できるようになります。しかもこの仮定はハードコードできます。

また、シェイプはクラスから切り離されており2、同じクラスの2つのインスタンスがそれぞれ異なるシェイプを持つことも可能です。つまり、他のインスタンスが変更されたからといって背後のオブジェクトを変更する必要はありません。

以上のすべてが、あるシェイプを自分が期待するシェイプと比較できることを示しています(おそらくプロセッサ上のシンプルなワード比較が使えるでしょう)。シェイプは、MRIでクラスのインスタンス変数テーブルが変更されるときのような形では変更できません。

未設定のインスタンス変数を設定する場合は、「遷移(transition)」と呼ばれるものを使います。シェイプは、インスタンス変数を追加・削除・変更したときにどのようなシェイプになるかについても認識しています。この図の場合、このシェイプは「height変数が追加されたら別のシェイプに遷移する」ことを認識しています。これで、このプログラムで行われる時間遷移をすべて記述するシェイプのグラフを得られます。

オブジェクトの遷移は低速パスではありません。既に得られたシェイプの情報のみを用いて素早く実行できます。

new_shape = obj.shape.transitions[:add_height]
index = new_shape[:height]
obj.shape = new_shape
obj.resize
obj[index] = value

以上のすべてのしくみは、マシンコードへのコンパイルと組み合わせたときに最大の効果を発揮します。マシンコードにはハードコードされた値を含められるので、比較するオブジェクトシェイプのアドレスをハードコードでき、利用するインデックスもそれに基づいてハードコードできます。これによって、マシン語のワード比較と読み取りが利用可能になります。比較に失敗した場合は、プロセッサが比較の成功を事前に予測して読み取りを続行可能なので、遠くを参照する低速パスのコードも同時に実行されます。

slow_path unless obj.shape == 0x12345678
obj[4]

TruffleRubyはシェイプをどう扱うか

TruffleRubyにおけるシェイプの実装方法については、研究論文『An Object Storage Model for the Truffle Language Implementation Framework』(Andreas Wößほか)に記載されています。

ここでは、TruffleRubyがRubyプログラムの解釈と最適化に利用する一種の内部グラフデータ構造(「中間表現」と呼ばれます)を示す形でTruffleRubyの機能を紹介します。グラフの可視化にはShopifyが開発したSeafoamというツールを用いました。

def getter
  @ivar
end

上のコンパイラグラフ断片では、ノードが操作を、太い赤線が制御フローを、細い緑の線がデータフローをそれぞれ表しています。これは、Rubyの暗黙の部分を明示的に表したRubyコードのフローチャート版です。

ここでのインスタンス変数の読み込みは、シェイプの読み込みと既知のシェイプとの比較に続いて、「ガード」または「比較が失敗した場合は低速パスへのジャンプ」のいずれかを経て、オブジェクト内の既知の場所primitive1を読み込む形で行われていることがわかります。

対応するマシンコードでは、予測されたシェイプとの比較、同じでない場合は低速パスにジャンプ、変数の読み込みと進むことがわかります。以下の3つのマシン語インストラクションがそれです。

0x1242ec600:    cmp dword ptr [rax*8 + 0xc], expected_shape
0x1242ec60b:    jne slow_path
0x1242ec611:    mov r10d, dword ptr [rax*8 + index]

上のグラフ断片には、既にオブジェクト内に存在するインスタンス変数を設定するセッターコードがあるので、ここではシェイプの変更が不要であることがわかります。このコードはゲッターと同じですが、最後の2つのオペランドが読み込みではなく書き込みになっている点が異なります。

def setter(value)
  @ivar = value
end
0x11fd28a00:    cmp dword ptr [rsi*8 + 0xc], expected_shape
0x11fd28a0b:    jne slow_path
0x11fd28a1f:    mov qword ptr [rsi*8 + 0x20], r10

インスタンス変数がまだ存在していない場合は、現在のシェイプからの遷移に沿う形でシェイプを変更します。シェイプにあるものはすべてハードコードされているので、遷移もハードコードされます。つまり新しい値を書き込むのと同様に新しいシェイプを書き換えればよくなります。

class Klass
  def initialize(a)
    @a = a
  end

  def transition(value)
    @b = value
  end
end

loop do
  instance = Klass.new(14)
  instance.transition rand(100)
end
0x12b53bb00:    cmp dword ptr [rax*8 + 0xc], expected_shape
0x12b53bb0b:    jne slow_path
...
0x12b53bb33:    mov dword ptr [rax*8 + 0xc], new_shape
...

シェイプに型付けする

これで、Ruby でシェイプを適用するためのより高度なアイデアについて説明する準備が整いました。TruffleRubyのシェイプは、インスタンス変数名と保存場所のマッピング(対応付け)を行うほかに、それらの型についてもマッピングできます。

現在のMRIでは、あらゆるインスタンス変数が完全なVALUEオブジェクトとして保存されます。小さな整数は、インスタンス変数として保存するためにタグ付け(tagged)されなければならず、取り出すときにはタグ解除(untagged)されます。コンパイラはタグ付けやタグ解除を避けたいので、インスタンス変数が常に整数であることをシェイプに書き込んでおけば、そこに保存する値にタグ付けする必要もなく、取り出す値をタグ解除する必要もないことがわかります。後でその変数が別の何かに変わった場合は、新しい変数を追加するときと同じようにシェイプを遷移させます。

この動作はTruffleRubyで確認できます。

以下のaddルーチンは1個のオブジェクト内に置かれ、2つのインスタンス変数は常に小さな整数です。生成されたマシンコードは、シェイプをチェックしてからオブジェクトから値を読み取りますが、タグ解除もアンボクシング(unboxing: 開封)も行わず、シンプルに値を利用していることがわかります。これが可能なのは、これらの変数が小さい整数型であることをシェイプからコンパイラに伝えているからです。シェイプをチェックするときに型もチェックされるので、1回のチェックで2つの変数を同時にカバーできました。これで「Cコンパイラから出力したかのようなコードが得られます。

訳注

アンボクシングについて動画では以下のように補足しています。

unboxing is java’s version of untagging
動画30:12字幕より(整形)

また、上のパラグラフの「これが可能なのは〜」以後は実際には以下のように話しています。

so it reads from the object into esi and eax which are two registers
then it does an add operation on them
but it doesn’t untag them normally you’d have to shift the value
to get rid of the tag data
so it knows when they come out they’re untagged
and they can do whatever you need to do for that point
you can just use them as it is in this case
um or it might want to tag them again to pass them on somewhere else
when we checked the shape we also checked the types right so that one check covered both variables at the same time and checked covering their types
because of the the shape includes the type information of all the variables
so just checking the shape checks all the types at once
and also checks the layout of the object
and also that that a shape check is already there for the layout
so we get it for free for checking the rest of them
so when we check this shape we also checked the types
and that one check covered both variables at the same time
so now we’re getting code that looks more like it came out of a c compiler than that it came out of a ruby
動画30:19字幕より(整形)

def add
  @a + @b
end
0x12258d380:    cmp dword ptr [rax*8 + 0xc], expected_shape
0x12258d38b:    jne slow_path
0x12258d391:    mov esi, dword ptr [rax*8 + index_a]
0x12258d398:    mov eax, dword ptr [rax*8 + index_b]
0x12258d39f:    mov r10d, eax
0x12258d3a2:    add r10d, esi
0x12258d3a5:    jo  overflow

シェイプの他の使いみち

さらに工夫を重ねるとすれば、他にどんなものをシェイプに移せるでしょうか。インスタンス変数と「同型(isomorphic: ここではインスタンス変数と少し似ているというニュアンス)」のオブジェクトのプロパティは、他にもクラスへの参照やfrozenステートなどがあります。これらをシェイプに保存すると2つのメリットが得られます。1つ目は、オブジェクトに保存するワードとビットが1つずつ減り、スペースを節約できることです。2つ目は、メソッド呼び出しでの「クラスチェック」「frozenチェック」「インスタンス変数書き込みチェック」という3種類のチェックをシェイプチェック一発で完了できることです。

TruffleRubyでは既にfrozenチェックをシェイプに取り込んでいます。上述のセッターの例で言うと、オブジェクトがfrozenかどうかをチェックするコードは以下のどこにも見当たりません。

0x11fd28a00:    cmp dword ptr [rsi*8 + 0xc], expected_shape
0x11fd28a0b:    jne slow_path
0x11fd28a11:    mov eax, dword ptr [rdx + index]

しかしチェックはしっかり行われています。この expected_shapeは統計的にfrozenでないことが判明しています。オブジェクトがfrozenの場合は、このシェイプではなく、frozenとマークされた別のシェイプということになります。これは、Ruby言語機能のオーバーヘッドを「完全に」取り除けることが示された素晴らしい例です。

現在のTruffleRubyは、シェイプにクラスを取り込んでいません。理由は、メソッド呼び出しで「クラスチェック」と「インスタンス変数のチェック」を混同するのはよろしくないと考えられているからです。つまり、複数のオブジェクトが異なるインスタンス変数を持っているという無関係な理由で、メソッド呼び出しでポリモーフィズムが発生してしまう可能性があるということです。

訳注

上のパラグラフに続いて以下も述べられています。

so two objects which you could dispatch the method called in the same way
you’d be looking differently just because they’re different instance variables which is unrelated
動画30:29字幕より(整形)

TruffleRubyでは、オブジェクトがスレッド間で共有されているかどうかについてもシェイプでマークしており、該当する場合は同期のためにロックを有効にします。同じアイデアはRactorのチェックでも利用可能でしょう。

オブジェクトで他に何かできるか

一般に、Rubyの最適化作業の多くは、Rubyの実行モデルの最適化です。MJIT、YJIT、Sorbet Compilerは、インタープリタをJITコンパイルされたマシンコードに置き換えますが、VMの他の部分には手を付けません。MJITとSorbet Compilerは、標準のMRIに接続するためにコードから事実上のC拡張を生成しており、YJITもだいたい似たような感じですが、(訳注↓)

訳注

動画では以下が続いています。

but it doesn’t work at the level of whole methods
動画34:27字幕より(整形)

私は、Ruby最適化を頑張るなら、もっとオブジェクトに目を向ける必要があると考えています。Shopifyではガベージコレクタのコンカレント圧縮やオブジェクトの可変長アロケーション(VWA: variable width allocation)、新しいガベージコレクタといったホットなアイデアに取り組んでいます。TruffleRubyではハッシュや配列がさらに最適化されたことで、サイズや内容に応じた特殊化が可能になりました。TruffleRubyでは、partial escape analysisと呼ばれる非常に強力な最適化をいくつか行っています。これにより、小さなメソッドグループ内でしか使われないオブジェクトが仮想化され、決してアロケーションされないようになります。

実用的な提案

私からの実用的な提案の筆頭は「MRIにオブジェクトシェイプを実装しよう」というものです。実際には実装の複雑さは比較的小さいと思いますし、現在のMRIインタプリタの高速化にも即効性が期待できるでしょう。今後MJITやYJITの開発を進める上でもきっと役に立つはずです。

このアイデアがうまくいくことはTruffleRubyで示されていますし、さらにすごいものを構築できる可能性もあります。この実践的な実装の詳細については先ほど紹介したTruffleRubyの論文で述べられており、そこで達成されたパフォーマンスについても他のいくつかのRubyメソッドと比較されています。

これについて解決の必要となる大きな問題はないと思います。本当に必要なのは、1980年代初頭からの一連の研究成果を元に構築してくれる勇者たちでしょう。

訳注

上に続いて以下も述べられています。

that we can follow that is proven that we can implement in ruby
and as we showed when we talked about the history
ruby is already philosophically aligned to the languages where it came from
and that the problems it’s trying to solve
both in terms of developer happiness and optimizing for that
but now trying to cover some of the performance
and we’re aligned to what ourselves are trying to do
so it makes sense to to keep using their techniques
動画36:42字幕より(整形)

MRIへのオブジェクトシェイプ導入について私からのお話は以上です。ご清聴ありがとうございました。

参考資料

  • L. Peter Deutsch and Allan M. Schiffman. 1984. Efficient implementation of the Smalltalk-80 system. In Proceedings of the 11th ACM SIGACT-SIGPLAN symposium on Principles of programming languages (POPL ’84).
  • David Ungar and Randall B. Smith. 1987. Self: The power of simplicity. In Conference proceedings on Object-oriented programming systems, languages and applications (OOPSLA ’87).

  • Elgin Lee. Object Storage and Inheritance for Self. Engineer’s thesis, Electrical Engineering Department, Stanford University, 1988.

  • C. Chambers, D. Ungar, and E. Lee. 1989. An efficient implementation of SELF a dynamically-typed object-oriented language based on prototypes. In Conference proceedings on Object-oriented programming systems, languages and applications (OOPSLA ’89).

  • Urs Hölzle, Craig Chambers, David Ungar. Optimizing Dynamically-Typed Object-Oriented Languages With Polymorphic Inline Caches. In ECOOP ’91.

  • David Ungar and Randall B. Smith. 2007. Self. In Proceedings of the third ACM SIGPLAN conference on History of programming languages (HOPL III).

  • Andreas Wöß, Christian Wirth, Daniele Bonetta, Chris Seaton, Christian Humer, and Hanspeter Mössenböck. 2014. An object storage model for the Truffle language implementation framework. In Proceedings of the 2014 International Conference on Principles and Practices of Programming on the Java platform: Virtual machines, Languages, and Tools (PPPJ ’14).

  • Stefan Marr, Chris Seaton, and Stéphane Ducasse. 2015. Zero-overhead metaprogramming: reflection and metaobject protocols fast and without compromises. In Proceedings of the 36th ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI ’15).

  • Benoit Daloze, Stefan Marr, Daniele Bonetta, and Hanspeter Mössenböck. 2016. Efficient and thread-safe objects for dynamically-typed languages. In Proceedings of the 2016 ACM SIGPLAN International Conference on Object-Oriented Programming, Systems, Languages, and Applications (OOPSLA 2016).

  • 本プレゼンテーションのスタイルの一部はBenoit Daloze氏による以下のシェイプに関する過去スライドにインスパイアされました。

メモ

撮影担当のSam Tamに感謝いたします。

  • ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin20]
  • jruby 9.2.19.0 (2.5.8) 2021-06-15 55810c552b OpenJDK 64-Bit Server VM 11.0.2+9 on 11.0.2+9 +indy +jit [darwin-x86_64]
  • truffleruby 21.2.0 (ruby 2.7.3に似ている)GraalVM CE JVM [x86_64-darwin]

関連記事

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


  1. 「物体には必ず形がある」というシャレにもなっています。 
  2. 原文のdivorcedには「離婚」の意味もあります。isolatedをカジュアルな話し方にしたと思われます。 

The post Rubyオブジェクトの未来をつくる「シェイプ」とは(翻訳) first appeared on TechRacho.

Ruby正規表現の後読みでは長さ不定の量指定子は原則使えない

$
0
0

更新情報

  • 2017/12/05: 初版公開
  • 2021/10/14: 更新

こんにちは、hachi8833です。「ライフ」カテゴリの記事でアドベント書きたかったのですが、こちらの小ネタにします。

正規表現の先読みと後読みについては「正規表現の先読み・後読み(look ahead、look behind)を活用しよう」をご覧ください。

以下は基本的にRubyの正規表現(onigmo)を使います。他の正規表現ライブラリではこのとおりにならない可能性があります。

Rubyの正規表現の後読みは長さを不定にできない

以下の文字列が対象です。

word work wording working interesting partitioning subscribe subscriber subscription

量指定子の場合

たとえば、ingで終わる1文字以上の長さの英単語のingだけにマッチさせたいと思って次の正規表現を書いたとします。+は1文字以上のマッチを表します。

(?<=[\p{L}]+)ing

しかしやってみると、Invalid pattern in look-behind.と表示されます。なお、+を最小一致の+?に変えてもだめでした。

代替|の場合(更新2021/10/14)

任意の長さの代わりに、代替|を用いて長さの異なる特定の語のリストを後読みで使うとどうなるでしょうか。

(?<=word|work|interest|partition)ing

代替|のリストは通ります。よかった!

しかし以下のように同じ代替|のリストをグループ()で囲むとどうなるでしょう。

(?<=(word|work|interest|partition))ing

代替|をグループ()で囲むと残念ながらInvalid pattern in look-behind.になりました。

ただし以下のようにリスト内の各要素の長さをすべて同じにすれば、代替|をグループ()で囲んでも通ります。

(?<=(word|work|weed))ing

なぜ後読みで量指定子が使えないのか

後読みは本体がマッチした後で文字どおり遡ってチェックされるはずなので、量指定子(quantifier: 量化子とも呼ばれます)の長さが不定だと効率が非常に落ちることは想像がつきます。Onigmoの仕様まではチェックしていませんが、おそらくそうした理由で長さ不定の後読みをサポートしていないのではないかと推測しています。

ちょっとだけPerlでも試してみましたが、こちらもnot implementedだそうです。

$ perl -e '"word work wording working interesting partitioning subscribe subscriber subscription" =~ /(?<=[\\p{L}]+)ing/;'
Variable length lookbehind not implemented in regex m/(?<=[\\p{L}]+)ing/ at -e line 1.

後読みに使える量指定子

Rubyの場合、少なくとも次のように{,10}{4}のように長さを固定した量指定子では後読みが機能します。これがなかったらわたし的につらいです。

(?<=\b[\p{L}]{4})ing

追記(2018/10/25): 少なくとも、+*?{N,M}{N,}のように長さ不定の量指定子はRubularのRuby 2.1.5ではだめでした。

他にも使えるものがあるかもしれませんが、いずれにしろ量指定子を不用意に使うと効率が落ちるので、あまりやんちゃしないようにしましょう。

先読みでは長さを不定にできる

Ruby正規表現の先読み(look ahead)では、次のように長さ不定の量指定子を使えます。

work(?=[\p{L}]+)

おまけ: .NET Frameworkだとできる

遠い昔の記憶では、.NET Frameworkでは後読みで長さ不定の量指定子を使えたはずだったので、チェックしてみました。当時はこれが当たり前だと思っていたので、他のライブラリでできないことを知ったときはショックでした。

たった今見つけたregexstorm.netというサイトで.NET Frameworkの正規表現をチェックしたところ、後読みであっさり長さ不定の量指定子を使えました。Mac環境だとおいそれと.NET Frameworkの正規表現を確認できないので、このサイトは助かります。

また、.NET Frameworkの正規表現ライブラリをGo言語に移植したdlclark/regexp2という私の大好きなパッケージで試したところ、こちらでも長さ不定の量指定子を使えました。

package main

import (
    "fmt"

    "github.com/dlclark/regexp2"
)

func main() {
    re, err := regexp2.Compile("(?<=[\\p{L}]+)ing", 0)
    if err != nil {
        fmt.Println("err compile: ", err)
    }

    ma, err := re.FindStringMatch("word work wording working interesting partitioning subscribe subscriber subscription")
    if err != nil {
        fmt.Println("err match: ", err)
    }

    fmt.Println(ma)
}
$ go run regexp2.go
ing

効率を犠牲にしても後読みで長さ不定の量指定子をサポートしているのか、それとも実装が凄いのかは調べていませんが、私の中ではやはり.NET Frameworkの正規表現が今のところ最強です。

フィードバック

ご指摘ありがとうございます!修正しました。

関連記事

正規表現: 文字クラス [ ] 内でエスケープしなくてもよい記号

正規表現の先読み・後読み(look ahead、look behind)を活用しよう

Ruby 2.4.1新機能: Onigmo正規表現の非包含演算子(?~ )をチェック

The post Ruby正規表現の後読みでは長さ不定の量指定子は原則使えない first appeared on TechRacho.


週刊Railsウォッチ: Railsリポジトリで進行中のPropshaft、inverse_ofを自動推論ほか(20211018前編)

$
0
0

こんにちは、hachi8833です。Kaigi on Rails 2021が今週の金曜と土曜に開催されますね。

週刊Railsウォッチについて

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

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

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

以下の公式更新情報を中心に見繕いました。

🔗 コントローラのコールバックでinstance_execを回避

procが(条件またはコールバック自身として)コールバックに渡されると、このprocがコントローラのインスタンス上でinstance_execを用いて評価される。instance_execを呼ぶとそのオブジェクトのシングルトンクラスが新たに作成され、インラインメソッドキャッシュが新たに要求される。

このコミットは、:only:exceptがコールバックに渡された場合にコントローラで余分なシングルトンクラスが作成されるのを回避する。
この効果を誇張した以下のベンチマークも作った。
https://gist.github.com/jhawthorn/bded5bc1d5f1afd4cdd7fb5b800312e1
同PRより


つっつきボイス:「なるほど、改修前のようにprocで処理するとオブジェクトのシングルトンクラスが生成されてしまう↓」

# actionpack/lib/abstract_controller/callbacks.rb#L77
      def _normalize_callback_option(options, from, to) # :nodoc:
        if from = options.delete(from)
-         _from = Array(from).map(&:to_s).to_set
-         from = proc { |c| _from.include? c.action_name }
+         from = ActionFilter.new(from)
          options[to] = Array(options[to]).unshift(from)
        end
      end

「代わりにActionFilterクラスを定義しておいてそれで処理する方が、procで回すよりも生成を削減できて高速化につながるということのようですね」「なるほど、そういえばJeremy Evansさんの『Polished Ruby Programming』にもこんな話がどこかにあった気がします」「考え方としてはValue Objectを連想しますね」

「呼び出される回数がものすごく多いコードをこうやって最適化すると、改善がたとえ数%であっても顕著な差が出る」「たしかにコールバックは呼び出しが多そう」

# actionpack/lib/abstract_controller/callbacks.rb#L38
+   class ActionFilter
+     def initialize(actions)
+       @actions = Array(actions).map(&:to_s).to_set
+     end
+
+     def match?(controller)
+       @actions.include?(controller.action_name)
+     end
+
+     alias after  match?
+     alias before match?
+     alias around match?
+   end

🔗 スコープ付き関連付けでinverse_ofを自動推論

  • スコープ付き関連付けでinverse_ofを自動的に検出できるようになった

inverse_ofの自動検出がスコープ付き関連付けで動くようになった。たとえば、以下のcomments関連付けで自動的にinverse_of: :postが検出されるので、このオプションを渡す必要がなくなる。

class Post < ActiveRecord::Base
  has_many :comments, -> { visible }
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

ただし、まだこの自動検出は逆関連付けにスコープがあると動作しない。この例では、post関連付けにスコープがあると、Railsがcomments関連付けの逆関連付けを探索できなくなる。

これはRails 7の新規アプリでデフォルトになる。オプトインするには以下の設定を用いる。

config.active_record.automatic_scope_inversing = true

Daniel Colson, Chris Bloom
同Changelogより


つっつきボイス:「お、can_find_inverse_of_automaticallyというメソッドを改修することで、スコープ付きの関連付けでinverse_ofを自動検出するようにしたんですね↓」「こんなメソッドがあったとは」

このコミットはcan_find_inverse_of_automaticallyを変更して、関連付けにスコープがあるが逆の関連付けにスコープが存在する可能性がない場合にinverse_ofを自動検出するようにした(can_find_inverse_of_automaticallyは関連付けのリフレクションで最初に呼び出され、trueが返されれば逆のリフレクションを探索し、最終的に逆のリフレクションでそのメソッドを再び呼び出すことでそのメソッドを確実に呼び出せるようにする)。
同PRより抜粋

inverse_ofを毎回手動で書くのは割と手間だったので、副作用がないならこういうのはありがたい👍」「Changelogには逆の関連付けにスコープがあると動かないと書かれていますね」「単純なものなら手動で書けば動かせますけど、逆の関連付けにスコープが付いていると自動でコードを生成するのは簡単ではなさそう」

「コミットメッセージによるとGitHubには関連付けが171個もあるので、これを使ってinverse_ofを自動追加したいとありますね: 確かにこれだけたくさんあると手動でやってたら間違えそう」「コミットしたcomposerinteraliaさんはGitHubのスタッフなんですね」

🔗 コネクションのスキーマキャッシュをlazy loadingできる機能を追加

  • コネクションのスキーマキャッシュをlazy loadingするオプションを追加

従来は、Active Recordでスキーマキャッシュを読み込むには起動時にRailtieを用いる方法しかなかった。このオプションは、コネクションが確立した後でコネクションのスキーマキャッシュを読み込める機能を提供する。コネクションの確立後にキャッシュが読み込まれるので、コネクションのキャッシュを遅延読み込みする機能はマルチプルデータベースを用いるRailsアプリケーションで重宝するだろう。現時点では、Railtiesは起動前にコネクションにアクセスできない。

このキャッシュを用いるには、アプリケーションのコンフィグでconfig.active_record.lazily_load_schema_cache = trueを設定する。さらに、デフォルトの”db/schema_cache.yml”パスを使いたくない場合は、データベースコンフィグにschema_cache_pathも設定するべき。
Eileen M. Uchitelle
同Changelogより


つっつきボイス:「コネクションのスキーマキャッシュをlazy loadingする機能、通常はそれほど必要なさそうに見えるけど、マルチプルデータベースで便利な可能性か、なるほど」「あくまでオプションなので、使いたければ使えるという感じですね」

🔗 pg:dumpでCOMMENTの出力を回避

このプルリクは、PostgreSQL利用時にCOMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';のような行がdb:structure:dump実行時に出力されないようにする。
この種のCOMMENTがあってもあまり価値はないように思える。
しかしこのCOMMENTがあると、#36816で指摘されているようにdb:structure:loadがDBのスーパーユーザーでないと動かなくなり、ActiveRecord::StatementInvalid: PG::InsufficientPrivilege: ERROR: must be owner of extensionのような一般的なエラーが出力されてデバッグがやりにくくなる点がよくない。
同PRより


つっつきボイス:「PostgreSQLバージョンが11以上のときはdb:structure:dumpでCOMMENTを出力しないようにしたんですね」

「なるほど、COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';みたいな行があるとスーパーユーザー以外はdb:structure:loadが失敗するので、失敗しないようにCOMMENTを抑制したということか」「なるほど」「作ったダンプが読み込めないのかと思って焦りそうなので、余分なエラーがなくなるのはいい👍」「ですよね」

🔗 PostgreSQLのクエリパラメータ構文を利用できるようになった

従来は以下を実行すると

ActiveRecord::DatabaseConfigurations::UrlConfig.new(:production, :production, 'postgres:///?user=user&password=passwd&dbname=theapp_production', {}).configuration_hash

以下が返された。

{ :user=>"user", :dbname=>"theapp_production", :adapter=>"postgresql" }

uri.passwordnilなのでpassword属性がnilを返し、このハッシュをマージしたものが上の結果となる。

このプルリクはこの点を修正して以下を返すようになる。

{ :user=>"user", :password=>"passwd", :dbname=>"theapp_production", :adapter=>"postgresql" }

この問題は#42797で指摘された。なお同issueでは、postgres://user:passwd@/theapp_productionはPostgreSQLのURIとして有効という指摘もあった。現在は、以下を実行すると

ActiveRecord::DatabaseConfigurations::UrlConfig.new(:production, :production, 'postgres://user:passwd@/theapp_production', {})

以下のエラーが出力される。これはURIが有効なRFC 2396実装でないことが原因。これを修正するアイデアがあれば求む。

/home/abeid/.rbenv/versions/3.0.1/lib/ruby/3.0.0/uri/generic.rb:207:in 'initialize': the scheme postgres does not accept registry part: user:passwd@ (or bad hostname?) (URI::InvalidURIError)

同PRより


つっつきボイス:「postgres:///?user=user&password=passwd&dbname=theapp_productiみたいにpostgres:///で始まるURIを指定できるようになったんですね」「こんなふうにuserやpasswordを指定するのか」「これはたぶん本当のURI形式とは違うんじゃないかな?」「あくまでURI風ということなのかも」「JDBCとかでこの形式のURIを使った覚えがあります」「そうそう」

参考: Java Database Connectivity - Wikipedia

🔗Rails

🔗 RailsリポジトリにあるPropshaft


つっつきボイス:「この間Railsウォッチをレビューいただいたときに(ウォッチ20211004)このPropshaftの存在を教わったので取り上げてみました」「そうそう、アセットパイプラインの新しい選択肢のひとつが作り中のはずと思って探してみたら、Railsのリポジトリの中にこのProfshaftがあったんですよね」

rails/propshaft - GitHub

「アセットパイプラインで従来のSprocketsの代わりにProfshaftも使えるようになるということみたい」「Propshaftって造語かと思ったらプロペラシャフトでした↓」「Sprocketsもそうですけど、機械の部品っぽい命名にしてるんでしょうね」「くるくる回り続ける部品感ある」

参考: propshaftとは何? Weblio辞書
参考: sprocketの意味・使い方・読み方 | Weblio英和辞書

「PropshaftのREADMEを見ると、アセットのバンドルを頑張らなくてよくなった時代のためのアセットパイプラインライブラリという感じの触れ込みですね」「Rails 7ではアセットのプリコンパイルを避ける方向に持っていきたいはずなので、Propshaftはその一環ということでしょうね: Rails 7で例のimport mapが導入されることで、Sprocketsのときよりも機能を軽量化できた感じ」「なるほど」

「READMEの末尾にある『PropshaftはRails 7に入るのか?』というFAQに『入る可能性は高いけど当面Sprocketsのサポートも必要』とありますね」「まだしばらくかかりそうかな」

Rails 7: import-map-rails gem README(翻訳)

🔗 Active Supportの#descendantsメソッドを深掘りする(Ruby Weeklyより)

参考: ActiveSupport::DescendantsTracker


つっつきボイス:「descendantsメソッドやancestorsメソッドは1〜2年おきぐらいに話題になる感じ」「クラスやモジュールの読み込みリストをチェックするメソッドですね」「記事にもあるように、モジュールをincludeprependした結果の順序などが罠になることがあります」

🔗 SeleniumによるシステムテストをCupriteに移行してみた(Ruby Weeklyより)


つっつきボイス:「ChromeでテストするならCupriteでいいんじゃないかな: 手順が丁寧に説明されていてよさそう👍

「記事でも取り上げていますけど、Ajax/Fetch周りが割と複雑」「記事ではテストを書き直さないといけなかったそうです」「イベントを取得するとか、通信が発生して書き換えを待つような動作のテストは難しい部分ですね」

「そういえば少し前にもCupriteに変えてみた記事があったのを思い出しました↓」「こういう実際に動かしてみた記事が増えてくると助かります🙏

参考: 2021年6月現在、Cupriteで”正しい”システムテストはできるのか?

🔗 SidekiqをActive Job経由ではなく直接使う(Ruby Weeklyより)

# 同記事より: Sidekiqを直接使う場合
class DoThingsInBackgroundJob
  include Sidekiq::Worker
  Sidekiq_options queue: "default"

  def perform(id)
    an_active_record_object = ActiveRecordObject.find_by(id: id)
    an_active_record_object.do_things
  end
end

つっつきボイス:「タイトルで言いたいことはだいたい理解できた😆」「気持ちわかります」「Active Jobは抽象化されている分機能が少な目なので、生のSidekiqを使う方が話が早い」

参考: Active Job の基礎 - Railsガイド

「記事の末尾に『SidekiqはPro版にするのがおすすめ』とありました」「Sidekiqの有料版を使ったことはないけど、十分普及していて経営も順調なサービスならサポートなどの面を考えてもお金を払う価値はあるでしょうね」「たしかに」

参考: Sidekiq: Simple, efficient background jobs for Ruby.

「お、Sidekiqの価格表に支払い方法が書かれているのがちょっと珍しいかも: Enterprise版は請求書払いができるとありますよ」「ほんとだ」「請求書払いに対応していないと日本企業でなかなか決済が通らなかったりしますよね」「そういう傾向はありますね」「最近はさすがに減りつつあると思いますけど」

🔗 RSpecのsubject


つっつきボイス:「RSpecのsubjectは、うまく適合するケースの方が少ないなと自分も思います: itがたくさんある場合にはsubjectがあると明らかに繰り返しが減りますけど、記事にもあるように以下のような使い方はたしかに最悪↓」「う、subjectの中でincrementしてる」「subjectの中での改変はやめて欲しい」

# 同記事より: subjectが向いてないケース
class Counter
  attr_reader :count

  def initialize
    @count = 0
  end

  def increment
    @count += 1
  end
end

describe 'Counter#increment' do
  let(:counter) { Counter.new }
  subject { counter.increment }
  it do
    subject
    expect(counter.count).to eq 1
  end
end

「行数の多いテストにsubjectがあると、このテストのsubjectはどこだっけと探さないとわからなくなりますよね」「たしかに」「subjectには名前を付けて呼び出せる機能もあるので、それを使うならsubjectがあってもいいかなと思います: 名前付きsubjectは嫌いじゃない」「なるほど」

参考: Explicit Subject - Subject - RSpec Core - RSpec - Relish

🔗 その他Rails

つっつきボイス:「上はr7kamuraさんのRailsアップグレード記事ですね」

「r7kamuraさんはたしか以前からRailsアップグレード作業を請け負っていますね↓」「お〜」「Railsのアップグレードは社内の人がなかなかやりたがらないこともあったりするので、Railsアップグレードに特化して業務を請け負うのはひとつの生存戦略だなと思いました」「そうそう、アップグレードのノウハウも蓄積できますよね」

参考:『Railsアップグレード百景』という題で発表した


前編は以上です。

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

週刊Railsウォッチ: Ruby 3.1にYJITマージのプロポーザル、Rubyのmagic historyメソッド、JSのPartytownほか(20211012後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: Railsリポジトリで進行中のPropshaft、inverse_ofを自動推論ほか(20211018前編) first appeared on TechRacho.

週刊Railsウォッチ: ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか(20211019後編)

$
0
0

こんにちは、hachi8833です。発表された新型MacBook Proのノッチが…

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 ruby/debugのChrome用リモートデバッグ機能


つっつきボイス:「BPS社内Slackに貼っていただいたツイートです」「これはいい機能👍

ruby/debug - GitHub

「これは?」「ruby/debugの中からHTTPサーバーを起動してそこにChromeでアクセスすると、Chromeからruby/debugを制御できるという、ruby/debugの新機能です」「お〜動画でChromeの中にデバッガコンソールが見えた」「いわゆるリモートデバッグ機能そのもの」「Chrome拡張を使うのかと思ったら不要なんですね」「Chrome Devtoolsで公開されているChromeの独自機能を使っているようです」

現時点ではmasterブランチに入っています。

🔗 Rubyアプリを最適化する(Ruby Weeklyより)


つっつきボイス:「ちょっと眺めた限りではシングルスレッドで複数のIOをチェインして同時処理させる話のようですね」

参考: class Thread (Ruby 3.0.0 リファレンスマニュアル)

Backend#chainを使うと、こんなふうにI/O操作をチェインできるらしい↓」

# 同記事より
Thread.current.backend.chain(
  [:write, @conn, "#{len.to_s(16)}\r\n"],
  [:splice, r, @conn, len],
  [:write, @conn, "\r\n"]
)

「このあたりのRESPOND_FROM_IO_PROGRAMの内側がインタプリタとかASTパーサーっぽく見える↓」

# 同記事より
# program references:
# 0 - headers
# 1 - io
# 2 - @conn
# 3 - pipe r
# 4 - pipe w
# 5 - chunk_size
RESPOND_FROM_IO_PROGRAM = [
  [:write, 2, 0],
  [:loop,
    [:splice, 1, 4, 5],
    [:break_if_ret_eq, 0],
    [:store_ret, :len],
    [:write_cte_chunk_size, 2, :len],
    [:splice, 3, 2, :len],
    [:write, 2, "\r\n"]
  ],
  [:write, 2, "0\r\n\r\n"]
]

def respond_from_io(request, io, headers, chunk_size = 2**14)
  formatted_headers = format_headers(headers, true, true)
  r, w = IO.pipe
  Thread.backend.submit(RESPOND_FROM_IO_PROGRAM, formatted_headers, io, @conn, r, w)
end

「お、同じ部分を今度はDSL化して読みやすくした感じですね↓」

# 同記事より
RESPOND_FROM_IO_PROGRAM = Polyphony.io_program(
  :headers, :io, :conn, :pipe_r, :pipe_w, :chunk_size
) do
  write :conn, :headers
  io_loop do
    splice :io, :pipe_w, :chunk_size
    break_if_ret_eq 0
    store_ret :len
    write_cte_chunk_size :conn, :len
    splice :pipe_r, :conn, :len
    write :conn, "\r\n"
  end
  write :conn, "0\r\n\r\n"
end

「最後のまとめを見ると、この記事ではトップの層でRubyのデータ構造を使い、I/O操作のような単純な処理は下の層のCで操作することで並列実行しやすくしたようですね」「Rubyでもこんなふうにすると速くできるよと」「Linuxのio_uringと似たアプローチとありますね」

本記事ではRubyアプリのパフォーマンス最適化のためにプログラムを2つの層に分離するアプローチを紹介しました。つまり、Rubyのデータ構造を用いて低レベル処理を表現するRubyのトップ層と、それらの処理を最適な形で実行するCの実装の層です。このアプローチでは、chunkedエンコーディングによるHTTPレスポンス送信、受信データ解析、I/Oのループ処理などの複雑な処理を長時間行う場合に特に有効です。
上述のようにこのアプローチはLinuxのio_uringで用いられているものと似ています。考え方は同じで、(I/O)操作をデータ構造で表現し、その実行を最適化された下の層(io_uringではカーネル、Rubyの場合はC拡張)に移行しています。
同記事より

参考: Transfer-Encoding - HTTP | MDN
参考: io_uringで高速IO処理(?) | κeenのHappy Hacκing Blog

「ところでさっきのRESPOND_FROM_IO_PROGRAMの中身はリテラルになっていますけど↓、こうするとRubyのインタプリタが仕事しなくて済むようになってJITが効きやすくなるのかもと思いました」「たしかにリテラルですね」

# 同記事より抜粋
RESPOND_FROM_IO_PROGRAM = [
  [:write, 2, 0],
  [:loop,
    [:splice, 1, 4, 5],
    [:break_if_ret_eq, 0],
    [:store_ret, :len],
    [:write_cte_chunk_size, 2, :len],
    [:splice, 3, 2, :len],
    [:write, 2, "\r\n"]
  ],
  [:write, 2, "0\r\n\r\n"]
]

🔗 HTTPI: RubyのHTTPクライアント向けの共通インターフェイス(Ruby Weeklyより)

savonrb/httpi - GitHub


つっつきボイス:「HTTPI、見たことなかった」「新しそうなライブラリですね」

# 同リポジトリより
require "httpi"

# create a request object
request = HTTPI::Request.new
request.url = "http://example.com"

# and pass it to a request method
HTTPI.get(request)

# use a specific adapter per request
HTTPI.get(request, :curb)

# or specify a global adapter to use
HTTPI.adapter = :httpclient

# and execute arbitrary requests
HTTPI.request(:custom, request)

「これは何をするものなんでしょう?」「Rubyにはこのサイトにも書かれているようなHTTPClientやNet::HTTPのようないわゆるHTTPクライアントライブラリがたくさんありますけど、それぞれのインターフェイスはまちまちなので、それらを統一して呼べるようにして、HTTPI.adapter = :httpclientみたいにアダプタを切り替えるだけでHTTPクライアントライブラリを切り替えられるということでしょうね」「なるほど、RubyのHTTPクライアントライブラリ向けのラッパーでしたか」

「ただ、HTTPクライアントライブラリを使い分ける機会はそうそうないと思いますけど」「ライブラリをこれと決めたら普通はそのまま使いますよね」「Active Jobを経由せずにSidekiqを直接使う話(ウォッチ20211018)と似ているかも」「MySQLからPostgreSQLに移行することがめったにないのもそうですね」「ライブラリごとに機能も違ってくるので、ライブラリ間の差異を共通化レイヤで吸収するのはそれなりに大変」

「HTTPIは新しいライブラリですし、今すぐproductionで使うものでもないので、もしかすると練習用として作ってみたのかなと想像してみました: こういうのを自分でやってみると楽しく勉強できると思います」「特定のHTTPライブラリのバグを切り分けるのに使えるかもしれませんね」

🔗 その他Ruby


つっつきボイス:「ruby/specリポジトリで、Ruby 3.0の新機能や機能変更のspecを書いて欲しいという募集だそうです」

#823を見ると、specを書いて欲しい3.0の機能がちゃんとリストになっているのがいいですね」「完了のチェックボックス、この間見たときより増えてるみたいです」「これならコントリビュートしやすそう」「やってみようかな」

その後もチェック済みは着々と増えているようです: Pull requests · ruby/spec

「お、IBM720なんてエンコーディングがあるんですって(#16233)」「聞いたことないですね」

参考: MFT で使用できるコード・ページ - IBM Documentation

🔗DB

🔗 SpannerにPostgreSQL互換インターフェイスが追加(Publickeyより)


つっつきボイス:「そうそう、GoogleのSpannerにこの機能が入りましたね: Spannerはかなり高価だったんですが、最近値下がりして以前より使いやすくなりつつあるので、PostgreSQL互換のインターフェイスが使えるようになればより手を出しやすくなりそう」

参考: Cloud Spanner  |  Google Cloud

「AWSにはRDSやAurora PostgreSQLがありますけど、これまでGCPにはマルチAZやフェイルオーバーまですべて備えたようなRDS的なサービスというとCloud SQLしかなかったと思うので、今回の発表でSpannerがGCPでの選択肢に入ってきそうですね」「なるほど」「AWSからSpannerを使おうとするとネットワーク的に遠いという問題はありますが」「GCPからSpannerを使う方が無理がなさそうですね」「レイテンシはその方がよいでしょうね」

参考: Amazon RDS(マネージドリレーショナルデータベース)| AWS
参考: Amazon Aurora PostgreSQL の特徴 | MySQL PostgreSQL リレーショナルデータベース | アマゾン ウェブ サービス
参考: Cloud SQL ドキュメント  |  Google Cloud

「Spannerはスケーラビリティがすごく高くてオートスケールも強いんですが、これほどの高性能が必要な案件はなかなかないでしょうね」「あ〜」「あるとすれば、ソシャゲやワクチン予約受付サイトのようにユーザー数がいくらでも増える可能性やアクセスが短時間にものすごく集中する可能性もあって、かつランニングコストに見合うサービスかな」「なるほど」「そのぐらいの規模になるとAWSのオートスケールだとアクセス急上昇に間に合わないかも」「ソシャゲだとSpannerはちょっともったいなさそうですけどね」

「その意味では、AWSのAurora PostgreSQLは負荷に応じてオートスケールできて、しかもRDSと値段がほとんど変わらないのがいいんですよ」「たしかに値段がほぼ同じなら迷わずAurora PostgreSQLを選ぶでしょうね」「SpannerはDBインスタンスを立てるのに比べて安くないので悩ましいところ」

「まだGCPやSpannerは本格的に運用したことはありませんが、Spannerは選択肢のひとつとしておくとよさそう👍


「ところで元記事ではSpannerがNoSQLとして紹介されていたけど、NoSQLだったかな?」「公式ブログ↓を見ると、当初はNoSQLキーバリューストアとして設計されたけどリレーショナルモデルも採用したとありますね」「なるほど、自分の中ではSpannerは無限といってもいいぐらいにスケールできるRDBという位置づけだったけど、合ってるみたいでよかった」

参考: NoSQL から新しい SQL へ : グローバルなミッションクリティカル DB へと進化を遂げた Cloud Spanner | Google Cloud Blog

🔗クラウド/コンテナ/インフラ/Serverless

🔗 AWS Lambdaバトル


つっつきボイス:「これは自分も見ましたけど、ベンチマークのソースコードを見るとJSONを取ってきてDynamoDBに格納しているだけで、実行環境での起動以外ではほとんどリソースを使っていないんですよ」「そんなに軽い処理なんですか?」「コードに分岐すらありません」

Aleksandr-Filichkin/aws-lambda-runtimes-performance - GitHub

# aws-lambda-runtimes-performance/ruby-lambda/app.rb
require 'json'
require 'aws-sdk-dynamodb'

 $client = Aws::DynamoDB::Client.new

def create(event:,context:)
  body = event["body"]
  book =JSON.parse(body)
  id = SecureRandom.uuid
  book["id"]=id

  table_item = {
    table_name: "book",
    item: book
  }
  $client.put_item(table_item)
  { statusCode: 201, body: JSON.generate(book) }

end

参考: Amazon DynamoDB(マネージド NoSQL データベース)| AWS

「このベンチマークは実行環境のプロセス起動を比較しているようなものなので、ビジネスで使うサービスを運用するうえではあまり参考にならないかなと思います」「なるほど」「Rubyのグラフはメモリーリークっぽく上昇してはいますけど、自分の印象ではRuby十分速いと思いました」

「ちゃんとベンチマークするのは難しい…」「言語が違えば実装も変わりますし、特にJSONパーサーはライブラリによって速度がかなり変わります」「それもそうですね」


なお、LambdaバトルでNodeが遅い件について、AWS_NODEJS_CONNECTION_REUSE_ENABLEDがオンになっていない可能性があるのではというissueが立っている↓とBPS社内メンバーからメンションをもらいました。

参考: Optimize NodeJS function warm starts by enabling TCP connection reuse by bilalq · Pull Request #6 · Aleksandr-Filichkin/aws-lambda-runtimes-performance

🔗JavaScript

🔗 jQuery UIとjQuery Mobileが開発終了(Publickeyより)


つっつきボイス:「今回はたまたまPublickeyの記事が多めになりました」「jQuery UIとjQuery Mobileがついに」

「ツイート↓で見かけましたけど、jQueryは今でもすごく使われているんですね」「実際jQueryで十分な場合も多いですし、ライブラリの種類も豊富だし、デザイン系も含めるとjQueryを扱える人は多いですね」「なるほど」

「jQueryがこれだけ人気を得た理由のひとつは、複数ブラウザで共通に利用できたことでしょうね: 今はその役割はほぼ終わったと思いますが」「そうですね」「蓄積されたライブラリは簡単にはなくならないと思います」

「jQueryは複数ブラウザで使えるJSライブラリのさきがけだったんでしょうか?」「Prototype.jsも同じぐらいの時期だったかな: 一時期はPrototype.jsが優勢だったこともありましたけど今は消えた」「Prototype.jsは2015年までメンテされてたんですね」

参考: Prototype JavaScript Framework - Wikipedia

「そういえばjQueryとPrototype.jsを両方置くと名前空間が衝突するのを思い出した↓」「そうだったかも」「どちらも$を使うからですね」「jQuery.noConflict();、なつかしい」

参考: prototype.jsと同時に使うには - jQuery 日本語リファレンス

🔗言語/ツール/OS/CPU

🔗 計算量とオーダー


つっつきボイス:「はてブでバズっていました」「O(m)みたいに書くビッグオー記法以外にもΘΩとかいろんな記法があるんですね」「私もビッグオーしか知りませんでした」「みっちり書かれてる」

参考: ランダウの記号 - Wikipedia

「計算量やオーダーの概念はプログラマーにとって大事ですし、アルゴリズムごとにオーダーもありますけど、現実のユースケースだとデータや検索にlocality(局所性)が絡んだりして、きれいに正規分布しないことも多いですよね」「そうそう」「アルゴリズムの特徴を知っておくのは大事だけど、計算量を覚えるだけだとあまり有効でない気がします」

参考: 参照の局所性 - Wikipedia

「具体的な計算量はググればわかるので、むしろ計算量という概念があることは知ってて欲しいですよね」「コードレビューで3重ネストがあったときに、これだと計算量がこのぐらいになるから早期脱出を入れようとか、そういうやりとりはしますね」「そうそう、丸暗記しなくてもいいので、コードに関してやりとりするときにその視点は持ってて欲しい」

「計算量やオーダーは知っていて損はしないので読んでおくといいと思います👍


後編は以上です。

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

週刊Railsウォッチ: Ruby 3.1にYJITマージのプロポーザル、Rubyのmagic historyメソッド、JSのPartytownほか(20211012後編)

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

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

Ruby Weekly

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ: ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか(20211019後編) first appeared on TechRacho.

RubyでHTTPサーバーをゼロから手作りする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

RubyでHTTPサーバーをゼロから手作りする(翻訳)

何かを始めるときはとりあえず動かしてみることが大事ですが、プログラミングをレベルアップするには、それまで慣れ親しんできた抽象概念より数段下の裏舞台を知っておくことも肝心です。

Web開発ではHTTPのしくみを理解しておくことが重要です。そのためにはHTTPサーバーを自作するのが一番です。

そもそもHTTPとは

HTTPはTCP上で実装されたプレーンテキストのプロトコルなので、リクエストの内容を調べるのも簡単です(HTTP/2は実際にはプレーンテキストではなく、効率化のためバイナリになっています)。リクエストの構造を見る方法のひとつは、以下のようにcurlコマンド に -v(verbose)フラグを付けて実行することです。

curl http://example.com/something -H "x-some-header: value" -v

以下のようなリクエストが出力されます。

GET /something HTTP/1.1
Host: example.com
User-Agent: curl/7.64.1
Accept: */*
x-some-header: value

このときのレスポンスは以下のようになります。

HTTP/1.1 404 Not Found
Age: 442736
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sat, 03 Jul 2021 15:02:03 GMT
Expires: Sat, 10 Jul 2021 15:02:03 GMT
...
Content-Length: 1256

<!doctype html>
<html>
<head>
...

実装の計画

実装で必要な手順を定義してみましょう。

  • 受信するTCPコネクションをローカルソケットでリッスンする
  • 受信したリクエストのデータ(テキスト)を読む
  • リクエストのテキストを解析して「HTTPメソッド」「パス」「クエリ」「ヘッダ」「本文」を抽出する
  • リクエストをアプリケーションに送信し、レスポンスを取得する
  • コネクション経由でリモートソケットにレスポンスを送信する
  • コネクションを閉じる

以上を踏まえて、プログラムの大まかな構成を考えてみましょう。

require 'socket'

class SingleThreadedServer
  PORT = ENV.fetch('PORT', 3000)
  HOST = ENV.fetch('HOST', '127.0.0.1').freeze
  # バッファに保存する受信コネクション数
  SOCKET_READ_BACKLOG = ENV.fetch('TCP_BACKLOG', 12).to_i

  attr_accessor :app

  # app: Rackアプリ
  def initialize(app)
    self.app = app
  end

  def start
    socket = listen_on_socket
    loop do # 新しいコネクションを継続的にリッスンする
      conn, _addr_info = socket.accept
      request = RequestParser.call(conn)
      status, headers, body = app.call(request)
      HttpResponder.call(conn, status, headers, body)
    rescue => e
      puts e.message
    ensure # コネクションを常にクローズする
      conn&.close
    end
  end
end

SingleThreadedServer.new(SomeRackApp.new).start

ソケットをリッスンする

listen_on_socketの「完全な」実装は以下のような感じになります。

def listen_on_socket
  Socket.new(:INET, :STREAM)
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
  socket.bind(Addrinfo.tcp(HOST, PORT))
  socket.listen(SOCKET_READ_BACKLOG)
end

ただし、これらについては定番の書き方が豊富に存在するので、以下のように書き換えられます。

def listen_on_socket
  socket = TCPServer.new(HOST, PORT)
  socket.listen(SOCKET_READ_BACKLOG)
end

リクエストを解析する

作業を始める前に、最終的な結果がどうあるべきかを定義しましょう。サーバーはRack互換にしたいと思います。以下は私が見つけた、Rackが環境にリクエストの一部として期待するパラメータの例です。

{“GATEWAY_INTERFACE”=>”CGI/1.1”, “PATH_INFO”=>”/”, “QUERY_STRING”=>””, “REMOTE_ADDR”=>”127.0.0.1”, “REMOTE_HOST”=>”localhost”, “REQUEST_METHOD”=>”GET”, “REQUEST_URI”=>”http://localhost:9292/”, “SCRIPT_NAME”=>””, “SERVER_NAME”=>”localhost”, “SERVER_PORT”=>”9292”, “SERVER_PROTOCOL”=>”HTTP/1.1”, “SERVER_SOFTWARE”=>”WEBrick/1.3.1 (Ruby/2.2.1/2015-02-26)”, “HTTP_HOST”=>”localhost:9292”, “HTTP_ACCEPT_LANGUAGE”=>”en-US,en;q=0.8,de;q=0.6”, “HTTP_CACHE_CONTROL”=>”max-age=0”, “HTTP_ACCEPT_ENCODING”=>”gzip”, “HTTP_ACCEPT”=>”text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8″, “HTTP_USER_AGENT”=>”Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36”, “rack.version”=>[1, 3], “rack.url_scheme”=>”http”, “HTTP_VERSION”=>”HTTP/1.1”, “REQUEST_PATH”=>”/”}

これらのパラメータをすべて返すつもりはありませんが、少なくとも重要なパラメータは返しましょう。

最初に必要なのはリクエスト行の解析(parse)です。この構造にはおそらく見覚えがあるでしょう。

MAX_URI_LENGTH = 2083 # HTTP標準に準拠

def read_request_line(conn)
  # 例: "POST /some-path?query HTTP/1.1"

  # 改行に達するまで読み取る、最大長はMAX_URI_LENGTHを指定
  request_line = conn.gets("\n", MAX_URI_LENGTH)

  method, full_path, _http_version = request_line.strip.split(' ', 3)

  path, query = full_path.split('?', 2)

  [method, full_path, path, query]
end

リクエスト行の次にはヘッダーが来ます。

ヘッダーがどのようなものかを思い出しましょう。以下のようにヘッダーごとに改行をはさみます。

Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Content-Length: 1256
MAX_HEADER_LENGTH = (112 * 1024) # WebrickやPumaなどのサーバーではこう定義する

def read_headers(conn)
    headers = {}
    loop do
        line = conn.gets("\n", MAX_HEADER_LENGTH)&.strip

        break if line.nil? || line.strip.empty?

        # ヘッダー名と値はコロンとスペースで区切られる
        key, value = line.split(/:\s/, 2)

        headers[key] = value
    end

    headers
end

これで以下を得られます。

{
    "Cache-Control" => "max-age=604800"
    "Content-Type" => "text/html; charset=UTF-8"
    "Content-Length" => "1256"
}

次は本文(body)の読み取りです。本文はすべてのリクエストにあるとは限らず、POSTとPUTにのみあることが期待されています。

def read_body(conn:, method:, headers:)
    return nil unless ['POST', 'PUT'].include?(method)

    remaining_size = headers['content-length'].to_i

    conn.read(remaining_size)
end

これでブロックがひととおり揃ったので、シンプルな実装が完成しました。

class RequestParser
  class << self
    def call(conn)
      method, full_path, path, query = read_request_line(conn)

      headers = read_headers(conn)

      body = read_body(conn: conn, method: method, headers: headers)

      # リモート接続に関する情報を読み取る
      peeraddr = conn.peeraddr
      remote_host = peeraddr[2]
      remote_address = peeraddr[3]

      # 利用するポート
      port = conn.addr[1]
      {
        'REQUEST_METHOD' => method,
        'PATH_INFO' => path,
        'QUERY_STRING' => query,
        # rack.inputはIOストリームである必要がある
        "rack.input" => body ? StringIO.new(body) : nil,
        "REMOTE_ADDR" => remote_address,
        "REMOTE_HOST" => remote_host,
        "REQUEST_URI" => make_request_uri(
          full_path: full_path,
          port: port,
          remote_host: remote_host
        )
      }.merge(rack_headers(headers))
    end

    # ... (上で実装したメソッド)

    def rack_headers(headers)
      # Rackは、全ヘッダーがHTTP_がプレフィックスされ
      # かつ大文字であることを期待する
      headers.transform_keys do |key|
        "HTTP_#{key.upcase}"
      end
    end

    def make_request_uri(full_path:, port:, remote_host:)
      request_uri = URI::parse(full_path)
      request_uri.scheme = 'http'
      request_uri.host = remote_host
      request_uri.port = port
      request_uri.to_s
    end
  end
end

レスポンスを送信する

Rackアプリの実装はひとまず後回しにして、先にレスポンスの送信を実装しましょう。

class HttpResponder
  STATUS_MESSAGES = {
    # ...
    200 => 'OK',
    # ...
    404 => 'Not Found',
    # ...
  }.freeze

  # status: int
  # headers: ハッシュ
  # body: 文字列の配列
  def self.call(conn, status, headers, body)
    # ステータス行
    status_text = STATUS_MESSAGES[status]
    conn.send("HTTP/1.1 #{status} #{status_text}\r\n", 0)

    # ヘッダー
    # 送信前に本文の長さを知る必要がある
    # それによってリモートクライアントが読み取りをいつ終えるかがわかる
    content_length = body.sum(&:length)
    conn.send("Content-Length: #{content_length}\r\n", 0)
    headers.each_pair do |name, value|
      conn.send("#{name}: #{value}\r\n", 0)
    end

    # コネクションを開きっぱなしにしたくないことを伝える
    conn.send("Connection: close\r\n", 0)

    # ヘッダーと本文の間を空行で区切る
    conn.send("\r\n", 0)

    # 本文
    body.each do |chunk|
      conn.send(chunk, 0)
    end
  end
end

これで以下のような例を送信できます。

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 53

<html>
<head></head>
<body>hello world</body>
</html>

Rackアプリ

Rackアプリはstatusheadersbodyを返す必要があります。ステータスは整数、本文は文字列(チャンク)の配列です。

以上を元に、リクエストパスに基づいてファイルシステムからファイルを読み込むアプリを作ってみましょう。

class FileServingApp
  # リクエストで受け取ったパスを元にファイルシステムからファイルを読み取る
  # 例: "/test.txt"
  def call(env)
    # セキュリティ的には非常によくないが、デモ用には十分
    path = Dir.getwd + env['PATH_INFO']
    if File.exist?(path)
      body = File.read(path)
      [200, { "Content-Type" => "text/html" }, [body]]
    else
      [404, { "Content-Type" => "text/html" }, ['']]
    end
  end
end

まとめ

かなりシンプルだと思いませんか?
それもそのはず、細かなエッジケースを丸ごとスキップしているからです。

もっと詳しく知りたい方は、ピュアRubyで実装されているWEBRickのコードをご覧になることをおすすめします。Rackについてはこちらの記事で詳しく説明されています。

今回書いたコードの完全版については、以下のGitHubリポジトリをどうぞ。

今後は、シングルスレッドサーバー、マルチスレッドサーバー、さらにはRuby 3のFibersとRactorなど、さまざまなリクエスト処理方法を試す予定です。パート2は以下をご覧ください。

関連記事

RailsのリクエストのライフサイクルとRackを理解する(翻訳)

The post RubyでHTTPサーバーをゼロから手作りする(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: insert_allやupsert_allのタイムスタンプ自動更新、app/contextsにロジックを置くほか(20211025前編)

$
0
0

こんにちは、hachi8833です。供給そんなにヤバいのかしら。


つっつきボイス:「電子部品の他に鉄も値上がりしてると聞いてますね」「あ〜」「給湯器の値上がりが著しいとか」「新型MacBook、部品のあるうちに買っとくのがいいのかな…」「Appleはそれなりに部品の流通を確保していると思いますけど、どれかが滞ったら詰まったりして」「欲しいときに買うのが一番」

週刊Railsウォッチについて

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

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

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

今回は以下の公式情報から見繕いました。

🔗 insert_allupsert_allでタイムスタンプを自動更新するオプションが追加

このプルリクは、insert_allまたはupsert_all(および関連するメソッド)でレコードが作成された場合にタイムスタンプのカラム(created_atcreated_onupdated_atupdated_on)を自動設定するオプションを提供する。現時点では、これらのカラムを確実に設定するクリーンな方法は、カラム自体にデフォルトを設定するか、さもなければ属性として明示的に渡したうえでさらに既存レコードのcreated_atを上書きしないようon_duplicateのSQLをカスタマイズするしかなかった。
同PRより


つっつきボイス:「insert_allupsert_allのタイムスタンプって自動更新なのでは?と思いましたけど、このプルリクが出たということは今までは自動更新じゃなかったんですね」「record_timestamps:オプションをtrueにすればinsert_allupsert_allでタイムスタンプが自動更新されるようになったようです↓」

# activerecord/lib/active_record/insert_all.rb#L10
-   def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
+   def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil, record_timestamps: nil)
      raise ArgumentError, "Empty list of attributes passed" if inserts.blank?

      @model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s)
      @on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
+     @record_timestamps = record_timestamps.nil? ? model.record_timestamps : record_timestamps

record_timestampsはデフォルトがnilか」「falseを明示的に設定すると今まで通りになるんですね」

# activerecord/test/cases/insert_all_test.rb#L410
+ def test_upsert_all_does_not_implicitly_set_timestamps_on_create_when_model_record_timestamps_is_true_but_overridden
+   with_record_timestamps(Ship, true) do
+     Ship.upsert_all [{ id: 101, name: "RSS Boaty McBoatface" }], record_timestamps: false
+
+     ship = Ship.find(101)
+     assert_nil ship.created_at
+     assert_nil ship.created_on
+     assert_nil ship.updated_at
+     assert_nil ship.updated_on
+   end
+ end

🔗 コントローラの_htmlサフィックスの挙動を修正


つっつきボイス:「これはi18n関連ですね」「以下のようにキーに_htmlというサフィックスを追加するとhtml_safe?がtrueになってそのままビューに出力される機能は前からありますね」「ウォッチでも何度か話題になりました(ウォッチ20180723)」「う、知らなかった」「以下のhelloはエスケープされるけど、hello_htmlhtml_safe?がtrue、つまりサニタイズ済みとして扱われる↓というものです」「なるほど〜」

# actionpack/test/abstract/translation_test.rb#L20
              translation: {
                index: {
                  foo: "bar",
+                 hello: "<a>Hello World</a>",
+                 hello_html: "<a>Hello World</a>",
+                 interpolated_html: "<a>Hello %{word}</a>",
+                 nested: { html: "<a>nested</a>" }
                },
                no_action: "no_action_tr",
              },

参考: 4.4 安全なHTML変換 — Rails 国際化 (i18n) API - Railsガイド

「そして今回のプルリクを見ると、今まではコントローラとビューで_htmlサフィックスの挙動が違っていたらしい」「え、コントローラでも使えるんですか?」

これは#27872をやり直したもの。

html_safeへの変換の一般的な動作を、Active Supportのprivateなモジュールに抽出するコミットを追加して、Action ViewとAction Packで異なっている挙動を合わせ忘れることのないようにした。

これにより、#39989で実現されたメモリ節約の一部が元に戻される(Action Viewの実装ではチェックが必要な可能性のあるキーごとにhtml_safeオプションをビルドするので)。Action Viewのループ内だけでオブジェクトのアロケーションをメモ化する方法が見つからなかったので、これについては妥協することにした。メモリを節約する方法についてアイデアがあれば求む。
同PRより

AbstractControllerが改修されているので↓、コントローラの実装はビューと別だったみたい: ActiveSupport::HtmlSafeTranslation.translateに切り出して共通化したことで修正したようですね」「なるほど」「この機能をコントローラで使ったことはなかったな〜」

# actionpack/lib/abstract_controller/translation.rb
# frozen_string_literal: true

+require "active_support/html_safe_translation"
+
module AbstractController
  module Translation
    mattr_accessor :raise_on_missing_translations, default: false

...

      i18n_raise = options.fetch(:raise, self.raise_on_missing_translations)
-     I18n.translate(key, **options, raise: i18n_raise)
+
+     ActiveSupport::HtmlSafeTranslation.translate(key, **options, raise: i18n_raise)
    end
    alias :t :translate

    # Delegates to <tt>I18n.localize</tt>. Also aliased as <tt>l</tt>.
    def localize(object, **options)
      I18n.localize(object, **options)
    end
    alias :l :localize
  end
end

🔗 ArelにFILTER句のサポートを追加


つっつきボイス:「PostgreSQLとSQLite3の場合にfilterメソッドが使えるようになった」「MySQLはサポートされてないのか残念」

機能リクエストの多かったrails/arel#518を再度オープンした(rails/arel#460にもある)。
以下のRubyコードを書くことで、

Model.all.pluck(
  Arel.star.count.as('records_total').to_sql,
  Arel.star.count.filter(Model.arel_table[:some_column].not_eq(nil)).as('records_filtered').to_sql,
)

以下のSQLが出力されるようになる。

SELECT
  COUNT(*) AS records_total
  COUNT(*) FILTER (WHERE some_column IS NOT NULL) AS records_filtered
FROM models

サポート対象はPostgreSQL 9.4以降(2014年12月、リリースノート)とSQLite 3.30以降(2019年10月、リリースノート
参考:

「FILTER構文って何でしょう?」「上のModern SQLサイトによると、以下のように集計関数に条件を指定できるとありますね: 集計関数をSELECT文で条件付けするよりも簡潔に書けそう」「お〜!」

# modern-sql.comより
SUM(<expression>) FILTER(WHERE <condition>)

「以下みたいにCASE WHENでもやれますけど↓、条件が増えてくるとどんどん行数が増えてしまう」「それは読みづらそう…」「filterメソッドはそういうときに便利でしょうね👍

# modern-sql.comより
SUM(CASE WHEN <condition> THEN <expression> END)

参考: Window関数のFILTER句を極める

🔗 ビューのplain textモードの箇条書きを改善


つっつきボイス:「to_plain_textの改善だそうです」「箇条書きがネストしたときの書式を改善したのね」「そもそもto_plain_textというメソッドがあったことを知らなかった」「plain textモードを使う人って少なそうですけど、いるんでしょうね」

# 同PRより
• Item 0
• Item 1
  • Item A
    1. Item i
    2. Item ii
  • Item B
    • Item i
• Item 2

「今実装を見てますけど、" " * (depth - 1)とか"\n#{text}"のあたりが何というか生々しいですね」「自分でゴリゴリ実装したときのような感じが出てる」

# actiontext/lib/action_text/plain_text_conversion.rb#L93
+     def indentation_for_li_node(node)
+       depth = list_node_depth_for_node(node)
+       if depth > 1
+         "  " * (depth - 1)
+       end
+     end
+
+     def list_node_depth_for_node(node)
+       node.ancestors.map(&:name).grep(/^[uo]l$/).count
+     end
+
+     def break_if_nested_list(node, text)
+       if list_node_depth_for_node(node) > 0
+         "\n#{text}"
+       else
+         text
+       end
+     end

🔗 CSRF防止戦略のカスタマイズをサポート

概要
このプルリクは、protect_from_forgeryでのカスタムCSRF防止戦略を渡すサポートをAPIドキュメントで公式に追加する。
その他
現在のRailsは、CSRF保護戦略のカスタマイズを偶然サポートしている。protection_method_classにはクラスまたはシンボルオブジェクトを渡せるし、.to_s.classify呼び出しも両方で同じように振る舞う。
そこで@rafaelfrancaに相談した結果、このメソッドの振る舞いを変更してcase/when文で既存のCSRF防止戦略を明示的に返すようにし、かつ早期リターンによってクラスを戦略として渡せるようにした。
同PRより


つっつきボイス:「protection_method_classでCSRF防止の挙動を変えられるようにしたんですね: csrf-tokenの生成ポリシーを修正したいことはあるかもしれないので」「どんなときに変更したいんでしょうか?」「CSRFトークンの生成をRailsサーバー側以外で行いたい時とかかなあ」「なるほど」「たぶん自分でカスタマイズすると相当複雑になると思いますが」

参考: 3 クロスサイトリクエストフォージェリ (CSRF) — Rails セキュリティガイド - Railsガイド

RailsのCSRF保護を詳しく調べてみた(翻訳)

🔗 has_secure_password利用時にpassword = nilしても値が残る問題を修正

# 更新情報より
user.password = 'something'
user.password = nil
# before:
user.password # => 'something'
# now:
user.password # => nil

つっつきボイス:「Active Modelのpassword=セッターでnilを代入してもpasswordリーダーで読み出すと消えていなかったのが修正されたのね」「これは修正しないといけないヤツ」「修正はinstance_variable_setを1行追加しただけなんですね↓」

# activemodel/lib/active_model/secure_password.rb#L95

        define_method("#{attribute}=") do |unencrypted_password|
          if unencrypted_password.nil?
+           instance_variable_set("@#{attribute}", nil)
            self.public_send("#{attribute}_digest=", nil)
          elsif !unencrypted_password.empty?
            instance_variable_set("@#{attribute}", unencrypted_password)
            cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
            self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
          end
        end

🔗Rails

🔗 Zeitwerkにアップグレードした話(Ruby Weeklyより)


つっつきボイス:「RailsアプリのオートローダーをZeitwerkにアップグレードしたときのノウハウ記事です」「ファイルやクラスのリネームも発生したのか」「今どきはたいていZeitwerkになっていると思いますけど、アプリが大きいと後からZeitwerkに乗り換えるのは大変でしょうね」

Rails: Zeitwerkオートロードの「1ファイルにクラスを複数置けない」問題を回避する

🔗 RailsにSorbetを導入

以下はつっつき後に見つけたツイートです。


つっつきボイス:「freee会計などを手掛けているfreeeがRubyの静的型チェッカーSorbetを導入した記事です」「記事でやっているようにビジネスロジックを中心に少しづつ型を追加していくのがよさそうですね」「SorbeとYARDを両方書くのは大変、たしかに」

「Sorbetを使っている会社のリストを見ると、開発元のStripe以外にShopifyなども使ってますね」「以前から型注釈を欲しいと思っていた会社が使っているんでしょうね」「SorbetはLanguage Server Protocolに対応しているのがありがたい」

参考: Official page for Language Server Protocol

先週土曜日のKaigi on Rails 2021のクロージングキーノートでも、RafaelさんがShopifyでSorbetを導入したことを話していましたね。

参考: Keynote by Rafael França - Kaigi on Rails 2021

🔗 Arel入門


つっつきボイス:「ArelはActive Record内部のクエリ生成APIですね」「Arel職人を目指す記事なのかな」「arel_tableとかを見ると昔のトラウマが😅

# 同記事より
Organization.where(
    Organization.arel_table[:id].in(
    Comment.where(
      Comment.arel_table[:user_id].eq(user.id)
    ).distinct.pluck(:organization_id)
  )
)

参考: ActiveRecordを支える技術 - Arelとは何者なのか? (全5回) その1 - TIM Labs

「Arelを使うとこんなふうに書ける↓」「gt(2)はgreater than 2なんですね」「自分は普通にwhereとプレースホルダ?で書きますけど、事情によってはArelで書くこともたまにあります」

# 同記事より
users[:id].in([1,2,3]).to_sql
=> "`users`.`id` IN (1, 2, 3)"

users[:id].gt(2).to_sql
=> "`users`.`id` > 2"

users[:id].eq(3).to_sql
=> "`users`.`id` = 3"

「Arelでjoinが絡んだりコンポジションしたりするうちにだんだん複雑になりがち」「そうそう」「Arelはありがたい存在だけど、毎回Arelで書く気にはなれないな〜」

# 同記事より
users.join(photos, Arel::Nodes::OuterJoin).on(users[:id].eq(photos[:user_id]))

「BPSだとkazzさんがよくArelを使ってたかも」「Railsで汎用的なモジュールを書こうとするとArelが必要になってくることがあるんですよ」「なるほど」

追記: 今週金曜日の銀座Rails#38で、@osyoさんが『AST を使って ActiveRecord の where の条件式をブロックで記述しよう』というタイトルでお話しされるそうです。

🔗 Railsのビジネスロジックを「contexts」で整理(Ruby Weeklyより)


つっつきボイス:「また新しめのパターン」「contextという言葉のメタ度が高くてどうとでも解釈できそうな感じ」

「contextは、Elixir言語で動くPhoenixフレームワーク↓が由来と記事に書かれていました」「Elixir、知らない世界」「Elixirは見た目がRubyに似てて、型が書けるそうです」

参考: Elixir (プログラミング言語) - Wikipedia

app/contexts/の下にファイルを作って、そこにビジネスロジックを置くスタイルなんですね」「見た感じでは、GoF本で言うところのFacade(ファサード)を普通にcontextsに置いている感じかな🤔

# app/contexts/accounts.rb
module Accounts
  def self.active_users
    User.all.active
  end

  def self.account_details(id)
    account = Account.find(id)
    # ...
  end
end

# app/contexts/accounting.rb
module Accounting
  def self.create_invoice
    # business logic magic
  end
end

参考: ギャング・オブ・フォー (情報工学) - Wikipedia — GoF

「ElixirのフォーラムにcontextsとFacadeのことが書かれている↓: まさにFacadeパターンですね」

参考: Contexts in Phoenix 1.3 and Facade Pattern - Phoenix Forum / Chat / Discussions - Elixir Programming Language Forum

「記事では、Service Objectを使いたくないのでcontextにしたそうです」「Service Objectはクラスがやたらと増える傾向があるので、Facadeパターンを使うのはわかる: 自分もその方が好みです」「たしかに」

参考: Facade パターン - Wikipedia

「そういえばService Objectに置くのはたいていFacadeか、もうひとつ何かのパターンのどっちかだと以前おっしゃってましたね」「もうひとつはCommandパターンですね: RailsでService ObjectというとこのCommandパターンを使ったものを指すことが多いようです」「なるほど」「Commandパターンだと基本的に1クラス=1機能になるけど、Facadeパターンはそこにこだわらない感じ」

参考: Command パターン - Wikipedia

Railsのパターンとアンチパターン4: コントローラ編(翻訳)

🔗 GitLabコメント欄にmermaid構文でグラフを書く


つっつきボイス:「GitLabのコメント欄でmermaidというグラフ生成構文を使ってグラフを生成できるそうです」「元記事を見るとGitLab 10.3と随分昔からあるようなので、最近これを発見したのかも」「PlantUMLも使えるのね」

参考: mermaid - Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.
参考: PlantUML: シンプルなテキストファイルで UML が書ける、オープンソースのツール

# docs.gitlab.comより: mermaidの例
graph TB

  SubGraph1 --> SubGraph1Flow
  subgraph "SubGraph 1 Flow"
  SubGraph1Flow(SubNode 1)
  SubGraph1Flow -- Choice1 --> DoChoice1
  SubGraph1Flow -- Choice2 --> DoChoice2
  end

  subgraph "Main Graph"
  Node1[Node 1] --> Node2[Node 2]
  Node2 --> SubGraph1[Jump to SubGraph1]
  SubGraph1 --> FinalThing[Final Thing]
end


docs.gitlab.comより

「コメント欄でちょっぴりグラフを書くのにいいのかも」「自分はDraw.ioを開いてスクショを貼る方が早いかな」「それもそうですね」

後で調べると、draw.ioドメインはセキュリティ上の理由でdiagrams.netドメインに移行していました。

参考: diagrams.net
参考: Blog - Open source diagramming is moving to diagrams.net, slowly


前編は以上です。

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

週刊Railsウォッチ: ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか(20211019後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: insert_allやupsert_allのタイムスタンプ自動更新、app/contextsにロジックを置くほか(20211025前編) first appeared on TechRacho.

週刊Railsウォッチ: YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか(20211026後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 YJITリーダーによるYJIT紹介記事


つっつきボイス:「以下↓はYJITリーダーであるMaxime ChevalierさんによるYJIT紹介記事ですが、翻訳リクエストを受けてひとまず一次翻訳終えました: リクエストありがとうございます🙏」「お〜翻訳楽しみ」「Ruby 3.1でYJITがマージする流れになっていますね: YJITはデフォルトでオフのはずなので、使わないときは意識せずに済むはず」「お〜YJIT楽しみ」

「続編記事としてNoah GibbsさんによるYJITお試し方法の紹介記事も出ていました↓」

以下はYJITのベンチマークサイトです。

🔗 YJITがマージされた

「ちなみについさっき(注: つっつき時点の10/21夜)YJITがマージされたというツイート↓を見かけたんですが、YJITがでかくてコミット数が多かったせいかdev環境のSlackボットや通知周りがエラーになって、いったんcloseされていました」「ありゃ残念」「ドンマイ」「分解するのは大変そう」

つっつきの後、無事YJITがmasterにマージされました🎉

参考: ruby-trunk-changes 2021-10-21 - ruby trunk changes

その後、OpenBSDでYJITを無効にするコミットや、JITでMJITを有効にするオプションを追加するコミットも追加されていました(YJITとMJITは同時には利用できないそうです)。

🔗 safe_regexp: 正規表現をタイムアウト

grosser/safe_regexp - GitHub

# 同リポジトリより
# normal
/a/.match?('a') # -> true in 0.0001ms
SafeRegexp.execute(/a/, :match?, 'a') # -> true in 0.13568ms

# bomb
require "safe_regexp"
regex = /aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?/
value = "a" * 46
regex.match? value # false in ~60s
SafeRegexp.execute(regex, :match?, value) # -> SafeRegexp::RegexpTimeout

# methods without arguments
regex = "^(The '(?<first_group>.*)' parameter of the (?<second_group>.*))$"
SafeRegexp.execute(regex, :names) # -> ["first_group", "second_group"]

つっつきボイス:「以下のregular-expressions.infoというサイトで知りました↓: サイトの人がこのgemを試したかどうかまではわかりませんでしたが」「今日のBPS Webチーム内発表で触れていたgemですね」「SafeRegexpを使うことで、正規表現がデフォルトで1秒以上かかるとタイムアウトしてエラーをraiseするそうです」

参考: Preventing Regular Expression Denial of Service (ReDoS)

「今Ruby本家でも正規表現にタイムアウトを入れようかという話が出ていましたね」「はい、RubyKaigi Takeout 2021のMartin先生の発表↓で触れていた#17837などですね」「これは?」「最近流行りのcatastrophic backtracking攻撃に対してRubyが対抗策を議論しているissueのひとつです」

参考: Regular Expressions: Amazing and Dangerous by Martin J. Dürst - RubyKaigi Takeout 2021

「READMEではThreadTimeoutを使わずに作ったとありました」「同じくREADMEによると、正規表現用に別プロセスを立ち上げて、タイムアウトしたらkill -9で止めるということをやってるらしい: 正規表現エンジンの中にタイムアウトを仕込むのではなく、エンジンの外側でやるという戦略かな」「シェルで強制終了するような感じなんですね」

「そういえば手元でsafe_regexpをちょっと試してみたところ、ヤバいパターンを食べさせたらコンソールが固まって、Ctrl-Cでも戻らなくなっちゃいました😢」「kill -9で止めるしかないでしょうね: 別プロセスにしないとRubyのメインプロセスがCPUタイムを食い尽くしてしまうので、OSのことも考えて正規表現用のプロセスを分けたんでしょうね」「なるほど」

「重たい別プロセスをRubyで立ち上げるのは泥臭いですが、ひとつの方法でしょうね」「テスト環境で使うとか、特定の正規表現を手元で検証するときにはよさそう👍

はじめての正規表現とベストプラクティス10: 危険な「Catastrophic Backtracking」前編

🔗 ripperのドキュメント化が進行中


つっつきボイス:「RubyのパーサーライブラリであるRipperは以前からドキュメントがないと言われていましたが、この方が頑張ってコードを読んでドキュメントを書き進めているそうです」「お〜すごい!」「READMEの内部リンクがまだ切れてるのでファイルを直接開いてください」

参考: class Ripper (Ruby 3.0.0 リファレンスマニュアル)

「RuboCopの作者の@bbatsovさんも、ドキュメントがないなどもろもろの理由でRipperを使うのをやめたそうです↓」「ドキュメントがなくても使う人はいるでしょうけど、やっぱりドキュメント大事」「動きがだいたいわかってても、ドキュメントがないと不安になりますよね」「RuboCopも使えるものならRipper使いたかったでしょうね」「ドキュメント化大変だろうけど頑張って欲しいです🙏

RuboCop作者がRubyコードフォーマッタを比較してみた: 後編(翻訳)

🔗 Rubyのワンライナーcookbook


つっつきボイス:「Rubyのワンライナー向けオプションはたくさんある分、Perlの-pieオプションに比べると長めになってしまう傾向がありますけど、Rubyの-neオプションあたりなら使いますね」

# 同記事より
$ # sample stdin data
$ printf 'gate\napple\nwhat\nkite\n'
gate
apple
what
kite

$ # print all lines containing 'at'
$ # same as: grep 'at' and sed -n '/at/p' and awk '/at/'
$ printf 'gate\napple\nwhat\nkite\n' | ruby -ne 'print if /at/'
gate
what

$ # print all lines NOT containing 'e'
$ # same as: grep -v 'e' and sed -n '/e/!p' and awk '!/e/'
$ printf 'gate\napple\nwhat\nkite\n' | ruby -ne 'print if !/e/'
what

参考: Perlのワンライナーでテキストの一括置換 - console.lealog();

「Rubyは標準機能が強力だし普段からRuby書いてるので、ワンライナーにもいいですよね」「いいっす」「ただ自分がRubyであまりワンライナー書かないのは、カスタマイズできない作業環境でRubyが使えるとは限らないからというのもあるんですよ」「その意味ではPerl強いですよね」「Perlはたいていの環境で最初から使えますね」

「Pythonも入っているとは限らない」「あってもPython 2系か3系かという罠があったりしますし」「さすがに新しい環境でPython 2は減ったと思いますけど、古い環境だと油断できない」

参考: Python 2.7.x と 3.x の決定的な違いを例とともに | POSTD

「macOSだとRubyが入っているけど、バージョンが古いんですよね」「1.8とかだったらどうしよう」「1.8はもう別物😆

後でBig Sur備え付けのRubyバージョンを調べてみました。Ruby 2.6は2022年3月にEOL(end-of-line)を迎えますが、果たしてmacOSはちゃんとバージョン上げてくれるでしょうか?
参考: Ruby Maintenance Branches

$ /usr/bin/ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin20]

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 セキュリティヘッダークイックリファレンス(StatusCode Weeklyより)


つっつきボイス:「HTTPのヘッダーのうち、セキュリティ関連のヘッダーのクイックリファレンスだそうです」「▼をクリックすると詳細が表示されるのね」

X-Content-Type-Optionsはたしかに使う」「X-Frame-Optionsもそういえばあった」

# 同サイトより
X-Content-Type-Options: nosniff
# 同サイトより
X-Frame-Options: DENY

「クロスオリジン関連のヘッダーにCross-Origin-Opener-Policy(COOP)やCross-Origin-Embedder-Policy(COEP)というのもあるんだ、へ〜細かい」「たしかに細かい」

# 同サイトより
Cross-Origin-Opener-Policy: same-origin-allow-popups
# 同サイトより
Cross-Origin-Embedder-Policy: require-corp

CORSはよく聞くヤツですね」「知ってるものが出てきた」「ちなみにCORSのヘッダーは、ない状態が最もセキュア」

# 同サイトより
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

参考: Rails アプリケーションのセキュリティ対策(CORS/CSP/HSTS)

「こうしたセキュリティ関連ヘッダーは適切に設定すべきですね: 特にSPAは1つのページ内であらゆるデータを読み書きするようになるので、一箇所でも不備があると他の部分にも影響しがち」「たしかに」「セキュリティヘッダーのリファレンスとして便利そうなサイト👍」「ヘッダーが多すぎないのが嬉しいです😂

参考: シングルページアプリケーション - Wikipedia — SPA

🔗 Priority Hintsによるリソース読み込み最適化


つっつきボイス:「Priority HintsはWebページ内の要素の読み込みで優先度を指定できる新しめの仕様ですね」「以下の記事は2018年の時点でPriority Hintsをチェックしていました↓」

「お〜、Blink(Chromeなどで使われているレンダリングエンジン)ではこれらに優先順位を設定できるのね↓」「『Priority Hintsはヒントであってディレクティブ(指示)ではない』、なるほど」

参考: Blink (レンダリングエンジン) - Wikipedia


同記事より

「具体的には以下のようにタグでimportance="low"のように指定する↓と、ブラウザ側で優先順位を割り当てる」「"low""high""auto"の3つか」「画像のimgにも指定できるんですね」「その場合CSSで画像サイズをきちんと指定しておく方がいいかも」

<!-- 同記事より -->
<!-- include trial token in the <head> -->
<meta http-equiv="origin-trial" content="{Replace with origin trial token}">

<!-- Lower the priority of script file -->
<script src="script.js" importance="low"></script>

<!-- Alter the priority of images -->
<img src="Background.jpg" width="400" importance="low">
<img src="Sunset.jpg" width="400" importance="high">

<!-- Note that importance="auto" is the default based on the spec if not specified -->
<img src="Flower.jpg">

「優先順位の仕様↓は定められているけど、細部の解釈はブラウザ依存になる可能性があるかもしれませんね」「あ〜たしかに」


同記事より(一部)

importanceを全部Highにしたりするのはダメなのかな?」「無意味だと思いますよ😆 : 上の仕様の上下矢印などを見た感じでは、仮に全部Highにしたとしても優先順位は同じにならないでしょうね」「あ、たとえばHighに下向き矢印がある項目は、それより低くなる可能性があるということなんですね」

「Priority Hints指定なし(左)とあり(右)では以下のように変わるんですね↓」「わかりやすい〜」

「優先順位をここまで追い込んで使うことはすぐにはないかもしれないけど、importance="high"asyncを指定する↓のは効果高そうな感じなので、これなら書いてもいいかなと思いました」

<!-- 同記事より -->
<script src="async_but_important.js" async importance="high"></script>

🔗言語/ツール/OS/CPU

🔗 Crontab.guru


つっつきボイス:「なるほど、crontabのスケジューリングの設定を支援するサイトですか」「これ面白いですね」「たしかGitHubのドキュメントでこのCrontab.guruが参考としてリンクされてたのを見たことがありますよ」

参考: crontab - Wikipedia

後で見つけました。

crontab guru を使うと、クーロン構文の生成および実行時間の確認に役立ちます。 また、クーロン構文の生成を支援するため、crontab guru のサンプルリストもあります。
ワークフローをトリガーするイベント - GitHub Docsよりより

🔗 crontabの罠

「ところでcrontab形式って、実は標準というものがないんですよ」「え?」「そうなんです😢

「たとえば、Linuxだと曜日は日曜始まりで0-6が割り当てられていて、さらに7も利用可能なので、07が日曜日になる: これなら7で割った余りで曜日を出すみたいな処理がやりやすい」「なるほど!」

参考: crontab 曜日設定などメモ - Qiita

「でもAmazon EventBridge↓(旧Amazon CloudWatch Events)のCrontab形式はかなり特殊で、曜日が日曜始まりの1-7になってる」「え〜それヤバいじゃないですか!」「きっとハマる自信ある」「誰もが一度はハマります」

参考: ルールのスケジュール式 - Amazon CloudWatch Events

「さらに厄介なのは、EventBridgeではcrontabで指定できる時刻がUTCのみという点」「え〜!JSTとかで書けないんですか?」「ローカルタイムが指定できないので、時刻を指定するたびに変換してあげないといけません」「ややこしい…」

参考: 協定世界時(UTC) - Wikipedia

「ちなみにLinuxのcrontabだとTZ=タイムゾーン名でローカルタイムを指定できます↓(指定し忘れると悲惨ですが)」「そういえばそうですね」「Linuxのcrontabに慣れているほどAWSでハマりがち」

参考: cron - How do you set the timezone for crontab? - Ask Ubuntu

「ところが面白いことに、AWSのマネージメントコンソール経由だとEventBridgeでローカルタイムを指定できるんですよ(↓証拠写真)」「へ〜!」「これについてさんざん調べたんですが、APIレベルではできない😢」「残念…」

「そういったわけで、crontab周りはクラウドの設定をレビューするときの要チェックポイントです」


後編は以上です。

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

週刊Railsウォッチ: insert_allやupsert_allのタイムスタンプ自動更新、rails/contextsにロジックを置くほか(20211025前編)

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

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

StatusCode Weekly

statuscode_weekly_banner

The post 週刊Railsウォッチ: YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか(20211026後編) first appeared on TechRacho.

Viewing all 1080 articles
Browse latest View live