【Ruby 3.0 Advent Calendar 2020】Ruby3.0に投げたPull Request【2日目】

はじめに

Ruby 3.0 Advent Calendar 2020 2日目の記事になります。

昨日は、【Ruby 3.0 Advent Calendar 2020】Ruby に右代入がやってくる【1日目】です。
secret-garden.hatenablog.com

右代入、このまま何事もなく入るといいなぁ……

この記事では、僕が投げたPull Requestで何をやっているかを書いています。

ちなみに

これまでマージされたPull Requestの数を数えてみたんですが、26個みたいです(以下のリンクから確認できます)

github.com

テスト修正関係

基本的には`make test-all`を複数回動かす際のテスト修正になっています。

github.com

これはdid_you_meanのテストでBook::Coverが二回目のテストで定義済みのため落ちていたのを修正しています。
当初はBook.send(:remove_const, :Cover)するだけで対応できそうかと思ったんですが、別の個所でCoverageが期待される挙動となってしまったので、Book::Spineへとリネームしています。

そのうえで既に定数として定義済みであればそれを削除するようにしています。

github.com

これは二回目以降のテスト時にAWESOMEが既に定義済みである警告を無くす対応です。
先ほど同様remove_constで削除しています。

github.com

こちらも同様に二回目以降のテストで警告が出ていたので、定義済みの定数MyBookStructremove_constで削除しています。

github.com

で、先ほどの修正で漏れていた箇所(シンボルで渡していなかったの)を修正したものです

github.com

複数回テストを回す際にdid_you_meanで使用するフォーマッタの初期化がされていなかったため意図しない挙動になっていたのを修正しています。
setupDidYouMean.formatter = DidYouMean::VerboseFormatter.newしているだけの対応ですが、原因が上手くつかめず苦労した覚えがありますね……

github.com

二回目以降のテストでwebrickserver[:DocumentRootOptions][:NondisclosureName]が初期化されていないため失敗していたのを修正しています。
具体的にはserver[:DocumentRootOptions][:NondisclosureName] = []のように初期化しています。

github.com

make test-all TESTS="--repeat-count="50""のように実行すると時々テストが落ちるという問題に対応したものです。
原因としてはReadline.special_prefixesなどの区切り文字の設定周りが初期化されていないことでテストが落ちていました。

そこでteardownにそのあたりを初期化するように修正しています。

CI修正関係

github.com

RubyではWindows用のCIとしてAppVeyorを使っているんですが、これが時々なぜか落ちるという現象がありました。
調べてみたところshallow_clone: trueと設定している場合でうまくソースコードをcloneできないということがあるとわかりました。

github.com

そこで、cloneする深さをclone_depth: 10のように設定して落ちないようにしています。
正直落ちるかどうか不安でたまらなかった覚えがあります……。

github.com

で、治ったみたいだったんですが、PR時のCIチェックが別のエラーを出してきたので直したのがこれです。
原因としてはソースコードをcloneしてくるときにGitのユーザーなどが設定されていないのでエラーになっていました。

で、とりあえず初期設定でユーザーなどを設定したんですが、あまりいい直し方ではなかった感じですね……。

ドキュメント関係

github.com

最近のRubyではRubyRuby自身(と一部のCコード)で組み込みメソッドを書くと高速化するケースがあります。
そういったケースでRuby内部のRubyソースコード上にドキュメント用のコメントが記載されてます。

github.com


それらのコメントは.documentに追記することでビルド時によしなにドキュメント生成を行ってくれます。
で、array.rbが無かったのでそれを追加しているPRですね。

github.com

これはHash#deleteのドキュメントで矢印になっていないのを修正したものですね。

github.com

これはドキュメント内のリンクがOracleのドキュメントにリダイレクトされたのでわかった修正ですね。
リダイレクト先のURLに変更しています。

github.com

これはrb_class_realの挙動についてのドキュメントでtypoしてそうと思って投げたPRですね。
実際にはドキュメントが違ってた模様(typoではなかった)

またこの一件でコードの修正とかもされてます(詳しくは下記を参照)

github.com


ruby-trunk-changes.hatenablog.com



github.com

これはアクセス先のURLがhttpsに変更されていたので修正しています。

性能向上関係

github.com

ko1さんがRubyKaigi2019で発表されていた「RubyRubyを実装して高速化する」件をPRにしたものですね。
今回はKernel#cloneでそれを行っています。

github.com

こちらはKernel#Floatのバージョンですね。

コードの修正関係

github.com

これはrb_str_clearが宣言されているが使われていないのでそれを削除しています。
多分、以前は使うこともあって残っていたんだろうなぁというコードですね。

github.com

これはstaticを付けてもよさそうな関数だったので付けた方がいいのではと投げたPRですね。

github.com

これも同様にstaticを付けてます。

github.com

hash.c内でいくつかの関数にstaticを付けています。

github.com

こちらも同様にstaticを付けています。

