読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

あなたが理解できない,たった一行のRubyのコード (動的言語に対する静的解析の限界)

ruby 小ネタ 計算機科学 プログラミング 正規表現

あなたは,下記のコードを理解できない。

p f /g+h/i

これはRubyのコードである。「p」は,コンソールに出力する関数である。


問:

  • だいたい,何をやっているコードですか? ※例えば,四則演算など。
  • 構文をおおまかに説明して下さい。 どれが変数で,どれが関数で,どれが演算子か?

↓回答










回答:

一意に決定できない。



下記に,

  • このコードの複数の解釈方法と,
  • この件が引き起こす問題

について述べる。


※なお,この問題が起きるのは動的言語に限らず,静的言語でも同様に発生しうることを前もって述べておく。


(1)分数の計算とみなすパターン

先行するコードを下記のように書いた場合:

test1.rb

# 変数に数値を代入
f = 2
g = 1
h = 2
i = 1

# 演算結果をpで出力する
p f /g+h/i


実行結果:

>ruby test1.rb
4

「分数の計算」とみなされる。


(2)正規表現の演算とみなすパターン

先行するコードを下記のように書いた場合:

test2.rb

# 正規表現の関数として定義する
def f( reg_exp )
  str = "gggh"
  return str.match( reg_exp ) != nil
end


# 演算結果をpで出力する
p f /g+h/i


実行結果:

>ruby test2.rb
warning: parenthesize argument(s) for future version

true

「正規表現の演算」とみなされる。


(3)両方を混在させてみる


上記のように2パターンの解釈が可能なわけだが,

両方の定義を混在させることは可能なのか?


例えば,「f」の定義が,ランダムに切り替わるようなコードを書いたらどうなるのか?

test3.rb


# ランダムに定義を振り分ける
if rand >= 0.5

  # 正規表現の関数として定義する
  def f( reg_exp )
    str = "gggh"
    return ( str.match( reg_exp ) != nil )
  end

else

  # 変数に数値を代入
  f = 2
  g = 1
  h = 2
  i = 1

end


# 演算結果をpで出力する
p f /g+h/i


これを実行すると,半分の確率で「4」と表示される。

そして,もう半分の確率で,下記のようなエラーになる。

