RuboCopの解析バージョンサポートを見直した

RuboCop は Ruby の EOL バージョンについて、EOL の 1年後までサポートしています。

RuboCop の Ruby バージョンサポートといった時に「ランタイムバージョン」と「解析バージョン」の 2 つがあり、このエントリを書いている時点の最新 RuboCop 1.30 リリースまで、このふたつのバージョンが混同された状態でライフサイクルを共にしていました。

ここでそれぞれのバージョンをおさらい。

  • ランタイムバージョン ... RuboCop を実行する Ruby のバージョン。rubocop コマンドを実行する ruby 処理系のバージョンです。
  • 解析バージョン ... RuboCop が解析する Ruby コードのバージョン。.rubocop.yml の TargetRubyVersion などで指定しているバージョンです。詳しくはこちらの過去記事をどうぞ。

例えば Ruby 2.5 は RuboCop 1.28 でサポート打ち切り、Ruby 2.4 は RuboCop 1.12 でサポート打ち切りとなっていますが、これは 「ランタイムバージョン」と「解析バージョン」の両方を意味していました。

RuboCop 1.30 では、Ruby 2.5 以下をランタイムバージョンは使うことはできませんが、TargetRubyVersionRuby 2.0 以上を指定することができるようになっています。

例えば、Gem 開発で古い Ruby バージョンから新しい Ruby バージョンまでをサポートしたいというニーズや、古い Ruby バージョンのアプリケーションだけど RuboCop はアップグレードして新しい機能を使いたいといったことに対応できます。

特に前者は「RuboCop がサポートバージョンを打ち切ったため、Gem の Ruby 最小要求バージョンが繰り上げる」という事例をいくつか見てきており、RuboCop が Gem のメンテナンスサイクルに作用するのがエンドユーザーにとって幸せかという疑問を解決する形になります (TargetRubyVersionRuby 2.0 まで解析可能になったので) 。

GitHub Actions などで RuboCop をリンターとしている場合は、Ruby 2.6 以上をワークフローのランタイム Ruby バージョンに指定して、.rubocop.yml の TargetRubyVersion に最小要求の Ruby バージョンを指定すると良いでしょう。

ちなみに、Ruby 1.9Ruby 2.0 以降との非互換による Cop のメンテナンスコストの方が高くつきそうなので、いずれもサポートしません。

github.com

先日公開された『2022 Ruby on Rails Community Survey Results』の Ruby バージョンの利用率調査だと、EOL の利用は Ruby 2.6 (15%) 、Ruby 2.5 (8%) 、Ruby 2.4 (4%) 、Ruby 2.3 (3%) 、Ruby 2.2 (1%) 、Ruby 2.1 (1%) 、Ruby 2.0 (0%) 、Ruby 1.9 (2%) 、Ruby 1.8 (1%) ということで、全体の 35% くらいあるようです。そのうち Ruby 2.0 ~ 2.6 までの 32% くらいは救えるだろうか?といったところ。

rails-hosting.com

とはいえ、アプリケーションであればはやめに Ruby バージョンのアップグレードをしていけると良いですね。

Ruby 3.2.0dev にマージされた Rust YJIT をビルドする

YJIT の Rust 実装がマージされました。いまのところ今年の Shopify からの代表作ではと見ています (YJIT 自体は Ruby 3.1 で C 実装導入されている機能です) 。

github.com

そういうわけで、現在の Ruby 3.2.0dev で YJIT を有効にする場合は、Rust の処理系 (1.60.0 1.58.1 以上) が必要になります。Rust のインストールに使う rustup については以下の公式ページなどを参照してください。

www.rust-lang.org

YJIT はデフォルトではビルドされません (なので YJIT を使わないビルドであれば Rust 処理系は不要です) 。

デフォルトの YJIT なしの Ruby をビルドしている場合は --yjit オプションを渡しても、ruby: warning: Ruby was built without YJIT support と以下のように警告が出力されます。YJIT の API は存在しないので、使おうとするとエラーになります。