github.com

こちらはrb_int_ceilrb_int_floorにstaticを付けています。

github.com

int_even_prb_int_odd_pで現在では使われてなさそうな処理を削除しています。

github.com

flo_prevflo_nextの処理がほとんど同じだったのでまとめています。

バグ修正関係

github.com

Time#strftimeの引数にTimezoneを渡した時に表示される出力結果がおかしいのを修正しています。
直近で似たような修正をnobuさんがされていたのでそれを参考に修正したやつですね。

github.com

Time#to_aでも同じようにTimezoneを渡した時の出力がおかしいのを修正しています。

雑感

そこそこ色々と投げれているなぁと感慨深くなりました。

自分が投げたパッチが取り込まれてるRuby3.0がリリースされるのが楽しみですね

社会人になってからプログラマーになった話

はじめに

少し前にFediverseのTLで「別業種とかからプログラマーになった話があるとよさそう」と盛り上がってた。

僕自身は社会人になってからプログラマーとして仕事してるので、その辺の経験とか経緯を記事にしてみると需要がありそうかなと思い、書いてみた。

 

プログラマーになるまでの経緯

学生時代

学生時代は特にプログラミングをしていたわけではなく、せいぜいHTMLとかCSSでWebサイトを作ってそれをFTPとか使ってサーバーにあげたりしてるくらいの日々でした。

 

あとはM.U.G.E.Nという自作のキャラで格ゲーができるゲームで自作のキャラを作るなどはしてました。

ja.wikipedia.org

 

まあ、ほとんどコードの意味とかは分からなくてコピペして「おお、動いた動いた」とかしてた。

 

大学ではCやC++でのゲーム制作(主にDXライブラリでの)とかに興味を持ってましたが、特に熱中してやることもなくそのまま卒業(なお、大学では民俗学とか行政関係の法律とかを学んでたので、今の仕事には全然活かせてない感じ……)

社会人になってからプログラミングを始めるまで

就職後、特にプログラミングに接する機会はない生活でした。あったとしてもせいぜいExcelとかでマクロ組んだりとかぐらい。

 

本格的にプログラミングをやり始めたのは社会人一年目もそろそろ終わりそうな時期でした。

 

契機は、自作のゲームを作りたくなったことでした。

 

大学時代、クトゥルフTRPGとかでゲームシナリオ書いたりしてよく遊んでて、ゲームを作る楽しさに触れてました。社会人になってからもちょくちょくシナリオを書いては気心知れた人たちと遊んでましたねー。

 

で、そんな中「やっぱりPCとかで動くゲームを作りたい……‼」という気持ちが高まってきてて、「よし、ちょっと頑張ってやってみようか!」となりゲームを作りはじめました。と同時にゲームリンクスというゲームを作るサークルも立ち上げたり(これがブログの名前の由来だったり)

ゲームをはじめたころ

とりあえずノベルゲームくらいなら作れるだろうと思い、手始めにティラノスクリプトとかジョーカースクリプトを触りましたね。

 

tyrano.jp

 

jokerscript.jp

 

確か一番最初に作ったゲームはジョーカースクリプトで作ったやつじゃなかったかな……?

 

まあ、素材の読み込みとかタグの理解とかかなりてこずりながら作ってたように思います。

その分、完成した時の達成感は何とも言えないものがありました。

 

しばらくはジョーカースクリプトとティラノスクリプトを使ってゲーム制作をしてたんですが、だんだんと開発スピードが遅くなってきてたんですね。

 

背景には、プログラマーが一人(僕だけ)に対してシナリオを書く人間が4人くらいいたことがあります。

ゲームにする際の人的リソースが圧倒的に足りなかったんですよね……。

「このままだと体がいくつあっても時間がない……より能率的にノベルゲーム作るものないかなぁ……」とか考えてた。

 

で、どうするかなぁと思ってたところに天啓が

 

「能率的にノベルゲーム作れるエンジンがないなら、自分で作ればいいじゃない」と

 

ここで僕のプログラミング学習が本格的にはじまりました。

 

最初に学んだ言語はCだった

とりあえず大学時代にDXライブラリについて調べていたこともあり、DXライブラリ + C言語で簡易なノベルゲームエンジンを作ることにしました。

 

確か当時は「猫でもわかるC言語」とかをよみながらC言語の文法とか学びつつ、試作でノベルゲームっぽいものを動かしてたと思います。

当時のソースコードはまあ酷いもので、タグを処理する関数が1000行とか超えてました……。

 

ただ、当時はそういうコードがよくないコードだとは思わず、動くようになるのが楽しくて仕方がなかったのをよく覚えています。

 

で、だいたい三か月くらいでファーストリリースできるくらいのものはできました。

かなり簡単なタグの処理(立ち絵とかBGMとか)とセーブとロードくらいしかない簡単なものでしたが、リリースできた時の感動はいまだに忘れられないです。

 

