株式会社クリアコード > ククログ

ククログ


RubyKaigi Takeout 2020 - Goodbye fat gem #rubykaigi

RubyKaigi Takeout 2020の1日目にGoodbye fat gemという話をする須藤です。今年もクリアコードはスポンサーとしてRubyKaigiを応援します。

関連リンク:

内容

RubyKaigiではRuby関連の自分の活動を自慢しているのですが、今年はfat gemまわりの活動を自慢します。

fat gemまわりの状況は2019年、fat gemをやめるとしてすでにまとめていて、今回の内容もこの記事の内容をベースにしています。ちなみに、私はRubyKaigiに応募する前に応募する内容を記事にして興味がある人はいそうかということを確認しています。fat gemの内容は少しは興味がある人はいそうで、去年のRubyKaigi 2019のcsvの話Ruby 2.6.0とより高速なcsvの反響をみる感じではそれなりにいそうで、RubyKaigi 2015のテスティングフレームワークの話Rubyのテスティングフレームワークの歴史(2014年版)の反響をみる感じではけっこういそうでした。

fat gemは私自身がかなり活用していたこともあり、rake-compilerなどfat gem関連のツールのメンテナンスをしています。しかし、RubyInstaller for Windowsの改良によりfat gemを使わなくてもよい状況になりつつあります。私が認識している最後の課題はBundlerにあったのですが、それも私が直しておきました。(どーん!けっこう難しかったんだよ!)Bundler 2.2.0がリリースされればfat gemを使わなくてもよい状況になるはずです。

ただ、この状況を知らずにfat gemを提供しつづけているgem開発者はまだそれなりにいるように私にはみえます。そのため、RubyKaigiの場でfat gem関連の最新情報を共有して、fat gemを提供しつづけるかどうか考える機会にして欲しいと思っています。fat gemをやめられればfat gemのメンテナンスコスト(それなりにあるはず)がなくなり、メンテナンスのためのリソースを別のことに使えます。これはgem開発者にとってよいことのはずです。

fat gemを提供しているgem開発者に届くといいなぁ。

動画作成方法

RubyKaigi Takeout 2020は事前録画した発表内容をストリーミングするスタイルです。私はDebian GNU/Linux上でOpen Broadcaster Softwareを使って録画しました。Shotcutを使って音量を調整したりとか動画編集もしてみたかったのですが、時間の関係でできませんでした。クリアコードのYouTubeチャンネルを作ってみたので、動画編集できたらアップロードしようと思っています。

2020年10月30日にはSciPy Japan 2020でApache Arrowの紹介をする予定で、それも事前録画なので、そのときは動画編集もしてみたいです。

まとめ

fat gemを提供しているgem開発者はぜひ聴いてみてね。

私がBundlerを直したように「手元で問題を回避するよりも問題が発生している原因を直すスタイル」で仕事をしたい人はクリアコードという会社がオススメなので会社説明会に応募してね。

タグ: Ruby
2020-09-03

2019年、fat gemをやめる

fat gemを簡単に作れるようにするgemであるrake-compilerをメンテナンスしている須藤です。過去にfat gemの作り方をまとめたこともあります。

fat gemが有用な時代もあったのですが、今はメリットよりもデメリットの方が大きいのでfat gemをやめたらどうか、という話をします。

fat gemについて

fat gemとはビルド済みバイナリーが入ったgemのことです。Pythonで言えばwheelのようなものです。

RubyはC言語でRuby用のライブラリーを実装することができます。これを拡張ライブラリーと呼びます。拡張ライブラリーの用途は主に高速化(Rubyで実装したよりCで実装した方が速い)とバインディング(C・C++言語で実装されたライブラリーをRubyから使えるようにするライブラリー)です。

拡張ライブラリーをインストールするにはC言語のプログラムをビルドしなければいけません。ビルドするためにはインストール時にC言語のビルド環境が必要になります。ユーザーの環境にC言語のビルド環境がないとインストールできません。つまり、インストールの敷居が高くなります。

このインストールの敷居が高い問題を解決するものがfat gemです。fat gemにはビルド済みのバイナリーが含まれているためユーザーはC言語のビルド環境を用意しなくてもいいのです。やったー!

fat gemの問題点

fat gemのおかげでユーザーはハッピーになれそうですね。でも、本当にそうでしょうか?実際に、fat gemを作ってきた経験からfat gemの問題点を説明します。

新しいRubyを使えるまでにタイムラグがある

Rubyは毎年クリスマスに新しいバージョンがリリースされます。(メンテナンスリリースは必要に応じて随時行われています。)

fat gemにはビルド済みのバイナリーが含まれています。これは、だれかが事前にビルドしてくれているということです。だれかというのはgemの開発者です。

Rubyはバージョン間でのABIの互換性を保証していません。たとえば、Ruby 2.6用の拡張ライブラリーはRuby 2.7では使えません。そのため、新しいバージョンのRubyがリリースされたらそのバージョンのRuby用にビルドしないといけません。(メンテナンスリリースでは互換性があるのでRuby 2.6.0用の拡張ライブラリーはRuby 2.6.5でも使えます。)

つまり、新しいRubyがでてもgemの開発者が新しいRuby用のfat gemをビルドしてくれていなければ新しいRubyを使えません。すべてのgemがRubyのリリースにあわせて開発スケジュールを立てているわけではないので、クリスマスから数ヶ月遅れて新しいRuby用のfat gemをリリースすることも十分ありえます。

リリースされるならまだいいですが、メンテナンスモードになっているgemは重大な問題がなければ数ヶ月経ってもリリースされないかもしれません。もし、リリースされないgemが変更無しで新しいRubyでビルドできたとしてもユーザーは使えません。(RubyのC APIはそんなに劇的に変わらないのでだいたいビルドできます。)

もし、ユーザーが複数のfat gemに依存している場合はすべてのfat gemが新しいRubyに対応しなければ新しいRubyを使えません。1つでも新しいRubyに対応していないとインストールできないからです。

脆弱性対応までにタイムラグがある

バインディングをfat gemにするということはバインディング対象のライブラリーもfat gem内に入れていることになります。もし、バインディング対象のライブラリーに脆弱性があった場合は迅速に修正版をリリースするべきです。そうしないとユーザーが危険な状態が伸びてしまうからです。

しかし、すべてのfat gem開発者がそんなにタイミングよく作業できるわけではありません。そのため、脆弱性対応リリースが遅くなりがちです。

念のため補足しておくと、これはfat gemにしていない場合でも発生しえます。たとえば、Nokogiriのようにデフォルトで特定バージョンの依存ライブラリーをビルドするタイプのgemでも発生しえます。指定したバージョンのライブラリーに脆弱性があったらバージョンを更新してリリースしないと脆弱なバージョンを使ったままのユーザーが増えてしまいます。明示的に--use-system-librariesを指定すればNokogiriのバージョンを上げなくても対応できるのですが、残念ながら多くのユーザーはそこまで頑張ってくれないでしょう。

新しい依存ライブラリーを使えるまでにタイムラグがある

これもバインディングの場合ですが、バインディング対象のライブラリーが新しいバージョンをリリースしてもfat gemを更新しなければユーザーは新しいバージョンを使えません。

fat gemに対応するとrequireが遅くなる

fat gemに対応するには次のようなコードを入れる必要があります。2.6/io/console.so(ビルド済みバイナリー)があればそっちを優先し、なければio/console.so(自分でビルドしたバイナリー)を読み込むというロジックです。

begin
  require "#{RUBY_VERSION[/\d+\.\d+/]}/io/console.so"
rescue LoadError
  require 'io/console.so'
end

すべてのケースでfat gemを使うなら↓だけで大丈夫です。

require "#{RUBY_VERSION[/\d+\.\d+/]}/io/console.so"

しかし、それではユーザーが自分でビルドして使うという選択肢がなくなります。逆に言うと、開発者がすべてのプラットフォーム向けにfat gemを用意する覚悟を決める必要があります。

それは現実的ではないので、普通は前述のように自分でビルドしたバイナリーにフォールバックします。

そうすると、fat gemを提供していない環境では必ずfat gem用のrequireが失敗します。この分requireが遅くなるということです。$LOAD_PATHにたくさんのパスが入っている環境では無視できないくらい遅くなります。gemをたくさんインストールしているとその分$LOAD_PATHも大きくなります。たとえばRuby on Railsアプリケーションではたくさんのgemを使うことになるので影響が大きいです。

これを回避するために、fat gemを提供している環境でだけフォールバックする対策をとっているgemもあります。(ありました。)

fat gemのリリースを忘れる

