LoginSignup
7
5

More than 5 years have passed since last update.

`BasicObject#?` は作れるか

Last updated at Posted at 2018-06-04

概要

動機

  • Rubyではメソッド名の末尾に !? が使える
  • BasicObject#! がある(→マニュアル
  • → ならば BasicObject#? も作れるのでは?

※ ここには重大な誤解がある

結果

  • 作りにくい
  • とても使いにくい
  • 悔しいのでRuby自体を改造してみた

さっそく挑戦

? のつくメソッドは条件式に使うような真偽値( true / false または obj / nil)を返す慣習がある。今回は最短のメソッド名に合わせて最もシンプルな真偽判定にしたいので、オブジェクトをbooleanに変換したものを返すことにする1

実装

マニュアルにある ! の再定義例と同じように、普通に def してみる。

bo_test.rb
class BasicObject
  def ?
    !!self
  end
end
$ ruby bo_test.rb
bo_test.rb:2: warning: invalid character syntax; use ?\n
bo_test.rb:2: syntax error, unexpected '?'
        def ?
             ^
bo_test.rb:5: syntax error, unexpected keyword_end, expecting end-of-input

構文エラーになってしまった。何かがダメらしい。


仕方ないので Module#define_method を使う。

bo_test.rb
class BasicObject
  define_method(:?) do
    !!self
  end
end
$ ruby bo_test.rb
bo_test.rb:2: syntax error, unexpected tCHAR, expecting tSTRING_CONTENT or tSTRING_DBEG or tSTRING_DVAR or tSTRING_END
        define_method(:?) do
                         ^
bo_test.rb:5: syntax error, unexpected keyword_end, expecting end-of-input

違う構文エラーが出た。シンタックスハイライトがネタバレしているが? が特別な解釈をされてしまったらしい。


ならシンボルの文字列をクォートで囲む(あるいはシンボルをやめて文字列を与える)。

bo_test.rb
class BasicObject
  define_method(:'?') do
    !!self
  end
end
$ irb
irb(main):001:0> require './bo_test'
=> true
irb(main):002:0> BasicObject.instance_methods.sort
=> [:!, :!=, :==, :"?", :__id__, :__send__, :equal?, :instance_eval, :instance_exec]

きちんと登録された。inspectされたシンボルがダブルクォートで囲っていることからも、単純に :? ではいけないことが分かる。

利用

シンボルを使わないと定義できなかった時点で察するが、普通のメソッド呼び出しは構文エラーになるObject#send などで呼び出す必要があり、使い勝手が悪い。(これをメソッドと呼んでいいのだろうか…?)

irb(main):003:0> nil.?   # ?の後ろに空白を入れてある
(irb):3: warning: invalid character syntax; use ?\s
SyntaxError: (irb):3: syntax error, unexpected '?', expecting '('
nil.?   # ...
     ^

irb(main):004:0> nil.send(:'?')
=> false

#map あたりならシンボルを渡すだけなのでまあまあ普通に使える。

irb(main):005:0> [true, false, nil, 0, "", []].map(&:'?')
=> [true, false, false, true, true, true]

! とは違い単項演算子が用意されているわけではないので、式の前に置くことはできない。

irb(main):006:0> ?nil
SyntaxError: (irb):6: syntax error, unexpected '?'

irb(main):007:0> ?_   # irbで変数 _ は直前(5行目)の結果を参照する…のだが?
=> "_"

文法の確認

? の使われ方

Rubyにおいて ? はどんな意味を持つのか調べる。マニュアルの「Rubyで使われる記号の意味」を調べると以下が載っている。

これまでのよくわからないエラーの多くは ? の次の文字を文字列に解釈しようとしていることを言っていた。 ?_"_" となったのも同じ。

メソッド名の規則

マニュアルから探し出すのが難しいのだが、以下の記述を見つけた。

