天泣記

2018-09-02 (Sun)

#1 Proof Summit 2018

Proof Summit 2018 で発表した。

依存型の話

2018-09-13 (Thu)

#1 Ruby開発者会議 + Cookpad Ruby Hack Challenge

mameさんにRubyのキーワード引数の問題を説明してもらった。

キーワード引数の現状と将来構想

少し議論したのだが、じつはキーワード引数をあまり使っていなくて 全体的な仕様を把握していなかったので、 現時点の parse.y から、仮引数と実引数のsyntaxを抜粋してみた。 実際にはだいたいの部分が省略可能で、そのために省略形の規則がたくさんあるのだが、 煩雑なので省略していないものだけにしてある。

仮引数:

f_args          : f_arg ',' f_optarg ',' f_rest_arg ',' f_arg opt_args_tail
f_arg           : f_arg_item | f_arg ',' f_arg_item
f_arg_item      : f_arg_asgn | tLPAREN f_margs rparen
f_arg_asgn      : f_norm_arg
f_optarg        : f_opt | f_optarg ',' f_opt
f_opt           : f_arg_asgn '=' arg_value
f_arg_asgn      : f_norm_arg
f_norm_arg      : tIDENTIFIER
f_rest_arg      : restarg_mark tIDENTIFIER
restarg_mark    : '*' | tSTAR
opt_args_tail   : ',' args_tail
args_tail       : f_kwarg ',' f_kwrest opt_f_block_arg
f_kwarg         : f_kw | f_kwarg ',' f_kw
f_kw            : f_label arg_value | f_label
f_label         : tLABEL
f_kwrest        : kwrest_mark tIDENTIFIER
kwrest_mark     : tPOW | tDSTAR
arg_value       : arg

ここで、tIDENTIFIER は識別子、tSTAR は *、tLABEL は foo: みたいに識別子にコロンがついたもの、 tPOW と tDSTAR は ** である。

実引数:

call_args       : args ',' assocs opt_block_arg
args            : arg_value | tSTAR arg_value
                | args ',' arg_value | args ',' tSTAR arg_value
assocs          : assoc | assocs ',' assoc
assoc           : arg_value tASSOC arg_value
                | tLABEL arg_value
                | tSTRING_BEG string_contents tLABEL_END arg_value
                | tDSTAR arg_value
arg_value       : arg

ここで tSTAR は * であり、tASSOC は => である。 また tSTRING_BEG string_contents tLABEL_END は文字列構文の後にコロンをつけたもので、これは "foo#{1+1}": みたいに式も書ける。

今回はキーワード引数 と positional argument (位置引数, ふつうの引数) の 関係だけに興味があるので、配列の分解の tLPAREN f_margs rparen と ブロック引数の opt_f_block_arg は除去し、 ある程度展開して簡単にしよう。

仮引数:

f_args          : f_arg ',' f_optarg ',' f_rest_arg ',' f_arg ',' f_kwarg ',' f_kwrest
f_arg           : tIDENTIFIER | f_arg ',' tIDENTIFIER
f_optarg        : f_opt | f_optarg ',' f_opt
f_opt           : tIDENTIFIER '=' arg
f_rest_arg      : '*' tIDENTIFIER
f_kwarg         : f_kw | f_kwarg ',' f_kw
f_kw            : tLABEL arg_value | tLABEL
f_kwrest        : '**' tIDENTIFIER

同様に、ブロック引数の opt_block_arg は除去して、 あと arg | tSTAR arg は arg_or_star にくくりだして簡単にする。

実引数:

call_args       : args ',' assocs
args            : arg_or_star | args ',' arg_or_star
arg_or_star	  : arg | star_arg
star_arg	  : tSTAR arg
assocs          : assoc | assocs ',' assoc
assoc           : arg tASSOC arg
                | tLABEL arg
                | tSTRING_BEG string_contents tLABEL_END arg
                | tDSTAR arg

で、今回は意味 (動作) が興味の対象なので、AST っぽくだいたいの記号は消して、 tSTRING_BEG string_contents tLABEL_ENDはstring_labelとまとめよう。 ('*' と '**' も消してしまった方が AST っぽいが、まぁなんとなくつけてある) あと、* で繰り返しを 0個以上っぽく表現することと、省略可能を示す ? をつけることで、 いろいろと省略可能な性質を復元してある。

仮引数:

f_args          : f_arg f_optarg f_rest_arg? f_arg f_kwarg f_kwrest?
f_arg           : tIDENTIFIER*
f_optarg        : (tIDENTIFIER arg)*
f_rest_arg      : '*' tIDENTIFIER
f_kwarg         : f_kw*
f_kw            : tLABEL arg_value | tLABEL
f_kwrest        : '**' tIDENTIFIER

実引数:

call_args       : args assocs
args            : (arg | star_arg)*
star_arg	  : '*' arg
assocs          : assoc*
assoc           : arg '=>' arg
                | tLABEL arg
                | string_label arg
                | '**' arg