多くのgemはrake releaseだけでgemをリリースできるようにしています。そしてこれはすぐに完了します。.gemファイルを作ってrubygems.orgにアップロードするだけだからです。(他にもgit tagをするとかちょろっとしたことをしています。)

しかし、fat gemをリリースするにはもう一手間必要です。各環境用のバイナリーをビルドしてそれぞれの環境毎にfat gemを作り、それらをrubygems.orgにアップロードします。

各環境用のバイナリーは大変です。rake-compiler-dockを使えば楽になりますが、それでも面倒です。

その結果どうなるかというとリリースが億劫になったり、fat gemのリリースを忘れたりします。たとえば、Ruby-GNOMEはリリースが億劫だなと思っていました。たとえば、io-console 0.4.8はfat gemのリリースを忘れていました

fat gemのリリースが大変

fat gemのリリースは大変なんです。特にバインディングのfat gemのリリースは大変です。

私はRuby-GNOMEでWindows用のfat gemを作っていました。バインディング対象のGTKなどのライブラリーはLinux上でMinGWを使ってクロスコンパイルしていました。これがすごく大変です。というのは、クロスコンパイルしている人がほとんどいないので、バインディング対象のライブラリーのバージョンを上げるとビルドエラーになることがよくあるからです。Ruby-GNOMEをリリースするたびにアップストリームにパッチを送っていたものです。ただ、librsvgがRustを使うようになってクロスコンパイルできなくなったときにfat gemをやめる決心をしました。

fat gemはそんなにポータブルじゃない(気がする)

fat gemは主にWindows向けに提供されていますが、Linux向けに提供している野心的なgemもあります。たとえば、sasscです。

Windowsはバージョンが限られていますし、後方互換性があるので古いWindows向けにビルドしていればいろんなWindowsでもだいたい大丈夫です。

しかし、Linuxディストリビューションはたくさんあり、使っているlibcも違います。スタティックリンクしたバイナリーを用意すれば大丈夫なのかというとそうでもない気がします。どうなんでしょうか。。。?

Pythonのwheelではmanylinuxという(だいたいの)Linux環境で動く仕組み(?)を用意しているので、このくらい頑張れば大丈夫なのかもしれません。が、私としては、この方向で頑張っちゃうの。。。?という気持ちになります。RubyGemsはそうなって欲しくないなという気持ちです。

fat gemの問題点の解決方法

ここまででfat gemの問題点をまとめました。それではfat gemの問題点を解決する方法を示します。それはfat gemをやめることです。どーん!

そもそもfat gemが必要だったのはユーザーがビルド環境を持っていないことが多かったからです。しかし、今は状況が変わっています。ユーザーがビルド環境を持っていないプラットフォームの代表はWindowsでしたが、今はRubyInstaller for Windowsがほぼ標準でDevKitを提供しています。Ruby 2.3以前はそうではなかったですが、Ruby 2.3がEOLになったので、今はWindowsユーザーでもビルド環境があるのです。

Linuxではパッケージをインストールすればすぐにビルド環境を整えられます。

macOSでもXcodeをインストールすればビルド環境を整えられます。Homebrewを使っている人はすでに整っているはずです。

他の環境(たとえば*BSD)でもビルド環境はすぐに整えられるでしょう。

つまり、今はユーザーがビルド環境を持っていると仮定してもよい状態になっています。そのため、fat gemを提供しなくてもユーザーがインストールできる状態が整っています。実際、私はWindowsユーザーがいるRuby-GNOMEでfat gemをやめましたが、最近はWindowsでのインストールトラブルはほとんど報告されていません。

それではfat gemをやめるとうれしいことをまとめます。

新しいRubyをすぐに使える

新しいバージョンのRubyは以前のバージョンのRubyとC APIが変わっている可能性があります。たとえば、Ruby 2.7ではrb_f_notimplement()が変わります。(Ruby 2.7の対応が必要な例)

しかし、多くのC APIは互換性があるのでなにも変更しなくても新しいRubyで動くことが多いです。その場合は、特になにもしなくてもすぐに新しいRubyを使えます。ユーザーは単に新しいRubyを使ってgemをインストールすればよいだけだからです。

また、もしRuby 2.7で動かない場合でも事前にプレビュー版で動作確認し、Ruby 2.7より前にRuby 2.7対応版をリリースしておくこともできます。こうすればクリスマス後にリリースしなくてもよくなるのでgem開発者に余裕があります。

fat gemを使った場合でも、プレビュー版でバイナリーを作ってリリースしておくことができなくはありませんが、rake-compiler-dockなど各種ツールが事前に対応していないと難しいです。

脆弱性対応をシステムに任せられる

バインディングをシステムのライブラリーを使ってビルドするようにしていた場合、バインディング対象のライブラリーに脆弱性があってもシステムのライブラリーを更新すれば対策できます。gemの開発チームよりシステムのライブラリーをメンテナンスしている人たちの方が層が厚いいので迅速に脆弱性に対応してもらえます。

なお、Nokogiriのようにデフォルトで依存ライブラリーを自前で管理するタイプのgemはfat gemでもそうでなくても関係ありません。

新しい依存ライブラリーをすぐに使える。。。こともある

バインディングをシステムのライブラリーを使ってビルドするようにしていた場合、バインディング対象のライブラリーの更新はシステムのパッケージシステムが面倒をみてくれます。Debian GNU/Linux sid、Fedora Rawhide、ArchLinux、Homebrew、MSYS2などのように最新のバージョンに随時アップデートされるシステムではgemの更新を待たずに新しいライブラリーを使えます。

ただし、ライブラリーのバージョンアップでAPIが変わった場合はgemの更新が必要です。

requireが遅くならない

fat gem用のrequireがいらなくなるので失敗するrequireを実行しなくてもよくなります。これによりrequireが遅くなりません。

bigdecimalがfat gemのサポートをやめたのはこれが理由です。

開発コストが下がる

面倒なfat gemのリリースをしなくてよくなるので開発者は本来の開発にリソースを注力できます。

最適化ビルドできる

fat gemは事前にビルドしたバイナリーをすべてのユーザーが共通で使うことになるので最大公約数の最適化しかできません。

しかし、fat gemをやめて各ユーザーごとにインストールする場合はその環境毎に最適化できません。たとえば、速度が非常に重要な拡張ライブラリーをGCCでビルドする場合は-O3 -march=nativeというオプションをつけてビルドするとその環境向けに最適化されます。たとえば、CPUがSIMDをサポートしていればSIMDを使ったバイナリーを生成することもあります。

fat gemをやめたときの問題点と解決策

fat gemをやめるとユーザーも開発者もハッピーになれそうですね。でも、本当にそうでしょうか?fat gemをやめたときの問題点とその解決策をまとめます。

インストール時間が長くなる

fat gemの場合はビルド済みのバイナリーをコピーするだけなのですぐにインストールは完了します。しかし、fat gemをやめるとインストールするたびにビルドすることになるので時間がかかります。

解決策は。。。特にありません。。。

依存ライブラリーがなくてインストールが失敗しやすくなる

バインディングはバインディング対象のライブラリーがないとインストールに失敗します。たとえば、RMagickはImageMagickがないとインストールに失敗します。Nokogiriがデフォルトで自分で依存ライブラリーをビルドするようになっているのはこの失敗を防ぐためです。

たしかに、自分でビルドしてしまうというのはこの問題の解決策の1つではあります。ただ、そんなに筋がよいとは思えません。脆弱性があったときの対応に関する問題があるからです。

私がオススメする方法はシステムのパッケージシステムを使って自動で足りない依存ライブラリーをインストールする方法です。このための便利gemがnative-package-installerです。私が開発しています。

native-package-installerはpkg-config gemと一緒に使うことを想定していて、extconf.rbに次のように書いておけば、cairoがインストールされていなければ自動でインストールします。

require "pkg-config"
require "native-package-installer"

unless PKGConfig.have_package("cairo")
  unless NativePackageInstaller.install(:arch_linux => "cairo",
                                        :debian => "libcairo2-dev",
                                        :homebrew => "cairo",
                                        :macports => "cairo",
                                        :msys2 => "cairo",
                                        :redhat => "cairo-devel")
    exit(false)
  end
  unless PKGConfig.have_package("cairo")
    exit(false)
  end
end

なお、RubyInstaller for Windows用のRubyではgemのメタデータにMSYS2のパッケージを指定しておくことで同じ機能(自動で依存ライブラリーをインストールする機能)を実現できます。以下はcairo.gemspecでの例です。