メソッド呼び出し(super・ブロック付き・yield)
メソッド名には通常の識別子の他、識別子に ? または ! の続いたものが許されます。慣習として、述語(真偽値を返すメソッド)には ? を、同名の(! の無い)メソッドに比べてより破壊的な作用をもつメソッド(例: tr と tr!)には ! をつけるようになっています。

字句構造
Rubyの識別子は英文字またはアンダースコア('_')から始まり、英文字、アンダースコア('_')または数字からなります。識別子の長さに制限はありません。

クラス/メソッドの定義
メソッド名としては通常の識別子の他に、再定義可能な演算子(例: ==, +, - など 演算子式 を参照)も指定できます(演算子式の定義参照)。

ということは「メソッド名の末尾に !? が使える」という認識は間違っていた(前に識別子が必要)。それでも ! 単体がメソッド名になれているのは再定義可能な演算子だからであり、従って ? 単体はメソッド名になれない。

とはいえ #define_method#send などではできているので、ここで言うメソッド名は def の後など決められた場所に限定された話かもしれない。

Rubyの改造

何となく悔しいので、 ? をメソッド名にすることを妨げているRubyの文法自体をいじってしまう。

前節までを踏まえてより ! に近づけるために以下のようにする。

  • 単項演算子 ? を導入
  • obj.? でも呼び出し可能
  • def ? で再定義可能
  • メソッドを組み込みで実装

※ 見よう見まねでいじるので方法の正しさに自信は無い

Rubyの用意

READMEを参考に準備。確認のため一旦インストールまでしてみる。なお、rbenvを導入済みの環境で試したが、追加で autoconf と bison をインストールする必要があった(エラーメッセージで親切に教えてくれた)。

# ソースコードの用意
git clone https://github.com/ruby/ruby.git
cd ruby/
git checkout -b myruby v2_5_1

# Makefile作成
autoconf
./configure --prefix=${HOME}/usr

# コード修正後、
# コンパイルとインストール
make
make install

# Ruby実行
${HOME}/usr/bin/ruby -v

メソッドを実装

rb_obj_not を真似て追加する。

object.c
VALUE
rb_obj_test(VALUE obj)
{
    return RTEST(obj) ? Qtrue : Qfalse;
}

...

    rb_define_method(rb_cBasicObject, "?", rb_obj_test, 0);

この時点でコンパイルすれば、Rubyの範囲内で実装したときと同様に nil.send(:'?') などが使える。

最適化目的のコードもあるようなので追加。(これで足りているのかはよく分からない)

internal.h
CONSTFUNC(VALUE rb_obj_test(VALUE obj));
vm_insnhelper.c
static VALUE
vm_opt_test(CALL_INFO ci, CALL_CACHE cc, VALUE recv)
{
    if (vm_method_cfunc_is(ci, cc, recv, rb_obj_test)) {
        return RTEST(recv) ? Qtrue : Qfalse;
    }
    else {
        return Qundef;
    }
}
insns.def
DEFINE_INSN
opt_test
(CALL_INFO ci, CALL_CACHE cc)
(VALUE recv)
(VALUE val)
{
    val = vm_opt_test(ci, cc, recv);

    if (val == Qundef) {
        PUSH(recv);
        CALL_SIMPLE_METHOD(recv);
    }
}

文法を追加

まず、トークン '?' は条件演算子が使用しているため別のトークン tUQMARK を定義する。

defs/id.def
# VM ID         OP      Parser Token
token_ops = %[\
  ...
  Not           !
  UQmark        ?       UQMARK
  ...
]
parse.y
...
%token tUMINUS          RUBY_TOKEN(UMINUS) "unary-"
%token tUQMARK          RUBY_TOKEN(UQMARK) "unary?"
...

演算子の優先順位! と同レベルのところに追加しておく。

parse.y
...
%right '!' '~' tUPLUS tUQMARK
...

構文の規則は '!' を真似る。

parse.y
expr            : command_call
                  ...
                | tUQMARK command_call
                    {
                        $$ = call_uni_op(method_cond($2, &@2), idUQmark, &@1, &@$);
                    }
                | '!' command_call
                  ...


