Speee×Asakusa.rb Ruby2.5リリースパーティー に参加してきました

speee.connpass.com

行ってきました。
twitter に適当に投稿はしましたがだいぶはしょっているのでそれの補完を。

なんか別方面にささったらしく妙にRTされましたが。これはnobuがCentOS6で落ちるコードをコミットしたことをネタにしていたそうです。コミッタ同士の仲が良さそうで微笑ましいですね。

最初は Matz からの挨拶がありました。無事リリースできましたねーというところと反省点。

知り合いのコミッタも12月になってからも気合いれていろいろやっていて、11月にはもうフィーチャーフリーズしてるもんだと思ってたのですがそうでもないんだなあと思っていました。どうもコミッタ陣にとってもさすがにちょっと、という感じがあったようで2.6では余裕をもたせたいですねという話がありました。

Rubyユーザーにとっては 2.5 が出たばかりですが、コミッタ陣はもう2.6を見据えているようです。

github.com

こちらは当日の朝に投げられた k0kubun さんのPRです。これをネタに 2.6 で mjit をいれるぞー、はやくなるぞー、と盛り上がりました。

スピードネタでいうと、某メディアの記事で「3倍速い」という噂のRuby2.5なので1速度ネタが続きます。
Speee さんでのイベントの前にあった開発者会議では、 block.call 形式の場合にも safe level を考慮しないようにしてもよいという意思決定があり、よりはやく実行できるようになる見通しが立っているようです(ちょっと曖昧です)

このあたりから safe level や tainted など、知らない人もいるような機能の話に深掘りされていきました。個人的なお気に入りは mame さんが放った以下のフレーズ。

その他、2.5で強化されたカバレッジの話や binding.irb の話が続きました。

座談会の後の懇親会を細かにレポートしてもアレなのでこのへんで。
非常に楽しい会でした。ありがとうございました。


  1. 正確には、「ブロックを渡すが block.call のような呼び出しをしないときには Proc の生成をしないようにすることで3倍早くなる」だがなぜか見出しに「前バージョン比約3倍に高速化、Ruby 2.5.0がリリース」とあって、登壇者にネタにされていた

#しがないラジオ とキャラ付け

この記事は しがないラジオAdventCalendar 5日目の記事です。

adventar.org

すでに2回もゲストとして出演させていただきまして、自分のキャリアについては話しきった感覚があります。そのため今回はしがないラジオに出会って何が起きたのか、何を考えることになったのかということ書こうと思います。
転職ネタが知りたい方はどうぞ過去の sp3 と sp5 をどうぞ(宣伝

最初にぼくとガミさんの関係性を確認しておこうかと思います。
1年ほど前の僕にとってのガミさんは、小中高の同級生であり、ときたま酒を飲むような仲でした。どっかのSIerに就職したらしい1と思っていたのですが、いつの間にかweb系に転職することが決まっていて、そこからまた交流が増えた気がします。これがたぶん去年の9月くらい。Rebuild.fm の話をしたり、青春のアフター1〜3巻をガミさんにその場で購入させたりというイベントがあった喜ばしい飲み会でした。

その後の今年3月、 twitter を見ていたらいきなりガミさんが「podcast始めました」とツイートしていて大変驚いたのを覚えています。当時は Rebuild.fm チルドレンと呼ばれるように新規 podcast が登場したあとで、うわー俺もやりたいと思ってたけどめんどくさくて足踏みしてたのに、ガミさんはちゃんとやっていってるなあと思いました。

さて、そんなこんなでガミさんには先を越されてしまったわけですが、逆に自分で始めずにゲストとして出れば podcast に出たい欲を満足させつつ、定期的にやるのめんどくさいという気持ちに抗う必要もないことに気づくのにそう時間はかかりませんでした。雑に話させてよというと二つ返事でOKが出たのでゲスト出演が決まりました。

1回目のゲストのときは、特に深く考えずに今までのキャリアと、今何やってるかと、あと青春のアフターのおすすめをしようと思っていました。だいたいその通りになったと思います。
ですが続く2回目のゲストのときはいろいろ考えました。自身の2回目の転職というトピックはありますが、番組的にどう立ち回ろうかなあということを意識しはじめたのです。僕はパーソナリティの2人と比べると2年ほど長くこの業界にいます。また、しがないラジオへのフィードバックの中には「ガミさんとずっきーさんのキャラが似てる」というようなものがありました。

というわけで2回目のゲストの際には「自分の意見を強く主張する」ことを意識していました。

個人的な感想としては、ずっきーさんもガミさんもそんなに主張が強くなく(「〜〜って思ったんですよね、違うかもしんないけど」くらい)、また自分たちの考えたことや試してみたことが正しくないんじゃないかということを気にしているように感じていました。2
彼ら2人の意見に「それはそう」「それは違う」とはっきりいう役割が必要なんじゃないかと思って話そうとしていました。

結果的にはどうだったんでしょうか。
僕も彼らと同じで悩みながら生きていくタイプの人間なのでそれほど強く言えてないんじゃないかなーと思っています。反省点としては、ちゃんとやりきれなかったというより、違うキャラ付けを選んじゃったかなという感じ。

また話したいことが溜まったら33回目のゲスト出演したいと思っていますが、どういう風に話すか、あるいは素で喋るのか。しがないラジオに限らず今後のパブリックなアウトプットの方向性を考えたほうがいいのかも、という話でした。


  1. これはとても適切な表現で、ラジオで繰り返されるまでマジでどこだか覚えてなかった

  2. Reactの書き方の話のときにずっきーさんがそう言ってたような気もする

  3. すでにネタ帳は存在する

LOLIPOP! マネージドクラウド触ってみた

↑を見たので触ってみました。

マネージドクラウドの特徴としては以下の3つが挙げられています。

  • 自動セットアップ
    • いろいろプリセットが用意されてて選んだらポチで立ち上がる
    • 数秒で立ち上がるらしい
  • いつでもサイト表示
    • オートスケールのことっぽい
  • 常にセキュアな環境
    • セキュリティアップデートとかが自動だという意味

コンテナという単語が出てきているのでLXCをベースにしたなにかなのでしょう。

とりあえずメールアドレスとパスワードをいれて登録していきます。
登録するとプロジェクト作成画面に進みます。

f:id:hkdnet:20171202013320p:plain

そういえば登録した覚えがないけどアイコンついてますね。gravatar かな。
どうも下部を見ると Rails 推しっぽいので Rails にしておきます。

f:id:hkdnet:20171202013443p:plain

まだ灰色で選択できませんが hubot とかあって適当に chatbot をデプロイするのにもよさそうです。
プロジェクトURLがあるのでうれしいですね(って書いたけど heroku も OpenShift もあったわ)
ベータ期間なので無料だしクレカ登録もなく使えるのは嬉しいのですが、ベータが終わったらどんくらいのコスト感なのかは気になるところですね。

さて、プロジェクトを作成するとプロジェクトのダッシュボードにいきなり放り出されます。
ここたぶん何をどうすればいいのかわからなくなる人が多そうですね……。

とりあえず ssh するために ssh 公開鍵を追加しておきましょう。画面が単純なので割愛します。

というわけでプロジェクトトップに書いてある ssh コマンドを叩いてみるのだ

$ ssh -p XXX YYY@ZZZ
Last login: Fri Dec  1 17:02:43 2017 from 10.1.1.1
  __  __  ____   _          _ _
 |  \/  |/ ___| | |    ___ | (_)_ __   ___  _ __
 | |\/| | |     | |   / _ \| | | '_ \ / _ \| '_ \
 | |  | | |___ _| |__| (_) | | | |_) | (_) | |_) |
 |_|  |_|\____(_)_____\___/|_|_| .__/ \___/| .__/
                               |_|         |_|