その後、一年くらい細かなアップデートを繰り返しながらノベルゲームをだいたい7~10本くらいリリースしてましたね。

 

Rubyとの出会い

そんな感じでC言語でのゲーム制作を楽しんでいましたが、だんだんつらいところも出てきました。たとえば、(自分が書いたコードながら)コードが長くて読みにくくなってきましたし、何よりC言語の書かなければならないコード量が煩わしく感じるようになってきました。

 

そんな時、Rubyと出会いました。たしかDXRubyというゲームライブラリを知って、そこからRubyをはじめたと思います。

 

Cよりもかなり短くコードが書けることに感動しつつ、Rubyを書くようになっていきました(といっても比重は相変わらずC言語のほうが多い日々でしたが)

 

これがOSS活動を知ったきっかけでした。

 

OSS活動との接点

OSS活動について知り、「僕もOSSみたくソースコードを公開してみたい」と思うようになってたんですが「何を公開するかなぁ」というのが当時の悩みでした。

 

当時の僕が持ってるプロダクトといえば自作のノベルゲームエンジンだけで、コードもきれいとはいいがたいものでした。

 

とはいえ他に公開できるようなものもないですし、僕の書いたコードをわざわざ見る人もそうそう居ないだろうと思い最終的にはGitHubで公開しました。

 

で、公開したところコードを読んだ方がおられまして、「これはよくないコードだ」とご指摘いただいたんですよね。

で、その方から「Cで書くよりはC++で書いたほうがいいんじゃないか?」と言われたことがきっかけで今度はC++をはじめることになったんですよね。

 

C++との出会い

まあ、Cに比べて覚えることも多く苦労した覚えがあります。特にコードの指摘をしてくださった方がC++にかなり詳しい方だったのでダメ出しとかももらいながら書いていた日々でした。

 

でも、Cよりもできることの幅が広がっている感じもあり、楽しい日々でもありましたね。

 

ちょうど、この頃にC++関係でいろいろ本を読んでいたんですが、だいたいの本が古くて(C++03とか下手したらC++98とか)つらかったですね。

 

まあ、そのおかげでプログラミング言語にもバージョン(や仕様)が年代ごとにあるということをよく理解できたので良かったと思います。

 

RubyKaigiに参加

で、そんなころ隣の県の広島でRubyKaigiをするという情報が入り単身乗り込むということをしてました。

rubykaigi.org

 

多分、はじめて参加した大きなカンファレンスだったんじゃないかな?

 

参加したことで、Rubyコミュニティの圧倒的Rubyへの情熱を浴び、気づいたら熱狂的なRubyistへと変貌していきましたね……。

結局、RubyKaigiへは毎年参加するようになってました(翌年は仙台、翌々年は福岡)。このころくらいからだいぶアクティブにコードを書いたりするようになったと思いますね。

 

本格的にプログラミングにのめりこむ

このころにはC言語はあまり書かず、だいたいRubyC++で何かしらコードを書くようになってました。

特にRailsでサークルのWebサイト作ったりとかしてた頃ですね。

 

で、そんなころにMastodonのブームがやってきました。

 

Mastodon(分散SNSとの出会い)

はじめ、Mastodonが登場した時は「へー、そういうのが出たんだ」くらいにしか思ってませんでした。

 

その後、Mastodon(というか分散SNSの特徴)を知っていくうちに「僕もサーバーを立ててみよう」となり、Creatodonを立てました。

 

gamelinks007.net

 

当時はCreatodonという名前でもなく一時創作系のサーバーと銘打ってました。

 

ただまあ、ほかの大規模なサーバーに比べて知名度とかもなかったので基本的に僕が使っているだけな感じになってました(しかし、それも仕事が忙しかったりすると使うのを忘れたり……)

 

しばらくアップデートとかもできない日々が続いてて重い腰を上げでアップデートを始めたのがその年の九月ぐらいでした。

 

 

gamelinks007.hatenablog.com

 

ちょうどこの頃からMastodonの使用率がTwitterを超えだしたので、TwitterをやめてMastodonへ引っ越ししたりもしましたね。

 

あと、Mastodonでログインできるアプリとか、PleromaとかHivewayとか立ててみたりとかしてましたね。

 

ちなみにこの辺の活動を見ていた方からお誘いいただき、松江Ruby会議でMastodon他分散SNSについて話す機会をもらったりもしました。

 

OSSへの初コントリビューション

アプリを作ってGitHubで公開とかそういう意味でのOSS活動はしてたんですが、大きなOSSプロジェクトにコントリビューションすることはない日々を過ごしていました。

 

そんな時、Redmineのドキュメント関係でコントリビューションチャンスが来たんですよね。

 

gamelinks007.hatenablog.com

 

HerokuでのRedmineのデプロイ手順が少し情報が古く、うまくいかなかったんですよ。

