RubyKaigi2018 基調講演2日目レポートで書かなかったことの補足

gihyo.jp

本記事は上記リンクの記事に関する補足です。プログラミング初心者です、Rails しかやってません、みたいな人でもわかることが望ましいとは思っていたのですが紙面の都合及びやたら技術解説がある記事になってもなあと思い書きませんでした。
明らかに説明足りてねーよな、というところについて補足します。

バインディング

Ruby/GTK3はGTK+プロジェクトのRubyバインディングです。言い換えると,C言語で書かれたGTK+にあるAPIRubyから実行できるようにするライブラリです。

バインディング」というのがそもそも「他言語で書かれたライブラリにあるAPIを、ある言語で実行できるようにするもの」みたいな感じですかね。別の言葉では ffi ( foreign function interface ) と呼ばれています。wikipedia によると ffiCommon Lisp 由来の言葉で、バインディングというのが Ada などに由来すると書かれています。
C や C++ で書かれているミドルウェアより下のレイヤについて Ruby などのからアクセスするのはだいたいバインディングの形式をとっている気がします。たまーにそういったバインディングではなくピュア実装です、と銘打ってるライブラリがあったりします。例えば nodejs の mysql ライブラリは "A pure node.js JavaScript Client implementing the MySql protocol." と自称しています。おそらくですがバインディングを使うとC, C++ のレイヤでブロックしてしまいノンブロッキングにならないからだと思います。

rb_gc_adjust_memory_usage

バインディングを利用していると、あるオブジェクトを new するときに実際には他にメモリ領域を確保している、ということが起きがちです。

  1. Ruby レベルで foo = MyLib::Foo.new する
  2. バインディングしてるCレベルで malloc がおきる <- コレ
  3. foo がスコープを抜けたとかでGCされる
  4. foo のデストラクタで free される

RubyGC があるので上記のままでも特に問題がないように思えますが、このままではGCを実行すべきタイミングをうまく予想できないという問題が隠れています。 Ruby から見るとあくまでも fooMyLib::Foo インスタンス1つ分のメモリしか使用していません。そのため Ruby は「まだ全然メモリ使ってないからGCしなくていいや」という判断をしてしまいます。しかし実際には malloc で確保されたメモリも使っています。これが記事中の「Ruby本体以外から確保されたメモリ量を認識できない」という問題です。
追加された rb_gc_adjust_memory_usage 関数は、malloc, free したメモリ量を記録することで Ruby に使用しているメモリ量を認識させることができます。

datadog の ruby ライブラリ dogapi で batch_metrics をネストするとダメそう

NOTE: v1.3.0 で見てます。

先日 datadog にメトリクスなげてーなと思ったのでライブラリの使い方を見てたのですがあんまり出来がよくなさそうです。気になったのはここ。

dog.batch_metrics do
  dog.emit_point('test.api.test_metric',10)
  dog.emit_point('test.api.this_other_metric', 1, :type => 'counter')
end

docs.datadoghq.com

batch_metrics というメソッド自体は、1回1回リクエストを投げずにためて投げるというやつですね。ふつーのバッチ処理

ですが、サンプルコードを見ただけでなんとなーくあやしい感じがします。内部実装を素直に予想するとこんな感じになりそうですね。

def batch_metrics
  @batch_mode = true # 逐次で投げないようにするフラグ
  yield
  @batch_mode = false
  flush! # ここでためたのを投げる
end

でもこれだとこういうときに困りそうです。手元で spec 書いたら落ちるしたぶんダメ。

dog.batch_metrics do
  # @batch_mode = true になる
  dog.batch_metrics do
    dog.emit_point('test.api.test_metric',10)
  end
  # @batch_mode = false になってしまう
  dog.emit_point('test.api.this_other_metric', 1, :type => 'counter') # 逐次で送られるんじゃね?
end

実際には @buffernil かどうかで同じようなことをやっています。このへんを参照。

https://github.com/DataDog/dogapi-rb/blob/d66698a363ae851845525d30a7d6a8afa1927dc5/lib/dogapi/facade.rb#L108-L116

https://github.com/DataDog/dogapi-rb/blob/d66698a363ae851845525d30a7d6a8afa1927dc5/lib/dogapi/v1/metric.rb#L52-L58

これより良さそうな API もあって、こんな感じ。

dog.batch_metrics do |batch_dog| # バッチリクエスト用のクライアントを返す
  batch_dog.emit_point('test.api.test_metric',10)
  batch_dog.batch_metrics do |batch_dog2| # batch_dog と batch_dog2 は同じものを返せばよい
    batch_dog.emit_point('test.api.test_metric',10)
  end
  # 最初の block から抜けるときに作られたものを flush すればよい
end

というのでなんかよくできるんじゃないかなーとプロジェクトを見てたら rspec なのに let 使ってなかったりでちょっと心が折れた日曜日なのでした。

まあ実際こういうの送るときにネストしたりしないからこまんないような気もしますね

Ruby でふつーの引数でもキーワード引数でも渡せるようにしたい

Swift の named parameters みたいな使い方のやつですかね。