******* Welcome to Lolipop! Managed Cloud *******

YYY@WWW:~$ pwd
/var/app
YYY@WWW:~$ ls
0000000000000000  current  shared  user_command.sh.sample
YYY@WWW:~$ ls current
Gemfile  Gemfile.lock  README.md  Rakefile  app  bin  config  config.ru  db  lib  log  package.json  public  test  tmp  vendor

普通にはいれますね。

オートスケールの設定はこんな感じ。

f:id:hkdnet:20171202233545p:plain

rails だとイメージ1個しかないんですね。nginx はさもうとかしたときにどこで何すればいいかはよくわからず。
とはいえたぶんそのへんってマネージドクラウドのサービス側でラウンドロビンとかはしてくれるんだろうなー。
static file の配信をうんぬんというのはいまんところ難しそう?CDN使えって感じかな。
ラウンドロビンするって考えると sticky な設定とかがあるのか気になります1

デプロイとかは medium にあるヘルプから察するに push すればよさそうです。
でも remote add したあとに push するだけだと --force いるんじゃないかなあ……。
という気持ちをもとにとりあえず git init して remote を追加した状態で fetch してみましょう

# local
$ git init
$ git remote add lolipop xxx # 表示されているやつをうつ
$ git fetch lolipop
fatal: '/var/lib/lolipop/projrepo/.git' does not appear to be a git repository
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

実はまだリモートレポジトリは存在していなかったようです。
念のためマネージドクラウド側のコンソールでも確認

# managed cloud
$ ls /var/lib/lolipop/projrepo/
ls: cannot access /var/lib/lolipop/projrepo/: No such file or directory

仕方ないので rails new しておきましょう。
データベースを選んだ記憶はありませんがホスト名が mysql なのでおそらく mysql なのでしょう。

# local
$ bundle init
# Gemfile を適当に編集
$ bundle install --path vendor/bundle
$ bundle exec rails new --database=mysql .

database.yml もなんかいい感じに編集すればいいんじゃないでしょうか
記載内容はダッシュボードから拾ってきてください。

diff --git a/config/database.yml b/config/database.yml
index 0c61325..1a636b9 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -49,6 +49,7 @@ test:
 #
 production:
   <<: *default
-  database: tmp-rails_production
-  username: tmp-rails
-  password: <%= ENV['TMP-RAILS_DATABASE_PASSWORD'] %>
+  database: your_database
+  username: your_username
+  password: your_password
+  host: mysql-1.mc.lolipop.lan

password はベタ書きせずに環境変数にいれたくなりますがどーもその機構はない気がするので強い気持ちでコミットします

dbつかえるか確かめたいので適当に scaffold しておきましょう

$ bin/rails g scaffold Comment text:text
$ git commit -m 'comment'
$ git push lolipop master

と、これは別に意図してなかったのですがうっかり db:create, db:migrate を打つのを忘れてました。てへぺろ
なので db/scheam.rb は更新がない状態でデプロイされましたが db:create, db:migrate されたログが残って普通に起動しましたね

$ git push lolipop master
(中略)
remote: Database 'XXX' already exists
remote: == 20171202150523 CreateComments: migrating ===================================
remote: -- create_table(:comments)
remote:    -> 0.0046s
remote: == 20171202150523 CreateComments: migrated (0.0047s) ==========================
remote:

普通にアクセスできますね。

f:id:hkdnet:20171203004551p:plain

最初は、レスポンスが返ってくるまでに13秒くらいかかってましたが、dev tool開いたあたりからいきなり早くなりました。なぞい。
謎いなーと思いながらダッシュボードを見てたらコンテナ数が増えてたみたいです。

f:id:hkdnet:20171203005200p:plain

うんうん、そしたらはやくなるかもねって思ったのですが、コンテナの数が増えたからって1ユーザーのリクエストがそんな早くなるわけないですよね。
うーん、増えたコンテナに性能の善し悪しがあるのでしょうか?

