RubyKaigi2018 基調講演2日目レポートで書かなかったことの補足
本記事は上記リンクの記事に関する補足です。プログラミング初心者です、Rails しかやってません、みたいな人でもわかることが望ましいとは思っていたのですが紙面の都合及びやたら技術解説がある記事になってもなあと思い書きませんでした。
明らかに説明足りてねーよな、というところについて補足します。
バインディング
Ruby/GTK3はGTK+プロジェクトのRubyバインディングです。言い換えると,C言語で書かれたGTK+にあるAPIをRubyから実行できるようにするライブラリです。
「バインディング」というのがそもそも「他言語で書かれたライブラリにあるAPIを、ある言語で実行できるようにするもの」みたいな感じですかね。別の言葉では ffi ( foreign function interface )
と呼ばれています。wikipedia によると ffi は Common 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 するときに実際には他にメモリ領域を確保している、ということが起きがちです。
- Ruby レベルで
foo = MyLib::Foo.new
する - バインディングしてるCレベルで malloc がおきる <- コレ
- foo がスコープを抜けたとかでGCされる
- foo のデストラクタで free される
Ruby は GC があるので上記のままでも特に問題がないように思えますが、このままではGCを実行すべきタイミングをうまく予想できないという問題が隠れています。
Ruby から見るとあくまでも foo
は MyLib::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
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
実際には @buffer
が nil かどうかで同じようなことをやっています。このへんを参照。
これより良さそうな 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 でふつーの引数でもキーワード引数でも渡せるようにしたい
def func(a, a:)
— はいと (@HaiTo_Linux) 2018年6月8日
puts a
end
ってしたときに、
func 'a'
ってしたら第一引数の a が利用できて
func a: 'a'
ってしたら第二引数の a が a として利用できる
みたいな、こう、Keyword引数としても使えるしそうでなくても使えるみたいなやつがほしいです
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
あたりでなんとかできないかなーとは思いつつ、実のところ自分ではそんなに使いそうにないからここでやめときます、
すごい雑にやるなら
— Ⓜ️ (@hanachin_) June 10, 2018
def func(_a = nil, a: _a)
p a
end
func(1)
func(a: 1)
みたいな感じでも...と思ってしまいましたがArgumentError投げたい場合はだるそうですね
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 に関するメモ書き
作ったけど微妙ですわこれ(手のひら返し
使い始めたら、 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}
RubyVM::AST::Node
の children を作成するときにいい感じに switch 文があったので拝借して、必要な情報を足すようにしました。extrainfo とかいう名前は仮置きだからまあ……。そして node type のあまりの多さにおののいています。
ちなみに、この辺の #define
によって「 nd_mid って結局 RNode 構造体のどこにあるんだっけ?」という疑問が解消されます。これめちゃくちゃ便利。見つけられてなかったら死んでた。最高!
RubyVM::AST を便利に使いたいので gem を作った
さっと作りました。 rubygems には登録してません(名前が重複してるかすら調べてないや)
経緯
Ruby 2.6.0preview2 から RubyVM::AST
モジュールが使えるようになりました。
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
を用いて遊ぶ予定があるのでそのときに使おうと思います
追記
便利そうだけど children 上書きするのやめて別メソッド生やすか、せめてRefinementsで上書きするようにしてほしい / “RubyVM::AST を便利に使いたいので gem を作った - Smoky God Ex…” https://t.co/HFKL9ChOK6
— tagomoris (@tagomoris) June 5, 2018
ハイパーそうですねという感じなので使い方を変えました
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
追試
~/.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
ほんとしょうもない……。