トップ 追記   RSS 1.0 FEED  

Journal InTime


2022-12-05 (Mon) [長年日記]

_ ...*, **, &の構文糖にした話

Ruby Advent Calendar 2022の5日目の記事です。

Feature #19134で以下の文法エラーのケースを修正し、**でキーワード引数だけ他のメソッドに渡すことができるように修正しました。

def foo(...)
  bar(*)  # OK
  baz(&)  # OK
  quux(**) # 文法エラー
end

つまり、def foo(...)def foo(*, **, &)の構文糖になりました。

また、この修正により、以下のケースでbarにキーワード引数が渡っていなかったバグも修正されました。

def foo(*, **, &)
  bar(...)
end

つまり、実引数の...*, **, &の構文糖になりました。

普通は冗長な表現に対して後から構文糖を用意することが多い(例えば{x: x, y: y}の構文糖として{x:, y:}を追加する)ので、こういうケースは珍しいのではないかと思います。

きっかけ

Bug #19132の修正中に以下のコードが目にとまりました。

#ifdef RUBY3_KEYWORDS
#define idFWD_KWREST idPow /* Use simple "**", as tDSTAR is "**arg" */
#else
#define idFWD_KWREST 0
#endif

仮引数で...を使用した場合は以下のように内部的な変数を用意していますが、Ruby 3ではキーワード引数も転送する必要があるので当然RUBY3_KEYWORDSというマクロはどこかで定義されているのだろうと思っていました。

static void
add_forwarding_args(struct parser_params *p)
{
    arg_var(p, idFWD_REST);
#if idFWD_KWREST
    arg_var(p, idFWD_KWREST);
#endif
    arg_var(p, idFWD_BLOCK);
}

ところが念のため以下のようにidFWD_KWRESTが真の時にエラーにするようにしてコンパイルしてもエラーが発生しませんでした。

static void
add_forwarding_args(struct parser_params *p)
{
    arg_var(p, idFWD_REST);
#if idFWD_KWREST
#error nokwrest
    arg_var(p, idFWD_KWREST);
#endif
    arg_var(p, idFWD_BLOCK);
}

ということは、

def foo(...)
  bar(**)
end

はエラーになるのではないか、と思って試してみたところ、たしかにエラーになったので冒頭の問題に気付きました。

なぜ...でキーワード引数が転送できていたのか

仮引数に...が使用されていた場合、以下のようにruby2_keywordsというフラグが立つようになっていました。

static NODE*
new_args(struct parser_params *p, NODE *pre_args, NODE *opt_args, ID rest_arg,
NODE *post_args, NODE *tail, const YYLTYPE *loc)
{
    ...
    args->ruby2_keywords = args->forwarding;
    ...
}

このフラグが立っていると、そのメソッドはModule#ruby2_keywordsが使用された場合と同様に、キーワード引数が*で受け取られるようになります。 他のメソッドに*を渡すとキーワード引数を含めて他のメソッドに転送できるので、**が定義されていなくてもキーワード引数を転送できていたわけです。

つまり、修正前の

def foo(...)
  bar(...)
end

ruby2_keywords def foo(*, &)
  bar(*, &)
end

の構文糖になっていたのでした(*はRubyレベルでは3.2で導入されましたが、それ以前のバージョンでも内部的には*という変数名が使われていました)。

まとめ

Ruby 3.2で***が導入されましたが、当初は...と併用されることを想定しておらず、すこしいびつな仕様になっていました。 ...*, **, &の構文糖であるというシンプルな仕様に修正しました。

ただ、この修正で...が遅くなっているようなのでひょっとしたらrevertされるかもしれません。

Tags: Ruby

2022-11-18 (Fri) [長年日記]

_ anonymous keyword rest parameterのバグ修正

とみたさんから報告があったanonymous keyword rest parameterのバグを修正した。 Ruby 3.2で

def foo(**)
  bar(**)
end