こんなんでどうでしょうか、というのを tw では雑に書いたもののちゃんと清書していなかったので置いときます。

class Foo
  FOO__ARG1 = Object.new
  private_constant :FOO__ARG1
  FOO__NAME = Object.new
  private_constant :FOO__NAME


  def foo(_arg1 = FOO__ARG1, name: FOO__NAME)
    unless FOO__ARG1.equal?(_arg1)
      name = _arg1
    end
    if FOO__NAME.equal?(name)
      raise ArgumentError
    end

    puts "name: #{name}"
  end
end

Foo.new.foo(name: 'taro')
Foo.new.foo('hanako')
Foo.new.foo
$ ruby foo.rb
name: taro
name: hanako
Traceback (most recent call last):
        1: from foo.rb:24:in `<main>'
foo.rb:15:in `foo': ArgumentError (ArgumentError)

nil を使うと nil を渡せないということがあるので適当に Object.new して object_id 比較をしています。
引数が足りない場合は自前で raise してます。

これ書くのめんどいなーと思って、self.class.instance_method(__method__).params あたりでなんとかできないかなーとは思いつつ、実のところ自分ではそんなに使いそうにないからここでやめときます、


class Foo
  FOO__ARG1 = Object.new
  private_constant :FOO__ARG1

  def foo(_arg1 = FOO__ARG1, name: _arg1)
    if FOO__ARG1.equal?(name)
      raise ArgumentError
    end

    puts "name: #{name}"
  end
end

仮引数使うというアイデアのおかげでちょっと短くなりました。

RubyVM::AST に関するメモ書き

hkdnet.hatenablog.com