gemspec.metadata["msys2_mingw_dependencies"] = "cairo"

参考:MSYS2 library dependencies - For gem developers - onclick/rubyinstaller2 Wiki

ビルドに失敗してインストールが失敗しやすくなる

fat gemではすでにビルド済みなのでビルドが失敗することはありません。開発者が用意した環境でビルドが成功すればOKです。

一方、ユーザーの環境でビルドする場合は、開発者の環境では成功しているのにユーザーの環境では失敗することがあります。

解決策は、CIでサポートしている環境を常にテストすることです。Travis CIやGitHub Actionsなどを使えば、いろんな環境でテストできます。Linuxの亜種はDockerを使うとよいでしょう。

まとめ

検討するべき項目が他にもある気がしますが、一通りまとめたので公開します。fat gemをやめたくなりましたか?それともfat gemはやめないで!という気持ちになりましたか?

もし、これはどうなの?という項目があったらなんらかの手段で私に聞いてください。回答します。

タグ: Ruby
2019-11-22

RubyKaigi 2019 - Better CSV processing with Ruby 2.6 #rubykaigi

RubyKaigi 2019の2日目にBetter CSV processing with Ruby 2.6という話をした須藤です。今年もクリアコードはシルバースポンサーとしてRubyKaigiを応援しました。

関連リンク:

内容

この1年、 @284km と一緒にcsvを改良していたのでその成果を自慢しました。せっかく2人で話をするので、時間を分割してそれぞれ話をするのではなく、ずっと掛け合いをしながら2人で話をしました。楽しんでもらえたでしょうか?

Ruby 2.6.3に入っているcsvはどんなケースでもRuby 2.5のcsvより高速になっています。この成果を使うためにぜひ最新のRubyにアップグレードしてください。

最後にも少し触れましたが、まだまだcsvやその周辺に改良すべきことがあります。今回の話でcsvの開発に興味がでてきた人は一緒に改良してきましょう。その気になった人はRed Data ToolsのGitterに書き込んでください。どうやって進めるか相談しましょう。

RubyKaigi 2019でまわりの人たちと相談してstrscanのメンテナンスを引き取ることにしたのでそのあたりの改良もできます。

RubyData Workshop

発表の後、RubyData WorkshopでもRed Data Toolsの開発に参加する人を募りました。すでに数人Red Data ToolsのGitterに書き込んでいる人もいます。やったね!

まとめ

RubyKaigi 2019で最近のcsvの開発の成果を自慢しました。今回は2人での発表だったのでやいのやいのした発表をしました。

Red Data Toolsの仲間が増えたのでよいRubyKaigiでした。

タグ: Ruby
2019-04-20

Ruby 2.6.0とtest-unitとデータ駆動テスト

Rubyのbundled gemのtest-unitをメンテナンスしている須藤です。

歴史

test-unitはxUnitスタイルのテスティングフレームワークです。Rubyのテスティングフレームワークの歴史(2014年版)にまとめてある通り、Ruby本体に標準添付されています。

