see: ブログを統合します - 不眠日記
2023年5月を振り返る
オクトパストラベラー2をクリアした
クリアしました。裏ボスは倒してないけど、これ以上進めない気がする(飽きたので)。
やはり、戦闘システムはとても良いが、アウトゲームやストーリーはあんまり好みに合わないなあという感想ではある。エルデンリングのときも思ったけど、「自分の思った順序で攻略したい」という欲求が自分にはあんまりない。
ギター買った
渋谷の楽器屋さんでこのギターを買いました(色は違う)。
雰囲気でクリープハイプの曲を弾いてみたり、気まぐれに入門書を読んで練習してみたり、そういった活動を行っております。
肉体を複雑に操作する活動をするのは久しぶりで脳が疲れる。シャル・ウィ・ダンス の序盤で役所広司がステップを踏めなくてテンパってるときに近い感情を日々味わっている。
全体的に学生のときよりもちょっとだけ上達してきてる感触がある。
出社強化月間を自主的に開催した
出社は良い(通勤時間を除けば)という結論である。
今は全体の出社比率が低く、人口密度が低いために特にそう感じるということはあるかもしれない。
読んだ本
大岳鍾乳洞
鍾乳洞を見に行った。
なかなか良い観光スポットでした。
これは道中にあったエモいトンネルの写真です。
鍾乳洞本体は険しすぎて写真を取れなかった。
オーケストラのコンサートに行った
オーケストラのコンサートに行くのは多分人生で初めてだったんだけど思いの外良かった。
今月のまとめ
現場では、実装がテストのバグを発見する!(カスのロシア的倒置法)
— 𝘼𝙧𝙧𝙖𝙮-𝙨𝙖𝙣 (@genya0407) 2023年5月16日
ABC size を可視化し、闇を払う
これは Ruby Advent Calendar 2022 の7日目の記事です。
TL; DR
ABC size を可視化する abc_size_visualizer という gem を作りました。 こんな感じで、メソッドの各行がどれだけ ABC size の増加に貢献しているかを可視化する CLI ツールです。
赤が代入、黄色がメソッド呼び出し、青が条件式の数を表しています。
また、各行の末尾にある # <0, 2, 1>
のようなコメントも abc_size_visualizer が付加したもので、それぞれ代入、メソッド呼び出し、条件式の大きさを表しています。
使い方
以下のコマンドでインストールできます。
$ gem install abc_size_visualizer
そして、以下のコマンドで ABC size の可視化を実行します
$ visualize_abc_size some_code.rb ... # ABC size の情報が付与された some_code.rb の内容が出力される
背景
abc_size_visualizer を実装するに至った背景を説明します。
ABC size はどのように理解されているか
多くの Rubyist は、以下のような警告によって ABC size を知ります。
$ bundle exec rubocop (略) Metrics/AbcSize: Assignment Branch Condition size for auth is too high. [20.5/15]
このような警告に直面した人々は、インターネットを検索して回った後、おもむろに .rubocop.yml
を編集し、ABC size に関する警告を「解決」します 1。
Metrics/AbcSize: Max: 100
このように、一般的に ABC size は、
- なんかよくわからないけど rubocop が怒ってくるもの
- (ホントは良くないと分かっているが... という disclaimer 付きで)無視するもの
と理解されています。
ABC size とは何であるか
では、そもそも ABC size とは何でしょうか?
ABC size は、代入(assignment)、メソッド呼び出し(branch)、条件式(condition)の出現回数をそれぞれ2乗して和をとり、その平方根を計算したもの2であり、コードの「サイズ」を表す指標です3。
定義を述べてもよくわからないと思うので、例を挙げます。以下のメソッドを考えると、
def some_method if [true, false].sample # if による条件式(C)、sample メソッドの呼び出し(B) puts "#{100 + 200}" # puts メソッドの呼び出し(B)、 `+` メソッドの呼び出し(B) end end
ABC size は、
となります。
ABC size の問題点
ABC size の問題点は分かりづらいことにあります。
例えば、以下のコードには「メソッド呼び出し(branch)」が何回出現するでしょうか?
puts user_name
正解は..................「わからない」です!
なぜなら、 user_name
がローカル変数なのかメソッド呼び出しなのか、この一行だけでは判定できないからです。
ABC size は、メソッド全体の文脈を加味しないと算出することができず、見た目が全く同じコード断片であっても、文脈によって ABC size は全く異なる可能性があります。
def some_method1 puts user_name # user_name というローカル変数がないので user_name はメソッド呼び出しである end def some_method2(user_name:) puts user_name # user_name というローカル変数が定義されているので user_name はメソッド呼び出しではない end
ほかにも、ABC size には非自明なケースがいくつも存在します。
- メソッドの引数は assignment としてカウントされるか?
&.some_method
は condition としてカウントされるか? branch としてカウントされるか?rescue SomeError => e
は condition としてカウントされるか? assignment としてカウントされるか?else
は condition としてカウントされるか?||=
は condition としてカウントされるか? assignment としてカウントされるか?>
は condition としてカウントされるか? branch としてカウントされるか?- デフォルト値付きのキーワード引数は assignment としてカウントされるか? condition としてカウントされるか?
また、これらの非自明なケースはドキュメント等にも特に記載がありません。 そもそも、Rubocop の Metrics/AbcSize では ABC の内訳は表示されません。
そのため、ABC size 削減チャレンジは、
- 当てずっぽうで色々書き換える
- rubocop に怒られないことを祈る
- 怒られたら 1 に戻る
という方法で行われる4ことになり、不毛度が高いです。
abc_size_visualizer で ABC size の闇を払う
abc_size_visualizer の存在する世界では、ABC size 削減チャレンジは
- ABC size を可視化し、ABC size が大きい原因を理解する
- 問題となっている行を書き換える
- rubocop に怒られないことを確認する
というステップで実施することができます。
また、rubocop に鍛えられて Ruby のベストプラクティスを学ぶように、人間側が ABC size をより深く理解するようになり、abc_size_visualizer がなくても適切に ABC size を削減できるようになっていくことでしょう。
実装方法とその問題点
abc_size_visualizer は、rubocop の内部で利用されている AbcSizeCalculator クラスを流用して実装しています。
- abc_size_visuzalizer の実装 https://github.com/genya0407/abc_size_visualizer/blob/trunk/lib/abc_size_visualizer/abc_size_calculator.rb#L1
- 元になったクラス https://github.com/rubocop/rubocop/blob/master/lib/rubocop/cop/metrics/utils/abc_size_calculator.rb
これは、rubocop の ABC size 計算と実装をなるべく一致させることを意図したものですが、今後 rubocop 側の実装が変更されたときに追従できない可能性が高いため、なんらかのうまいやり方を考える必要があるナアと思っています...
まとめ
ABC size を可視化する CLI ツールを実装しました。 これにより、rubocop の Metrics/AbcSize の闇を払い、Rubyist の皆さんをサポートすることができれば嬉しいです。
また、この CLI ツール自体の実装には闇が多いため、どうにかマトモにしていきたいなと考えています。
-
# rubocop:disable Metrics/AbcSize
によって「解決」することもある。↩ - https://nacl-ltd.github.io/2016/02/23/ruby-abcmetrics.html から。↩
- https://en.wikipedia.org/wiki/ABC_Software_Metric から。ソースコードの「複雑さ」や「計算量」を表す値ではないことに注意。↩
- ABC size を完全に理解している人は別ですが↩
Minutus という mruby の Rust バインディングを作った
このところ、夏休みの自由研究として「mruby と Rust をいい感じにつなぎこむ」というのをやっていました。
お盆休みのすべてを費やし、なんとか「実用可能」といえそうなレベル*1まで来たので、この記事で簡単に説明したいと思います。
(↓は Matz にリツイートされてとても嬉しかったツイート)
めちゃくちゃいい感じに Rust から mruby の中身に手を突っ込めるようになった...(キモいという説はある) pic.twitter.com/WJzmSh2T8o
— 𝘼𝙧𝙧𝙖𝙮-𝙨𝙖𝙣 (@genya0407) 2022年8月9日
Minutus とは
Minutus は、Rust と mruby をいい感じに連携するためのライブラリです *2。
Minutus を使うと、Rust の中で mruby のスクリプトを eval できます。以下のようなイメージです。
minutus/minutus-test/src を覗くと、他にも多くの例を見ることができます。
例えば、mruby のメソッドを Rust から呼べたりします。
また、それとは別に、 mrbgem (mruby 用の ライブラリ) を Rust で作ることもできます。
minutus/examples/mruby-polars はその例で、Rust のデータフレームである polars を mruby に(全く不完全な形で)ポートした mrbgem です。
Minutus で達成したいこと
Minutus で達成したいことは2つあります。
- 最新の mruby (3系) で動く
- 利用者が難しいことを考えずに、とにかく mruby と Rust を接続できる
これらを目指す理由を次で述べます。
mruby と Rust を接続する際の問題点
人類には「Rust で作ったツールに mruby の DSL を入れたい」という普遍的な欲求がある、ということは広く知られています。また、mruby のネイティブ拡張を Rust で書きたいという人類も多く存在していると思います。
しかし、その実現には高いハードルがあります。
まず、新しい mruby (3系)で動く Rust / mruby のバインディングは存在しません。例えば、mrusty がサポートする mruby のバージョンは 1.2.0 です。現在の mruby の最新のバージョンは 3.1.0 なので、これは相当古いと言えるでしょう*3*4。
かといって、mruby の C API を、 Rust から直接触るのは茨の道です。
- 避けられない unsafe 祭り
- 自前で mruby をビルドし、リンクする必要がある
- mruby の C API に習熟する必要がある
- つまり、Rust も mruby も書ける、というだけではダメ
- mruby の C API はマクロの形態で提供されるモノが多く、bindgen が認識してくれない*5
- mruby から取り出した値を Rust の型に変換するのは面倒
- Rust で作ったバイナリを mrbgem にするのは地味にハマりどころが多い
むかし私もこの問題にぶち当たり、Rust に mruby を埋め込むのを諦めたことがあります。
Rubyで設定を書けるLinux用キーマッパー 「rumap」をRustで作った - さんちゃのblog
Minutus の工夫
これらの問題を受け、最新の mruby で動き、かつ mruby の C API に詳しくなくても使える mruby / Rust のバインディングが必要だと感じました。
特に後者に関しては、以下のような工夫をしています。
- mruby と接続するためのコードを生成するマクロを提供する
- mrbgem のテンプレートを生成する CLI ツールを提供する
- 実装例を提供する
- 前述の CLI ツール自体も Minutus の利用例となっています
これらの工夫により、mruby の C API に詳しくない人でも、気軽に mruby と Rust を接続できます。
Minutus の課題
「実用可能」といえるレベルにはなったと思うものの、Minutus には様々な問題が残っています。
- GC と向き合えていない
- mruby のメソッドを Rust から安全に呼ぶ方法がない
- 現状では、`define_funcall!` で定義した関数で問題が発生するとパニックするようになっている
- これはマクロを改良したらいい感じにできるはずなので、暇を見つけて直したい
- キーワード引数のある mruby のメソッドを呼ぶ方法がない
- Rust の関数にはキーワード引数がなく、`define_funcall!` に渡すシグネチャが Rust の文法からどうしても乖離してしまうため、マクロの文法をどうするか悩んでいる
- mruby 側に適当なメソッドを定義すれば、そのメソッド経由で呼び出すことはいちおう可能
- マクロのエラーメッセージが雑すぎて文法ミスをデバッグできない
- Rust のモジュールシステムと組み合わせて動かせるのか未検証
- `define_funcall!` や `wrap` と、それらを利用するコードが同じファイルにまとまってるパターンしか検証しておらず、それらが別モジュールに分離したときにうまく動くか怪しい
- パフォーマンス
- mruby と Rust の世界で値をやり取りするときの関数呼び出しの回数がエグいし、inline 展開とかもしてない
- `wrap` で生成したクラスに initialize_copy を実装してないので、たぶん dup すると死ぬ
- やるだけなので暇なときにやる
- 実用的な例が存在しない
- Rust の資産を mruby にポートするというようなことをやって、minutus 自体の欠陥を洗い出して洗練させつつ、minutus 利用者が参考にできるようにしたい
- 例えば actix-web の mruby ラッパーを作りたいなと思っている
書き出してみると色々ありますが、暇を見つけて一つづつ潰していこうと思っています。
おわりに
この記事では、mruby と Rust をいい感じに接続するためのライブラリである Minutus について説明しました。
個人的には割と良いコンセプトのものができたと思っているので、興味のある方は使っていただけるととても嬉しいです。
こんなことはできないの?とか、ここがイケてないとか、ドキュメントが分かりづらいとか、なんでも良いのでフィードバックをいただけると泣いて喜びます。
以下の記事で言及されている通り、mruby 界隈は、特に周辺のエコシステムが古くなりつつあり*8、厳しい気持ちになることもあるのですが、mruby 自体は触っていてとても面白いので、盛り上げていけたらいいなと思っています。
以上
*1:割と限定された用途ではあるが...
*2: Ruby の Rust バインディングである Magnus をめっちゃ参考にしました
*3:2系に追従しようとしている人もいるが、PR がスルーされている Support mruby 2.1.0 by wordijp · Pull Request #100 · anima-engine/mrusty · GitHub
*4:頑張って探したら他にもあるかもしれないが、サッと見た感じでは無さそう
*5: see: Improve macro defined constants to recognize #define CONSTANT ((int) 1) · Issue #316 · rust-lang/rust-bindgen · GitHub
*6: つまり、mrbgem をインストールするときに `cargo build --release` を実行させる方法がない
*7:このケースでは、引数名を書き忘れていることが原因。そもそも引数を省略しても動くようにしろという話もある。
*8: mrusty が 1.2.0 しかサポートしていないのはその一例
Ruby の拡張ライブラリを、Rust を使ってお手軽に実装する
magnus というcrate を利用すると、超簡単に Ruby の拡張ライブラリが実装できます。
具体的には、Rust 側の記述はこんな感じになります。
use magnus::{define_class, function, method, prelude::*, Error}; #[magnus::wrap(class = "Point")] struct Point { x: isize, y: isize, } impl Point { fn new(x: isize, y: isize) -> Self { Self { x, y } } fn x(&self) -> isize { self.x } fn y(&self) -> isize { self.y } fn distance(&self, other: &Point) -> f64 { (((other.x - self.x).pow(2) + (other.y - self.y).pow(2)) as f64).sqrt() } } #[magnus::init] fn init() -> Result<(), Error> { let class = define_class("Point", Default::default())?; class.define_singleton_method("new", function!(Point::new, 2))?; class.define_method("x", method!(Point::x, 0))?; class.define_method("y", method!(Point::y, 0))?; class.define_method("distance", method!(Point::distance, 1))?; Ok(()) }
これをビルドすると、Ruby 側に Point クラスが生えます。 Ruby の世界 と Rust の世界 の間で相互に自動で型が変換されていてすごいですね。
point_1 = Point.new(0, 0) point_2 = Point.new(100, 100) point_1.distance(point_2) # => 141.4213562373095
この記事では、渡された文字列をシャッフルする Shuffle という gem の実装を通じて、Rust で拡張ライブラリを作る方法を説明します。
なお、実行環境は macOS Monterey 12.4、CPU は M1 Pro です。とはいえ、他の環境でも似たような手順で行けると思います。
gem を作る
拡張ライブラリを入れるための gem を作っていきます。
gem を初期化する
$ bundle gem shuffle
$ cd shuffle
諸々のバージョンを揃える
Ruby のバージョンを 3.1.1 以上にする必要があります*1。 2022/06/12 現在では、 3.1.2 を入れておけば良いと思います。
$ rbenv install 3.1.2 $ rbenv local 3.1.2
また、Rubyems のバージョンを 3.3.11 以上にする必要があります*2。
$ gem -v 3.3.15 # 古い場合はバージョンを上げる $ gem update --system
Rust をセットアップする
rustup を利用して適当に Rust をセットアップします。 1.57.0 以上をインストールしてください*3。
gemspec を適当に埋める
shuffle.gemspec
の TODO になっている欄を適当に埋めます。埋めないとこの先の作業ができません。
参考: fix gemspec · genya0407/shuffle@5563f9c · GitHub
Rust で拡張ライブラリを作る
いよいよ拡張ライブラリを作っていきます。
拡張ライブラリをビルドする環境を作る
まず、拡張ライブラリをビルドするのに必要な gem を dependency として追加します。
shuffle.gemspec
に以下を書き加え、bundle install を実行します。
spec.add_dependency "rake-compiler" spec.add_dependency "rb_sys"
$ bundle install
次に、bundle exec rake compile
で拡張ライブラリをビルドするため、 Rakefile
に以下を書き加えます。
require "rake/extensiontask" Rake::ExtensionTask.new("shuffle") do |ext| ext.lib_dir = "lib/shuffle" ext.source_pattern = "*.{rs,toml}" end
そして、 ext/shuffle/extconf.rb
を作成します。
require "mkmf" require "rb_sys/mkmf" create_rust_makefile("shuffle/shuffle")
最後に、 shuffle.gemspec
に extconf.rb
を認識させるため、以下を書き加えます。
spec.extensions = ["ext/shuffle/extconf.rb"]
ここまでで、Rust 拡張をビルドする準備が整いました。
参考: setup rake-compiler && rb_sys · genya0407/shuffle@61af062 · GitHub
Rust を書く
まず、 ext/shuffle
で Rust のプロジェクトを初期化します。
$ cd ext/shuffle $ cargo init --lib $ echo 'target' >> .gitignore
そして、 Cargo.toml
に以下を書き加えます。
[lib] crate-type = ["cdylib"] # 拡張ライブラリを作るために必要 [dependencies] magnus = "0.3" rand = "0.8.5" # 文字列をシャッフルするのに使う
最後に、文字列をシャッフルするクラスを定義しましょう。
ext/shuffle/src/lib.rs
に以下を記述します。
use magnus::{define_class, function, method, prelude::*, Error}; use rand::seq::SliceRandom; #[magnus::wrap(class = "Shuffle")] struct Shuffle { original: String, } impl Shuffle { fn new(original: String) -> Self { Self { original } } fn shuffle(&self) -> String { let mut rng = rand::thread_rng(); let mut v = self.original.chars().collect::<Vec<_>>(); v.shuffle(&mut rng); v.into_iter().collect() } } #[magnus::init] fn init() -> Result<(), Error> { let class = define_class("Shuffle", Default::default())?; class.define_singleton_method("new", function!(Shuffle::new, 1))?; class.define_method("shuffle", method!(Shuffle::shuffle, 0))?; Ok(()) }
そしておもむろに bundle exec rake compile
を実行すると、 lib/shuffle/shuffle.bundle
というファイルが生成されます。
$ bundle exec rake compile
$ file lib/shuffle/shuffle.bundle
lib/shuffle/shuffle.bundle: Mach-O 64-bit dynamically linked shared library arm64
このファイルを require
すると、 Shuffle
クラスが使えます。
$ bundle exec irb irb(main):001:0> require './lib/shuffle/shuffle' => true irb(main):002:0> Shuffle.new('abc').shuffle => "bca"
gem のお作法的には、 lib/shuffle.rb
から require_relative しておくのが良いです。
# frozen_string_literal: true require_relative "shuffle/version" require_relative "shuffle/shuffle" # これを追加 class Shuffle class Error < StandardError; end # Your code goes here... end
なお、 bundle gem
実行時に、Shuffle
は module として宣言されているので、そのままだと「Shuffle は class じゃなくて module だよ」みたいなエラーが出ます。
これを回避するために、 lib/shuffle.rb
や lib/shuffle/version.rb
を修正しておきましょう。
参考:implement extension · genya0407/shuffle@4389ef1 · GitHub
作った gem を利用する
RubyGems に publish するのも何なので、 github からインストールしてみます。
先程の shuffle gem を github に push しておきつつ、以下のような Gemfile
を作って bundle install
してみましょう。
$ cat Gemfile # frozen_string_literal: true source "https://rubygems.org" gem 'shuffle', github: 'genya0407/shuffle', branch: 'trunk' $ bundle install Fetching https://github.com/genya0407/shuffle.git Fetching gem metadata from https://rubygems.org/. Fetching rake 13.0.6 Installing rake 13.0.6 Using bundler 2.3.15 Fetching rake-compiler 1.2.0 Fetching rb_sys 0.9.4 Installing rb_sys 0.9.4 Installing rake-compiler 1.2.0 Using shuffle 0.1.0 from https://github.com/genya0407/shuffle.git (at trunk@f24fe1f) Bundle complete! 1 Gemfile dependency, 5 gems now installed. Bundled gems are installed into `./vendor` $ bundle exec irb irb(main):001:0> require 'shuffle' => true irb(main):002:0> Shuffle.new('abcdefg').shuffle => "gdecafb"
このように、 gem として利用できることが確認できました。 なお、利用者側の環境にも Rust が必要である点に注意して下さい。
magnus に関する雑感
magnus の型変換がちゃんとしててすごいと思いました。 感動ポイントを箇条書きします*4。
- 関数の型宣言を見て、勝手に型変換をやってくれる
- GitHub - matsadler/magnus: Ruby bindings for Rust の表のとおり、変換の対応付けが妥当。
- しかも、変換不能な場合は IllegalArgument エラーを raise してくれる。panic したりしない。
- 型変換ができないときは手動でも変換できる
- Ruby のオブジェクトと Rust の構造体の紐付けを勝手にやってくれる
- C拡張みたいに TypedData_Wrap_Struct とかしなくていい。
全体的に、「そうなってほしい」という思いが達成されていて、おまじないが少ない作りになっているのが良いです。
まとめ
拡張ライブラリを実装したくなる機会は現状ではそれほどないと思いますが、このレベルまで簡単に実装できるようになったことで、Rust の拡張ライブラリを作るという選択肢が一般的になってくると Ruby の将来という観点でも面白い と思いました。
例えば、 GitHub - genya0407/reing_print_image は、 GitHub - genya0407/reing_text2image を拡張 gem にしたものです。
reing_text2image はもともと、 Rails で作った 自作質問箱 でOGPの画像を生成するために作ったツールでした。
Crystal 別にそんなに好きじゃないし業務で使ってないけど、Ruby ぽくて速いので競プロするときに使おうかなと思っている https://t.co/rb9kAJMXeM #reing
— 𝘼𝙧𝙧𝙖𝙮-𝙨𝙖𝙣 (@genya0407) May 29, 2022
以前はこれをCLI ツールとしてRuby から呼び出して、一度ディスクに画像ファイルとして書き出したものを Ruby から read して send_data で返却するという、結構すごい作りをしていました。
しかし、Rust を使った拡張 gem が手軽に作れるようになったことで、ファイルを介すことなく、png のバイナリを直接 Rust → Ruby で受け渡せるようになりました。
reing_print_image/reing_print_image_spec.rb at trunk · genya0407/reing_print_image · GitHub
このように、全体的には Ruby や Rails で実装しておき、無理なところだけ Rust に切り出す、ということができるようになったことで、Ruby が関与できる世界が広がっていく*6のではないかと思いました。
Rust で拡張ライブラリを作るのは思いの外面白かったので、他にもネタを見つけてやっていきたいと思います。
*2:Rust extension support を利用するため 3.3.11 Released - RubyGems Blog
*3:magnus がサポートしているのが 1.57.0 以上なので GitHub - matsadler/magnus: Ruby bindings for Rust
*4:筆者は拡張ライブラリを真面目に書いたことがないので、的はずれなことを言っている可能性も高いですが...
*5:Rust の String は UTF-8 として valid な文字列しか入らない
*6:あるいは縮小を緩和できる