のように匿名でキーワード引数を他のメソッドに転送する機能が追加されたが、

def foo(x: 1, **)
  bar(**)
end

のようにキーワード引数を一部だけ転送する場合に文法エラーになるというもの。

修正自体はごく短いが、修正したらRubyVM::AbstractSyntaxTreeの動作が変わってしまったりしてあるべき動作を確認するのが大変だった。

調査の過程で動作に疑問がある部分があったので別のissueを作成しておいた。

def foo(...)
  bar(*) # OK
  baz(&) # OK
  quux(**) # NG
end

上記のように ... を使った時に *& は使えるが、 ** は使えない、というもの。 全部使えるか、全部使えない、のどちらかがよいように思うが、 & はRuby 3.1でも使えるので、全部使えるようにした方が互換性の面ではよさそう。

Tags: Ruby

2022-11-12 (Sat) [長年日記]

_ フクオカRuby大賞応募

mruby/c会議に来られていた福岡県庁の方から応募を勧めていただいたのでフクオカRuby大賞にTextbringerを応募してみた。

※MicrosoftOfficeがご利用いただけない場合は、応募用紙PDFをご覧いただき、記載されている各項目にご記入の上、ご提出ください。

ということだったが、Textbringerの応募資料をWordで書くのはプライドが許さなかったので、Markdownで書くことにした(「MicrosoftOffice」(スペースなし)という製品は知らないので「MicrosoftOfficeがご利用いただけない場合」に該当すると判断した)。

用紙サイズはA4です。

というレギュレーションをクリアするため、md-to-pdfでPDFに変換して、Textbringer上のMournmailから送信した。

問題は、

最近(概ね過去1年程度)のものを対象とします。

という部分だが、Mounrnmail 1.0.0を出したのが昨年6月なので周辺プラグインを含めて成熟したのがそのあたり、新規性を主張したString#byteindexの提案と利用も過去1年以内である。 担当者の方が「だいたいでいいですよ」とおっしゃっていたので大丈夫だろう。


2022-11-05 (Sat) [長年日記]

_ Rubyで理解する統計解析の基礎: 4〜5章

4章(notebook

4章はPyCallを使ったらほとんどPythonのコードそのままで動いてしまったので、Rubyで書き直した意味があまりなかったかも。

5章(notebook

5章は確率関数などの表現にあまりnumpyの機能が必要なさそうだったので、普通の配列で書いて、グラフの描画だけMatplotlibを使うことにした。 確率関数などをメソッドで表現すると、メソッド定義内で外側の変数が見えない、first class objectでない、などの問題があってPythonのサンプルコードの直訳が難しいので、Procで表現することにした。 また、大文字ではじまる変数名はRubyだと定数になってしまって不都合があるので、 _X のように先頭に _ を付けることにした。


2022-10-30 (Sun) [長年日記]

_ Rubyで理解する統計解析の基礎: 3章

numo-gslにpolyfitがなさそうなのと、rb-gslのインストールにRuby 3.xだと失敗するので、3章の途中からあきらめてPyCallを使った。

poly_fit = Numpy.polyfit(english_scores, math_scores, 1)
poly_1d = Numpy.poly1d(poly_fit)
xs = Numpy.linspace(english_scores.min, english_scores.max)
ys = poly_1d.(xs)

だとysがpoly1d([37. , 37.85714286, 38.71428571, ... になってしまって関数が適用されないので

ys = Numpy.array(xs.to_a.map { |x| poly_fit[0] * x +  poly_fit[1] })

としたけど何かもっといい方法がありそうな気がする。

10/31追記

Numpy.poly1dは関数風のクラスを返すようで、

poly_1d = Numpy.poly1d.(poly_fit)

のように .() を使用したら poly_1d.(xs) で期待した結果を得られた。 クラスなので、 Numpy.poly1d.(poly_fit)Numpy.poly1d.new(poly_fit) でもよいようだ。