で、そんなことをMastodonでつぶやいていたところフォロワーさんからフィードバックをもらってうまいことデプロイできたと。

 

ちょうどその時の手順もメモしていたのでRedmineの公式ドキュメントを編集するといいんじゃないかとなり、OSSに初コントリビュートとなりました。

 

転職

この頃には、そこそこコードを書くようになってきていたんですが、相変わらず仕事ではコードを書くような場面はありませんでした。

 

そんな時、現職の会社へと転職するチャンスが来たんですよね。で、これまで作ってきたものとかを紹介して転職することができました。

ほかの人と違ってすでにあれこれやっていたのでポートフォリオを作るのに困ったりとかそういうのは無かったですね。

 

プログラマーになってから

転職後

転職後は、初学者向けにプログラミングを教えたりしながら教材を作ったりしてました。

最近は開発もやるようになってきたので「スキルが身についてきたなぁ……」とか思ってます。

 

転職後のOSS活動

転職後もちょっとしたアプリを作って公開したりはしてました。がRedmineのドキュメント編集した時のような大きなOSSへのコントリビューションはあまりありませんでした(あってもBoostjpのドキュメントにPR送ったりとかぐらいなじゃいかなぁ……)

 

で、そんな中Ruby自体の実装に興味を持ち始め「いつかRubyにコントリビューションできるといいなぁ」とか思ってました。

 

そんな時、ruby-jpでコミッターの方から「テストの修正してくれる人いませんか?」という話が出たんですよね。

で、さっそく飛びついて「やりたいです!」といい、テストの修正に取り掛かりました。

 

その後、テストの挙動の把握など苦労しながらも修正PRを出せたんですよね

ちなみに初めてマージされたのはこれ。

 

github.com

 

まあ、マージされたのがうれしくて小躍りしましたね!

 

で、このPRがマージされたのがうれしくてどんどんRubyの実装を読むようになっていきました。

その後、builtin対応とかCの関数の修正(使われていないifの分岐を削除とか)してPRを投げはじめましたね。

 

そして、最終的にはこういうイベントもはじめたり……

 

hamadarb.connpass.com

 

 

おわりに

これがこれからプログラマー(ITエンジニアのほうが近いか?)になりたい人に参考になるかはわかりませんが、こういう道順を経た人もいるんだよと知っていただければ幸いです。

 

ちなみにC言語学びだしてからRubyに初コントリビューションするまでで5年くらいだったということに記事をまとめながら気づいてびっくりしましたね。

なんと遠くまで来たんだなぁと感慨深いです。

 

 

Google Cloud Run + Nginx でティラノスクリプトのゲームをPWAでリリース

はじめに

そこそこ規模の大きいティラノスクリプトで作られたゲームをPWAでリリースしようとした際に起きた現象とその対策について書いた記事です。
内容としてはPWAでリリースすることを考えている人向けの記事になります。

またDockerなどを使っているので、どちらかといえばプログラマー向けの内容になっています。

起きたこと

とある大学から「学生の学習用にティラノスクリプトを使ったゲームを作りたい」と話がありました。
ちなみに、既にある程度の実装などは済んでいて、追加で実装してほしいとのこと。

逐一修正したものをZIP形式でやり取りするのも億劫なので、Netlifyなどのホスティングサービスを使うことにました。
Netilfyとか使えば、GitHubから自動的に最新のアプリをリリースできるので

ただ、実際に動作させると

  • 効果音がずれる
  • 処理がもっさりする

などの問題が発覚。

で、どうしたもんかなぁと考えてました。

原因

原因としては使用しているホスティングサービスのサーバーとの物理的距離の問題とサーバーのスペックの問題のようでした。
ちなみにリロードしてブラウザで表示されるまで15000msとかかかってた……

またブラウザへ表示するまでのレンダリング時間がかなりかかっていることからキャッシュか静的コンテンツを高速に配信することができれば大幅な改善が期待できそうでした。

対策検討

とりあえず処理が重いのはサーバーのスペックと物理的距離のようなので、それらをまず変更することを検討。
また、静的コンテンツを高速に配信してくれると評判のNginxを間にかますことで改善されると考えました。

できればリリースなどはコマンドでサクッとしたいですし、なおかつ使用するときだけ料金が発生するのがいいと考えました。
で、大阪リージョンがあるし、かつコマンドでリリースが楽にできるGoogle Cloud Run がよさそうと思い、そちらを使うことしました。

やったこと

まずは、ティラノスクリプトで作ったゲームのソースコードにDockerfileを以下のように追加します。

FROM nginx:alpine
COPY . /usr/share/nginx/html
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf

EXPOSE $PORT

CMD ["nginx", "-g", "daemon off;"]

COPY . /usr/share/nginx/htmlはティラノスクリプトの静的コンテンツをNginxで処理するディレクトリにコピーしています。