まぁ、こんなものか。

2018-09-15 (Sat)

#1 大江戸Ruby会議07

大江戸Ruby会議07で、キーワード引数についてさらに議論した。

昼休みにごはんを食べながら議論したり、 壇上で Ruby 3のキーワード引数について考える をネタに議論したり、 懇親会にいく路上や懇親会の中で議論したりした。

位置引数とキーワード引数を分離するには、 メソッド呼び出しで伝わる情報に、フラグをひとつ追加するだけでいいことに (ごはんを食べながら) 気がついたのは大きな進歩だったと思う。

現在のメソッド呼び出しで位置引数とキーワード引数はあわせて値の並びという情報が伝達される。 キーワード引数が渡された場合、キーがシンボルのHashオブジェクトとして表現され、値の並びの最後の値として追加される。

値の並びの最後にHashオブジェクトがあったときにそれがキーワード引数として渡されたものか、 そうでないのかが呼び出される側ではわからないのが問題なら、 フラグ (仮名 keyword_given) を伝達する情報として追加して、最後のHashオブジェクトがキーワード引数かどうかを 呼び出される側に伝えればいい。

そのフラグをどのように呼び出される側に伝えるか、 つまりメソッド定義の中でどう扱えるようにするかはいろいろな形がありうる。

フラグそのものをboolとして取得させることはせず、 mameさんの提案のようにメソッドの動作を変えてしまう実装の中で フラグを利用するというのもひとつの方法ではある。

また、フラグを参照する手段を提供するのもひとつの方法ではある。 (たとえばC言語では rb_keyword_given_p 関数, Rubyでは keyword_given? メソッド)

それらの折衷というのもひとつの方法ではある。 (たとえばC言語では後者、Rubyでは前者)

また両方というのもありうるだろう。 (rb_keyword_given_p, keyword_given? を提供し、さらに前者のようにメソッドの動作を変えてしまう)

フラグの利点は、フラグを伝達するメカニズムを提供するだけなら互換性を壊さないことである。 フラグを利用しない限り、動作は変わらないので互換性は壊れない。

互換性が壊れるのはいままでフラグを利用していなかった動作を、フラグを利用するように変更したときに起こる。

mameさんが説明している「キーワード拡張」の安全性を得るには、互換性を壊してフラグを利用する必要がある。 これはメソッドごとにもできるが、Rubyでふつうに定義したメソッドが安全になるには ふつうに定義したメソッドの動作をフラグを利用するように変える (メソッド呼び出しの機構を変える) 必要がある。

なおフラグの完全な互換性は、未来永劫に必要かどうかは議論の余地があるが、 フラグだけ先行して実装可能という点で非常に重要である。

さて、フラグを参照できるとした場合で、互換性が保たれている状況では移譲メソッドは以下のように書ける。

def f(*a)
  if keyword_given?
    g(*a, **a.pop)
  else
    g(*a)
  end
end

そして、メソッドの動作も変わった後には移譲メソッドは以下のように書く必要があるだろう。

def f(*a, **kw)
  g(*a, **kw)
end

うぅむ、メソッドの動作が変わる前後の両方で動くような移譲メソッドを書けるようにできないかな。

2018-09-16 (Sun)

#1 キーワード引数

考えを吐き出すと次のアイデアが出てくるもので、思いついた。

とりあえず中間的な状態として、以下のふたつが同じ動作になるようにするのはどうか。

def f(*a) g(*a) end
def f(*a, **kw) g(*a, **kw) end

これを実現するため、キーワード引数を **kw で受け取るときに、 位置引数からキーワード引数を取り出さなかった (最後の位置引数を削らなかった) ときには 仮引数の **kw が nil を束縛し、 また、実引数で **nil と与えた場合には位置引数にHashオブジェクトを追加しない、 という動作にする。 (なお、渡された位置引数の最後に空ハッシュ {} があった場合にはそれを削って kw に {} が束縛され、 実引数の **{} は位置引数の最後に {} を追加する) (あと、Hashオブジェクトの分割はやめる)

これにより、*a,**kw で受け取って *a,**kw で渡すという形で移譲メソッドが書けるようになり、 それは現在の *a で受け取って *a で渡すのと同等になる。

ただ、フラグを考えると、それらは移譲メソッドとして完璧ではなく、 フラグは保存されない。

フラグを保存するためには、以下のようにがんばる必要がある。

def f(*a, **kw)
  if keyword_given?
    g(*a, **kw)
  else
    g(*a, kw)
  end
end

もし将来に位置引数とキーワード引数が混ざらなくなったときには、がんばらなくてもよくなる。

また、上記のようにがんばってしまったコードも問題なく動かすためには、 keyword_given? が常に真となるようにすればいいかもしれない。

まぁ、そんなに込み入った細工を理解してフォローできるひとは多くない気はする。


[latest]


田中哲