作ったけど微妙ですわこれ(手のひら返し
使い始めたら、 children にアクセスするのに node_type とかそんなに意識したくないんだよなーということに気づきました

いまやろうとしてるのは RubyVM::AST を使って Ruby インタプリタRuby で書くことなんですけど。
例えば puts 1 という文字列をみて、「あーノード的にはFCALL呼び出しで、メソッドがこれなのね、はいはい」という気持ちで処理したいわけです。
なんだけども、実はメソッド名(= mid, ここでは puts ) って NODE としては情報持ってないんですよね。

$ ruby -v
ruby 2.6.0preview2 (2018-05-31 trunk 63539) [x86_64-darwin17]
$ ruby --dump=pa -e 'puts 1'
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (line: 1, location: (1,0)-(1,6))
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_FCALL (line: 1, location: (1,0)-(1,6))*
#     +- nd_mid: :puts
#     +- nd_args:
#         @ NODE_ARRAY (line: 1, location: (1,5)-(1,6))
#         +- nd_alen: 1
#         +- nd_head:
#         |   @ NODE_LIT (line: 1, location: (1,5)-(1,6))
#         |   +- nd_lit: 1
#         +- nd_next:
#             (null node)

puts(1,0)-(1,4) のはずなんだけどそうした位置情報を持つ NODE はありません。一方で nd_mid という情報で :puts はもっています。ここでいきなりメソッド名がシンボルになっていますが、Ruby は内部的なメソッド名などはシンボルとしてもっているので(たしか)そうなっています。

位置のほうに話をもどすと、引き算をしたりすればたぶんなんとかなるんですが puts 1 とか puts(1) の差異とか考えたくないし AST っていうなら NODE_FCALL にまつわる mid もくださいよ!という気持ちになったのでした。そうすると c コード書く必要がありますね。

とゆーわけでとりあえず積んでるのがこのへん。これによって extrainfo とかやると Hash で情報が返ってきます。

$ ./miniruby -e 'p RubyVM::AST.parse("puts 1").children[1].extrainfo'
{:mid=>:puts}

https://github.com/hkdnet/ruby/compare/b7595f2c2ec14389170808dfb36b1d99c9d0e899...814746725cbb26d139feb9f3a5cc74b22b200dd0?expand=1

RubyVM::AST::Node の children を作成するときにいい感じに switch 文があったので拝借して、必要な情報を足すようにしました。extrainfo とかいう名前は仮置きだからまあ……。そして node type のあまりの多さにおののいています。

ちなみに、この辺の #define によって「 nd_mid って結局 RNode 構造体のどこにあるんだっけ?」という疑問が解消されます。これめちゃくちゃ便利。見つけられてなかったら死んでた。最高!

ruby/node.h at trunk · ruby/ruby · GitHub

RubyVM::AST を便利に使いたいので gem を作った

github.com

さっと作りました。 rubygems には登録してません(名前が重複してるかすら調べてないや)

経緯

Ruby 2.6.0preview2 から RubyVM::AST モジュールが使えるようになりました。

Ruby 2.6.0-preview2 Released

RubyVM::AST [Experimental] Ruby 2.6 introduces RubyVM::AST module.

This module has parse method which parses a given ruby code of string and returns AST (Abstract Syntax Tree) nodes, and parse_file method which parses a given ruby code file and returns AST nodes.

RubyVM::AST::Node class is also introduced you can get location information and children nodes from Node objects. This feature is experimental. Compatibility of the structure of AST nodes are not guaranteed.

要約:

  • Experimental だよ。互換性とか全然まるっきり全く担保する気がないよ
  • parse, parse_file というメソッドがあって AST を返すよ

こいつは parse すると RubyVM::AST::Node を返します。tree なので子供がいるのですが children と呼ぶと RubyVM::AST::Node | NilClass の配列が返ります。

なんですけど、ちょっとわかりにくいんですよね。children の数は node_type によって固定なんですが覚えられないし、何番目が何かわからないし。ちなみに具体的な対応はこの辺を参照してください

なので雑に node_type 文字列を key にした Hash を返すようにしてみました。わーべんり(べんり?)

RubyVM::AST.parse("1 + 2").children
# => [nil, #<RubyVM::AST::Node(NODE_OPCALL(36) 1:0, 1:5): >]
require 'ast_tools/hash'
RubyVM::AST.parse("1 + 2").children
# => {"node_opcall"=>#<RubyVM::AST::Node(NODE_OPCALL(36) 1:0, 1:5): >}

他にももう少し RubyVM::AST を用いて遊ぶ予定があるのでそのときに使おうと思います


追記

ハイパーそうですねという感じなので使い方を変えました

https://github.com/hkdnet/ast_tools/pull/2

RubyVM::AST.parse("1 + 2").children
# => [nil, #<RubyVM::AST::Node(NODE_OPCALL(36) 1:0, 1:5): >]

AstTools::Hash.convert(RubyVM::AST.parse("1 + 2").children)
# => {"node_opcall"=>#<RubyVM::AST::Node(NODE_OPCALL(36) 1:0, 1:5): >}

# or refinement

module Foo
  using AstTools::Hash
  def self.foo
    RubyVM::AST.parse("1 + 2").children
    # => {"node_opcall"=>#<RubyVM::AST::Node(NODE_OPCALL(36) 1:0, 1:5): >}
  end
end

追試

hkdnet.hatenablog.com

~/.g/g/h/m/y/E22 ❯❯❯ ruby -v
ruby 2.6.0preview2 (2018-05-31 trunk 63539) [x86_64-darwin17]
~/.g/g/h/m/y/E22 ❯❯❯ time ruby --jit test.rb
略
NG: 0
ruby --jit test.rb  711.81s user 95.96s system 27% cpu 48:08.82 total

はやーい

オフラインリアルタイムどう書く E24 の回答

負けました。ほっとんどのテストケースは通るが4つほど通らない、というのがあるというのが時間内の回答です。

が、終わったあとの懇親会でぼーっと考えてたらあそこじゃね?というところに気づいて解けました。そしてその diff がしょーもない。

とりあえず方針はこんな感じです。

単調増加数は各桁に一定の大小関係があるので、桁間で「いくつプラスになるか」というのを割り振るものだと思えばよい。
この捉え方だとある桁数・進数における単調増加数のパターン数をよくある場合の数に落とせる。具体的には、 b-1個のボール(増加分)をr人(桁数)で分けるような何かである。

↓こんな感じ。

5個のボールを2つの仕切りでわける = 4進数2桁における単調増加数のパターン数

こういうマッピングができる
○|○|○○○ -> 12 (5)
○|○○|○○ -> 13 (5)
○○|○○|○ -> 24 (5)

これは nCk で表わせる場合の数である。

これを使うと b 進数 r 桁で表せる単調増加数のパターン数がわかるので m を超えないような最大の r で桁数が確定。次は r 桁のうち idx 番目を num にしたときの単調増加数のパターン数を調べていって桁を確定させると求める数がわかる。

実行速度はたぶんかなりはやい気がします。

$ ruby test.rb
(略)
ruby test.rb  0.43s user 0.18s system 74% cpu 0.818 total

回答のコードは以下

class Solver
  def solve(input)
    @b, @m = input.split(',').map(&:to_i)
    rank = 1
    count = 0


    loop do
      return '-' if rank > (@b - 1)
      # rank 個の仕切りを n - 1 箇所にいれればよい
      # rank 末尾と n の末尾になんか変なのいれてることに注意
      # → nCrankでよい
      tmp = count_for(n: @b - 1, k: rank)
      break if count + tmp >= @m
      count += tmp
      rank += 1
    end
    # 桁数確定

    idx = 1
    num = 1
    nums = []

    loop do
      t_n = @b - 1 - num
      t_k = rank - idx
      tmp = count_for(n: t_n, k: t_k)
      if count + tmp >= @m
        nums << num
        num += 1
        idx += 1
        break if nums.size == rank
        next
      end
      count += tmp
      num += 1
    end

    nums.map { |e| e.to_s(36) }.join('')
  end

  def count_for(n:, k:)
    raise ArgumentError if k > n

    ret = 1
    k.times do |i|
      ret = ret * (n - i)
    end
    k.times do |i|
      ret = ret / (k - i)
    end
    ret.to_i
  end

  def incr?(num)
    tmp = num.to_s(@b).chars
    return true if tmp.size == 1
    tmp.each_cons(2).all? do |c1, c2|
      c1 < c2
    end
  end
end

なお最終コミットはこちらです ↓

fix · hkdnet/misc@2efdfd7 · GitHub

ほんとしょうもない……。