またGoogle Cloud Runで使用するポートが8888固定なのでEXPOSE $PORTで使用するポートをよしなにできるようにしています。
またNginxの設定ファイルもCOPY ./nginx/default.conf /etc/nginx/conf.d/default.confでコピーし、Google Cloud Runで動作するようにしています。

次に、nginxディレクトリをソースコード内に作成します。
nginxディレクトリにdefault.confを以下のように作成します。

server {
    listen       8080;
    server_name  localhost;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

使用しているポートを変更しているだけですね。

あとはGoogle Cloud Run で使用するコンテナをビルドしてデプロイすればOKです。

# コンテナのビルド
gcloud builds submit --tag gcr.io/<PROJECT-ID>/tyrano

# コンテナをデプロイ
gcloud run deploy --image gcr.io/<PROJECT-ID>/tyrano

これでリリースができました。

結果

Netlifyを使っていた時にリロードしてブラウザで表示されるまで15000msとかかかっていたのが、大体700msくらいまでに抑えることができました!

かなり快適になったので良い感じです。

GitHub Pages + PWAでティラノスクリプトのゲームをスマホで動かしてみた

はじめに

ティラノスクリプトで作ったゲームをPWAにしたい人でGitHub Pagesを使いたい人向けの記事です。

動機

以前、この記事を読んでティラノスクリプトでもPWA対応ができることは知っていたんですが試したことがなかったのでやってみた。

qiita.com

で、PWA対応ができているかを確認するのにいちいちレンタルサーバー借りたりするのも面倒臭かったのでGitHub Pagesで代用してみました。

やったこと

まずはGitHubに適当な名前でリポジトリを作ります。
リポジトリ名はあとで使うので控えておいてください。

次にティラノスクリプトの公式サイトからサンプルのゲームをダウンロードします。
今回は数多くのゲームがリリースされていることを鑑みてv4系列を使用しました。

tyrano.jp

また、PWA対応なのでスタンダードパッケージを使っています。

ダウンロードしてきたサンプルを適当なディレクトリに入れてから git initします。

あとは、manifest.jsonを記事を参考に以下のように作成します。
追加する箇所はindex.htmlと同じ階層です。

{
    "short_name": "SHORTNAME",
    "name": "GAME_TITLE",
    "icons": [
        {
            "src": "link.png",
            "sizes": "144x144",
            "type": "image/png"
        },
        {
            "src": "link_02.png",
            "sizes": "192x192",
            "type": "image/png"
        }
    ],
    "start_url": "/<作成したリポジトリ名>/",
    "display": "fullscreen",
    "background_color": "#000",
    "theme_color": "#fff",
    "orientation": "landscape"
}

一つ注意が必要なのはstart_urlの項目です。

GitHub Pagesでは作成したリポジトリ名でドメインの階層が作られています。
なので/にするとファイルが見つからないことになります。

あとはgit commitとかして、git pushリポジトリにpushすればOKです。

最後に、作成したリポジトリの設定からGitHub Pagesを使うようにすればOKです。

ちなみに実際に作ったものがこちら

リポジトリ:
github.com

PWA対応したゲーム:
s-h-gamelinks.github.io

おわりに

やってみて、GitHub Pagesを使うことで「開発しながらリリース処理もできるのでかなり良いんじゃないかなぁ」と思いました。
特にチームで開発してて、かつ皆Gitを使えるとかであればかなり恩恵はありそう(Issue管理とかやりやすそう)

JavaScriptのexportとimportっぽいものをRubyで書いてみた

はじめに

とある勉強会で以前聞いた「JavaScriptのimportとexportみたいなのがRubyで出来ないか?」という話をもとにRubyでやってみた記事です。

実際のコード

module Klass
  @@klasses = {}
  def export(*klasses)
    klasses.each{
      @@klasses[_1.to_s.to_sym] = _1
    }
  end

  def import
    @@klasses
  end
end

class C1
  def self.value
    42
  end
end

class C2
  def self.value
    21
  end
end

include Klass

export C1, C2

import in { C1: c1, C2: c2 }

p c1.value
# => 42
p c2.value
# => 21

Klassというモジュールの中にexportimportというメソッドをそれぞれ定義しています。
exportメソッドでは複数のクラス(定数)を受け取り、それをクラス変数に保存させています。

で、保存したクラスをimportでメソッドで全て返し、パターンマッチでマッチしたクラスだけを変数として受け取っています。

こんな感じでとりあえず書けそう

ただ、exportでエクスポートしたクラスがどんどん同じ変数に格納されていくのでJavaScriptのそれとはちょっと挙動が違うので意図したものかは微妙かもしれないけど……

Ractor超入門

はじめに

この記事はRuby3で導入される並列・並行処理のための新機能Ractorに興味のある方向けの記事になります。 簡単なRactorを使ったサンプルコードを解説しつつ、理解を深めることができるように書いてみました(ほとんど未来の僕へ向けた記事になっている感じはありますが……)。

また、Ractor自体でどういったことができるのかを調べた結果をまとめた記事でもあります。 そのため後半はRactorで色々遊んだ時のコードをもとに挙動を解説しています。

環境

  • Windows10 2004
  • WSL2(Ubuntu 18.04)
  • ruby 3.0.0dev (2020-09-07T04:29:42Z master 17a27060a7) [x86_64-linux]

Ractorとは?

そもそもRactorについて知らない人もいると思いますので、簡単に紹介します。

RactorとはRuby3で導入される並列・並行処理のための新機能になります。機能自体の提案は数年前からあり、当時はGuildという名前で提案されていました。

しかし、ゲーム業界から「Guildという名前は使っているので、別の名前にしてほしい」という声があり、現在のRactorへと変わりました。

Actorモデルを参考にしているようで、そのためRactor(Ruby’s Actor)という名前に変更されたようです。

Ractorは並行実行の単位であり、それぞれが並行して実行されます。 たとえば以下のコードではputs :helloputs :helloはそれぞれ並行して実行されます。

Ractor.new do
  5.times do
    puts :hello
  end
end

5.times do
    puts :world
end

このコードを実行すると以下のような結果になります。

world
helloworld

hello
world
helloworld

helloworld

hello

このようにそれぞれの処理を並行して実行できます。

またRactorは別のRactorへとオブジェクトを送受信し、同期しながら実行することもできます。同期の方法としてはpush型pull型の二つがあります。

たとえばpush型の場合は以下のようなコードになります。

r1 = Ractor.new do
    :hoge
end

r2 = Ractor.new do
    puts :fuga, Ractor.recv
end

r2.send(r1.take)

r2.take
# => :fuga, :hoge

Ractorではsendメソッドを使い、別のRactorへとオブジェクトを送ることができます。上記のコードだと

r2.send(r1.take)

の部分でr2へと送信しています。

送信されたオブジェクトはRactor内でRactor.recvで受け取ることができ

r2 = Ractor.new do
    puts :fuga, Ractor.recv # 
end

r2.send(r1.take)で送られたオブジェクトを受け取ってputsメソッドに渡すことができます。

またRactorが実行された結果を受け取る際にtakeメソッドを使います。 ですのでr1.take:hogeを受け取っています。

つまり、r2.send(r1.take)ではr1の実行結果を受け取り、それをr2へと送信しています。 そして、r2内のputs :fuga, Ractor.recvputs :fuga, :hogeとなり、fugahogeがそれぞれ出力されるということです。

これがpush型でのオブジェクトをやり取りしている流れになります。

対してpull型は以下のようなコードになります。

r1 = Ractor.new 42 do |arg|
    Ractor.yield arg
end

r2 = Ractor.new r1 do |r1|
    r1.take
end

puts r2.take

Ractor.newで渡した引数は|arg|のようにブロック内で使用できる変数として受け取ることができます。

例えば以下のコードはr1takeメソッドが実行されるまで処理を待ちます。

r1 = Ractor.new 42 do |arg|
    Ractor.yield arg
end

またRactor.newには別のRactorを渡すことができるため以下のように書くことができます。

r2 = Ractor.new r1 do |r1|
    r1.take
end

これでr1が引数として受け取った42r2の中で受け取ることができます。

最後にputs r2.take42を受け取って出力しています。

pull型はこういった流れになります。

ざっくりと解説すると

  • push型: Ractor#send + Ractor.recv
  • pull型: Ractor.yield + Ractor#take

という感じです。

より詳細なRactorの解説に関しては下記のリンクを参照していただければと思います。

Ractorのコード

Ractorの生成

RactorRactor.newでブロックに実行したい処理を書きます。

Ractor.new do
  # このブロックが並行に実行される
end

このブロック内の処理が並行実行されます。

つまり、以下のようなコードの場合

Ractor.new do
    10.times do
        puts :hoge
    end
end

10.times do
    puts :fuga
end

:hoge:fugaがそれぞれ並行に出力されます。

またRactor.newに実行したい処理をブロックとして渡しますので以下のように書くこともできます。

Ractor.new{
    10.times{
        puts :hoge
    }
}

またキーワード引数nameを使って名前を付けることもでき、Ractor#nameで名前をを受け取ることもできます。

r = Ractor.new name: 'r1' do
    puts :hoge
end

p r.name
# => "r1"

これにより、どのRactorで処理が実行されているかを確認することもできそうです。

Ractorへ引数を渡す

Ractor.newに引数を渡すことでブロック内にオブジェクトを渡すことができます。

r = Ractor.new :hoge do |a|
    p a
end

r.take
# => :hoge

このように引数を経由してオブジェクトを渡すことができます。

また複数の引数を渡すこともできます

r = Ractor.new :hoge, :fuga do |a, b|
    p a
    p b
end

r.take
# => fuga
# => hoge

このようにArrayを渡すこともできます。

r = Ractor.new [:hoge, :fuga] do |a|
    p a.inspect
end

r.take
# => "[:hoge, :fuga]"

ちなみに、|a||a, b|に変更すると

r = Ractor.new [:hoge, :fuga] do |a, b|
    p a
    p b
end

r.take
# => :hoge
# => :fuga

という出力結果になります。これはa, b = [:hoge, :fuga]と同じ挙動と解釈されているようです。

またHashの場合は

r = Ractor.new({:hoge => 42, :fuga => 21}) do |a|
    p a
    p a[:hoge]
end

r.take
# => {:hoge=>42, :fuga=>21}
# => 42

と出力されます。 ちなみに、Ractor.newの後に()で括っていないとSyntaxErrorになるので注意が必要です。

r = Ractor.new({:hoge => 42, :fuga => 21}) do |a|
    p a
    p a[:hoge]
end

r.take
# => SyntaxError

Ractorでの返り値

Ractorでは実行されるブロック内の返り値をtakeメソッドで受け取ることができます。

r = Ractor.new do
    :hoge
end

p r.take
# => :hoge

ちなみに、ブロック内でreturnをするとLocalJumpErrorとなるようです。

r = Ractor.new do
    return :fuga
    :hoge
end

p r.take
# => LocalJumpError

Ractor内での例外

Ractor内での例外は以下のようにして受け取ることができます。

r = Ractor.new do
    raise 'error'
end

begin
    r.take
rescue Ractor::RemoteError => e
    p e.message
end

ちなみに、Ractor内でも例外処理を書くことはできます。

r = Ractor.new name: 'r1' do
    begin
        raise 'error'
    rescue => e
        p e.message
    end
end

r.take

またドキュメントによるとRactorのブロック内から返ってきた値を受け取る領域で例外をキャッチできるようです。 つまり以下のようなコードも書くことができます。

r1 = Ractor.new do
    raise 'error'
end

r2 = Ractor.new r1 do |r1|
    begin
        r1.take
    rescue Ractor::RemoteError => e
        p e.message
    end
end 

r2.take
# => "thrown by remote Ractor."

Ractorでの並行実行

簡単な例

こんな感じでRactorで並行実行を行うことができます。

Ractor.new do
    3.times do
        puts 42
    end
end

3.times do
    puts 21
end

実行すると4221という出力がそれぞればらばらに表示されます。

ちょっとした例

以下のような感じで複数のworkerRactorで生成し、それをpipeで経由して値を渡し、結果をまとめることができます。

require 'prime'

pipe = Ractor.new do
  loop do
    Ractor.yield Ractor.recv
  end
end

N = 1000
RN = 10
workers = (1..RN).map do
  Ractor.new pipe do |pipe|
    while n = pipe.take
      Ractor.yield [n, n.prime?]
    end
  end
end

(1..N).each{|i|
  pipe << i
}

pp (1..N).map{
  r, (n, b) = Ractor.select(*workers)
  [n, b]
}.sort_by{|(n, b)| n}
# => 0 ~ 999 までの数値が素数かどうかの結果を出力

このコードでは10個のworkerを生成し、pipeを経由してそれぞれのworkerにオブジェクトを渡しています。 また、受け取ったオブジェクトをRactor.yield [n, n.prime?]で返しています。

こんな感じでworkerを複数作り、pipe経由で処理させたり、結果を受け取ることができます。

よしなにworkerなどを生成して処理させるクラスを書いてみる

先ほどのコードだとworker内の処理が後々で大きくなることもありそうだったので、以下のようにworkerをよしなに生成してくれるクラスを書いてみました。

class Ninsoku
    def initialize(task, worker_count: 10)
      @task = task
      @pipe = create_pipe
      @workers = create_workers(worker_count)
    end

    def send(arg)
        @pipe.send arg
    end

    def run
        yield Ractor.select(*@workers)
    end

    def create_pipe
        Ractor.new do
            loop do
                Ractor.yield Ractor.recv
            end
        end
    end

    def create_workers(worker_count)
        (1..worker_count).map do
            Ractor.new @pipe, @task do |pipe, task|
                loop do 
                  arg = pipe.take
                  task.send arg
                  Ractor.yield task.take
                end
            end
        end
    end
end

Ninsoku.newpipeworkerを生成しています。またtaskは処理させたい内容をRactorで渡し、workerで実行させています。

実際に使うケースとしてはこんな感じです。

task = Ractor.new do
  func = lambda{|n| n.downcase }
  loop do
    Ractor.yield func.call(Ractor.recv)
  end
end

ninsoku = Ninsoku.new(task)

('A'..'Z').each{|i|
  ninsoku.send i
}

('A'..'Z').map{
    ninsoku.run{|r, n|
        puts n
    }
}
# => a ~ z までが並行して出力される

このクラスはあとでgemにでもしてみようかと思います。

gemにしてみました(rubygemsにはpushしていませんが……)

S-H-GAMELINKS/rorker

例えば、僕の住んでいる島根県浜田市AED位置情報をRactorを使って処理してみます。 なお、島根県AED位置情報は島根県が公開しているオープンデータを使用させていただきました。この場を借りて感謝を申し上げます。

島根県 オープンデータカタログサイト

require "rorker"
require "csv"

task = Ractor.new do
  func = lambda{|row| 
    row.map{|value|
      if value =~ /浜田市/
        row
      end  
    }.compact
  }
  loop do
    Ractor.yield func.call(Ractor.recv)
  end
end

rorker = Rorker.new(task)

csv = CSV.read "a.csv"

csv.each do |row|
  rorker.send row
end

n = 0

while n < csv.count
  rorker.run{|worker, result|
    if !result.empty?
      puts result
    end
  }
  n += 1
end

こんな感じで読み取ったCSVを一行づつworkerに渡して並行処理で必要なデータをとってくることもできます。

RactorでNumbered Parameterを使う

Ractorでは処理をブロックで渡しますので、Numbered Parameterを使って引数を受け取ることもできます。

r = Ractor.new :hoge do
    puts _1
end

r.take
# => hoge

ちなみに、複数の引数でも動作します。

r = Ractor.new :hoge, :hoge do
    puts _1
    puts _2
end

r.take
# => hoge
# => fuga

複数渡した場合は渡された順番通りに_1から_9に渡されるみたいです。

ちなみに、Hashを渡した場合はこんな感じになります。

r = Ractor.new ({hoge: 1, fuga: 2}) do
    _1.map do |key, value|
        p ":#{key} => #{value}"
    end
end

r.take
# => ":hoge => 1"
# => ":fuga => 2"

=>を使ったHashも同様の結果になりました

r = Ractor.new({:hoge => 1, :fuga => 2}) do
    _1.map do |key, value|
        p ":#{key} => #{value}"
    end
end

r.take
# => ":hoge => 1"
# => ":fuga => 2"

ただし、Arrayの場合は少し挙動が異なります

r = Ractor.new [1, 2, 3] do
    puts _1
    puts _1.class
    puts _2
    puts _2.class
    puts _3
    puts _3.class    
end

r.take
#=> 1
#=> Integer
#=> 2
#=> Integer
#=> 3
#=> Integer

どうやら通常通り複数の引数を渡した時のようにArrayの先頭から順番に渡されるようです。 おそらくは以下のように解釈されているのではないかと思います。

_1, _2, _3 = [1, 2, 3]

ちなみに、Numbered Parameterで受け取れる数より大きなArrayを渡すと

r = Ractor.new [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] do
    puts _1
    puts _2
    puts _3
    puts _4
    puts _5
    puts _6
    puts _7
    puts _8
    puts _9
end

r.take
#=> 1
#=> 2
#=> 3
#=> 4
#=> 5
#=> 6
#=> 7
#=> 8
#=> 9

このように取得できるのはNumbered Parameterが受け取れる範囲までのようです

Ractor内でNumbered Parameterを使う場合はHashを引数に渡した際か

r = Ractor.new ({hoge: 1, fuga: 2}) do |hash|
    hash.map do
        p ":#{_1} => #{_2}"
    end
end

r.take
":hoge => 1"
":fuga => 2"

またはいくつかの引数を渡した時に省略して書きたいときに使うことになりそうです

r = Ractor.new :hoge, :fuga do
    p _1
    p _2
end

r.take
# => :hoge
# => :fuga

おわりに

この記事を読んでRactorについて興味を持って頂ければ幸いです。 今後もRactorを使って試したコードを追加していこうと思います

参考

Rubyで型宣言っぽくコードを書けるようにしてみた

結論

以下のようなコードが動くようになります。

n = int 42
# => 42が代入される

f = int 4.2
# => TypeError!

やったこと

以下のようにKernelモジュールにモンキーパッチします

module Kernel
    module_function
        
    def int(var = 0)
        if var.is_a?(Integer)
            var
        else
           raise TypeError, "#{var} isn't Integer"
        end
    end 
end

あとは、n = int 42のように書くだけで型宣言っぽくRubyの変数を作ることができます。また異なる型(というかクラス)の値を渡した場合は例外としてTypeErrorが発生します。

n = int 42
i = int 21

p n
# => 42
p i
# => 21

n = int 4.2
# => `int': 4.2 isn't Integer (TypeError)

影響範囲を狭めるのであれば、refinementsで以下のように書けばいいかと

module Type
    refine Kernel do
       module_function
        
       def int(var = 0)
           if var.is_a?(Integer)
               var
           else
              raise TypeError, "#{var} isn't Integer"
           end
       end
    end
end

あとは使いたい箇所でusing TypeとすればOkです

おわりに

とりあえず、IntegerとかStringとかはこんな感じで型宣言っぽく書けそう。ArrayとかHashとかは何かいい書き方ないか考えてみよう