Rubyに標準添付されているライブラリーには実は次の3種類あります。

  • ただの標準添付ライブラリー(例:URI
    • requireするだけで使えるライブラリー
  • default gem(例:csv)
    • requireするだけで使えるライブラリー
    • RubyGemsで更新できる
    • Gemfileでgemを指定しなくても使える
  • bundled gem(例:test-unit)
    • requireするだけで使えるライブラリー
    • RubyGemsで更新できる

どれも標準添付ライブラリーなのでrequireするだけで使えます。違いはRubyGems・Bundlerとの関係です。

ただの標準添付ライブラリーはRubyGemsでアップグレードすることはできませんし、Bundlerで特定のバージョンを指定することもできません。使っているRubyに含まれているものを使うだけです。

default gemはRubyGemsでアップグレードすることもできますし、Bundlerで特定のバージョンを指定することもできます。Bundlerを使っていてgem名を指定しなかった場合は使っているRubyに含まれているものを使います。

bundled gemはRubyGemsでアップグレードすることもできますし、Bundlerで特定のバージョンを指定することもできます。Bundlerを使っていてgem名を指定しなかった場合は使えません。Bundlerを使っていなければrequireするだけで使えます。

Ruby 2.6.0でより高速になったcsvはRuby 2.6.0からdefault gemになっています。

test-unitはRuby 2.2.0で再度標準添付されるようになってからbundled gemになっています。

そんなtest-unitのデータ駆動テスト機能をさらに便利にしたものがRuby 2.6.0に入っています。

データ駆動テスト

データ駆動テストとは同じテスト内容をいろいろなデータで実行するテスト方法です。パラメーター化テストと呼ばれることもあります。いろいろな入力に対するテストを簡潔に書きたいときに便利です。

test-unitでは結構前からデータ駆動テストをサポートしています。

たとえば、正の数同士の足し算と負の数同士の足し算をテストすることを考えます。データ駆動テスト機能を使わない場合は次のようにそれぞれのケースについてテストを作ります。

require "test-unit"

class TestAdd < Test::Unit::TestCase
  def test_positive_positive
    assert_equal(3, my_add(1, 2))
  end

  def test_negative_negative
    assert_equal(-3, my_add(-1, -2))
  end
end

データ駆動テスト機能を使う場合はテストは1つで、テストに使うデータを複数書きます。

require "test-unit"

class TestAdd < Test::Unit::TestCase
  data("positive + positive", [3, 1, 2])
  data("negative + negative", [-3, -1, -2])
  def test_add(data)
    expected, augend, addend = data
    assert_equal(expected, my_add(augend, addend))
  end
end

データが増えてくるほど、データ駆動テスト機能を使った方がテストを書きやすくなります。データを追加するだけで済むからです。ただ、読みやすさは従来のテストの方が上です。テストに使うデータがベタ書きされているからです。

test-unitのデータ駆動テスト機能をもっと知りたくなった人はRuby用単体テストフレームワークtest-unitでのデータ駆動テストの紹介を参照してください。

データ表生成機能

Ruby 2.6.0に入っているtest-unitではデータ駆動テストがさらに便利になっています。

まだなんと呼ぶのがよいか決めかねているのですが、今のところデータ表(data matrix)と呼んでいるものを生成する機能が入っています。

データ表というのは各テストで使うデータをまとめたものです。前述のテストの場合は次のようになります。dataを使う毎に1行増えます。

ラベル expected augend addend
"positive + positive" 3 1 2
"negative + negative" -3 -1 -2

このデータ表をいい感じに生成する機能が入っています。

前述のテストで正の数と負の数を足す場合もテストしたくなったとします。その場合、従来のデータ駆動テスト機能の書き方では次のように書きます。dataを2つ増やしています。

require "test-unit"

class TestAdd < Test::Unit::TestCase
  data("positive + positive", [3, 1, 2])
  data("negative + negative", [-3, -1, -2])
  data("positive + negative", [-1, 1, -2]) # 追加
  data("negative + positive", [1, -1, 2])  # 追加
  def test_add(data)
    expected, augend, addend = data
    assert_equal(expected, my_add(augend, addend))
  end
end

データ表は次のようになります。

ラベル expected augend addend
"positive + positive" 3 1 2
"negative + negative" -3 -1 -2
"positive + negative" -1 1 -2
"negative + positive" 1 -1 2

データ表生成機能を使うと次のように書けます。dataの第一引数にSymbolを指定しているところがポイントです。テストに渡されるデータはHashになっていてキーがシンボルで値が対象データです。

require "test-unit"

class TestAddDataMatrix < Test::Unit::TestCase
  data(:augend, [1, -1])
  data(:addend, [2, -2])
  def test_add(data)
    augend = data[:augend]
    addend = data[:addend]
    assert_equal(augend + addend,
                 my_add(augend, addend))
  end
end

これで次のデータ表を生成できます。

ラベル augend addend 備考
"addend: 2, augend: 1" 2 1 正+正
"addend: 2, augend: -1" 2 -1 正+負
"addend: -2, augend: 1" -2 1 負+正
"addend: -2, augend: -1" -2 -1 負+負

期待する結果(expected)は生成できないのでRuby組み込みのInteger#+の結果を使っています。これは実は大事なポイントです。データ表生成機能を使えるのは次の場合だけです。

  • 期待する結果がデータに依らず一意に定まる
  • データから期待する結果を計算できる

今回の場合は期待する結果を計算できるので使えました。

なお、期待する結果は必ずしも正しい結果を返すはずの既存の実装(今回の場合はInteger#+)を使わなくても大丈夫です。次のように「エンコードしてデコードしたら元に戻る」ようなときでもデータ表生成機能を使えます。これは性質をテストしているケースです。(性質をテストすることについてはここを参照してください、とか書いておきたいけど、どこがいいかしら。)

assert_equal(raw_data,
             decode(encode(raw_data)))

この例ではパラメーターはaugendaddendの2つでそれぞれに正と負があるので、4パターンでしたが、パラメーター数が増えたりバリエーションが増えると一気にパターンが増えます。そのときはこのデータ表生成機能が便利です。

なお、この機能はRed Chainer(Rubyだけで実装しているディープラーニングフレームワーク)で使うために作りました。もともとRed Chainerのテスト内でデータ表を生成していたのですがこの機能を使うことでだいぶスッキリしました。

データを使い回す

実はRed Chainerのテストをスッキリさせるためにはデータ表を生成するだけでは機能が足りませんでした。同じデータ表を複数のテストで共有する機能が必要でした。

前述の例で言うと、同じデータ表を足し算のテストでも引き算のテストでも使いたいという感じです。コードで言うと、以下をもっといい感じに書きたいということです。

require "test-unit"

class TestCalc < Test::Unit::TestCase
  data(:number1, [1, -1])
  data(:number2, [2, -2])
  def test_add(data)
    number1 = data[:number1]
    number2 = data[:number2]
    assert_equal(number1 + number2,
                 my_add(number1, number2))
  end

  data(:number1, [1, -1])
  data(:number2, [2, -2])
  def test_subtract(data)
    number1 = data[:number1]
    number2 = data[:number2]
    assert_equal(number1 - number2,
                 my_subtract(number1, number2))
  end
end

そこで、dataメソッドにkeep: trueオプションを追加しました。これで一度dataを書けば後続するテストでも同じデータを使うようになります。

require "test-unit"

class TestCalc < Test::Unit::TestCase
  data(:number1, [1, -1], keep: true) # keep: trueを追加
  data(:number2, [2, -2], keep: true) # keep: trueを追加
  def test_add(data)
    number1 = data[:number1]
    number2 = data[:number2]
    assert_equal(number1 + number2,
                 my_add(number1, number2))
  end

  # ここにdataはいらない
  def test_subtract(data)
    number1 = data[:number1]
    number2 = data[:number2]
    assert_equal(number1 - number2,
                 my_subtract(number1, number2))
  end
end

データ表を複数生成する

実はRed Chainerのテストをスッキリさせるためにはデータを使い回せても機能が足りませんでした。1つのテストに対して複数のデータ表を生成する機能が必要でした。

前述の例で言うと、小さい数同士と大きい数同士で別のデータ表を作りたい、ただし、小さい数と大きい数の組み合わせはいらないという感じです。(わかりにくい。)

データ表で言うと次の2つのデータ表を使う感じです。

小さい数用のデータ表:

内容 augend addend
小さい正 + 小さい正 2 1
小さい正 + 小さい負 2 -1
小さい負 + 小さい正 -2 1
小さい負 + 小さい負 -2 -1

大きい数用のデータ表:

内容 augend addend
大きい正 + 大きい正 20000 10000
大きい正 + 大きい負 20000 -10000
大きい負 + 大きい正 -20000 10000
大きい負 + 大きい負 -20000 -10000

コードで言うと、以下をもっといい感じに書きたいということです。

require "test-unit"

class TestAdd < Test::Unit::TestCase
  data(:augend, [1, -1])
  data(:addend, [2, -2])
  def test_add_small(data)
    augend = data[:augend]
    addend = data[:addend]
    assert_equal(augend + addend,
                 my_add(augend, addend))
  end

  data(:augend, [10000, -10000])
  data(:addend, [20000, -20000])
  def test_add_large(data)
    augend = data[:augend]
    addend = data[:addend]
    assert_equal(augend + addend,
                 my_add(augend, addend))
  end
end

そこで、dataメソッドにgroup:オプションを追加しました。同じグループ毎にデータ表を生成します。

require "test-unit"

class TestAdd < Test::Unit::TestCase
  data(:augend, [1, -1], group: :small) # 小さい数用
  data(:addend, [2, -2], group: :small) # 小さい数用
  data(:augend, [10000, -10000], group: :large) # 大きい数用
  data(:addend, [20000, -20000], group: :large) # 大きい数用
  def test_add(data)
    augend = data[:augend]
    addend = data[:addend]
    assert_equal(augend + addend,
                 my_add(augend, addend))
  end
end

setupでもデータを参照可能にする

実はRed Chainerのテストをスッキリさせるためにはデータ表を複数作れても機能が足りませんでした。テスト実行中にデータを参照しやすくする機能が必要でした。

従来のデータ駆動テスト機能ではテストメソッドの引数でデータを渡していました。そのため、setup中でデータを参照できませんでした。

require "test-unit"

class TestAdd < Test::Unit::TestCase
  def setup
    # ここでデータを参照できない
  end

  data(:augend, [1, -1])
  data(:addend, [2, -2])
  def test_add(data)
    augend = data[:augend]
    addend = data[:addend]
    assert_equal(augend + addend,
                 my_add(augend, addend))
  end
end

Red Chainerのテストではデータを前処理したかったので次のように明示的に前処理メソッドを呼んでいました。

require "test-unit"

class TestAdd < Test::Unit::TestCase
  def my_setup(data)
    # 前処理
  end

  data(:augend, [1, -1])
  data(:addend, [2, -2])
  def test_add(data)
    my_setup(data)
    augend = data[:augend]
    addend = data[:addend]
    assert_equal(augend + addend,
                 my_add(augend, addend))
  end
end

これは微妙なのでdataでデータを参照できるようにしました。

require "test-unit"

class TestAdd < Test::Unit::TestCase
  def setup
    p data # データを参照できる!
  end

  data(:augend, [1, -1])
  data(:addend, [2, -2])
  def test_add(data)
    augend = data[:augend]
    addend = data[:addend]
    assert_equal(augend + addend,
                 my_add(augend, addend))
  end
end

また、テストでも引数でデータを受け取らなくてもよくなりました。(従来どおり受け取ってもよいです。)

require "test-unit"

class TestAdd < Test::Unit::TestCase
  def setup
    p data # データを参照できる!
  end

  data(:augend, [1, -1])
  data(:addend, [2, -2])
  def test_add # test_add(data)としなくてもよい!
    augend = data[:augend]
    addend = data[:addend]
    assert_equal(augend + addend,
                 my_add(augend, addend))
  end
end

まとめ

Red Chainerのためにtest-unitにデータ表生成機能を追加しました。Ruby 2.6.0にもこの機能を使えるtest-unitが入っています。ぜひ活用してください。

なお、Ruby 2.6.0でなくてもRubyGemsで新しいtest-unit(3.2.9以降)にアップグレードすれば使えます。Red Chainerでもそうやって使っています。

Red Chainerの開発に参加したい人はRed Data Toolsに参加してください。オンラインのチャット東京で毎月開催している開発の集まり(次回は2018年1月22日)でどうやって進めていくか相談しましょう。

タグ: Ruby
2018-12-26

Ruby 2.6.0とより高速なcsv

Rubyの標準添付ライブラリーのcsvをメンテナンスしている須藤です。

歴史

csvは名前の通りCSVを読み書きするための便利ライブラリーです。

もともとRuby本体とは別に開発されていたのですが、Ruby 1.8.0のときにRuby本体にバンドルするようになりました。dRubyやREXMLがRuby本体にバンドルされたのも同じタイミングです。Ruby 1.8.0のときにバンドルするライブラリーをすごく増やしたのです。(その頃の様子がわかるURLをここに置いておきたかったけど見つけられなかった。。。)

Rubyではcsvのようにrequireするだけで使えるライブラリーを「標準添付ライブラリー」と呼んでいます。Stringのようにrequireしなくても使えるライブラリーは。。。なんだろう。組み込みクラスかしら。

その後、Ruby 1.9.0のタイミングで実装をFasterCSVに置き換えました。FasterCSVは名前の通りもともとのcsvよりも速いライブラリーです。もともとのcsvもFasterCSVもRubyだけで実装してあり、Cを使っていません。Rubyで実装したCSVライブラリーでは最速です。今のcsv(FasterCSVベースのcsv)よりも速いといっているCSVライブラリーはCを使っているはずです。

そんなcsvをさらに速くしたものがRuby 2.6.0に入っています。

FasterCSV実装がなぜ速いか

FasterCSVがなぜ速いかというと各行をline.split(",")でパースしているからです。String#splitはCで実装されているので速いのです。

ただ、世の中にはline.split(",")でパースできないCSVがたくさんあります。たとえば、次のようなCSVです。

a,"b,c",d

このCSVではダブルクォートで囲んでいる中にコンマがあるのでline.split(",")では次のようにパースしてしまいます。

[
  "a",
  "\"b",
  "c\"",
  "d",
]

このようなケースにも対応するために、FasterCSVはline.split(",")した後の各要素のダブルクォートの数を数えます。ダブルクォートの数が偶数ならダブルクォートの対応が取れていて、奇数なら取れていないというわけです。ダブルクォートの対応が取れていない場合は後続する要素と連結します。

このようにして速さを維持したまま複雑なCSVもパースできるようになっています。ただ、複雑なCSVをパースするときは速度が落ちてしまいます。次の表はcsvが使っているベンチマークを使ったパース性能の計測結果です。複雑になるほど性能が落ちている(単位時間あたりでのパース回数が減っている)ことがわかります。

100msでのパース回数
ダブルクォートなし 373
ダブルクォートあり 207
ダブルクォート中にコンマあり 140
ダブルクォート中に改行あり 82

Ruby 2.6.0に入っているcsvでは次のようになります。「ダブルクォートあり」の場合は少し性能が落ちています(207から194に減っている)が、ダブルクォート内が複雑になっても「ダブルクォートあり」と性能が変わりません。(「コンマあり」と「改行あり」が193と192で194とほとんど変わらない。)「ダブルクォートなし」の場合は少し性能があがっています。(373から401に増えている)

100msでのパース回数
ダブルクォートなし 401
ダブルクォートあり 194
ダブルクォート中にコンマあり 193
ダブルクォート中に改行あり 192

Ruby 2.6.0のcsvがなぜ速いか

「最速」だったcsvがどうやってさらに速くなったかというとStringScannerを使うようになったからです。StringScannerは標準添付ライブラリーの1つで、正規表現を使って高速に文字列をスキャンできます。

ただ、単にStringScannerを使っても「最速」だったcsvよりも速くはなりません。line.split(",")は強敵です。@284kmが取り組んだ、まずline.split(",")StringScannerに置き換えるpull requestでも全体的に遅くなっています。ただ、これでも高速にするための工夫をした後の結果です。Red Data Toolsの開発の集まりなどで@284kmと一緒に高速にするための書き方を模索していました。その結果、次の知見を得ました。

  • どの正規表現を使ってどの順番でスキャンするかが重要

StringScannerを使ったコードは次のようなコードになります。ポイントは「次はこういう値が来るはず、来なかったらエラー」というのをつなげていくところです。

row = []
scanner = StringScanner.new(line)
column_value = scanner.scan(/[^",\r\n]+/) # カラムの値
raise "no column value" unless column_value
row << column_value
raise "no comma" unless scanner.scan(/,/) # カラムの区切り文字(コンマ)
column_value = scanner.scan(/[^",\r\n]+/) # 次のカラムの値
raise "no column value" unless column_value
row << column_value
raise "no comma" unless scanner.scan(/,/) # カラムの区切り文字(コンマ)
raise "extra data" unless scanner.eos?    # すべてのデータを使ったか
p row

CSVのように複雑なものだと、「次はこういう値が来るはず、来なかったら別のこの値なはず」というようにフォールバックしていきます。たとえば、「ダブルクォートで囲まれていない値があるはず、なかったらダブルクォートで囲まれた値のはず」といった具合です。

line.split(",")を超える性能を出すためにはフォールバックをいかに減らすかが大事になります。フォールバックのオーバーヘッドがあるとline.split(",")に負けてしまうのです。

フォールバックを減らすには「次はこういう値が来るはず」ができるだけ当たるような順番にします。カラムの値の次はコンマがきやすいので、次の2つでは後者の方がフォールバックの回数が減ります。

カラムの値もコンマも並列に扱う(フォールバックが多い):

row = []
column_value = nil
until scanner.eos?
  if scanner.scan(/[^",\r\n]+/) # カラムの値
    column_value = scanner.matched
  elsif scanner.scan(/,/) # コンマ
    row << column_value
    column_value = nil
  else
    raise "invalid"
  end
end
row << column_value if column_value
p row

カラムの値の後はコンマがくるはず(フォールバックが少ない):

row = []
until scanner.eos?
  if (column_value = scanner.scan(/[^",\r\n]+/)) # カラムの値
    if scanner.scan(/,/) or scanner.eos?
      row << column_value
    else
      raise "no comma"
    end
  else
    raise "invalid"
  end
end
p row

line.split(",")に勝つには正規表現のマッチ回数をいかに減らすかを頑張る必要があります。これが基本的なコンセプトです。それではさらに具体的な方法を説明していきます。

行ごとの処理をやめる

line.split(",")ベースのアプローチでは次のようにダブルクォート中が複雑になる処理で性能劣化が大きかったです。

100msでのパース回数
ダブルクォートあり 207
ダブルクォート中にコンマあり 140
ダブルクォート中に改行あり 82

これを解決するために行に分割してから処理することをやめました

行に分割せずに、ダブルクォート中がどうなっていても(たとえば改行文字を含んでいても)統一的に処理することで性能劣化を防ぎました。

100msでのパース回数
ダブルクォートあり 194
ダブルクォート中にコンマあり 193
ダブルクォート中に改行あり 192

これが一番大変でした。というのは、パースするロジックをすべてStringScannerらしく書き換える必要があるからです。

書き換えた後は次のようなコードになりました。すっきりですね。

row = []
while true
  value = scanner.scan(/[^",\r\n]+/)
  if scanner.scan(/,/)
    row << value
  elsif scanner.scan(/\r\n/)
    row << value
    p row
    row = []
  elsif scanner.eos?
    row << value
    p row
    return
  else
    raise "invalid"
  end
end

これでダブルクォートを使っていても性能劣化しなくまりました。(ダブルクォート中に改行がある方が速くなっているのはなぜだ。。。)

100msでのパース回数
ダブルクォートあり 165
ダブルクォート中にコンマあり 160
ダブルクォート中に改行あり 187

これを実現することによりコードをメンテナンスしやすくなり、最適化や機能追加をしやすくなりました。line.split(",")ベースのコードも200行未満の実装なのでそんなに長すぎるわけではないのですが、状態が多くて適切な場所に適切な処理を入れるのが難しかったのです。

以前と同じくらいの性能にできればStringScannerベースのパーサーで開発を進められます。

loopwhile trueにする

性能改善の大きなポイントは正規表現のマッチ回数を減らすことですが、それ以外の部分でも少しずつ性能改善できます。その1つでやりやすいものがloop do ... endではなくwhile true ... endでループするようにすることです。

Rubyを使っている場合はdo ... endを使いたいので私は普段は次のようにループを書きます。

loop do
  # ...
end

しかし、今回のように性能改善したいケースでは次のようにwhile true ... endを使った方が高速です。これはloopだとdo ... endの中でスコープが変わるのでその準備をしないといけないのに対し、whileはスコープが変わらないのでその準備が必要ないからです。

while true
  # ...
end

csvのケースではwhile trueの方が15%ほど高速です。

100msでのパース回数:

ダブルクォートなし ダブルクォートあり
loop 377 166
while 401 192
string[start...end]string[start, end - start]にする

Stringには文字列データの一部を共有する機能があるため、既存のStringの一部で必要なStringを作れる場合は共有機能を使うことで高速になります。

文字列データを共有するにはString#[]を使います。String#[]は便利なメソッドでいろんな引数を受けつけます。たとえば、次の2つは同じ結果を返します。

string[1...6]
string[1, 5]

ただし、string[1, 5]の方が速いです。これは、引数が2つの場合は特別扱いされているためです。

csvの場合、string[1, 5]のスタイルを使った場合の性能改善の度合いは軽微です。

100msでのパース回数
string[1...6] 405
string[1, 5] 409
必要になるまで処理を遅らせる

みなさんはCSV#lineというメソッドがあるのを知っていますか?私は知りませんでした。このメソッドは最後に処理した行そのもの(パース前の行)を返します。普通はパース結果だけを使うので、この処理のために通常のパース処理が遅くなるのは微妙です。そのため、このための情報を必要になったときにはじめて取得するように変更しました。

100msでのパース回数
#line用のデータを逐次処理 409
#line用のデータを遅延処理 416

微妙に速くなっています。

通常は必要ない処理を必要になるまで処理しないことによる高速化はCSVの書き込み処理で顕著です。

CSVはCSVを読み書きできるのでインスタンスを作るときに読み書き両方用の初期化をしていました。そのため、CSVを読むだけ、書くだけのときに余計な処理をしていました。

この読む用の初期化・書く用の初期化を必要になるまで遅延するようにしました。これにより、読むだけのときは書く用の初期化を一切しなくなり、高速になります。

以下は書き込み処理のベンチマーク結果です。

100msでの処理回数:

CSV.generate_line CSV#<<
読む用の初期化を毎回実行 350 2177
読む用の初期化を遅延実行 850 3506

2倍ほど速くなっています。CSV.generate_lineCSVオブジェクトを作らずに1行生成する便利機能ですが、たくさんの行を生成する時はCSVオブジェクトを作った方が高速です。これは、CSV.generate_lineの場合は1行生成する度に書く用の初期化を毎回しなければいけないためです。

つまり、次のように書くのは遅いということです。

rows.each |row|
  puts CSV.generate_line(row)
end

それよりは次のように書いた方が速いです。

output = ""
csv = CSV.new(output)
rows.each |row|
  csv << row
end
puts output
String#each_charではなくString#indexを使う

読む用の初期化時に改行文字を自動検出しているのですが、そこも高速化できた処理でした。

従来はString#each_charで一文字ずつ確認していたのですが、そこをString#indexを使って書き換えました。次のような感じです。

cr_index = sample.index("\r")
lf_index = sample.index("\n")
if cr_index and lf_index
  if cr_index + 1 == lf_index
    "\r\n"
  elsif cr_index < lf_index
    "\r"
  else
    "\n"
  end
elsif cr_index
  "\r"
elsif lf_index
  "\n"
else
  :auto
end

String#indexを使うとCレベルで文字チェックをできるので速くなりました。(後でどのくらい速くなったか追記できるといいな。)

なんか野暮ったいコードなのでもう少しシュッとできるといいですね。

効果がなかった高速化

これで速くなるんじゃないかと試したものの逆に遅くなったアイディアもありました。

特化メソッドを持つモジュールをextend

csvにはまじめにパースするモードとゆるくパースするモードがあります。従来はメソッド内でifで分岐していました。インスタンス作成時にモードにあわせたモジュールをextendしてパース時はメソッド内のifを減らすと速くなるのではないか、という案です。こんな感じです。

module StrictMode
  def parse
    # ...
  end
end

module LiberalMode
  def parse
    # ...
  end
end

class CSV::Parser
  def initialize(options)
    if @options[:liberal_mode]
      extend LiberalMode
    else
      extend StrictMode
    end
  end
end

CSV::Parser.new(:liberal_mode).parse # LiberalMode#parse

実際にやってみてところむしろ遅くなりました。メソッド内でifで分岐する方が速かったです。

さらに速いCSVパーサー

csvはRubyレベルで実装してあるCSVパーサーでは最速です。さらに速くするにはCで実装する必要があります。たとえば、Cを使っているfastest-csvはcsvよりも数倍高速です。

100msでのパース回数
csv 16
fastest-csv 76

なお、私のオススメはApache Arrowです。Apache ArrowはCSV用のライブラリーではありませんが、CSVパーサーもついています。Apache ArrowのCSVパーサーを使うとfastest-csvよりもさらに数倍高速です。

100msでのパース回数
csv 16
fastest-csv 76
Apache Arrow 223

使い方も簡単です。次のコードでCSVをロードできます。

require "arrow"
Arrow::Table.load("/tmp/a.csv")

参考:Apache Arrowの最新情報(2018年9月版)

今後

コードを整理でき、最適化・機能拡張の準備ができました。たとえば、次のような改良をしていきたいです。興味がある人は一緒に開発しましょう。

  • バックスラッシュでダブルクォートをエスケープ #61
  • クォート文字を指定しなかったらline.split(",")を使う高速化 #56
  • ヘッダーがあるときの高速化 #59

まとめ

Ruby 2.6.0にあわせてcsvのコードを整理して高速化しました。より開発しやすいコードベースになったので一緒に開発していきましょう。

リリース直前にいろいろ変更をぶちこんでごめんなさい。

タグ: Ruby
2018-12-25

RubyData Tokyo Meetup - Apache Arrow #RubyData_tokyo

Apache ArrowのC・Ruby・パッケージ関連を主に開発している須藤です。

RubyData Tokyo MeetupでApache ArrowのRubyまわりの最新情報を紹介しました。

関連リンク:

内容

(いつ頃か忘れましたが)前にApache ArrowのRubyまわりを紹介した時はデータ交換まわりの話がメインでした。それは、データ交換まわりの実装しかなかったからです。

しかし、最近はデータ処理まわりの実装も進んできたので、そのあたりのことも盛り込みました。たとえば、素のRubyの機能で数値演算する場合と、Numo::NArrayを使って数値演算する場合と、Gandiva(Apache Arrowの式処理モジュール)を使って数値演算する場合のコードとベンチマーク結果を紹介しました。

私のマシンで計測したところNumo::NArrayが一番高速でした。Numo::NArrayすごい!発表中、@sonotsさんがNumPyの方がさらに速いと思うけどねーと言いながら同じパターンをNumPyでも計測していました。計測したところ、NumPyよりもNumo::NArrayの方が速く、@naitohさんもその場で計測したところ、確かに速かったです。この内容はその後の@naitohさんの発表に盛り込まれています。発表をきっかけに新たな事実の発見が進むなんていい集まりですね!

他には最近Apache Arrowで実装が進んでいるCSVパーサーが速いよ!ということを自慢したりしました。

集まりに関して

今回の集まりはとてもいい集まりだなぁと思えるいい集まりでした。

@mrknさんがポジティブな話をするようになっていたのもよかったですし、Juliaバックエンド案は面白いなぁと思いました。

@shiro615さんのOSS GateワークショップでOSSの開発に参加しはじめて、Red Data Toolsで継続的にApache Arrowの開発に参加し続けて、この間コミッターになった、という話は感慨深かったです。OSS GateもRed Data Toolsもはじめてよかったな。

@hatappiさんがイベント中にRed ChainerのCumo対応ブランチをマージしていたのもよかったです。@sonotsさんの発表で変更の概要を聞いて、発表の後のコード懇親会で直接相談しながらマージ作業を進めていました。開発が進むなんて、なんていい集まりなんでしょう。

@sonotsさんはこのイベントがあったからCumo対応プルリクエストを作ったと言っていました。開発が進む集まり!

@colspanさんのMenoh-RubyとFluentdを使って推論サーバーを作る話は面白いなぁと思いました。なるほどなぁ。

Red Data ToolsとしてもMenohとMenoh-Rubyを応援していきたいので、いい感じに協力できないか少し相談しました。11月20日(火)の夜のOSS Gate東京ミートアップ for Red Data Tools in Speeeで続きを相談できそうです。

@v0droさんの発表でXND関連の理解が深まりました。調べないとなぁと思っていたんですよねぇ。型を文字列で定義するのは、いいのかな、悪いのかな。まだ判断できないんですが、面白いアプローチだなぁとは思いました。

Red Data ToolsとしてもXND関連の開発に協力していきたいな。

まとめ

2018年11月17日にRubyData Tokyo Meetupという開発が進むいい集まりがありました。

Rubyでもっといい感じにデータ処理できるようになるといいなぁ思った人は次のアクションとして以下を検討してみてください。

タグ: Ruby
2018-11-20

Windows Subsystem for LinuxでプレゼンツールのRabbitを動かす

皆さんはRabbitというプレゼンテーションツールをご存じでしょうか。2018年のRubyKaigiでのMatz氏によるKeynoteでも使われており、「名前は知らないが見た事はある」という方もいらっしゃるかもしれません。

RabbitはRubyで開発されており、Git等でバージョン管理しやすいMarkdown形式やRD形式などのプレーンテキスト形式でスライドを記述できたり、「スライドの進行割合」を表すウサギと「時間の経過度合い」を表すカメのアイコンでスライドの進行状況を把握できたりと、痒い所に手が届くツールです。

RabbitはWindowsでも動作するのですが、開発は主にLinux上で行われているため、Windows上では動作が不安定だったり表示が崩れたりと、期待通りの動作結果にならない場合があります。このような場面に出くわした場合、Windows上での動作を改善するためのフィードバックやパッチの作成に挑戦してみるいい機会なのですが、発表の日程が差し迫っているためそこに時間をかけられない、というような場合もあるでしょう。

一般的に、有志の個人開発者によって開発・メンテナンスされているフリーソフトウェアは、開発者自身が日常的に使っている環境に近い環境で最も安定して動作します。そういう意味では、仮想マシンを用意したり、使用していないPCを用意したりしてLinuxディストリビューションをインストールしたりという形をとるのが常道なのですが、この変形として、Windows 10であればWSL(Windows Subsystem for Linux)とWindows用のXサーバーを組み合わせるという方法が使えます。

以下、本記事での解説は、WSLのLinuxディストリビューションとしてUbuntuを使用している状況を前提とする事にします。

WSLでRabbitが動く理由

WSLは、Windowsのカーネルに対して、Linuxカーネルのように振る舞うための層を被せることで、UbuntuやFedoraなどのディストリビューションで提供されているビルド済みバイナリをそのまま動作させられるようにする技術です(動作イメージ、導入手順はまんがでわかるWSLなどをご覧ください)。

画面描画の基盤技術はWindowsとLinuxで顕著に異なる部分であるため、Windows向けにRabbitやその依存ライブラリ群を完全対応するのは非常に大変な作業です。しかし、WSLであれば基盤部分はLinux用の物がそのまま使われるため、Rabbitのように主にLinux上で開発されているGUIアプリは、「Windows用の移植版」を動かすよりも「WSL上でLinux用のオリジナル版」を動かした方が良い結果を得られる場合がままあります。

ただし、そのために1つだけ欠かせない物があります。それがWindows用のXサーバーです。

Windows Subsystem for Linuxで使えるXサーバー

X(X Window System)は、現在多くのLinuxディストリビューションで一般的に使われている、GUIを実現するための最も重要な基盤技術です。「物理的な画面をキャンバスとして管理し、各ウィンドウを描画する」「ユーザーのクリック操作やキー入力などの操作を一元的に受け付けて、情報を各アプリケーションに引き渡す」といった事を行う物なのですが、現在の所WSL上ではXサーバーは動作しません。何故かというと、WindowsはWindowsでXとは別の画面描画の仕組みを持っており、両方が同時に動作するとリソースの奪い合いになってしまう(マウスやキーの入力をWindowsが受け取ればXが受け取れなくなるし、Xがそれらを受け取ればWindowsが受け取れなくなる)からです。

そこで登場するのがWindows用のXサーバーです。これはWindowsアプリケーションの1つとして振る舞いつつ、一般的なXサーバーと同じ機能を提供する(アプリケーションがXに対して指示した位置にWindowsのウィンドウを開き、そのウィンドウに対して操作が行われた場合はXが操作を受け付けたものとしてアプリケーションに情報を引き渡す、という働きをする)物です。VcXsrv(※リンク先はダウンロードページ)はその代表的な例で、VcXsrvを起動しておき、WSL上のアプリケーションに対してVcXsrvをXサーバーとして使うよう指示する事により、WSL上のGUIアプリケーションがWindows上でも動作するようになります。

という事で、まずはリンク先のページのダウンロード用ボタンからVcXsvrのインストーラをダウンロードして、インストールしましょう。

VcXsrvを初めて起動すると、「Display settings」というタイトルのウィザードが表示されます。ここではとりあえず以下のように設定して下さい。

  1. 「Multiple Windows」を選んで、「次へ」を押す。
  2. 「Start no client」を選んで、「次へ」を押す。
  3. オプションは変えず、「次へ」を押す。
  4. 「完了」を押す。
  5. WindowsのファイアウォールがVcXsvrによるネットワーク接続に対して警告を出すので、接続を許可する。

初期設定を終えると、VcXsrvのアイコンがタスクトレイに表示されます。VcXsvrを使わないときは、このタスクトレイ上のアイコンから終了させておくと良いでしょう。

VcXsvrのアイコンがタスクトレイに表れている様子

Windows上のXサーバーを使うように、WSLの環境を設定する

Windows上でXサーバーが動作しているだけでは、WSL上のGUIアプリケーションは動作しません。WSL上のGUIアプリケーションに対して、Windows上のXサーバーを使うように指示する必要があります。これは以下のようにすれば実現できます。

  1. WSLのUbuntuが提供するBashを起動する。
  2. echo 'export DISPLAY=localhost:0.0' >> ~/.bashrc と実行する。
  3. exitでWSL上のBashを終了し、再度WSL上のBashを起動する。

この操作により、Bash起動時に読み込まれる初期化用の設定ファイルに、入出力用の画面としてWindows上のXサーバーが提供する仮想的な画面を使うように指示するための指定が追記され、それが反映された状態でBashが起動します。

Rabbitのインストール

Xサーバーの準備ができたら、いよいよRabbitのインストールです。Rabbitは、プラットフォームのパッケージ群と、RubyGemsのパッケージ群の2段階に分けてインストールする必要があります。

  1. Rabbitの実行に必要なパッケージ群をインストールする。 sudo apt install rubygems ruby-dev build-essential fonts-ipafont で一通りインストールされる。
    (フォントはfonts-notoなど他の選択肢もありますが、行の高さの違いが原因で表示が崩れてしまう事があるので、安全のためにはfonts-ipafontをインストールする事をお勧めします。)
  2. Rabbitをインストールする。 sudo gem install rake rabbit で必要なGemパッケージ群が一通りインストールされる。
    • この時、Windowsのファイアウォールがgemコマンドによるネットワークへの接続に対して警告を出すので、明示的に許可を与える。
    • 2019年4月11日追記:新しい環境で試行した際に、何故か依存パッケージがインストールされず、Markdown形式のファイルを開こうとすると[警告] サポートしていない形式です。(サポートしている形式: [Wiki, PDF, image, RD])というエラーが表示される結果となる事がありました。RabbitでMarkdown形式を取り扱うためにはkramdownkramdown-parser-gfmの両方が必要なので、どちらかがインストールされていないとこのエラーが表示されますので、パッケージの有無を確認し、無ければ追加でインストールして下さい。

以上でRabbitのインストールは完了です。

スライドの作成と実行

Rabbit用のスライドは、前述したとおりRDやMarkdownなどの形式で作成できます。試しに、以下のような内容でファイルを作成し、Windowsのデスクトップ上にsample.mdのような名前で保存して下さい。文字エンコーディングや改行コードは自動判別されますが、安全のためにはLinux上で一般的な「UTF-8(BOM無し)、改行コードLF」を使用する事をお勧めします。

# Rabbitでプレゼン

subtitle
:   WSL上のRabbitで表示

author
:   自分の名前

institution
:   所属会社

allotted_time
:   45m

# 準備

 * Xサーバーのインストール
 * Rabbitのインストール

# おわり

ご静聴ありがとうございます

Markdownファイルができたら、ファイルを置いたディレクトリーに移動し、ファイル名を引数に指定してrabbitコマンドを実行しましょう。RabbitのウィンドウがWindows上で開かれ、スライドの1ページ目が表示されます。

$ cd /mnt/c/Users/(ログオン中のユーザーアカウント名)/Desktop
$ rabbit ./sample.md

WSL上のRabbitのウィンドウがWindowsのウィンドウとして表示されている様子

Rabbitのスライドは、Enterキーで次のページに進み、BackSpaceキーで前のページに戻ります。サンプルを見ると分かる通り、最大レベルの見出しがそのままスライドの各ページのタイトルになります。詳しくはMarkdown形式でのスライドの書き方を参照してください。

ウィンドウの最下部のウサギは現在スライドの何ページ目が表示されているかを示しています。allotted_timeで時間を指定してある場合(この例では「45分」という意味になります)、その時間に合わせてウィンドウの最下部をカメが進んでいくようになります。カメが先行していれば進行が遅れ気味、ウサギが先行していれば逆に走り気味という事になります。

Rabbitのスライド下部に表示されているウサギとカメ

まとめ

以上、Windows上でWSL経由でRabbitを動作させる手順をご紹介しました。

Rabbitのサイトでは、Rabbitで作成された様々なスライドの例が公開されています。どんなスライドを作れるのか、ぜひ参考にしてみて下さい。

タグ: Ruby
2018-07-27

RubyKaigi 2018 - My way with Ruby #rubykaigi

RubyKaigiの2日目のキーノートスピーカーとして話した須藤です。今年もクリアコードはシルバースポンサーとしてRubyKaigiを応援しました。

関連リンク:

なお、RubyKaigi 2018に合わせてRabbit Slide Showをレスポンシブ対応したので、画面が小さな端末でも見やすくなりました。

内容

例年の内容よりキーノートっぽい内容にできるといいなぁと思って内容を考えました。最初は「インターフェイス」というテーマでまとめていたのですが、うまくまとまりませんでした。そのため、他の人と違う活動という観点でまとめてみました。その結果、「Rubyでできることを増やす」・「ライブラリーをメンテナンスする」という内容になりました。キーノートっぽかったでしょ?

この話を聞いて、私と同じように「Rubyでできることを増やす」・「ライブラリーをメンテナンスする」に取り組む人が増えるといいなぁと思ってこんな内容になりました。その取り組みの中で、Ruby本体をよくする機会もでてくるとさらにいいなぁと思っています。その気になった人はぜひ取り組んでみてください。

やりたいけどどこから始めればいいんだろうという人はRed Data Toolsに参加するのがよいでしょう。まずはチャットで相談したり東京での毎月の開発イベントに参加してください。

やりたくてお金はあるんだけど技術が足りない・時間が足りないという会社の人は、クリアコードにお仕事として発注してください。この話を聞いた人ならクリアコードに頼めば安心だと思ってくれるはず!ご相談は問い合わせフォームからどうぞ。

参考情報:

やりたいんだけど時間が足りないという人はクリアコードに入社して仕事としてやるのを検討するのはどうでしょうか。ただ、そういう仕事がないと仕事の時間ではできないですし、そもそも仕事がないと給料を払うのが難しいです。そういうことも考えた上でまだ選択肢としてよさそうならまずは会社説明会に申し込んでください。(このページの内容は少し古くなっているので更新しないといけない。。。。)

あ、そうだ、「クリアコードをいい感じにする人」として入社して、私が「Rubyでできることを増やす」・「ライブラリーをメンテナンスする」に使える時間を増やすという方法もあるかも。うーん、間接的すぎて微妙かな。。。

RubyData Workshop

RubyKaigi 2017に引き続き、RubyKaigi 2018でもRubyData Workshop(Data Science in RubyRed Data Tools Lightning Talks)を開催しました。Rubyでデータ処理したくなったでしょ?その気になった人はRed Data Toolsに参加して一緒に取り組んでいきましょう。

Data Science in Rubyの資料はRubyData/rubykaigi2018にあります。

Red Data Tools Lighting Talksの資料(の一部)は以下にあります。

なお、ワークショップのおやつのどら焼きはエス・エム・エスさんから提供してもらいました。ありがとうございます。

コード懇親会

Rubyは楽しくプログラムを書けるように設計されています。実際、RubyKaigiに参加するような人たちはRubyで楽しくプログラムを書いています。だったら、Rubyでコードを書く懇親会は楽しいんじゃない?というアイディアを思いつきました。それを実現する企画が「コード懇親会」です。実現にあたりSpeeeさんと楽天 仙台支社さんに協力してもらいました。ありがとうございます。

Speeeさんには運営や飲食物の提供などイベント開催のもろもろ、楽天さんには会場提供で協力してもらいました。参加者多数のため急遽定員を増やしたのですが、それにはSpeeeさんの飲食物の追加、楽天さんの机・椅子の追加がなければ実現できませんでした。

参加したみなさんは楽しんでくれたようです。興味がある人はアンケート結果を見てみてください。

参考情報:

懇親会の様子:

来年もあるかどうかはまだわかりません。「今回よかった!」と思った人はぜひインターネット上に思ったことなどをまとめてみてください。

今回はSpeeeさんに協力してもらいましたが、いろんなスポンサーが開催するようになるといいなぁと思っています。やりたい人・やりたい企業の方はぜひやってみてください。コード懇親会のリポジトリーのREADMEに説明がありますし、声をかけてもらえれば相談にのります。Speeeさんのコード懇親会レポートも参考にしてください。

なお、「コード懇親会」という企画をSpeeeさんが独占するよりもみんなで共有する方がSpeeeさんにとってメリットがあります。「最初に開催したのはSpeee」ということで名声が広まるからです。よさそう!と思った人はどんどん「コード懇親会」を開催してください。そのまま真似してもいいですし、アレンジを加えながら開催してもよいです。今回の実装を自由に使ってください。

リーダブルコードサイン会

3日目のAfternoon Breakのときにジュンク堂さんがサイン会をやっていました。通りかかったら長田さんに声をかけてもらったのでサイン会に混ぜてもらってリーダブルコードの解説にサインしていました。4,5冊売れました。「すでに持っている」という人の方が多かった気がします。いい本だから何冊あってもいいよね!

まとめ

RubyKaigi 2018でキーノートスピーカーとして話をしてきました。クリアコードは今年もシルバースポンサーとしてRubyKaigiを応援しました。

RubyData Workshop・コード懇親会・リーダブルコードサイン会のこともまとめました。

コード懇親会の進行のこともまとめようと思ったのですが、力尽きました。いつか、機会があれば。。。

タグ: Ruby
2018-06-04

GObject Introspectionを使ったRubyバインディングの開発方法

日本ではだいぶGObject Introspectionに詳しい方だと思っている須藤です。

バインディングの開発には5年くらい前からバインディングの開発にGOject Introspectionが有用だと思っています。

2013年には各種バインディング開発方法についてまとめたりGObject Introspection対応ライブラリーの開発方法を導入部分だけ説明したりしました。

2016年にはRubyKaigi 2016で各種バインディング開発方法を紹介しました。

2017年には名古屋Ruby会議03でGObject Introspectionを使ったバインディングの開発方法のRubyレベルの部分だけを紹介しました。

そして今年、1からGObject Introspection対応ライブラリーを開発する方法を1つずつ説明する文書をまとめました!OpenCVをGObject Introspectionに対応させています。GObject Introspection対応ライブラリーを開発するための日本語の文書としては一番よい文書になっているはずです。

この文書はRubyDataのリポジトリーで管理しています。RubyDataというのはSpeee@mrknさんが始めた取り組みです。Ruby用のデータ処理ツールを開発する人たちとそのツールを使う人たちを増やすことを目指しています。

これまで、RubyKaigi 2017でワークショップを開催したり、サイトで関連情報をまとめたりしていました。RubyKaigi 2018でもワークショップを開催する予定です。

Rubyで使えるデータ処理関連のライブラリーが増えるとRubyでできることが増えます。バインディングの開発はデータ処理関連のライブラリーを増やす1つのやり方です。GObject Introspectionが有用なケースもあるはずです。ぜひ、この文書を活用してRubyで使えるデータ処理関連のライブラリーを増やしていきましょう。

興味のある人はRed Data Toolsチャットルーム(オンライン)や東京で開催している開発イベント(オフライン)にどうぞ!

タグ: Ruby
2018-03-28

沖縄Ruby会議02 - Red Data Tools #okrk02

沖縄から東京へ帰る飛行機を待っている須藤です。25分遅延しているのでこれを書いています。

沖縄Ruby会議02でゲストスピーカーの1人としてRed Data Toolsの話をしました。

関連リンク:

内容

Red Data Toolsに参加する人(Rubyでもっと便利にデータ処理できるようになるために取り組む人)が増えるといいなぁと思って次のことを紹介しました。

  • Red Data Toolsのポリシー

    • こういうポリシーなら一緒に開発したい!と思ってもらえるかも!
  • Red Data Toolsで開発しているコード

    • こういうコードを一緒に開発したい!と思ってもらえるかも!

私達はインターネットを通じてやりとりできます。場所も時間も超えて一緒に開発できます。まずはチャットでなにに取り組むか相談するところから始めましょう。沖縄Ruby会議02に参加したみなさん、待っていますよ!!!

スライド中で紹介したコードや省略したコードは前述の「リポジトリー」の中にちゃんと動くコードとしてまとまっています。コードに興味がでてきた人はリポジトリーの中ものぞいてみてください。

スライドサイズ

会場が大学だったのでプロジェクターの出力の縦横比は4:3だと思っていたんですが、現地について確認してみたら16:9でした。琉球大学すごい!

Rabbit 2.2.2(未リリース)ではスライドの縦横比が16:9でもいい感じになるようになっています。Rabbit Slide Showgem pushすればスライドを公開できるかっこいいスライド共有サービス)も16:9に対応していて、前述の埋め込んだスライドも16:9になっています。

Rabbitは内容(ソース)と描画(テーマ・描画システム)を分離する設計になっています。(現実では内容の中に描画のための情報をちらちら入れちゃうけど。)

そのため、スライドの縦横比を4:3から16:9に変更したい場合はrabbitコマンドの引数に--size 800,450を指定するだけです。

% rabbit --size 800,450 red-data-tools.rab

あとはその縦横比に合わせてRabbitがいい感じにレイアウトを調整します。便利ですね!

まとめ

沖縄Ruby会議02でRed Data Toolsに参加しようぜ!という話をしました。

Rabbitの最近の新機能を自慢しました。

タグ: Ruby
2018-03-11

タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|