op              : '|'           { ifndef_ripper($$ = '|'); }
                  ...
                | tUQMARK       { ifndef_ripper($$ = tUQMARK); }
                | '!'           { ifndef_ripper($$ = '!'); }
                  ...


arg             : lhs '=' arg_rhs
                  ...
                | tUQMARK arg
                    {
                        $$ = call_uni_op(method_cond($2, &@2), idUQmark, &@1, &@$);
                    }
                | '!' arg
                  ...

最後にスキャナに tUQMARK を判別させる。文字 ? を読み取ったときの解析は parse_qmark で行われている。

parse.y
static enum yytokentype
parse_qmark(struct parser_params *parser, int space_seen)
{
...
    c = nextc();
    if (IS_AFTER_OPERATOR()) {
        /* メソッド名を期待する状況 → ? はメソッド名と判断 */
        SET_LEX_STATE(EXPR_ARG);
        if (c != '@') pushback(c);
        return tUQMARK;
    }
    if (IS_BEG() || (IS_SPCARG(c) && arg_ambiguous('?'))) {
        /* 式の始まり → ? は単項演算子と判断(文字リテラルは潰す) */
        SET_LEX_STATE(EXPR_BEG);
        pushback(c);
        return tUQMARK;
    }
    if (c == -1) {
...

これで文字リテラルは実質動作しなくなってしまったので、付属のRubyスクリプトを直さないといけない。コンパイル中のエラーメッセージを頼りに直せば10ファイル程度で終わるが、それでは未修正のコードが残ってしまうので、RuboCopで一気に直す3

文字リテラルを除去
gem install rubocop
echo 'AllCops:
  TargetRubyVersion: 2.5' > .rubocop.yml
git ls-files | xargs rubocop --auto-correct --only Style/CharacterLiteral

# 直さない方がよさそうなものを復元
git checkout -- sample/

以上で完成したと思ったのだが、実行して ? を呼び出すと NoMethodError が出てしまった。

Symbol の動作を修正

:"?":? が別物と扱われてしまったので直す。原因は symbol.c で最初に「1文字の記号」と「演算子」を登録する際に ? が二重登録されたせいのようなので、演算子のみにする。

symbol.c
static void
Init_op_tbl(void)
{
    ...

    for (i = '!'; i <= '~'; ++i) {
        if (!ISALNUM(i) && i != '_' && i != '?') { /* '?' を外すよう変更 */
            char c = (char)i;
            register_static_symid(i, &c, 1, enc);
        }
    }
    for (i = 0; i < op_tbl_count; ++i) {
        register_static_symid(op_tbl[i].token, op_tbl[i].name, op_tbl_len(i), enc);
    }
}

ついでに、 :"?" をinspectした際に :? となるようやっつけで改善する。

symbol.c
static int
rb_enc_symname_type(...)
{
    ...
      case '?':
        if (len == 1) return ID_JUNK;
      default:
    ...
}

実行

\$ \${HOME}/usr/bin/irb
irb(main):001:0> BasicObject.instance_methods.sort   # #? は組み込み済
=> [:!, :!=, :==, :?, :__id__, :__send__, :equal?, :instance_eval, :instance_exec]

irb(main):002:0> ?true      # 単項演算子
=> true
irb(main):003:0> false.?    # メソッド呼び出し
=> false
irb(main):004:0> 0.send(:?) # シンボルにクォート不要 (※)
=> true
irb(main):005:0> ?x         # 文字リテラルとは認識されない
NameError (undefined local variable or method `x' for main:Object)

irb(main):006:0> class Hoge
irb(main):007:1>   def ?   # 普通に再定義できる
irb(main):008:2>     puts "#{self.class}##{__callee__} called"
irb(main):009:2>     super
irb(main):010:2>   end
irb(main):011:1> end
=> :?
irb(main):012:0> ?Hoge.new
Hoge#? called
=> true

irb(main):013:0> !?!?!?nil   # 何個並べても大丈夫
=> true
irb(main):014:0> ?:?.? ? ? ?:?:? ?:?.?   # 4番目の ? と3番目の : が条件演算子
=> true

(※) irbではうまく動作しない。Ctrl-Dでirbを終了させると結果が表示される。

課題

  • parse.y の仕組みの把握
    • LEX_STATEarg_ambiguous が分かっていない
  • 文字リテラルとの共存
    • gemが意外と文字リテラルを使っていて動かない
    • ?\\ のようにスキャナで判別できる部分は簡単そう
    • ?( などのとき後ろが式かどうか見分けられるか

雑多なメモ

parse.yの読み書きに取り組む中で初めて知ったこと。

  • 構文エラーのメッセージにはトークンが出ている
    • unexpected '?' と言われたら、単なる文字 ? という意味ではなく、条件演算子と認識されていることが確定する。
      • ただし、不正な文字リテラルの際にも条件演算子をあてるので、warningが無いか確認が必要。
    • 今回追加した単項演算子と認識されていれば unexpected unary? のように言われる。
  • シンボルの怪現象: (:!@) == (:!)(:!@) != (:"!@")
    • : で始まるシンボルをパースする際は、後ろの文字列はメソッド名や変数名を期待している。クォートで囲ったときだけは普通の文字列。
    • スキャン時にメソッド名 !@ は特別に ! と同じと扱われる。だから違うように見えるシンボルが同じになる。
      • この仕様は ? でも真似した。 def ?@ などできる。
    • 文字列の場合は書いた通り !@ 。だからクォートの有無で異なるシンボルになる。
  • オプション -y でスキャナやパーサの動作を確認できる
    • ruby -yce '!nil' | sed 's/^Entering/\n&/'

本来の目的

よくよく考えたら、文字リテラルとぶつかっているのは単項演算子であって、本来の目的であるメソッド作成とは関係ない。なら単項演算子を諦めればgemを問題なく使えるのではないか。

修正~gemインストール
vim parse.y   # parse_qmark から単項演算子の分を取り除く
git checkout v2_5_1 -- basictest/ bin/ ext/ lib/ sample/ spec/ test/ tool/ KNOWNBUGS.rb *prelude.rb
make && make install
${HOME}/usr/bin/gem install --no-document pry pry-doc
\$ \${HOME}/usr/bin/pry
[1] pry(main)> BasicObject.instance_methods.sort
=> [:!, :!=, :==, :?, :__binding__, :__id__, :__send__, :equal?, :instance_eval, :instance_exec]
[2] pry(main)> false.?
=> false
[3] pry(main)> 0.send(:?)
=> true

[4] pry(main)> ?true   # 単項演算子はもう無い
SyntaxError: unexpected '?', expecting end-of-input
?true
^
[4] pry(main)> ?x      # 文字リテラル復活
=> "x"

[5] pry(main)> class Hoge
[5] pry(main)*   def ?
[5] pry(main)*     puts "#{self.class}##{__callee__} called"
[5] pry(main)*     super
[5] pry(main)*   end
[5] pry(main)* end
=> :?
[6] pry(main)> Hoge.new.?
Hoge#? called
=> true

できた。

結論

  • BasicObject#? はRubyのメソッド名の規則に反する
  • define_method(:'?')send(:'?') などを使っていいのなら作って使える
  • Rubyを改造すれば当然いける
    • ただし単項演算子まで入れようとすると文字リテラルとぶつかる

参考


  1. 本当に一番シンプルなのは Object#itself だが、それを再作成する意味は無いし #? という名前には合わないと判断した 

  2. 記号説明のページでは「文字列リテラル」としているが、リテラルのページでは「文字リテラル」を別に説明しているのでそちらに合わせた 

  3. ?\C-a のようなものは直されないが、テストコードの類に登場するだけなので影響は無いはず 

7
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5