undefined method `/' for nil:NilClass (NoMethodError)


最後の行を書き換えてみると,正規表現としての評価結果はいろいろ変わる。

p f( /g+h/i )  # => true

p f( /g/ )  # => true

p f /g/  # => syntax error, unexpected $end

なぜ,このようなエラーになるのか?


分数計算ではない方の分岐が選択された場合,正常に正規表現で演算をしてくれるはずではないのか?

少なくとも,そういう動作を期待するはずだ。


期待通りに動作しない原因を絞るために,下記のコードを動かしてみよう。

# if文の中で定義する
if true

  # 正規表現の関数として定義する
  def f( reg_exp )
    str = "gggh"
    return ( str.match( reg_exp ) != nil )
  end

end


# 演算結果をpで出力する
p f /g+h/i

これはちゃんと動作し,「true」が返る。正規表現の演算が走っている。

つまり,if文は悪さをしていない。



悪さをしているのは,,,何と,Rubyインタプリタによる,スクリプト全体の「事前コンパイル」である。

Rubyの動かないコード (中級編) ローカル変数の「暗黙の初期化」に関するエラー(ローカル変数のスコープが事前コンパイルで決まる)
http://language-and-engineering.hatenablog.jp/entry/20101205/p1

  • Rubyは,最初にスクリプト全体をコンパイルして,ローカル変数を決定する
  • (if文内の異なる分岐ブロックで,実行されないとしても)Rubyは「ローカル変数の宣言」を勝手に見抜き,ローカル変数のデフォルトの値としてnilで初期化してしまう


前述のコードで,NilClass の NoMethodError になった理由が理解できただろうか。


(4)引き起こされる問題

そういうわけで,下記の一行だけでは,分数計算なのか正規表現の演算なのか判断できない。

p f /g+h/i

出力結果はおろか,構文の意味すら判定できないのである。



このサンプルコードは,かなり特殊なケースに見えるかもしれない。


作為的にRubyの限界にチャレンジしているかのようだ。

  • どこまで,物事を動的に宣言する事が可能か? ※(3)のコードのように。
  • どこまで,括弧を省いてシンプルな記法が可能か?

その極みが冒頭のコードである,と考えることもできる。



だが,この件は,ちょっとした問題になる。

「先行するコードを読まない限り,半角スラッシュの意味を決定できない。」

これは,大した問題だろうか?




2つの半角スラッシュの間に,もしも「#」が含まれていたらどうなるのか?

p f /g#h/i


このコードは,正規表現で「半角シャープ」を検出するためのロジックなのかもしれない。


ところが,もしも先行するコードが「分数計算」を前提とした数値代入ロジックだとすると,

後半のコードは「コメントアウト」と解釈され,実行されないのである。

(現に,上記のHTMLでは,そのように自動シンタックスハイライトされてしまっている。)


これでもし,コメントアウト部分の後に,別のコードが後続していたらどうなるか?

p f /g#h/i; p "x";

この行を見た時,

  • 「どこからどこまでが,コードとして評価・実行されるのか」

を判定できないのだ。


半角シャープよりも後の部分は,コードかもしれないし,コードではないかもしれない。

コメントかもしれないし,そうではないかもしれない。


先行するコードを読まない限り,対象となる部分が「コードなのか・コメントなのか」の判断すらつかないのである。



これは,コードの静的解析を試みた時に,結構深刻な問題になる。

(※動的言語を静的に解析する時点で間違ってるだろ,というツッコミは保留。RDocとかメトリクス算出ツールとかあるでしょ,と言ってお茶を濁す)



2つの半角スラッシュに挟まれた部分が何を意味しているのか,

分数の計算なのか,

それとも正規表現の演算なのか,

あるいはコードの一部ですらないのか?


その正確な答えを知るためには,コードを実際に実行にかけてみるしかない。

安易に,部分的に「パース」するのは不可能なのだ。



コードのパーサを作る際に,正規表現リテラルがどれほどややこしい存在となるか,少しは伝わると思うのだが・・・。

これは,正規表現リテラルをサポートしているプログラミング言語を構文解析しようとする者が直面する,共通の壁だ。

例えばJavaScriptの場合,メソッド呼び出しはカッコが強制されるので,その分だけRubyよりましだろうと思いきや,静的解析はそうそう楽ではない。

JavaScript Syntax探訪
http://d.hatena.ne.jp/Constellation/2...

  • JSのlexerというものは単体では生成できずParserと密着している(主にRegExpとRegExpとRegExpのせいです)
  • 正規表現は文脈依存…これが一番困るのですが, 正規表現の初めの文字, /は演算子としても使われています. これが問題で, 実はlexerの時点ではどちらの意味なのかの解釈はできないです.
  • ※補足:まず余分なスペースとか改行とかを除去するためにlexerで字句解析してから,parserで構文解析し,結果として構文木が得られて,それでようやく記述内容の意味が分かって実行を開始できる。という順番が普通の流れなのだが,この場合,最初のスラッシュが来た時点で構文解析上の妥当性をParser側で検証しない限り,後続する字句の抽出をlexerに命令できない

裏を返せば,Rubyはそれほど柔軟,という事なのだが。


補足

という具合にRubyの柔軟さを味わった所で,

冒頭の問いかけを,クイズとして誰か他の人に出題してみると面白い。


「人間がコードの意味を理解できない」

→「このコードは解釈の仕方が複数あるので,一意には決まらないのだな。」

というふうに感づく事ができるかどうか,

出題の意図を把握する勘の良さや,コード・リーディングのセンスを問うのだ。


で,この件を冷めた目で見て批評してくるのではなく

「面白い!」と感じられるのであれば,その人は技術者として良き仲間になるだろう。


それに加え,

字句解析→構文解析,という順序が成立できないのはどうしてか?

という点も併せて出題してみれば,相手の計算機科学の素養を測る事にもなる。


※なお,冒頭のコードのネタに気づいたのは3年以上前なのだが,忙しかったため記事として公開するタイミングをずっと逃していた。

追記

ブコメにて,「動的言語は関係ないのでは?先行する文脈に依存するだけでは?」というツッコミを数件頂いた。

じっさい,冒頭の問いだけ見るとその通りだ。

記事のタイトルを見ると誤解を招きかねない。


静的な言語であっても,解釈に迷う点は同じだ。

正規表現リテラルなのか,分数の計算なのか判定できない。

その点には,Rubyが動的な言語である事は関係ない。





ただ,(3)のコードが,50%の確率で動いたり動かなかったりする(=実行時の「/」メソッドの未定義エラーになる)事に注目したい。

これは,動的言語ならではの面白さを伝える挙動ではなかろうか。


そしてその面白さを具体的に伝えるためには,

「先行するコード次第で構文の意味が丸ごと化けてしまうようなコード」の例(=f /g+h/i)

が必要になる。



あと,この記事のタイトルには,

私自身が「f /g+h/i」とか「f /g#h/i」の存在に気づいたきっかけとなる出来事が含められている。

文字通り動的言語を静的に解析しようとして,派手に転んだのだ。


要するに「お手製RDoc」みたいなものを自作しようとしていた。
(※注:このバッチの事ではない。もっと前の話で,もうちょい規模があり,公開していない。)

BNFを使わないタイプで,字句解析+状態遷移ベースの単純なソースコード解析器だったのだが,

その時,いくらがんばっても「/g#h/」を解きほぐす事ができなかった。

その記法をやられると,後続する構文がどうしてもうまく解釈できない。

制御構文すらコメントアウトされた事になってしまう。

「静的解析」という行為自体の限界を感じたのだった。



そういう思惑や経緯があり,本稿を執筆する際,

この事例を紹介するにあたって,タイトルでも「動的」という特性を前面に打ち出す事とした。



「動的なのが原因で,こういう複数解釈の問題が起きるのか」

という誤解を生んだ場合,そうではないです…という補足をもってお詫びとしたい。


補足2

冒頭に挙げた、二重解釈が可能なコードのPerl版が下記で紹介されている。

あなたが理解できない、たった一行のPerlのコード
http://d.hatena.ne.jp/tokuhirom/20120...

関連する記事:

あなたが正規表現の中級者か判別する10問テスト (文字列処理の必須知識)
http://language-and-engineering.hatenablog.jp/entry/20131028/RegExpProgrammin...


JavaScriptの動かないコード (中級編) 正規表現で同じ文字の連続を検出したい - 置換前パターン中での後方参照
http://language-and-engineering.hatenablog.jp/entry/20080927/1222508705


「ラムダ計算」を独学で学習するための,講義ノートやPDFのリンク集 (復習用の問題付き)
http://language-and-engineering.hatenablog.jp/entry/20130313/LambdaCalculusBa...


数学が苦手な人向けの,「暗号理論」の入門テキスト・PDF。 RSA暗号から認証・署名などのセキュリティ技術
http://language-and-engineering.hatenablog.jp/entry/20140519/EncryptionTheory...