% ruby --yjit -e 'p RubyVM::YJIT.enabled?'
ruby: warning: Ruby was built without YJIT support
-e:1:in `<main>': uninitialized constant RubyVM::YJIT (NameError)

p RubyVM::YJIT.enabled?
        ^^^^^^

このあたり Ruby 3.1 と異なるデフォルト (のビルド) 挙動なので、YJIT を有効にしているユーザーは気に留めておくと良さそうです。

% ruby --yjit -ve 'p RubyVM::YJIT.enabled?'
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) +YJIT [x86_64-darwin19]
true

Ruby 3.2.0dev において、YJIT 込みで Ruby 本体をビルドする場合は以下のように ./configre にオプションを渡します。

% ./autogen.sh
% ./configure --enable-yjit=dev ...
% make -j install
building Rust YJIT (dev mode)
% ruby --yjit -ve 'p RubyVM::YJIT.enabled?'
ruby 3.2.0dev (2022-04-27T15:00:54Z master b43eb54a0c) +YJIT [x86_64-darwin19]
true

リリースモード --enable-yjitデバッグ開発モード --enable-yjit=dev でオプションが異なります。詳しくはドキュメントを参照してください。

github.com

rbenv を使っているようであれば、環境変数 RUBY_CONFIGURE_OPTS を使ったオプション渡しが手早いです。

% RUBY_CONFIGURE_OPTS=--enable-yjit rbenv install 3.2.0-dev
% ruby --yjit -ve 'p RubyVM::YJIT.enabled?'
ruby 3.2.0dev (2022-04-27T15:00:54Z master b43eb54a0c) +YJIT [x86_64-darwin19]
true

さらに Ruby のランタイムでも YJIT はデフォルト無効なので、YJIT を有効化する場合は ruby コマンドのオプションに --yjit を渡すか、環境変数 RUBY_YJIT_ENABLE=1 なんかを渡します。直接 ruby コマンドを使わずに rake 経由なんかで実行する場合は環境変数の指定方法を使うことになると思います。

あと当然ですが、Ruby のビルド環境に Rust がインストールされていなければ、YJIT 有効のビルド時に以下のようなエラーになります。

% RUBY_CONFIGURE_OPTS=--enable-yjit rbenv install 3.2.0-dev
(snip)

Last 10 log lines:
checking whether -pie is accepted as LDFLAGS... yes
checking whether wrapper for LD_LIBRARY_PATH is needed... no
checking whether dtrace USDT is available... no
checking for __builtin_setjmp... yes with cast ()
checking for _setjmpex as a macro or function... no
checking for _setjmp as a macro or function... yes
checking for sigsetjmp as a macro or function... no
checking for setjmp type... __builtin_setjmp
checking for rustc... no
configure: error: rustc is required. Installation instructions available at https://www.rust-lang.org/tools/install

Rust 処理系をインストールしましょう。

コード的には ruby/yjit ディレクトリ以下が、Rust の世界になっているようで、興味のある人は見てみると発見があるかもしれません。

github.com

RubyGems にも Rust 拡張に向けた Cargo ビルダーがマージされたこともあり、Ruby エコシステムと Rust との関わりが今後の関心どころです。

github.com

そういった流れもあるので、個人的には The Rust Programming Language (日本語訳) の後に『プログラミング Rust 第2版』を読んでいるところですが、痒い所に手が届く感じでおすすめです (先に The Rust Programming Language で全体の雰囲気を掴んでおくと理解がスムースかも) 。

🦀

【iCARE Dev Meetup #32】OSSとの向き合い方に登壇した

@ogijun からオファーをもらって、【iCARE Dev Meetup #32】OSSとの向き合い方に登壇しました。

icare.connpass.com

@ogijun とはゼロ年代Java の勉強会なんかでお互いによく遭遇していたのがはじまりで、かつて高橋会長でかい文字のプレゼン本を深夜の大阪の JAL ホテルラウンジでゲラハックしたりと古くからの繋がり。そんな @ogijun からの「OSS をテーマにしたイベントをするので、RuboCop でひとつ」となったのがきっかけ。

私の登壇について、昨年の RubyKaigi Takeout 2021 の登壇で話の展開と尺の都合で収まらなくて削った秘蔵パートを、まるっと全面改訂して再構成したもの。なので、スライドが英語は何かを狙っていたというより、ベースの流れでそのまま作っていた部分が大きいです。

タイトルは『パターンハッチング』と『Test Driven Development: By Example』をモチーフにしたもので、ウォードのサンプルではなく、RuboCop を元にしたコントリビューションへの発見といったものをテーマにした。

当日のスライドは以下です (時間の関係上、凝ったことはしていませんが、スライドの表紙も『TDD by Example』を意識して、それっぽくしていた) 。

技術的な何かを持ち帰ってもらう話の全体像としては ghq, gem-src と git の tips を交えつつ、最後はサピア=ウォーフの仮説を含めた OSS への向き合い方みたいな話をした。

ghq もろもろの tips について、弊社から参加していたメンバーの反応はこんな感じでした (弊社に入社するとよろこんで OSS メンタリングします) 。

こんな感じで今日からはじめることができるので、まだの人はおすすめの環境構築です。また、もし既存のローカルリポジトリghqディレクトリスタイルに移管するのが手間ということであれば、ghq_transfer という gem など用意していますのでよければどうぞ。

github.com

特に今回のイベントでは、他の登壇者方の OSS 開発者の開発観を聞くことで、自分の過ごしている OSS の世界を再認識できたことでした。何となく OSS 開発者の考え方は似通ってくるようなところを感じたのは、印象的でおもしろかったです。登壇者の皆さん、iCARE さんありがとうございました!

最後に余談ですが、本発表をとおした「OSS と人生」について、X.Y.Z.→A のサードアルバム『LIFE』収録曲の『生きるとは何だ』を聴きながらストーリーを練っていました。ジャパメタ好きの人におすすめです。What is life all about?

『プログラミング言語Rust入門』を読んだ

AmazonKindle Unlimited で読めたので『プログラミング言語Rust入門』を読んでみた。いまのところ Kindle Unlimited で読める唯一の Rust 本っぽい。

書評にもあるように、誤字脱字やコーディングのコピペミス、用語ミスなどが多々あって、レビューが一度でもあればかなりブラッシュアップできたのでは、、、というものだった。また、クロージャの章に出てきた参照の参照の引数など、ここにこそ説明が欲しかったところなんかで、説明が割愛されていたのが惜しいところだった。

なので、これから読んでみようという人は、以下の The Rust Programming Language (The Book) や Rust By Example なんかを一読して基礎知識を付けた2冊目としてみるのが良さそうです (つまり、自分は The Book なんかを先に読んでいた) 。

一方で、Rust での Web やデータベース、並列処理、FFI の基本を crates.io のクレートを交えて説明されているので、言語仕様の先の活用を示した入門として分かり易かったのと、move と借用をポインタで確認する「付録A」はよかったです。

Rust 2021 に対応した『プログラミングRust 第2版』はいま読んでいるところだけれど The Book と類型の書籍ということもあって、『プログラミング言語Rust入門』ではそれらとは異なるエコシステム観点の後半が特に楽しめました。

株式会社永和システムマネジメントのエンジニアリングマネージャーをはじめていた

ちょうど先日 (2022-03-02) 、勤務先で岡島さんとエンジニアリングマネージャ職について社内ラジオ配信する機会があったので、かつて下書きしていたエントリを公開しておきます。

なので、これは勤務先を移籍したとかそういった話ではなく、昨今の社会背景に対する事業課題を解決していく必要性から、勤務先での立ち位置を変えることにした話です。

受託開発企業でのエンジニアリングマネージャーというのはあまり聞かないものですが、その実、事業課題においてエンジニアのキャリアと事業成長が有機的に繋がっている点など、サービス開発企業が抱える課題とも重複する部分の方が多いと思っています。42期に突入した弊社でも、その役割によって解決できる問題を顕在化させて解決していくぞと、主に事業部長の @m_pixy と考えてこれまで勤務先に明確に存在していなかったポジションとしたのが今回。あと、世の中にないパスなら作れば良いので、そういったキャリアパスを切り拓いていくという気持ちもあったりします。

また、個人的にはもっとも現場の開発者の気持ちが分かるのは現場であろうという考え方から、これまで現場での開発に主軸を置いていたのですが、特定の現場に軸足を置いているとどうしても事業全体の課題に対して手を動かす時間を作るのが難しかったので、事業の全体最適の観点をとったことになります。とはいえ現場のエンジニアリングから手が離れているわけではなく、パートタイム技術顧問的に関わりながら、諸課題に対する戦略の立案と遂行をしています。

といったことを、昨年8月に勤務先の第42期事業計画共有会で話していて、それを進めているのが現在です (以下、表紙のみ) 。

f:id:koic:20210807185344j:plain

連続した時間の中で様々な利害関係があるのが現実社会という枠組みということもあり、いきなりドラスティックに立ち位置を変えていたわけではなく、方向性を定めたのちに昨年から徐々にシフトしていました。

最後に、会社をプロダクトと見做す Basecamp 社の考え方が好きで、少し立場を変えて「情報化技術を通じて社会と共生する」プロダクトを良い感じにしていく気持ちです。

これは RubyKaigi Takeout 2021 より前に公開しようと思っていたエントリだったので、半年越しでようやく日の目を浴びました。

引き続きよろしくお願いします。

`Lint/InheritException` copにテコ入れした

Standard gem でのイシューについてジャスティンからメンションをもらったのがきっかけで、Lint/InheritException がいろいろと🤔だったのでテコ入れした。対象となる以下の PR に書いていることのサマリーとなる。

Lint/InheritException cop は、カスタム例外クラスの親クラスに Exception クラスを指定している場合、StandardError クラスか RuntimeError クラスに変えるように警告をする cop (RuboCop 1.25.1 時点では RuntimeError がデフォルト) 。とりわけ Java だとカスタムの例外クラスを作る際に java.lang.Exception を継承するので、Java から Ruby へスキル転換で不慣れな人のコードでかつて何度か見た覚えがあった。Lint として存在しているのは、そんなところが始まりかなと思っている (調べたら自分が RuboCop にパッチを送るようになる以前に導入されていた) 。

StandardError を継承しない、組み込みの例外クラスを許容するようにした

ひとつ目のテコ入れ。組み込みの Interrupt 例外クラスに対しても RuntimeError にするよう警告するのは、継承ツリーのまったく異なるものへの置換でおかしいのではというのが個人的な見解。以下、PR に書いた AA から抜粋。

        ---------------
        |   Object    |
        ---------------
               ^
               |
        ---------------
        |  Exception  |
        ---------------
               ^
               |
      |------------------|
---------------  -----------------
|StandardError|  |SignalException|
---------------  -----------------
      ^                  ^
      |                  |
---------------  -----------------
|RuntimeError |  |   Interrupt   |
---------------  -----------------

Interrupt と同様に、SystemStackError, NoMemoryError, SecurityError, NotImplementedError, LoadError, SyntaxError, ScriptError, SignalException, SystemExit にも同様のことが起きるようになっていて、通常これらを直接拡張することはないものの、ライブラリなどで意図して拡張したいのであれば良いのではということから許容するようにした (実際手元にある OSS リポジトリでも対象となるものがあった) 。Style ならともかく Lint としては厳しすぎる気がするし、The Open-Closed Principle の観点からも拡張に対して開けていて良かろうという判断。

Exception の変わりに RuntimeError ではなく StandardError をデフォルトで継承するようにした

ふたつ目のテコ入れとして、Exception の置換先のデフォルトが RuntimeError だったのを StandardError にした。これも PR に書いた AA を抜粋しておく。

---------------
|  Exception  |
---------------
      ^
      |
---------------
|StandardError| (default for `rescue`)
---------------
      ^
      |
---------------
|RuntimeError | (default for `raise`)
---------------

おさらいとして、StandardError (とそのサブクラス) は rescue に例外クラスが指定されていないときにデフォルトで補足するクラス、RuntimeError は例外クラスを指定せずに raise する際のデフォルトのクラスとなる。

カスタム例外クラスの親クラスをデフォルトで RuntimeError の継承とするとクラス指定なしの raise と、クラス指定ありの raise のいずれも is-a として違いがでなくなる。コンテキストによるので、RuntimeError の継承が間違っているわけではないが、抽象度の高い StandardError の方が良かろうと Exception のデフォルトの置換先を StandardError に変更した。

安全でない自動修正としてマークした

最後のテコ入れ、そもそもこれらのクラスについて振る舞いが変わるため、安全でない自動修正とマークした。

次のリリース (1.25.1 の次) でこのあたりの変更がざっと入る予定です。

mrubyにパッチを送ったきっかけ

作品を出せるアイデアが浮かぶかどうかは置いておいて、今年の TRICK に向けて『あなたの知らない超絶技巧プログラミングの世界』を読んでいます。それがきっかけで、巡り巡って mruby にパッチを送る機会があったのを書き残しておきます。新しいことを学ぶ際にいろいろと迂回する良い (?) 事例かもしれません。

eval s=%q(puts %(eval s=%q(#{s}))) のようなコードはふだん使わないプログラミング脳の使い方のトレーニングになるのですが、複雑性が上がっていくと処理系なしで検証するのはなかなか難しかったりします。そこでスマホでも通信なしで Ruby を実行できると便利ということで、最近は rubyist.app という iOS アプリで実験することがあり、その処理系が mruby となっています。

rubyist.app

主題の mruby へのパッチですが、CRuby と mruby の処理系の違いで、超絶技巧プログラミングにおけるアスキーアート定石の %w(...)*'' が動かず、差分としては Array#* に文字列の引数を渡したときの挙動の違いが原因でした。

mruby の振る舞いということで、パッチを書く前に規格を調べてみます。『X 3017:2013 (ISO/IEC 30170:2012) 』規格の『15.2.12.5.2 Array#*』からの抜粋となりますが、引数 num が文字列のときは以下のように未規定でした。

動作: a) numがIntegerクラスのインスタンスでない場合,このメソッドの動作は未規定とする。

kikakurui.com

ここで CRuby の挙動が参考となるのですが、CRuby では Array#* の引数に文字列を渡したときは、Array#join と同様の振る舞いになります。

% ruby -ve "p ['a', 'b', 'c']*''"
ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-darwin19]
"abc"

このあたりの流れで mruby に送ったパッチが以下で、このエントリの時点でまつもとさんにマージしてもらっています。ありがとうございます。

github.com

パッチを書くにあたって、CRuby と mruby で API は異なるものの CRuby の rb_ary_times 関数は参考になりました。

本業の Web+DB の世界では直接的に関わりのなかったことから mruby をあまり見てこなかったのですが、デフォルトで Regexp クラスが組み込まれていなかったり、RubyGems とは異なる mrbgems という仕組みに則ったりと異なる世界観がおもしろく、超絶技巧への知識を身に付ける副産物になりました。

ちなみに処理系の検証で毎回スマホで打ち込むのはめんどうなので PC の方で mruby を試したりする際、正規表現を使うには mruby/mruby リポジトリの build_config/default.rb の conf.gem に mrbgems を指定するようです。ここでは mattn さんの mruby-onig-regexp を指定していますが、正規表現の mrbgems にはいくつかあるようです (調べていないですが rubyist.app の Regexp はまた違う実装かも) 。

diff --git a/build_config/default.rb b/build_config/default.rb
index f5e2cbb71..d64e92730 100644
--- a/build_config/default.rb
+++ b/build_config/default.rb
@@ -13,6 +13,8 @@ MRuby::Build.new do |conf|
   # conf.gem :github => 'mattn/mruby-onig-regexp'
   # conf.gem :git => 'git@github.com:mattn/mruby-onig-regexp.git', :branch => 'master', :options => '-v'

+  conf.gem :github => 'mattn/mruby-onig-regexp'
+
   # include the GEM box
   conf.gembox 'default'

今後 mruby を使う機会があるかもしれない人へのご参考まで。

もうひとつ余談ついでに書き残すと、[1, 2, 3] * '' という書き方に対して RuboCop を適用してみたところ Style/ArrayJoin cop が既に存在していて [1, 2, 3].join('') にするよう警告がでました。