Rubyで http://example.com を実行する

TL;DR

$ ruby -cwe "https://example.com"
Syntax OK

これを知った経緯

今日読んだとあるブログに書いてあった。
そのブログではソースコード内のコメントのうち、 Rubyコードとして解釈可能なもの (コメントアウトされたコード) を検出するツールについて語っていて、 その中に以下のような一文が出てくる。

http://example.comは Ruby コードとして解釈することが可能であるが、人間からしたら URL に見えるだろう。

……え、そうなの?

http://example.comがRubyコードとして解釈可能?
このURLの一体どこがRubyコードなんだろう?

しかしそのブログでは、この点についてはこれ以上言及されていない。 半信半疑で試してみると、TL;DR にあるように本当にRubyコードとして解釈できた。

でもなぜなのだろう? ずいぶんサラっと書かれていたけど、Rubyに詳しい人にとっては驚く事でもないのだろうか?
自分には不思議だったので調べてみた。

$ # Environment: macOS Sierra
$ ruby --version
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-darwin16]

Rubyはhttp://example.comをどう解釈するか

まずは構文をチェックするだけでなく、 http://example.coma.rbというファイルに保存して実際に実行してみる。

# a.rb
http://example.com

すると以下のようなエラーが出た。

$ ruby a.rb
a.rb:2:in `<main>': undefined local variable or method `example' for main:Object (NameError)

exampleというオブジェクトは存在しないというエラーだ。 どうやらあの文字列をRubyコードとして実行した場合、exampleという部分が最初に評価されるらしい。

example.comの部分は唯一、自分でもRubyとして解釈可能だとわかる箇所だ。 exampleをレシーバとした単なるメソッド呼び出しだろう。 そこでcomというメソッドを持つexampleというオブジェクトを定義して、再度実行してみる。

example = Struct.new(:com).new
http://example.com
$ ruby a.rb
a.rb:2:in `<main>': undefined method `/' for :/:Symbol (NoMethodError)

今度は:/というシンボルに/というメソッドがないと言われた。 なるほど:/はシンボルであり、続く/はそのメソッドとして解釈されていたのか!

ここまで来ればhttp://example.comがRubyコードとして解釈可能な理由がわかる。 つまりはこういう事だ。

# 括弧をつけて糖衣構文をなくすとこうなる
http(:/./(example.com))

# もっと冗長に書くと:

# 1. `example`オブジェクトの`com`メソッドを呼び出して
v1 = example.com

# 2. その戻り値をシンボル`:/`の`/`メソッドに引数として渡し
v2 = :/ / v1

# 3. 更にその戻り値を`http`というメソッドに渡している
http(v2)

な、なるほど…。http:/://なんて書き方ありだったのか。

鍵はメソッド呼び出しの糖衣構文

肝となるのは以下の2点だと思う。

  • /などの演算子メソッドを呼び出す場合、.と引数を囲う括弧orスペースを省略できる

    1.+(1)
    1+1
    4/4
    
    • メソッドの第一引数がシンボルや文字列の場合、括弧のみならずスペースも省略できる
    puts(:bar)
    puts :bar
    puts:bar # valid
    puts"bar" # valid
    

前者はまあそうだろうという感じだけど、後者は知らなかった。
でも後者のような書き方を許容するメリットってあるのかな…? 😅

実際に実行してみる

というわけでようやく構文が理解できたので、実際にhttp://example.comを実行できるようにしてみよう。

# Add `/` method to Symbol class.
class Symbol
  def /(value)
    "#{self}/#{value}"
  end
end

# Define `http` method.
def http(path)
  "http:#{path}"
end

# Create `example` object.
example = Struct.new(:com).new("example.com")

# Run.
result = http://example.com
puts result
$ ruby a.rb
http://example.com

できた!

まさかただのURLが有効な式になりうるとは。Rubyの構文の奥深さを垣間見た日だった。

場所によっては解釈できない

ちなみに上記のスクリプトの最後の部分で、 http://example.comの結果を直接putsに渡そうとすると構文エラーになる。

# ...

puts(http://example.com)
$ ruby a.rb
a.rb:13: unknown regexp options - apl

一瞬あれ!? となるけど、理由はエラーメッセージに思いっきり書いてある。

unknown regexp options - apl

なんと今度は//が正規表現リテラルと解釈されたようだ。 もうおわかりだと思うが、 この書き方だとhttp:の部分がHashのキーとして解釈されている。 そしてその値に//という空の正規表現が指定されており、 更にそのオプション・修飾子としてexampleという文字列が指定されている形になる。

# つまりはこう
regexp = //example
puts({ http: regexp.com })

しかしa, p, lは無効なオプションなので、 そんなオプションはないというエラーになっていた (doc)。
この場合.comは正規表現のインスタンスメソッドになるので、 それさえ実装すれば以下のようなURLは実行可能になる。

# valid options/encodings: i,m,x,o, u,e,s,n
puts(http://mixin.com)

面白い。
探せば他にも面白いケースがあるかもしれない。

感想

こういうケースから察するに、Rubyの構文解析って実は相当複雑な仕事なんじゃないだろうか。 すごいなぁ。