あと適当にさわってるとときたま落ちます。落ちたときに ssh してログをみてみましょう。
適当にエスパーして $HOME/current/ が現在のアプリケーションだと考えるとその先の $HOME/current/log/production.log を見ればよさそうです。
vim が入ってるので適当にひらいておきます(less はなかった)

I, [2017-12-02T15:56:45.157297 #4]  INFO -- : [de41a671-8b69-4771-8c0e-83187b2c6456] Started POST "/comments" for x.x.x.x at 2017-12-02 15:56:45 +0000
(略)
W, [2017-12-02T15:56:45.159596 #4]  WARN -- : [de41a671-8b69-4771-8c0e-83187b2c6456] Can't verify CSRF token authenticity.
I, [2017-12-02T15:56:45.159979 #4]  INFO -- : [de41a671-8b69-4771-8c0e-83187b2c6456] Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms)
F, [2017-12-02T15:56:45.161589 #4] FATAL -- : [de41a671-8b69-4771-8c0e-83187b2c6456]
F, [2017-12-02T15:56:45.161650 #4] FATAL -- : [de41a671-8b69-4771-8c0e-83187b2c6456] ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):

うーん、CSRF対策のトークンの検証で死んでますね。
たぶんなんですけど画面をレンダリングしたコンテナと違うところにいったら死ぬのかな……。

まとめ

現状での懸念点は以下でしょうか

  • 環境変数を管理するクチがない?
  • プロジェクトに対してミドルウェアを挟む余地が今のところなさそう
    • いずれちゃんと対応されそう
  • コンテナベースなんだけどライフサイクルがわからない
    • テクニカルなドキュメントが充実すれば……
  • デプロイ時に何が起きるのかブラックボックス過ぎて気になる
    • 知りたいし変更したい
  • そもそも ssh してる先ってどこなんだろ?
    • コンテナの中なの?コンテナのホストなの?
    • これもまあドキュメントが充実すれば
  • ssh先のディレクトリ構成がよくわからん
    • cap っぽい雰囲気を感じたけど
  • ログみたいときにいまのところ ssh するしかない
    • いずれちゃんと対応され……るのだろうか?
    • 他サービスに流しちゃうほうが楽かも
  • どんくらいのコスト感なのか知りたい
    • 有料化が決まる頃にはわかるので待つだけ

正直、いまのところはお手軽感よりブラックボックス感がつよい印象です。今後に期待ということで。


追記: つかったプロジェクト削除しようとしたらできなくて困っている……
削除確認用にプロジェクト名をいれたあとパスワードをいれても削除に失敗しました、と出てしまう。
変な文字列いれるとパスワードが違うと言われるので認証はできてるみたいなんだけど。
時間課金とかになるだろうから、削除リクエスト自体はこの時間で受けてほしいなーとおもいつつリソースはまだ使えちゃうからどうなんだろうなと思っている


  1. (フラグ

gitしぐさ 2017年11月編

最近の git コマンド運用録です。
雑に書いていきます。

前提

リモートレポジトリ戦略

常に以下の状態を維持します。

upstream: みんなの更新が集まってくるところ
origin: 自分の変更をいれるところ

なので例えば ruby/ruby のようなOSSだと fork 前提なのでこうなります。

upstream: git@github.com:ruby/ruby.git
origin: git@github.com:hkdnet/ruby.git

あるいは自分しか使わないプロジェクトだとforkできない(する意味もない)ので upstream と origin が一致します。

upstream: git@github.com:hkdnet/stdlinux.git
origin: git@github.com:hkdnet/stdlinux.git

一見無駄ですが、どのようなプロジェクトだろうと全く同じ操作ができるというのが強みです。

upstream から更新をとってくる

git fetch upstream でおk

ローカルで作業を始める

git checkout -b branch_name upstream/master

upstream/master からブランチを切ります。
ローカルレポジトリのmasterは存在する意味がないので強い気持ちで消しましょう。

ブランチ切るときは上記のでいいし、ローカルで最新版を見たいときも git checkout upstream/master で十分です。

pr出す前に upstream/master に更新がはいった

git fetch upstream はもちろんしてあるものとして、 git rebase upstream/master でおkです。
ちゃんとこまめにやってればそんなにコンフリクトしないでしょう。
したとしても気合で解決を試みて、ダメなら git rebase --abort でやめればいいので気軽にやっています。

pushする

git push origin head です。
headは現在のブランチ名として解釈されるのでどんなブランチ名のときもこれでおkです。


なんか他に操作あったっけ……。思い出したら書きます。

ruby/ruby にパッチ送ろうとしたけど失敗してる話 part2

注: part2といいつつpart1は書き上がってないのでこれが最初です

背景

るりまのサンプルコードを整備しようというプロジェクトがあるのはご存知ですか。

tbpgr.hatenablog.com

id:tbpg さんがこちらで精力的に活動されているのですが、ある日こういう話を聞きました。

Enumerator::Lazy#chunk の定義が可変長引数に見えるんだけど実際は引数渡すとエラーになるんだよね〜」

で、サンプルを書こうとしたときにちょっと詰まったと聞いたので、定義を可変長引数でなくできないだろうかと Ruby hack Challenge もくもく会#2 のネタにさせてもらいました。

connpass.com

事前調査

まずはざっと定義をみておきましょう。ruby/rubyにおいて組込みで用意されているクラスのメソッドはC言語で書かれていることが多く、その場合は rb_define_method を使ってメソッドが定義されます1。 使い方は以下のような感じです。

rb_define_method(rb_cKlass, "method_name", cfunk, argc)

rb_cKlass クラスに "method_name" メソッドを宣言しており、実体が cfunk、その時の引数の数が argc です。覚えてしまえばかんたんですね。

さて、今回は chunk メソッドを探すので "chunk" で検索すればよさそうです。

$ git grep '"chunk"'
enum.c:    rb_define_method(rb_mEnumerable, "chunk", enum_chunk, 0);
enumerator.c:    rb_define_method(rb_cLazy, "chunk", lazy_super, -1);

2件ヒットしましたね。ruby/rubyのことは全然わかりませんが幸い少しはRubyの知識があるので rb_mEnumerableEnumerable モジュール、 rb_cLazyEnumerator::Lazy クラスだろうと推測できます。

一応それぞれの宣言を見ておきましょう。まずは Enumerator::Lazy のほうから。

/* enumerator.c */
void
InitVM_Enumerator(void)
{
...
    rb_cEnumerator = rb_define_class("Enumerator", rb_cObject);
    rb_include_module(rb_cEnumerator, rb_mEnumerable);
...
    /* Lazy */
    rb_cLazy = rb_define_class_under(rb_cEnumerator, "Lazy", rb_cEnumerator);
...
    rb_define_method(rb_cLazy, "chunk", lazy_super, -1);
...
}

void Init...(void) という関数はRubyを実行したときに、Rubyコードを実行する前の初期化をする関数です(だと思っています)。

rb_define_class_under は以下のシグネチャをもちます。名前的に outer がネームスペース、super が親クラスでしょう。

VALUE rb_define_class_under(VALUE outer, const char *name, VALUE super)

よって rb_cLazyrb_cEnumerator のインナークラスであり、なおかつ rb_cEnumerator の子クラスであることがわかります。Rubyでかくとこんな感じでしょうか。

class Enumerator
  class Lazy < Enumerator
  end
end

関数の実体は lazy_super で argc が -1なので可変長です。lazy_super の定義も参照しましょう。

/* enumerator.c */
static VALUE
lazy_super(int argc, VALUE *argv, VALUE lazy)
{
    return enumerable_lazy(rb_call_super(argc, argv));
}

enumerable_lazy は、まあたぶん lazy 化してくれるんでしょう。 rb_call_super は、まあsuper呼んでそうな気がしますね。こういうのは深追いすると時間が溶けていくので一旦はここで放置します。

rb_call_super で呼ばれる関数を一応確認しましょう。先程確認したように rb_cLazy の親クラスは rb_cEnumerator ですのでそちらの定義を確認します。

抜粋した中に rb_include_module(rb_cEnumerator, rb_mEnumerable) という箇所があり rb_mEnumerable が include されていることが確認できさらに grep 結果から実体が enum.cenum_chunk であることそしてargcが0であり引数をとらないことがわかります(早口

enum.c:    rb_define_method(rb_mEnumerable, "chunk", enum_chunk, 0);

enum_chunk まで追う必要はなさそうですがシグネチャくらいは置いておきます。

static VALUE
enum_chunk(VALUE enumerable)
{
...
}

ここまでで、以下のことが確認できました。

  • Enumurator::Lazy#chunk が可変長引数であること
  • 実体は Enumerator#chunk といってよさそうなこと
  • Enumerator#chunk は引数をとらないこと

hackしてみる

さて、では引数の数を変えてみましょう。さきほど見たように rb_define_method の argc を -1 から 0 に変えればよいでしょう。かんたんですね。こんなんで「hackしてみる」というような見出しをつけていいものでしょうか。心が痛みます(フラグ

diff --git a/enumerator.c b/enumerator.c
index d61d79e897..da109c8116 100644
--- a/enumerator.c
+++ b/enumerator.c
@@ -2379,7 +2379,7 @@ InitVM_Enumerator(void)
     rb_define_method(rb_cLazy, "drop", lazy_drop, 1);
     rb_define_method(rb_cLazy, "drop_while", lazy_drop_while, 0);
     rb_define_method(rb_cLazy, "lazy", lazy_lazy, 0);
-    rb_define_method(rb_cLazy, "chunk", lazy_super, -1);
+    rb_define_method(rb_cLazy, "chunk", lazy_super, 0);
     rb_define_method(rb_cLazy, "slice_before", lazy_super, -1);
     rb_define_method(rb_cLazy, "slice_after", lazy_super, -1);
     rb_define_method(rb_cLazy, "slice_when", lazy_super, -1);

さっそくビルドして検証しましょう。こういうgemが絡まないような検証のときは miniruby でやるものだと聞いた気がするのでそうします。検証コードも、どうせ動くし雑でいいんじゃないでしょうか(フラグ2

# tmp/lazy.rb
arr = %w(a a b)

p Enumerator.instance_method(:chunk).arity
p Enumerator::Lazy.instance_method(:chunk).arity
p arr.chunk do |e|
  p e
end
p arr.lazy.chunk do |e|
  p e
end

さて、minirubyを作って実行しましょう。

$ make miniruby
$ ruby -v
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]
$ ruby tmp/lazy.rb
0
-1
#<Enumerator: ["a", "a", "b"]:chunk>
#<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator::Lazy: ["a", "a", "b"]>:chunk>>
$ ./miniruby tmp/lazy.rb
0
0
#<Enumerator: ["a", "a", "b"]:chunk>
Traceback (most recent call last):
        2: from tmp/lazy.rb:8:in `<main>'
        1: from tmp/lazy.rb:8:in `chunk'
tmp/lazy.rb:8:in `chunk': wrong number of arguments (given 1309040872, expected 0) (ArgumentError) 

ん……?

given 1309040872

( д) ゚ ゚

引数の数が合わず、 expected が 0 なのに 1309040872 もの引数が渡されたことになっています。んなわけあるかーい。

なんどか実行してみると given の値が変化していることがわかります。また、この大きさからだいたいメモリアドレスを指しているんじゃないかなあという気持ちになります。

関数の中身をモックにしたりして原因を切り分けてみてはどうだろう、ともくもく会参加者の yui-knk さんにアドバイスをもらったのでやってみましょう。

diff --git a/enumerator.c b/enumerator.c
index d61d79e897..381f039c8a 100644
--- a/enumerator.c
+++ b/enumerator.c
@@ -2270,6 +2270,7 @@ lazy_uniq(VALUE obj)
 static VALUE
 lazy_super(int argc, VALUE *argv, VALUE lazy)
 {
+    return INT2FIX(1);
     return enumerable_lazy(rb_call_super(argc, argv));
 }

@@ -2379,7 +2380,7 @@ InitVM_Enumerator(void)
     rb_define_method(rb_cLazy, "drop", lazy_drop, 1);
     rb_define_method(rb_cLazy, "drop_while", lazy_drop_while, 0);
     rb_define_method(rb_cLazy, "lazy", lazy_lazy, 0);
-    rb_define_method(rb_cLazy, "chunk", lazy_super, -1);
+    rb_define_method(rb_cLazy, "chunk", lazy_super, 0);
     rb_define_method(rb_cLazy, "slice_before", lazy_super, -1);
     rb_define_method(rb_cLazy, "slice_after", lazy_super, -1);
     rb_define_method(rb_cLazy, "slice_when", lazy_super, -1);

INT2FIXC言語での int を Ruby の世界の Fixnum に変換するマクロです。必ず1を返すようにしてみました。

$ make miniruby
$ ./miniruby tmp/lazy.rb
0
0
#<Enumerator: ["a", "a", "b"]:chunk>
1

これでは普通に通りますね。lazy_super の中身は、 enumerable_lazyrb_call_super にわけられるのでとりあえず後者だけを呼ぶようにしてみます。

diff --git a/enumerator.c b/enumerator.c
index d61d79e897..d022c09ebd 100644
--- a/enumerator.c
+++ b/enumerator.c
@@ -2270,6 +2270,7 @@ lazy_uniq(VALUE obj)
 static VALUE
 lazy_super(int argc, VALUE *argv, VALUE lazy)
 {
+    return rb_call_super(argc, argv);
     return enumerable_lazy(rb_call_super(argc, argv));
 }

@@ -2379,7 +2380,7 @@ InitVM_Enumerator(void)
     rb_define_method(rb_cLazy, "drop", lazy_drop, 1);
     rb_define_method(rb_cLazy, "drop_while", lazy_drop_while, 0);
     rb_define_method(rb_cLazy, "lazy", lazy_lazy, 0);
-    rb_define_method(rb_cLazy, "chunk", lazy_super, -1);
+    rb_define_method(rb_cLazy, "chunk", lazy_super, 0);
     rb_define_method(rb_cLazy, "slice_before", lazy_super, -1);
     rb_define_method(rb_cLazy, "slice_after", lazy_super, -1);
     rb_define_method(rb_cLazy, "slice_when", lazy_super, -1);

実行してみると……

$ ./miniruby tmp/lazy.rb
0
0
#<Enumerator: ["a", "a", "b"]:chunk>
Traceback (most recent call last):
        2: from tmp/lazy.rb:8:in `<main>'
        1: from tmp/lazy.rb:8:in `chunk'
tmp/lazy.rb:8:in `chunk': wrong number of arguments (given -477732640, expected 0) (ArgumentError)

同じ事象が出ました。どうもsuper側で何かが起きているようですね。

super側の定義をタグジャンプして追ってみたのですがよくわかりません。仕方がないのでlldb(デバッガ)で無理やり止めてみるとあっさりわかりました2ブレークポイントを挟むのですが、 vm_call_super に挟むといろんなものでとまってめんどくさそうです。mid かなんかでとめるとよさそうな気がしました。rb_intern で文字列から ID3 を取り出せるのでif文つくっておけばいいでしょう。

diff --git a/enumerator.c b/enumerator.c
index d61d79e897..da109c8116 100644
--- a/enumerator.c
+++ b/enumerator.c
@@ -2379,7 +2379,7 @@ InitVM_Enumerator(void)
     rb_define_method(rb_cLazy, "drop", lazy_drop, 1);
     rb_define_method(rb_cLazy, "drop_while", lazy_drop_while, 0);
     rb_define_method(rb_cLazy, "lazy", lazy_lazy, 0);
-    rb_define_method(rb_cLazy, "chunk", lazy_super, -1);
+    rb_define_method(rb_cLazy, "chunk", lazy_super, 0);
     rb_define_method(rb_cLazy, "slice_before", lazy_super, -1);
     rb_define_method(rb_cLazy, "slice_after", lazy_super, -1);
     rb_define_method(rb_cLazy, "slice_when", lazy_super, -1);
diff --git a/vm_eval.c b/vm_eval.c
index f0f336a233..a2efe31736 100644
--- a/vm_eval.c
+++ b/vm_eval.c
@@ -229,6 +229,9 @@ vm_call_super(rb_execution_context_t *ec, int argc, const VALUE *argv)
        return method_missing(recv, id, argc, argv, MISSING_SUPER);
     }
     else {
+ if (id == rb_intern("chunk")) {
+     printf("break\n");
+ }
        return vm_call0(ec, recv, id, argc, argv, me);
     }
 }

lldb でブレークポイントを貼って実行します。bブレークポイントを貼って run ARGS で実行。とまったあとは p で中身見て bt でトレースが見れます。n がnext, s が step, c がcontinueなのまで覚えておけばだいたい大丈夫でしょう。

$ make miniruby
$ lldb ./miniruby
(lldb) target create "./miniruby"
Current executable set to './miniruby' (x86_64).
(lldb) b vm_eval.c:233
Breakpoint 1: where = miniruby`vm_call_super + 251 at vm_eval.c:233, address = 0x0000000100272a0b
(lldb) run tmp/lazy.rb
Process 35984 launched: './miniruby' (x86_64)
0
0
#<Enumerator: ["a", "a", "b"]:chunk>
Process 35984 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100272a0b miniruby`vm_call_super(ec=0x0000000100606e78, argc=25583824, argv=0x00000001018660d0) at vm_eval.c:233
   230      }
   231      else {
   232          if (id == rb_intern("chunk")) {
-> 233              printf("break\n");
   234          }
   235          return vm_call0(ec, recv, id, argc, argv, me);
   236      }
Target 0: (miniruby) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100272a0b miniruby`vm_call_super(ec=0x0000000100606e78, argc=25583824, argv=0x00000001018660d0) at vm_eval.c:233
    frame #1: 0x00000001002728a1 miniruby`rb_call_super(argc=25583824, argv=0x00000001018660d0) at vm_eval.c:244
    frame #2: 0x000000010008c60f miniruby`lazy_super(argc=25583824, argv=0x00000001018660d0, lazy=0) at enumerator.c:2273
    frame #3: 0x00000001002844d3 miniruby`call_cfunc_0(func=(miniruby`lazy_super at enumerator.c:2272), recv=4320551120, argc=0, argv=0x0000000102000048) at vm_insnhelper.c:1735
    frame #4: 0x000000010028329d miniruby`vm_call_cfunc_with_frame(ec=0x0000000100606e78, reg_cfp=0x00000001020fffa0, calling=0x00007ffeefbfe428, ci=0x000000010051c910, cc=0x000000010051ca98) at vm_insnhelper.c:1924
    frame #5: 0x000000010027e9bd miniruby`vm_call_cfunc(ec=0x0000000100606e78, reg_cfp=0x00000001020fffa0, calling=0x00007ffeefbfe428, ci=0x000000010051c910, cc=0x000000010051ca98) at vm_insnhelper.c:1940
    frame #6: 0x000000010027de9e miniruby`vm_call_method_each_type(ec=0x0000000100606e78, cfp=0x00000001020fffa0, calling=0x00007ffeefbfe428, ci=0x000000010051c910, cc=0x000000010051ca98) at vm_insnhelper.c:2238
    frame #7: 0x000000010027dc30 miniruby`vm_call_method(ec=0x0000000100606e78, cfp=0x00000001020fffa0, calling=0x00007ffeefbfe428, ci=0x000000010051c910, cc=0x000000010051ca98) at vm_insnhelper.c:2359
    frame #8: 0x000000010027db85 miniruby`vm_call_general(ec=0x0000000100606e78, reg_cfp=0x00000001020fffa0, calling=0x00007ffeefbfe428, ci=0x000000010051c910, cc=0x000000010051ca98) at vm_insnhelper.c:2402
    frame #9: 0x000000010026a5d8 miniruby`vm_exec_core(ec=0x0000000100606e78, initial=0) at insns.def:933
    frame #10: 0x0000000100279586 miniruby`vm_exec(ec=0x0000000100606e78) at vm.c:1797
    frame #11: 0x000000010027a2fb miniruby`rb_iseq_eval_main(iseq=0x0000000101866dc8) at vm.c:2045
    frame #12: 0x000000010009b928 miniruby`ruby_exec_internal(n=0x0000000101866dc8) at eval.c:246
    frame #13: 0x000000010009b831 miniruby`ruby_exec_node(n=0x0000000101866dc8) at eval.c:310
    frame #14: 0x000000010009b7f0 miniruby`ruby_run_node(n=0x0000000101866dc8) at eval.c:302
    frame #15: 0x0000000100001881 miniruby`main(argc=2, argv=0x00007ffeefbfed10) at main.c:42
    frame #16: 0x00007fff5bd3b145 libdyld.dylib`start + 1

長いのが出てきてウゲーとなりますが、よくみるとargcという文字列も見えますね。そこだけ追うと以下の箇所がめちゃくちゃあやしいです。

    frame #2: 0x000000010008c60f miniruby`lazy_super(argc=25583824, argv=0x00000001018660d0, lazy=0) at enumerator.c:2273
    frame #3: 0x00000001002844d3 miniruby`call_cfunc_0(func=(miniruby`lazy_super at enumerator.c:2272), recv=4320551120, argc=0, argv=0x0000000102000048) at vm_insnhelper.c:1735

call_cfunc_0 では 0 だった argc が lazy_super には 25583824 で渡されています。こいつっぽい。

vm_insnhelper.c:1735 らしいので call_cfunc_0 を見てみましょう。

static VALUE
call_cfunc_0(VALUE (*func)(ANYARGS), VALUE recv, int argc, const VALUE *argv)
{
    return (*func)(recv);
}

な、なんもわからん……。もう1つ前のフレームを見ましょう。vm_call_cfunc_with_framevm_insnhelper.c:1924 だそうです。いらなそうなところを消していくとこんな感じです。

static VALUE
vm_call_cfunc_with_frame(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
    VALUE val;
    const rb_callable_method_entry_t *me = cc->me;
    const rb_method_cfunc_t *cfunc = vm_method_cfunc_entry(me);
    int len = cfunc->argc;

    VALUE recv = calling->recv;
    VALUE block_handler = calling->block_handler;
    int argc = calling->argc;
...
    val = (*cfunc->invoker)(cfunc->func, recv, argc, reg_cfp->sp + 1);
...
    return val;
}

1924行目は val = ... の行です。cfunc->invokerが関数へのポインタになっていて4、invokerポインタの指す先の関数を cfunc->func, recv, argc, reg_cfp->sp + 1 という引数で呼んでいるようです。 どうして invoker を使っているのでしょうか。これは cfunc への呼び出しをラップするためのもののようです。例えばさっき書いた call_cfunc_0 もinvokerで、第一引数にCの関数をもらって呼び出しています。

Array のメソッドを例にとると、引数が0個の rb_ary_uniqシグネチャは以下です。

static VALUE rb_ary_uniq(VALUE ary)

可変長引数の rb_ary_maxシグネチャはこんな感じです。

static VALUE rb_ary_max(int argc, VALUE *argv, VALUE ary)

呼び出し方が全然違うので invoker でラップしているんだろう、という感じです。呼び出すときに呼び出し方を考えるより、定義したときに呼び出し方を決めておいたほうが楽だって話かと思いました。

さて、chunkのbacktraceに戻りましょう。invoker である call_cfunc_0 を再掲します。

static VALUE
call_cfunc_0(VALUE (*func)(ANYARGS), VALUE recv, int argc, const VALUE *argv)
{
    return (*func)(recv);
}

このとき func は lazy_super へのポインタなので lazy_super を引数 recv で呼び出しています。

static VALUE
lazy_super(int argc, VALUE *argv, VALUE lazy)
{
    return enumerable_lazy(rb_call_super(argc, argv));
}

lazy_superの第一引数はargcですね。recvはVALUEで実体はunsigned intです5。なのでargcを期待しているところにrecvを入れてしまったのが原因のようですね。lazy_super に関しては recv 相当のものは第三引数になければなりません。 lazy_super が可変長引数のシグネチャをもち、引数0個のシグネチャと異なるのにargcだけ変えたことが原因のようです。 別の関数を切って動かしてみましょう。

diff --git a/enumerator.c b/enumerator.c
index da109c8116..31b18251b0 100644
--- a/enumerator.c
+++ b/enumerator.c
@@ -2273,6 +2273,12 @@ lazy_super(int argc, VALUE *argv, VALUE lazy)
     return enumerable_lazy(rb_call_super(argc, argv));
 }

+static VALUE
+lazy_super_0(VALUE obj)
+{
+    return enumerable_lazy(rb_call_super(0, NULL));
+}
+
 static VALUE
 lazy_lazy(VALUE obj)
 {
@@ -2379,7 +2385,7 @@ InitVM_Enumerator(void)
     rb_define_method(rb_cLazy, "drop", lazy_drop, 1);
     rb_define_method(rb_cLazy, "drop_while", lazy_drop_while, 0);
     rb_define_method(rb_cLazy, "lazy", lazy_lazy, 0);
-    rb_define_method(rb_cLazy, "chunk", lazy_super, 0);
+    rb_define_method(rb_cLazy, "chunk", lazy_super_0, 0);
     rb_define_method(rb_cLazy, "slice_before", lazy_super, -1);
     rb_define_method(rb_cLazy, "slice_after", lazy_super, -1);
     rb_define_method(rb_cLazy, "slice_when", lazy_super, -1);

どうせargc, argvは使わないはずなのでsuperの呼び方は適当でいいでしょう。

$ make miniruby
$ ./miniruby tmp/lazy.rb
0
0
#<Enumerator: ["a", "a", "b"]:chunk>
#<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator::Lazy: ["a", "a", "b"]>:chunk>>

無事通りました。

パッチを送るべきか?

よっしゃ、通った!パッチ送ろう!と思ったんですが。そもそも元のコードってRubyにおけるsuperへの委譲のよくあるパターンなんですよね。Rubyコードだとこんな感じ。

class Calc
  def add(x, y)
    x + y
  end
end

class Calc2 < Calc
  def add(*) # 可変長引数にしてsuperに委譲。superの引数の数を考えなくてよい
    super * 2 # 全部2倍にする、みたいな処理
  end
end

これは Calc#add の引数の数がなんであろうと絶対2倍にするんだってときに役にたつんですよね。親クラスに依存しすぎないようにしてる。

一方今回いれたhackはこの Calc2#add で仮引数が x, y であると明示しているようなパターンになります。具体的には、こういうコードでの挙動が変わります。

# tmp/include.rb
arr = %w(a a b)
module Foo
  def chunk(arg)
    puts arg # override
  end
end
Enumerator::Lazy.include Foo
arr.lazy.chunk(1) { |e| e }

まあこのコードが何がしたいのかはおいといて……。include によって Enumerator::Lazy#chunk の super 呼び出しが Enumerator#chunk から Foo#chunk に変わっています。Foo#chunk は明示的に引数を1つとるメソッドです。

$ ruby tmp/include.rb
1
$ ./miniruby tmp/include.rb
Traceback (most recent call last):
        1: from tmp/include.rb:9:in `<main>'
tmp/include.rb:9:in `chunk': wrong number of arguments (given 1, expected 0) (ArgumentError)

今回のパッチがあたっている miniruby のほうでは Enumerator::Lazy#chunk が委譲先の引数の数を気にするようになったことで ArgumentError になってしまいました6。 とゆーわけでそもそものアプローチが筋悪だったのかな、というところで今回はおしまい。いつになったらそれっぽいパッチが送れるようになるのでしょうか……。


  1. Rubyの世界でいう def みたいなもん。これは覚えちゃったほうが楽なので覚えてしまいましょう

  2. ざっと書いてるけどデバッガ準備してなかったからやるのめんどくさーいって言いながらタグジャンプで1時間ほどコード眺めていた。最初からやっておけば……

  3. Rubyプロセス内で一意に識別するためのもので、メソッドやインスタンス変数はキーバリューを保持できるテーブルからIDをキーにして探索されている

  4. cfuncはcで定義されたメソッドを表す構造体だと思っておいてよさそう

  5. たぶん環境依存だけど。僕の手元だと VALUE = uintptr_t = unsigned intっぽい

  6. エラーではあるが given 1 に安心感がある

MDNのドキュメントにコントリビュートしてみたのでやり方書くよ

MDN、ご存知ですか。jsとかcssとかでググってみるとだいたいひっかかると思います。

developer.mozilla.org

んで、これをちょっと読んでたんですがサンプルコードがわけわかんなかったんですよね。
僕が作った例のがイケてるんじゃね?と思ったのでコントリビュートしてみました。
やってみると結構かんたんだったので日本語でやったよってブログを書いたら、みんなもMDN書いてくれないかなー、ついでにブログのアクセス数増えないかなーみたいなお気持ちでこの記事を書いてます。

元ドキュメントのなにがわからんかったのか

arrow function は独自の arguments を持たないっていうところ1の説明のサンプルが3つあってこんな感じでした。

var arguments = 42;
var arr = () => arguments;

arr(); // 42

レキシカルスコープ上の arguments 変数を見てる例。これはまあわかる

function foo() {
  var f = (i) => arguments[0] + i; // foo's implicit arguments binding
  return f(2);
}

foo(1); // 3

アロー関数で定義された関数 f の中では arguments は参照できず、
関数 foo の arguments がレキシカルに見えてて、
関数 f の arguments はとれてないぞという例。
なんだけど関数 f に仮引数名がついてるせいで
arguments で取りたいんだろうなという気持ちが推測しにくい……。

ここで「rest parameters を代わりに使えるよ」という説明2がはいる

function foo() { 
  var f = (...args) => args[0]; 
  return f(2); 
}

foo(); // 2

この例がキツくて、 arguments の代わりに使えるって言ってるのにさっきのfooと全然違うことしてる。

というわけで一連の流れがよくないんですよね。
たとえばこんな感じの例だとどうだろうかーと書いてみたのが以下。

var arguments = [1, 2, 3];
var arr = () => arguments[0];

arr(); // 1

function foo(n) {
  var f = () => arguments[0] + n; // foo's implicit arguments binding. arguments[0] is n
  return f(10);
}

foo(1); // 2

/* ここに説明がはいる */

function foo(n) { 
  var f = (...args) => args[0] + n; 
  return f(10); 
}

foo(1); // 11

id:alluser に確認してみたら、こっちのがいいんじゃないのとのこと。
というわけでこの差分を突っ込むぜー

MDNの記事の編集方法

じゃあどうやってなおすかという話なんですが割りとあっさりできました。

とりあえずはアカウント作らないとなので作成。
ページ右上にGitHubでログインできそうななにかがあるのでクリック

f:id:hkdnet:20171022011454p:plain

そうするとよくある2FAの画面がでてくるので、よく読んでOK。
僕がみたときはGitHubに登録してあるメールアドレス3へのアクセスをしてもいいかと聞かれました。
その後の登録フォームはちゃちゃっと埋めましょう。どのメールアドレス使うとか。
これでアカウント登録が完了です。

んで、次はここをやるぞーってページで編集ボタンをおします。
これも右上のほうにあります。

f:id:hkdnet:20171022012025p:plain

そうするとエディタ画面にとぶので、もうあとは直すだけです。
見たまま編集的な感じなので使い方でそんなに戸惑うことはないんじゃないかなー、ということで省略。

最後投稿する前にページ下部の「リビジョンのコメント」はわすれずに。
コミットログと同じなので丁寧に書いておきましょう4

f:id:hkdnet:20171022012353p:plain

そうするとこんな感じ(リンク)に何を変更したよーみたいな履歴が残ります。

f:id:hkdnet:20171022012615p:plain

はい、かんたーん。

というわけで気になるところがあったらバンバン変更してみましょう。

参考

あとからみたら日本語でコントリビュートのやり方書いてあった

developer.mozilla.org


  1. ここ Arrow functions - JavaScript | MDN だけどもう書き換えちゃった

  2. In most cases, using rest parameters is a good alternative to using an arguments object.

  3. public にしてないアドレスも含まれます。たぶん form に使うメールアドレスの候補として入れるのにしか使ってないと思うんだけど確証はないです。

  4. これのちょっと上に「レビューを受ける」みたいなチェックボックスがあるんだけどこれチェックしても直後に反映されてビビった

Ruby hack Challenge もくもく会 に参加してきました

connpass.com

一言感想。コミッターの方が多数いてすげーなーという気持ちになりました


えーとこの会自体は Ruby hack Challenge という企画のもくもく会です。
企画自体はこちらのレポを参照するのがよいかと。

codeiq.jp

僕は rhc 自体には参加してないのですが RubyKaigi 以降はよく ruby/ruby を眺めているのでいってきました。

もくもく会なのでみんな適当になんかする、という感じだったのですが、僕はあまり何をするか考えていませんでした。とりあえずruby/rubyよんで気になったとこを見ようかなという気持ちです。
特に目的も定めずにざーっと読んで読んで、読み進めて。
気になったのが array.c の Array#uniq! の実装。frozen チェックのあたりがちょっと気になります。

static VALUE
rb_ary_uniq_bang(VALUE ary)
{
    VALUE hash;
    long hash_size;

    rb_ary_modify_check(ary);
    if (RARRAY_LEN(ary) <= 1)
        return Qnil;
    if (rb_block_given_p())
        hash = ary_make_hash_by(ary);
    else
        hash = ary_make_hash(ary);

    hash_size = RHASH_SIZE(hash);
    if (RARRAY_LEN(ary) == hash_size) {
    return Qnil;
    }
    rb_ary_modify_check(ary);
    / * ary を削ってからいれるような処理 */
}

rb_ary_modify_check は freeze 状態だったら例外を投げる的なやつです。
でも関数の最初のほうで rb_ary_modify_check をしているのにもう1回やってます。どういう理由でしょう。

最初に読んだときは「マルチスレッド対応かな?」と思っていたのですが、質問してみるとそうではないとのこと1
それより rb_block_given_p が truthy のときではないかとアドバイスを受けました。
RubyArray#uniq! はブロックをとると、その評価値をもとに uniq をしてくれる仕様です。このブロック評価時に freeze させた場合のためのチェックのようですね。
blame してみるとたしかにそういったコミットがひっかかります。

github.com

そんでなんやかんやしてたらすぐに時間がきて終了。パッチは作ったものの、既存の動作を変更するのでどうしたもんかなーと悩み中です2
できたらパッチ送りたかったけど送れず……。次回はもう少し先にアタリをつけておいてすすめたいですね。
次回が来月にあるということなので次回はパッチ投げるところまですすみたいものです


  1. なんでそうではないんだか忘れたんだけどGVLがある中で、(ブロック評価とかを除いて)Cの関数実行中にコンテキストスイッチが入らないみたいな話だった気がする……

  2. まだ悩んでる