Archive for the ‘プログラミング’ Category.

Scala: Haskell 本の引き写しでパーサジェネレータ

プログラミング Haskell」の第 8 章「関数型パーサ」の写経をしようかと思ったのですが、そのまま引き写しても面白くないので、Scala で書いてみました。せっかくですので、Haskell のモナド連結の糖衣構文 do を、極力そのまま Scala の for 式で書けるよう、Parser[A] には、map(), flatMap() を備え、Scala の型とします。

…できた。Haskell 版から変えたところとしては、入力文字列にパーサを適用した際には、Scala らしく Option[(A, String)] を返すようにした、Haskell で [Char] であるところは、適宜 String にした、程度か。案外そのまま行けますね、というよりも、Scala が Haskell を色濃く反映しているんですね。

abstract trait Parser[A] {
  def apply(inp: String): Option[(A, String)]
  def parse(inp: String) = apply(inp)
  def map[B](f: (A) => B) = {
    def p_parse = parse _
    new Parser[B] {
      def apply(inp: String) = {
        p_parse(inp) match {
          case None => None
          case Some((v, out)) => Some((f(v), out))
        }
      }
    }
  }
  def flatMap[B](f: (A) => Parser[B]): Parser[B] = {
    def p_parse = parse _
    new Parser[B] {
      def apply(inp: String) = {
        p_parse(inp) match {
          case None => None
          case Some((v, out)) => f(v)(out)
        }
      }
    }
  }
  def +++(q: Parser[A]) = {
    def p_parse = parse _
    new Parser[A] {
      def apply(inp: String) = {
        p_parse(inp) match {
          case None => q.parse(inp)
          case s @ Some(_) => s
        }
      }
    }
  }
}

def return_[A](v: A): Parser[A] = {
  new Parser[A] { def apply(inp: String) = { Some((v, inp)) } }
}

def failure[A]: Parser[A] = {
  new Parser[A] { def apply(inp: String) = { None } }
}

def item: Parser[Char] = new Parser[Char] {
  def apply(inp: String) = inp match {
    case "" => None
    case _ => Some((inp.head, inp.tail))
  }
}

def parse[A](p: Parser[A], inp: String): Option[(A, String)] = p(inp)

/*

parse(return_(1), "abc")
res1: Option[(Int, String)] = Some((1,abc))

parse(failure, "abc")
res2: Option[(Nothing, String)] = None

parse(item, "abc")
res3: Option[(Char, String)] = Some((a,bc))

 */

/*

def p = for {
  x <- item
  _ <- item
  y <- item
} yield (x, y)
p: Parser[(Char, Char)]

def p_dash = item.flatMap(x =>
  item.flatMap(__ =>
    item.map(y =>
      (x, y) )))
p_dash: Parser[(Char, Char)]

parse(p, "abcdef")
res4: Option[((Char, Char), String)] = Some(((a,c),def))

parse(p, "ab")
res5: Option[((Char, Char), String)] = None

 */

/*

parse(item +++ return_('d'), "abc")
res6: Option[(Char, String)] = Some((a,bc))

parse(failure +++ return_('d'), "abc")
res7: Option[(Char, String)] = Some((d,abc))

parse(failure +++ failure, "abc")
res8: Option[(Nothing, String)] = None

 */

def sat(p: (Char) => Boolean): Parser[Char] = for {
  x <- item
  r <- if (p(x)) return_(x) else failure
} yield (r)

def digit = sat(_.isDigit)

def lower = sat(_.isLower)

def upper = sat(_.isUpper)

def letter = sat(_.isLetter)

def alphanum = letter +++ digit

def char(x: Char) = sat(x.==)

/*

parse(digit, "123")
res9: Option[(Char, String)] = Some((1,23))

parse(digit, "abc")
res10: Option[(Char, String)] = None

parse(char('a'), "abc")
res11: Option[(Char, String)] = Some((a,bc))

parse(char('a'), "123")
res12: Option[(Char, String)] = None

 */

// これは文字列を返すパーサとする。Scala の String は List[Char] ではない
def string(s: String): Parser[String] = s match {
  case "" => return_("")
  case _ => for {
    x <- char(s.head)
    xs <- string(s.tail)
  } yield (x +: xs)
}

/*

parse(string("abc"), "abcdef")
res13: Option[(String, String)] = Some((abc,def))

parse(string("abc"), "ab1234")
res14: Option[(String, String)] = None

 */

object Many {
  def many[A](p: Parser[A]): Parser[List[A]] = {
    many1(p) +++ return_(Nil)
  }
  def many1[A](p: Parser[A]): Parser[List[A]] = {
    for {
      v <- p
      vs <- many(p)
    } yield (v :: vs)
  }
}
import Many._

/*

parse(many(digit), "123abc")
res15: Option[(List[Char], String)] = Some((List(1, 2, 3),abc))

parse(many(digit), "abcdef")
res16: Option[(List[Char], String)] = Some((List(),abcdef))

parse(many1(digit), "abcdef")
res17: Option[(List[Char], String)] = None

 */

// 仕方ないので文字列にしておく
def ident = for {
  x <- lower
  xs <- many(alphanum)
} yield ((x :: xs).foldLeft("")(_ + _))

// これも
def nat: Parser[Int] = for {
  xs <- many1(digit)
} yield (xs.foldLeft("")(_ + _).toInt)

def space: Parser[Unit] = for {
  _ <- many(sat(_.isWhitespace))
} yield (Unit)

/*

parse(ident, "abc def")
res0: Option[(java.lang.String, String)] = Some((abc, def))

parse(nat, "123 abc")
res1: Option[(Int, String)] = Some((123, abc))

parse(space, " abc")
res2: Option[(Unit, String)] = Some(((),abc))

 */

def token[A](p: Parser[A]): Parser[A] = for {
  _ <- space
  v <- p
  _ <- space
} yield (v)

def identifier: Parser[String] = token(ident)

def natural: Parser[Int] = token(nat)

def symbol(xs: String): Parser[String] = token(string(xs))

/*

def p: Parser[List[Int]] = for {
  _ <- symbol("[")
  n <- natural
  ns <- many(
    for {
      _ <- symbol(",")
      x <- natural
    } yield (x)
  )
  _ <- symbol("]")
} yield (n :: ns)

parse(p, " [1, 2, 3] ")
res19: Option[(List[Int], String)] = Some((List(1, 2, 3),))

parse(p, "[1, 2,]")
res20: Option[(List[Int], String)] = None

 */

def int: Parser[Int] = (
  for {
    _ <- symbol("-")
    n <- natural
  } yield (- n)
) +++ natural

object Expr {
  def expr: Parser[Int] = for {
    t <- term
    r <- (
      for {
        _ <- symbol("+")
        e <- expr
      } yield (t + e)
    ) +++ (
      for {
        _ <- symbol("-")
        e <- expr
      } yield (t - e)
    ) +++ return_(t)
  } yield (r)
  def term: Parser[Int] = for {
    f <- factor
    r <- (
      for {
        _ <- symbol("*")
        t <- term
      } yield (f * t)
    ) +++ (
      for {
        _ <- symbol("/")
        t <- term
      } yield (f / t)
    ) +++ return_(f)
  } yield (r)
  def factor: Parser[Int] = (
    for {
      _ <- symbol("(")
      e <- expr
      _ <- symbol(")")
    } yield (e)
  ) +++ int
}
import Expr._

/*

parse(expr, "2*3+4")
res26: Option[(Int, String)] = Some((10,))

parse(expr, "2*(3+4)")
res28: Option[(Int, String)] = Some((14,))

parse(expr, "2 * (3 + 4)")
res30: Option[(Int, String)] = Some((14,))

parse(expr, "2*3-4")
res33: Option[(Int, String)] = Some((2,))

parse(expr, "-1")
res36: Option[(Int, String)] = Some((-1,))

parse(expr, "(-3 * 4 * - 2) / 6")
res43: Option[(Int, String)] = Some((4,))

 */



Scala: エラトステネスのふるいで無限長素数列

簡潔すぎワロタw

import Stream._
def sieve[A <% BigInt](xxs: Stream[A]): Stream[A] = xxs match {
  case p #:: xs => cons(p, sieve(xs.filter(_ % p != 0)))
}
lazy val primes = sieve(from(2))

先頭 10 個。

scala> primes.take(10).toList
res0: List[Int] = List(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)

Fri Apr 22 2011: ついでに、遅延評価版のクイックソートも。まずは、普通に eager 評価のリストで書いた場合:

def qsort[A <% Ordered[A]](xxs: List[A]): List[A] = xxs match {
  case Nil => Nil
  case p :: xs =>
    qsort(xs filter(_ <= p)) ++ (p :: qsort(xs filter(p < _)))
}

ストリームで書くと、以下のように:

import Stream._
def qsort[A <% Ordered[A]](xxs: Stream[A]): Stream[A] = xxs match {
  case Empty => Empty
  case p #:: xs =>
    qsort(xs filter(_ < p)) ++ (p #:: qsort(xs filter(p <= _)))
}

ほぼ同じですが、”Empty” や “#::” などが特徴的です。不用意に無限長ストリームのソートなんかしちゃダメですよ。filter() が返ってきません。




WSH JScript の REPL を書いてみた

秀丸エディタ用、Emacs の eval-print-last-sexp 風マクロ for Scala, Clojure, Gauche, Groovy, Python and Ruby – Ayutaya.com」で、REPL が無いので流していた WSH の JScript (JavaScript) ですが、eval() があるのだからサクッと書けば良いので、どうせ世の中にはすでにたくさんの実装があるとは思われますが、書いてみました。お入り用でしたらこちらからどうぞ。

eval() が環境を汚すので、識別子には “_” を前置しています。それでも衝突すると言うならば、”_” を “_HOGEHOGE_” とでも全置換しても平気なように書かれています。ダブルクォートもバックスラッシュも使っていないので、別プログラム内へ埋め込むのも簡単です。一点ハマったのは、eval() は現在の環境内で実行されるので、当初別ファイルをロードする処理を関数にしたら、呼ばれた関数内で eval() されてしまい、そこから戻ったら “var” も “function” も消えていたことです。仕方がないので、入力ハンドラをスタックするようにしています。

実行例としては、以下のような感じです。

C:\>cscript.exe wshrepl.js
Microsoft (R) Windows Script Host Version 5.7
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

> 1 + 2 +
|   3 + 4
= 10
> function foo() {
|   WScript.Echo("Hello!");
| }
= undefined
> foo()
Hello!
= undefined
> :load c:\priv\prog\bat\wsh.js
= Hello, World!
= undefined
> ^Z

C:\>

以下は、ついでの tips です。

最新のブラウザの JavaScript に比べるといろいろと見劣りする Jscript ではありますが、そこは腐っても JavaScript 1.5 ですから、prototype を拡張して、map(), reduce(), filter() その他を拡張しておくと、なかなかに使える環境になります。

include が無い JScript ですが、そこは eval() で何とでもなります。以下は、実行スクリプトと同一ディレクトリにある “wsh.js” を初期化ファイルとして取り込みます。記述を短くしようと思って “WScript” を “with” すると、なぜか挙動がおかしくなります。

eval(WScript.CreateObject('Scripting.FileSystemObject').OpenTextFile(
 WScript.ScriptFullName.slice(0,-WScript.ScriptName.length)+'wsh.js' ).ReadAll());



秀丸エディタ用、Emacs の eval-print-last-sexp 風マクロ for Scala, Clojure, Gauche, Groovy, Python, Ruby, JScript, Haskell and Perl

Emacs の lisp-interaction-mode に、編集中のテキスト上のカーソル直前位置の Emacs Lisp の S 式を評価する便利なコマンド “eval-print-last-sexp” がありますが、その秀丸エディタ用、各種プログラミング言語対応版です。下記は HTML で文章を書きながら、その中の Clojure のサンプルコードを評価している例です。矢印位置にカーソル (|) がある状態で、この場合は “eval-clojure.mac ” を実行してみます。

<p>下記は、クイックソートのサンプルです:

<pre>
(defn qsort [[x & xs]]
  (when x
    (let [smaller #(< % x)]
      (lazy-cat
        (qsort (filter smaller xs))
        [x]
        (qsort (remove smaller xs)) ))))| ←
</pre>

定義が評価されてシンボルが返ります。

<p>下記は、クイックソートのサンプルです:

<pre>
(defn qsort [[x & xs]]
  (when x
    (let [smaller #(< % x)]
      (lazy-cat
        (qsort (filter smaller xs))
        [x]
        (qsort (remove smaller xs)) ))))
#'user/qsort ←
</pre>

そのまま試しに実行してみます。

(qsort '(5 3 8 9 2))|

その場に結果が返ります。

(qsort '(5 3 8 9 2))
(2 3 5 8 9)

いずれも、日本語も通ります。下記は Scala の例です。

val l = "日本語" :: "テキスト" :: Nil
l: List[java.lang.String] = List(日本語, テキスト)

どこか、昔の Basic インタプリタで開発をしていたような快適さがあります (あいにく SmallTalk の経験は無い)。とりわけ、関数型言語と相性が良いんでないかしら、このスタイルは。メリットとしては、以下のような感じかと思います:

  • 言語をその場でチャンポンに書ける: ガッツリとアプリを開発するのであれば Emacs なり Eclipse なりの各言語のモードを利用すれば良いのでしょうが、つらつらと考えながら各言語を横断的に、サンプルなどのスクリプト片を次々に試しながらコメントも書きたい場合などには、同一テキスト上でその場で評価し、すぐに結果が返る方が快適です
  • コピペをしなくてすむ: いちいちエディタ上で書いて、それを REPL にコピペ実行、というのは、思考を阻害します
  • 起動オーバーヘッドが無い: REPL のセッションをバックグラウンドで維持しますので、とりわけ Scala や Clojure などの、JVM を使う、起動が異様に遅い REPL でも、2 度目以降の評価はサクサクです。非力なノートマシンには、これがありがたい
  • エディタから離れなくて済む: 一日のほとんどをエディタ上で過ごす身としては、ひととおりのスクリプト実行がその場でできるというのはありがたいです。コード片も、無くさず記録として残りますし
  • キー一発: キーを割り当てておけば、普段書きのテキストから各言語にキー一発 (2 ストロークならば二発ですが) でアクセスできます

秀丸エディタは、ver .8.0 以降のマクロで COM オブジェクトの操作をサポートしたため、以前にはできなかった複雑なアプリケーション連携や、テキストを開いている間だけ永続するオブジェクト等を持つことができるようになりました。今回のマクロも、バックグラウンドで各言語の REPL セッションを起動して、テキストをクローズするまでの間、状態を維持することができるようになったことで実現できました。

詳細は以下ですが、詳しくはコードを読んでください。なお、範囲選択をして実行すれば、選択範囲を評価します。

マクロ名 カーソル以前のどこまでを一単位として評価? 入出力方式 既知の問題
eval-scala.mac カーソルのある行、あるいは対応する括弧かヒアドキュメントによって継続している複数行 (Scala に “\” による行継続は無い) 入力は REPL の :load、出力は STDOUT (SJIS) あいかわらず初回起動が遅いが、こればかりはどうしようもない
eval-clojure.mac カーソル直前の S 式 入力は専用 REPL、出力は STDOUT (SJIS) 初回起動は、そこそこに遅い。STDERR をファイルに出力する方法が分からなかったが、多分あまり支障はない
eval-gauche.mac カーソル直前の S 式 入出力とも専用 REPL (UTF-8) 特に無い。Scheme は Gauche がお気に入りなので、Guile は確認していない。
eval-groovy.mac カーソルのある行、あるいは対応する括弧かヒアドキュメントによって継続している複数行、あるいは “\” で継続している複数行 入力は REPL の \l、出力は STDOUT (SJIS) jline が “unix” モードでないと STDIN を正しく扱えなかったため、動作に MinGW の sh(1) と stty(1) が必要
eval-python.mac 第一カラムが空白ではないカーソル行、あるいはカーソル行以前で第一カラムが空白ではない行までの複数行、あるいは “\” で行継続をしている複数行 入力は専用の REPL、出力は STDOUT (UTF-8) REPL の返事が淡白すぎて、返り値が分かりづらい
eval-ruby.mac カーソルのある行、あるいはカーソル行上の “end” とカラムが合う直前の開始行までの複数行、あるいは “\” で行継続している複数行 入力は REPL の irb_source()、出力は STDOUT (SJIS) irb_source() が、たまにファイルハンドルを離してくれないのか、一時ファイルを delete できないことがある。上書きするので、おそらく動作に支障はない
eval-jscript.mac Groovy とだいたい同じ (本当なら、いろいろ違うんだけど…) 入力は自前実装の REPL で “:load”、出力は STDOUT (SJIS) :load は、通常の eval() ではなく、REPL 的に行単位で評価されるので注意。ブロック内の空行はインデントしておいてください
eval-haskell.mac 第 1 カラム目が空白であれば、直前の “module” までをモジュールとしてロードする。第 1 カラム目が空白でなければ、式として解釈する モジュールの入力は REPL の “:load”、式の入力は STDIN から、出力は STDOUT GHC では、日本語入力は Unicode に落ちるものの、出力はできないようだ
eval-perl.mac カーソル行か、括弧を辿って戻れるところまで 入力は自前 REPL の “:load” から、出力は STDOUT から Perl の eval() は新しくブロックを作ってしまうので、評価単位内の “my”, “local” 変数や “$1″ などは、eval() が終わると破棄されてしまう。”$s =~ /([a-z]+)/; $mstr = $1″ のように複文で保持するなど要工夫

あとは Haskell (トップレベルの扱いがよく分からなくて作れなかった) と、JavaScript (WSH には REPL が無い) 用が欲しいところです。

Wed Apr 13 2011: JScript 用を追加。REPL 単体については「WSH JScript の REPL を書いてみた – Ayutaya.com」から。

Fri Apr 15 2011: Haskell (GHC) 用を追加。言語が言語なので、少々特殊。

このカーソル位置(↓)でマクロを起動すると、

module Test where
  fib 1 = 1
  fib 2 = 1
  fib n = fib (n - 1) + fib (n - 2)
|

モジュールをロード。もう一度実行すれば、再ロード。

module Test where
  fib 1 = 1
  fib 2 = 1
  fib n = fib (n - 1) + fib (n - 2)

[1 of 1] Compiling Test
( C:\DOCUME~1\KIICHI~1\LOCALS~1\Temp\haskell_input_2821200.hs, interpreted )
Ok, modules loaded: Test.
|

続けて式を評価すると、

fib 10

値が返る。

fib 10
55|

Sun Apr 17 2011: Perl を追加。eval() の仕様故に、若干心残りが…。残るは PHP と VBScript くらいか。




Scala: コンストラクタの意味合いとクロージャ

以前、「Scala: コンストラクタ中の一時オブジェクトに消えて欲しい – Ayutaya.com」などと言っていた私ですが、ちょっと考え違いをしていました。なまじ逆コンパイルなどをしてしまったために、C++~Java 的な構造体指向の発想にとらわれていましたが、文法的に見ると、Scala のクラス~コンストラクタは、静的なクロージャセットの構築だと見た方が自然です。であれば、クロージャに束縛された「スタック上の変数」が残るのも道理です。

例として、ベクトル型 (+ 絶対値を求めるメソッド) を Scala で下記のように書いたとして、

class Vect(x: Int, y: Int) {
  def abs = {
    math.sqrt(x * x + y * y)
  }
}

val v = new Vect(2, 3)
println(v.abs)

同じものを、例えば JavaScript で、プロトタイプを使わずにクロージャで書いたとします。

function Vect(x, y) {
  this.abs = function() {
    return (Math.sqrt(x * x + y * y));
  }
}

var v = new Vect(2, 3);
WScript.Echo(v.abs());

一目瞭然でそっくりです。Scala でオブジェクト生成の際に “new” が省略されないことも、それが意味するところが、通常の関数呼び出しと違って、クロージャの配列領域を確保する命令であると考えることができますし (上記の JavaScript でも “new” が、”this” に相当するオブジェクトを作っていますし)、パブリックな「メンバ変数」が実際にはアクセサとなっているところも、Scala のコンストラクタが「クロージャ群の構築子」であると考えれば一貫しています。

以上、Scala で腑に落ちた点をちょろっと。では。




Windows 7 で WSC をコマンドラインから登録する

WSC (Windows Script Components) を登録する際に、「右クリックして『登録』をクリック」がダサくて仕方がありません。複数マシンへ大量に登録したい時にどうしろってんだ、と。そこで、コマンドラインから登録できないものかと探していると、ありました、regsvr32.exe です。どうやら、OLE コントロールとしてひとくくりに、DLL や OCX の登録と同じ要領でよいようです。

# どうでもいいけど、あいかわらずマイクロソフトの、カタカナ単語間をスペースで切る表記はきもちわるい。”・”を使おうよ

当方 Windows 7 ですので、UAC (User Account Control) をパスするなり何なりして、管理者権限で実行する必要があるようです。

コマンドプロンプトを管理者権限で起動してから実行しても良いのですが、それもあまりスマートではありません。su(1) か sudo(8) のようなコマンドはないのですか? なに? runas.exe を使え? 了解しました。

> runas.exe /savecred /env /user:administrator "regsvr32.exe /s Foo.wsc"
administrator のパスワードを入力してください:

administrator アカウントのパスワード? そんなもの設定したおぼえがないよ。しかたがないので、administrator 権限を持つアカウントでコンソールを管理者権限で起動、パスワードを設定します。まあ、これは初回だけですし。

> net user administrator *
ユーザーのパスワードを入力してください:
確認のためにパスワードを再入力してください:
コマンドは正常に終了しました。

>

これで、こんどこそは行け…たのですが、何度かしくじっているうちに、以下のようになってしまいました。

> runas.exe /env /user:administrator "regsvr32.exe /s Foo.wsc"
regsvr32.exe /s Foo.wsc をユーザー "SVX\administrator" として開始して
います...
RUNAS エラー:  実行できません - regsvr32.exe /s /u AyutayaCalc.wsc
1327: ログオン失敗: ユーザー アカウントの制限。考えられる理由として、空
 のパスワードが許可されていない、ログオン時間制限、またはポリシーによる制
 限が適用された、などが挙げられます。
>

どうやら、パスワードなしの状態の時に適当なパスワードを入れてログオン認証に失敗しまくったせいで、アカウント停止を食らったようです。

> net user administrator | findstr /r アカウント有効
アカウント有効                       No
>

あらら。再度有効化します。

> net user administrator /active:yes
コマンドは正常に終了しました。

>

はい、何よりです。

面倒なので、”/env” で、administrator になった後も環境を引き継ぎます。これで、カレントディレクトリの “Foo.wsc” を登録できるようになります。su(1) で “-” をつけないような挙動ですね。”/savecred” で、安い Windows 以外ならばパスワードを記憶できるそうです。

> runas.exe /savecred /env /user:administrator "regsvr32.exe /s Foo.wsc"
administrator のパスワードを入力してください:
regsvr32.exe /s Foo.wsc をユーザー "SVX\administrator" として開始しています...
>

さて、JScript な WSH で WSC を書くとします。




ソース斜め読み: Eucalyptus Cloud Controller の Java コードのエントリはどこ?

表題の答えを先に書いておきますと、com.eucalyptus.bootstrap.SystemBootstrapper です。Eucalyptus のバージョンは、2.0.2 を見ています。

まずは、ソースツリーのおおまかな構成について見ておきます。Cluster Controller の本体は、C で書かれた Axis2/C のモジュール (libaEucalyptusCC.so) です。Apache + mod_axis2 配下で動きます。サブディレクトリは “cluster/” です。

Node Controller の本体も、C で書かれた Axis2/C のモジュール (libEucalyptusNC.so) で、Apache + mod_axis2 配下で動きます。ソースのサブディレクトリは “node/”。

問題は、それ以外――ソースのディレクトリでいう “clc/” 以下です。

追記 (Wed Feb 09 2011): eucalyptus-cloud 下の web サービス群 (ソース中では “component” と呼称される) は、Mule ESB バージョン 2.x で動作しています。別項で、いずれまた書きます。

Cloud Controller だけでなく、Walrus や Storage Controller は Java で書かれていて、呼び出し方からすると、複数機能を束ねた統一バイナリ (eucalyptus-cloud) のかたちをとっています。ところが、プロセスを見てみると、Java のものとおぼしき大量のスレッドはあるのですが、よく見かけるようには、オプションをごちゃごちゃつけて起動されている java コマンドが見あたりません。

$ pstree -alp
...
  |-eucalyptus-clou,28159 -h / -u eucalyptus --pidfile /var/run/eucalyptus ...
  |   `-eucalyptus-clou,28176 -h / -u eucalyptus --pidfile /var/run/eucaly ...
  |       |-bttrack,28514 /usr/bin/bttrack --port 6969 --dfile //var/lib/e ...
  |       |-{eucalyptus-clo},28193
  |       |-{eucalyptus-clo},28195
  |       |-{eucalyptus-clo},28202
...

ソースを見てみると、C でかかれた実行ファイル (eucalyptus-cloud) から、libjvm を経由して Java のコードを起動する作りになっています。しかたがないので clc/modules/bootstrap/eucalyptus-bootstrap.c の main() の定義から下って読んで行くと、fork(2) をして子プロセス (上の例で言う 28176) を作っています。子プロセスのその先の処理は、child()→java_init() で、JNI_CreateJavaVM() の呼び出しです。

JNI の呼び出しの詳細は、デバッグ出力で出るようになっています。eucalyptus-cloud コマンドに “–debug”、もしくは “–verbose” オプションをつけて呼び出せば、標準出力 (いや、エラー出力だったかな?) にデバッグ出力を出してくれます。Ubuntu-10.10 のパッケージだと upstart が出力を捨ててしまいますので、以下のようにして、適当なところにリダイレクトしておきます。

--- /etc/init/eucalyptus.conf.orig
+++ /etc/init/eucalyptus.conf
@@ -37,6 +37,7 @@
        . /etc/eucalyptus/eucalyptus.conf
        [ -n "$JVM_MEM" ] || JVM_MEM="512m"
        opts="-h $EUCALYPTUS -u $EUCA_USER --pidfile \
         /var/run/eucalyptus/eucalyptus.pid -l $LOGLEVEL -L console-log
        -Xmx$JVM_MEM"
+       opts="$opts --debug"
        services=""

        # If the -cloud package is not installed, disable the cloud service
@@ -72,7 +73,7 @@
        # Start the appropriate service(s)
        if [ -n "$services" ]; then
                # Cloud services to run
-               exec eucalyptus-cloud $opts
+               exec eucalyptus-cloud $opts > /tmp/eucalyptus.log 2>&1
        elif [ -r "/etc/init/eucalyptus-nc.conf" ] || \
         [ -r "/etc/init/eucalyptus-cc.conf" ]; then
                # Node or CC services to run
                # Why do we sleep here, rather than emitting a signal?
                #  Such that we can run:

すると、以下のような呼び出しが出ます (いい具合に改行を入れました)。

Using classpath: -Djava.class.path=//etc/eucalyptus/cloud.d:
 //etc/eucalyptus/cloud.d/scripts:
 //usr/share/eucalyptus/eucalyptus-component-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-storage-common-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-config-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-groupmgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-db-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-interface-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-walrus-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-storagecontroller-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-imagemgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-commons-ext-0.4.jar:
 //usr/share/eucalyptus/eucalyptus-www-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-auth-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-msgs-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-dns-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-core-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-clustermgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-cloud-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-keymgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-ws-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-db-hsqldb-ext-2.0.0.jar:
 //usr/share/eucalyptus/wstx-lgpl.jar:
 //usr/share/eucalyptus/gnumail-providers.jar:
 //usr/share/eucalyptus/jetty-util.jar:
 //usr/share/eucalyptus/commons-logging.jar:
 //usr/share/eucalyptus/regexp.jar:
 //usr/share/eucalyptus/jcl-over-slf4j.jar:
 //usr/share/eucalyptus/drools-compiler.jar:
 //usr/share/eucalyptus/geronimo-jms-1.1-spec.jar:
 //usr/share/eucalyptus/commons-pool.jar:
 //usr/share/eucalyptus/geronimo-jpa-3.0-spec.jar:
 //usr/share/eucalyptus/axiom-dom.jar:
 //usr/share/eucalyptus/jibx-bind.jar:
 //usr/share/eucalyptus/commons-io.jar:
 //usr/share/eucalyptus/jetty-rewrite-handler.jar:
 //usr/share/eucalyptus/backport-util-concurrent.jar:
 //usr/share/eucalyptus/gnumail.jar:
 //usr/share/eucalyptus/wsdl4j.jar:
 //usr/share/eucalyptus/xercesImpl.jar:
 //usr/share/eucalyptus/servlet-api-2.5.jar:
 //usr/share/eucalyptus/commons-httpclient.jar:
 //usr/share/eucalyptus/euca_ipt:
 //usr/share/eucalyptus/slf4j-log4j12.jar:
 //usr/share/eucalyptus/commons-collections3.jar:
 //usr/share/eucalyptus/slf4j-api.jar:
 //usr/share/eucalyptus/chgrp-dhcpd:
 //usr/share/eucalyptus/geronimo-ejb-3.0-spec.jar:
 //usr/share/eucalyptus/dd-lv:
 //usr/share/eucalyptus/google-collections.jar:
 //usr/share/eucalyptus/activation.jar:
 //usr/share/eucalyptus/serializer.jar:
 //usr/share/eucalyptus/bcprov.jar:
 //usr/share/eucalyptus/jibx-run.jar:
 //usr/share/eucalyptus/jetty-sslengine.jar:
 //usr/share/eucalyptus/dom4j.jar:
 //usr/share/eucalyptus/axiom-impl.jar:
 //usr/share/eucalyptus/commons-discovery.jar:
 //usr/share/eucalyptus/hsqldb.jar:
 //usr/share/eucalyptus/jaxen.jar:
 //usr/share/eucalyptus/javassist.jar:
 //usr/share/eucalyptus/axiom-api.jar:
 //usr/share/eucalyptus/add_key.pl:
 //usr/share/eucalyptus/antlr.jar:
 //usr/share/eucalyptus/jaxp-1.3.jar:
 //usr/share/eucalyptus/chmod-dhcpd:
 //usr/share/eucalyptus/geronimo-interceptor-3.0-spec.jar:
 //usr/share/eucalyptus/proxool.jar:
 //usr/share/eucalyptus/commons-logging-adapters.jar:
 //usr/share/eucalyptus/hsqldbutil.jar:
 //usr/share/eucalyptus/jibx-extras.jar:
 //usr/share/eucalyptus/geronimo-jta-1.0.1b-spec.jar:
 //usr/share/eucalyptus/jetty.jar:
 //usr/share/eucalyptus/gwt-servlet.jar:
 //usr/share/eucalyptus/xpp3.jar:
 //usr/share/eucalyptus/jul-to-slf4j.jar:
 //usr/share/eucalyptus/commons-cli.jar:
 //usr/share/eucalyptus/xalan2.jar:
 //usr/share/eucalyptus/commons-beanutils.jar:
 //usr/share/eucalyptus/mvel.jar:
 //usr/share/eucalyptus/janino.jar:
 //usr/share/eucalyptus/commons-fileupload.jar:
 //usr/share/eucalyptus/commons-logging-api.jar:
 //usr/share/eucalyptus/junit.jar:
 //usr/share/eucalyptus/geronimo-j2ee-connector-1.5-spec.jar:
 //usr/share/eucalyptus/ant.jar:
 //usr/share/eucalyptus/gwt-user.jar:
 //usr/share/eucalyptus/commons-codec.jar:
 //usr/share/eucalyptus/wss4j.jar:
 //usr/share/eucalyptus/bsf.jar:
 //usr/share/eucalyptus/commons-lang.jar:
 //usr/share/eucalyptus/bcel.jar:
 //usr/share/eucalyptus/excalibur-logkit.jar:
 //usr/share/eucalyptus/netty.jar:
 //usr/share/eucalyptus/el-api-2.1.jar:
 //usr/share/eucalyptus/drools-core.jar:
 //usr/share/eucalyptus/cglib.jar:
 //usr/share/eucalyptus/populate_arp.pl:
 //usr/share/eucalyptus/inetlib.jar:
 //usr/share/eucalyptus/jug-asl.jar:
 //usr/share/eucalyptus/ezmorph.jar:
 //usr/share/eucalyptus/groovy.jar:
 //usr/share/eucalyptus/commons-jxpath.jar:
 //usr/share/eucalyptus/log4j-1.2.jar:
 //usr/share/eucalyptus/xml-security.jar:
 //usr/share/eucalyptus/xom.jar:
 //usr/share/eucalyptus/modprobe-aoe:
 //usr/share/eucalyptus/dnsjava.jar:
 //usr/share/eucalyptus/ecj.jar:
 //usr/share/eucalyptus/json-lib.jar

Version:                       10004
Ignore Unrecognized Arguments: 0
Extra options:                 33
  "-Xbootclasspath/p://usr/share/eucalyptus/openjdk-crypto.jar    " (0x(nil))
  "-Xmx512m                                                       " (0x(nil))
  "-XX:MaxPermSize=128m                                           " (0x(nil))
  "-XX:+UseConcMarkSweepGC                                        " (0x(nil))
  "-Djava.net.preferIPv4Stack=true                                " (0x(nil))
  "-Djava.security.policy=//etc/eucalyptus/cloud.d/security.policy" (0x(nil))
  "-Djava.library.path=//usr/lib/eucalyptus                       " (0x(nil))
  "-Dsun.java.command=Eucalyptus                                  " (0x(nil))
  "-Deuca.home=//                                                 " (0x(nil))
  "-Deuca.var.dir=//var/lib/eucalyptus                            " (0x(nil))
  "-Deuca.run.dir=//var/run/eucalyptus                            " (0x(nil))
  "-Deuca.lib.dir=//usr/share/eucalyptus                          " (0x(nil))
  "-Deuca.conf.dir=//etc/eucalyptus/cloud.d                       " (0x(nil))
  "-Deuca.log.dir=//var/log/eucalyptus                            " (0x(nil))
  "-Deuca.version=2.0.0                                           " (0x(nil))
  "-Deuca.log.exhaustive.db=FATAL                                 " (0x(nil))
  "-Deuca.log.exhaustive.cc=FATAL                                 " (0x(nil))
  "-Deuca.log.exhaustive.user=FATAL                               " (0x(nil))
  "-Deuca.log.exhaustive.external=FATAL                           " (0x(nil))
  "-Deuca.log.level=DEBUG                                         " (0x(nil))
  "-Deuca.log.appender=console-log                                " (0x(nil))
  "-Deuca.db.port=9001                                            " (0x(nil))
  "-Deuca.db.host=127.0.0.1                                       " (0x(nil))
  "-Deuca.walrus.host=localhost                                   " (0x(nil))
  "-Deuca.remote.dns=true                                         " (0x(nil))
  "-Xdebug                                                        " (0x(nil))
  "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005  " (0x(nil))
  "-Dcom.sun.management.jmxremote                                 " (0x(nil))
  "-XX:+HeapDumpOnOutOfMemoryError                                " (0x(nil))
  "-XX:HeapDumpPath=//var/log/eucalyptus/                         " (0x(nil))
  "-Xmx512m                                                       " (0x(nil))
  "-Djava.class.path=//etc/eucalyptus/cloud.d://etc/eucalyptus/..." (0x(nil))
  "abort                                                          " (0x0x4018c0)

バラバラとパラメータを渡しています。おケツの “abort” の追加情報 0x4018C0 は、abort の際のコールバック関数ですね。表示が変ですが。

static void java_fail(void) { exit(1); }

int java_init(euca_opts *args, java_home_t *data) {
  ...
  opt[++x].optionString="abort";
  opt[x].extraInfo=java_fail;

エントリの jar ファイルも指定していませんので、ここではまだ起動はしません。単に、さっきの “JNI_CreateJavaVM” で、VM を作るだけです。かなめは、さきほどの java_load_bootstrapper() です。以下のマクロで別名ですので、euca_load_bootstrapper() を探します。

#define java_load_bootstrapper euca_load_bootstrapper

euca_load_bootstrapper() では、構造体インスタンス “bootstrap” を作ります。これが Java コードのエントリポイントになります。まず、クラスの取得です。

#define EUCA_MAIN "com/eucalyptus/bootstrap/SystemBootstrapper"

bootstrap.clazz=((*env)->FindClass(env,EUCA_MAIN))

次に、static なファクトリメソッドを呼び出して、ブートストラップのインスタンスを作ります。

bootstrap.constructor=(*env)->GetStaticMethodID(env, \
 bootstrap.clazz,euca_get_instance.method_name, \
 euca_get_instance.method_signature );

この後は、ここから始まる Java コードの仕事ですね。続きは後ほど。

# なぜにこうもツギハギ感溢れる作りになってるんでしょうね? Eucalyptus は

今なら JNI よりも、JNA というのがありますよー、と同僚に紹介されました。後で読む。

JNIより簡単にJavaとC/C++をつなぐ「JNA」とは(1/4)-@IT




私家版 Oracle Cheat Sheet

理解はしていても忘れてしまうが、見れば思い出す断片の羅列です。

PL/SQL

リンク: Oracle Database PL/SQLユーザーズ・ガイドおよびリファレンス 10.2

SQL*Plus の操作
  • $ sqlplus ID/PASS@HOST:1521/SID
  • コンソール出力をオンに: “set serveroutput on
  • ファイルの読み込み実行: “@C:\priv\2010\test.plsql
  • 直前のエラーの表示: “show errors
  • コメント /* コメント */, — コメント
    配列
  • int index な map
  • メソッド: count … 歯抜けで存在する数を返す, delete … 全て, delete(i) … Pascal 風 1 から添字, delete(i, j) … j は末尾自体なので注意, first … 以下は添字を返す, last, prior(i), next(i), exists(i)
  • コード断片

    諸々:

    -- Oracle Database PL/SQLユーザーズ・ガイドおよびリファレンス 10.2
    
    drop table foo;
    create table foo(k integer, v varchar2(30));
    insert into foo values(1, 'first');
    insert into foo values(2, 'second');
    insert into foo values(3, 'third');
    
    declare
      n1 number not null default 100;
      A$_#09 constant number := 1000; -- ID に利用可能な文字
      s1 char(3) := 'abc';
      type rectag is record (
        n number(3) default 200,
        s nvarchar2(30) := 'こんにちは' -- サイズ省略不可
      );
      rec rectag;
      type tabtag is table of rectag index by pls_integer;
      tab tabtag;
      type nestedtag is table of foo%rowtype;
      nested nestedtag := nestedtag();
      -- i pls_integer; -- 不要: ループ変数は暗黙で
    begin
      dbms_output.put_line('d0: ' || n1 || ', ' || s1);
      -- rec := tab(1); -- 参照しても生成されません
      tab(1).n := 300;
      for i in 2 .. 3 loop
        tab(i).s := 'さようなら';
      end loop;
      for i in 1 .. tab.count loop -- 他に loop, while ~ loop
        dbms_output.put_line('d0: ' || tab(i).n || ', ' || tab(i).s);
      end loop;
      if n1 < 0 then
        dbms_output.put_line('d1');
      elsif 0 <= n1 and n1 < 100 then
        dbms_output.put_line('d2');
      else
        dbms_output.put_line('d3');
      end if;
      case n1 /* 検索子式 */
        when 100 then
          dbms_output.put_line('d4');
        else
          dbms_output.put_line('d5');
      end case;
      case -- 検索 case 文
        when n1 = 100 then
          dbms_output.put_line('d6');
        else
          null;
      end case;
      declare /* ブロック静的スコープ */
        /* rec0 foo%rowtype; -- 不要: ループ変数は暗黙 */
        cursor cur0(arg number) is
         select * from foo where k >= arg for update;
         -- パラメータ付き cursoer
        n number;
      begin
        /* open cur0; -- エラー: 明示 open ならば fetch cur into rec0 */
        -- n := amp;tmp;
        n := 2;
        for rec0 in cur0(n) loop
          if rec0.k = 2 then
            update foo set k = k * 2 where current of cur0;
             -- "current of CURSOR" が使えるのは for update にだけ
          end if;
          if cur0%isopen then
           -- カーソル属性: %notfound, %found, %rowcont, %isopen
            dbms_output.put_line('cp0: is open');
          else
            dbms_output.put_line('cp1: not open');
          end if;
          dbms_output.put_line(rec0.k); -- これは当然、変更前の値
        end loop;
        open cur0(2);
        if cur0%isopen = true then
          dbms_output.put_line('cp1: is open');
        else
          dbms_output.put_line('cp1: not open');
        end if;
      end;
      nested.extend(10);
      nested(1).k := 500;
      nested(1).v := 'five hundred';
      raise_application_error(-20001, 'ほげほげ');
    exception
      /* 10 PL/SQLエラーの処理/事前定義のPL/SQL例外のまとめ */
      when no_data_found then
        dbms_output.put_line('error 0: ' || sqlcode || ', ' || sqlerrm);
      when others then
        dbms_output.put_line('error 1: ' || sqlcode || ', ' || sqlerrm);
    end;
    /
    

    トリガ:

    drop table foo;
    create table foo(k integer, v varchar2(30));
    
    create or replace trigger foo_trigger
     before insert or update on foo
     for each row
     when (new.k is not null)
    begin
      if inserting or updating then
        :new.v := :new.v || ' foo';
      end if;
    end;
    /
    
    insert into foo values(1, 'first');
    insert into foo values(2, 'second');
    insert into foo values(3, 'third');
    
    select * from foo;
    



    私家版 C++ Cheat Sheet

    理解はしていても忘れてしまうが、見れば思い出す断片の羅列です。

    真偽値 bool で true, false

    Standard Library

    リンク: vector – C++ Reference

    string
  • コンストラクタ: string(), string(string), string(string, pos, len = to_the_end), string(sz, len = to_the_end), string(n, ch), string(i, j)
  • メソッド: const char * c_str(), const char * data(), find(str, pos = 0), find(sz, pos = 0), find(ch, pos = 0) … pos は対象の位置, rfind(…), string substr(pos = 0, len = to_the_end), compare(), copy(szDst, len, pos = 0) … \0 は付かない
  • その他: operator<<(os, const & str), operator>>(is, & str), operator+(& str と ch, sz, psz の 2 項)
  • シーケンス共通
  • コンストラクタ: vector(), vector(n, x), vector(i, j), vector(v) … コピコン
  • メソッド: size(), empty(), swap(v), insert(i, x) … i の前に入る, insert(i, n, x), insert(i, iSrc, jSrc)? … もちろんコピーが入る, erase(i) ⇒ i の次, erase(i, j) … [i, j) を削除 ⇒ j の次, clear(), assign(i, j), assign(n, x), max_size(), pop_back(x), push_back(x), resize(n, x), get_allocator()
  • イテレータbegin(), end(), rbegin(), rend(), front() ⇒ begin(), back() ⇒ end() - 1
  • ::value_type, ::pointer, ::const_pointer, ::reference, ::const_reference, ::iterator, ::const_iterator, ::const_reverse_iterator, ::reverse_iterator, ::size_type, ::difference_type, ::allocator_type
  • vector
  • メソッド: ランダムアクセスで at(ind), operator[](ind). 拡大時要全コピーなので capacity(), reserve(n)
  • deque
  • ランダムアクセスで at(ind), operator[](ind). フロント O(1) で push_front(x), pop_front()
  • list
  • merge, remove, remove_if, reverse, pop_front, push_front, sort, splice, unique
  • set, multiset
  • set(cmp), set(i, j, cmp), set(s)
  • empty(), size(), max_size(), insert(x), insert(i, x) … i はヒント, insert(i, j), erase(i), erase(x), erase(i, j), clear(), swap(s), key_comp(), value_comp(), get_allocator()
  • find(x), count(x) ⇒ set ならば 0 か 1, lower_bound(x), upper_bound(x), pair(itr, itr> equal_range(x)
  • begin(), end(), rbegin(), rend()
  • map, multimap
  • map(cmp), map(i, j, cmp), map(m)
  • m[key] = val, i->second = val, 右辺 m[ind] ⇒ 無ければ新規 pair(key, val)
  • アルゴリズム
  • non-modifying: find(i, j, x), find_if(i, j, cmp) … 1stのみ, count(i, j, x), count_if(i, j, cmp), adjacent_find(i, j), adjacent_find(i, j, cmp), for_each(i, j, f) ⇒ f, bool mismatch(i, j, iAnother), bool equal(i, j, iAnother), bool search(iHay, jHay, iNdl, jNdl),
  • modifying: copy(i, j, iDest) ⇒ コピー先の尻の次, copy_backward(i, j, jDest) ⇒ コピー先の頭, fill(i, j, x), fill(i, n, x), generate(i, j, f) … f に引数は無い, partition(i, j, f) ⇒ 後半 ! f の頭, random_shuffle(i, j), erase(remove(v.begin(), v.end(), x), v.end()), replace(i, j, x, y), reverse(i, j), rotate(i, k, j) … [i, j) の k が新しい頭になる, swap(cnt1, cnt2), swap_ranges(i, j, iOther), transform(…), unique(i, j) … 返り値は remove() と同方式,
  • ソート:
  • 関数 obj
  • bind1st, bind2nd



  • 私家版 JavaScript Cheat Sheet

    Warning: include(/var/www/wordpress//home/knaka/pub/inc/javascript-cheat.html): failed to open stream: No such file or directory in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102 Warning: include(): Failed opening '/var/www/wordpress//home/knaka/pub/inc/javascript-cheat.html' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102




    私家版 Python Cheat Sheet

    Warning: include(/var/www/wordpress//home/knaka/pub/inc/python-cheat.html): failed to open stream: No such file or directory in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102 Warning: include(): Failed opening '/var/www/wordpress//home/knaka/pub/inc/python-cheat.html' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102




    私家版 Scala Cheat Sheet

    Warning: include(/var/www/wordpress//home/knaka/pub/inc/scala-cheat.html): failed to open stream: No such file or directory in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102 Warning: include(): Failed opening '/var/www/wordpress//home/knaka/pub/inc/scala-cheat.html' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102




    Scala: 中置記法→逆ポーランド記法のパーサ練習

    コップ本の第 31 章「パーサー・コンビネーター」の初っ端のサンプルです。とりあえず、いきなり Scala によるパーサジェネレータの記述方法を見せられたらビビると思われます。私はビビった。

    import scala.util.parsing.combinator._
    object Arith extends JavaTokenParsers {
      def expr: Parser[Any] = term ~ rep("+" ~ term | "-" ~ term)
      def term: Parser[Any] = factor ~ rep("*" ~ factor | "/" ~ factor)
      def factor: Parser[Any] = floatingPointNumber | "(" ~ expr ~ ")"
    }
    

    実行してみます。

    scala> Arith.parseAll(Arith.expr, "1 + 2 - 3 * 4 / (5 + 6)")
    res9: Arith.ParseResult[Any] =
     [1.24] parsed: ((1~List())~List((+~(2~List())),
     (-~(3~List((*~4), (/~(((~((5~List())~List((+~(6~List())))))~))))))))
    
    scala>
    

    良いようですが、省略記法の使いすぎで、パッと見、何をやっているのかがさっぱり分かりません。省略せずに、書き下してみます。

    import scala.util.parsing.combinator._
    object Arith extends JavaTokenParsers {
      def expr: Parser[Any] =
       term.~(rep((literal("+").~(term)).|(literal("-").~(term))))
      def term: Parser[Any] =
       factor.~(rep((literal("*").~(factor)).|(literal("/").~(factor))))
      def factor: Parser[Any] =
       floatingPointNumber.|(literal("(").~(expr).~(literal(")")))
    }
    

    パーサ間で「演算」を繰り返して、新しいパーサを構築していくプロセスが見えます (いや、あいかわらず分かりづらいのは同じですが)。実行してみます。

    scala> Arith.parseAll(Arith.expr, "1 + 2 - 3 * 4 / (5 + 6)")
    res10: Arith.ParseResult[Any] =
     [1.24] parsed: ((1~List())~List((+~(2~List())),
     (-~(3~List((*~4), (/~(((~((5~List())~List((+~(6~List())))))~))))))))
    
    scala> res9.toString == res10.toString
    res11: Boolean = true
    
    scala>
    

    以上の例では、生成されたパーサが変換処理を含まないので、出力はデフォルトの垂れ流しで、分かりづらいです。そこで、練習がてら、中置記法から逆ポーランド記法への変換パーサのジェネレータに修正してみます。

    import scala.util.parsing.combinator._
    object Arith extends JavaTokenParsers {
      def expr: Parser[String] = term ~ rep("+" ~ term | "-" ~ term) ^^ {
       case term ~ rest => reduce(term, rest) }
      def term: Parser[String] = factor ~ rep("*" ~ factor | "/" ~ factor) ^^ {
       case factor ~ rest => reduce(factor, rest) }
      def reduce(x: String, pairs: List[~[String, String]]) =
        (x /: pairs) ((x, pair) => "%s %s %s".format(x, pair._2, pair._1))
      def factor: Parser[String] = floatingPointNumber | "(" ~> expr <~ ")"
    }
    

    実行してみます。

    scala> Arith.parseAll(Arith.expr, "1 + 2 - 3 * 4 / (5 + 6)")
    res12: Arith.ParseResult[String] = [1.24] parsed: 1 2 + 3 4 * 5 6 + / -
    
    scala>
    

    この機能があれば、外部 DSL のパーサが、外部ツールの助けなしにチョチョイのチョイで書けてしまうわけですね。すごいですねぇ…。




    Scala: コンストラクタ中の一時オブジェクトに消えて欲しい

    Scala については、まだまだ学習中の身ですが、このへんを越えないと安心できない気がするので、書きます。

    Scala クラスの基本コンストラクタ (primary constructor) では、クラス定義、コンストラクタ定義、フィールド定義が一緒くたに書けるので、大抵の場合は既存の OOP 言語のそれらと比べて、記述がスッキリして、とても良いです。

    class Point(val x: Int, val y: Int)
    val p = new Point(3, 4)
    println(p.x + ", " + p.y) // 3, 4
    

    ところが、コンストラクタ中で一時変数が必要になると、何やら気持ち悪いことになります。

    例としては、「コップ本」こと「Scala スケーラブルプログラミング」から。コップ本の最初の OOP の例で、有理数をとりあげています。最大公約数を求めて分母・分子を約分するのですが、基本コンストラクタ内で求めた最大公約数が、このクラスのインスタンスが存続する限りフィールドとして残ってしまいます、もう不要なのに。

    class Rational (
      numArg: Int,
      denArg: Int ) {
      def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
      private val n = gcd(numArg.abs, denArg.abs) // GCD を計算
      val num = numArg / n
      val den = denArg / n
      // private だからアクセスはできないものの、この後、n は永続
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    うーん…。

    なお、基本コンストラクタの引数は、普通に記述すると、インスタンスの private なフィールドになりますが、val や var で宣言すると、値は隠しフィールドに代入され、そこへのアクセサが自動生成されます (「パラメータフィールド」と呼ぶそうです)。どうやら、Scala のアクセスコントロールが JRE のそれよりも細かいので、アクセサで何とかしているようですよ。多分。下記参照:

    間違ってたら直します。

    追記 (Wed Oct 27 2010): よく考えたら、overide や lazy にも必要ですね。

    それならばと、分子・分母を reader にすれば、n は不要にはならないので、気分的には若干マシになります。けれども、何の解決にもなっていません。

    class Rational (
      numArg: Int,
      denArg: Int ) {
      def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
      private val n = gcd(numArg.abs, denArg.abs)
      def num = numArg / n // 呼び出し時の評価…
      def den = denArg / n
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    どうしたものかといろいろやっていましたが、当然すでに、いろいろと議論があります。

    上記を参考にしつつ、いろいろやってみます。要は、一時変数をスタック上に確保できれば良いのです。

    まずは、基本 constructor を private にして、代替 constructor から呼ぶことを考えましたが…、残念、通りません。Scala の代替 constructor では、まず最初に基本 constructor を呼ばなければならないルールになっています。インスタンスの初期化以前にいろいろやるな、というお達しでしょうが、厳しいです。

    class Rational private ( // コンパイルが通りません
      val num: Int,
      val den: Int,
      dummy: Unit ) {
      def this(num: Int, den: Int) = {
        def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
        val n = gcd(num.abs, den.abs)
        this(num / n, den / n, ())
      }
    }
    

    次に、コンパニオンオブジェクトで、ファクトリメソッドを用意します。apply にしたところで、話は同じ。これは悪くなさそうですが、初期化引数の正規化は、ファクトリではなくコンストラクタの仕事にしたいかなぁ…。インスタンス生成のしかたも変わってしまいますし。

    class Rational(val num: Int, val den: Int)
    object Rational {
      def create(num: Int, den: Int) = {
        def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
        val n = gcd(num.abs, den.abs)
        new Rational(num / n, den / n)
      }
    }
    val r = Rational.create(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    それではと、フィールドの生成に、タプルを使ってみます。一見良いものの、今度は Tuple2 が private フィールドに残ります (自動的に付与される名前の “x$1″ とかで、アクセスできたりする。Scala 的には、メンバとしての位置づけなんだろう)。ですので、かえってデカいんじゃ?

    class Rational (
      numArg: Int,
      denArg: Int ) {
      val (num, den) = {
        val n = gcd(numArg.abs, denArg.abs)
        (numArg / n, denArg / n)
      }
      private def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    逆アセンブルすると、以下のような具合です:

    $ javap -c -private Rational
    Compiled from "Rational.scala"
    public class Rational extends java.lang.Object implements scala.ScalaObject{
    private final int den;
    
    private final int num;
    
    private final scala.Tuple2 x$1;
    
    public Rational(int, int);
      Code:
    (中略)
    $
    

    これで最後。いろいろやってみましたが、一時変数が lexical scope 内に入るので、これが一番綺麗かな? フィールドを、private とはいえ var にせざるを得ないので、アクセスコントロール (リードオンリー) は accessor で行ないます。ブロックを、単なるブレースで括ろうとすると、ここでは print からの戻り値への行継続で、関数の引数と解釈されてコンパイルがコケますので、do-while-false ループ (C 言語で、複文を、値を返さない式の位置に書く際に使うイディオム) にしてみます (素直にセミコロンを置いても良いのですが、カッコ悪いので)。

    class Rational (
      private var numField: Int, // 仕方がないので var にします
      private var denField: Int ) {
      print("Initializing ...")
      do { // C でよくやるループ
        val n = gcd(numField, denField) // これはスタック上に確保される
        numField /= n // フィールド変数へ再代入 (実際はアクセサ)
        denField /= n
      } while (false) // 最適化されるので、ループにはならない (多分…)
      println(" Done.")
      def num = numField // public かつ read-only
      def den = denField
      def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    一応、do-while-false がループになっていないことと、num と den がリードオンリーであることと、numField と denField へのアクセスが private のアクセサを経由していることを確認します。

    $ javap -c -private Rational
    Compiled from "Rational.scala"
    public class Rational extends java.lang.Object implements scala.ScalaObject{
    private int denField;
    
    private int numField;
    
    public Rational(int, int);
     Code:
      0:  aload_0
      1:  iload_1
      2:  putfield       #13; //Field numField:I
      5:  aload_0
      6:  iload_2
      7:  putfield       #15; //Field denField:I
      10: aload_0
      11: invokespecial  #20; //Method java/lang/Object."<init>":()V
      14: getstatic      #26; //Field scala/Predef$.MODULE$:Lscala/Predef$;
      17: ldc            #28; //String Initializing ...
      19: invokevirtual  #32; //Method scala/Predef$.print:(Ljava/lang/Object;)V
      22: aload_0
      23: iload_1
      24: iload_2
      25: invokevirtual  #36; //Method gcd:(II)I
      28: istore_3
      29: aload_0
      30: iload_1
      31: iload_3
      32: idiv
      33: invokespecial  #40; //Method numField_$eq:(I)V
      36: aload_0
      37: iload_2
      38: iload_3
      39: idiv
      40: invokespecial  #43; //Method denField_$eq:(I)V
      43: getstatic      #26; //Field scala/Predef$.MODULE$:Lscala/Predef$;
      46: ldc            #45; //String  Done.
      48: invokevirtual  #48; //Method scala/Predef$.println:(Ljava/lang/Object;)V
      51: return
    
    (中略)
    
    public int den();
      Code:
       0: aload_0
       1: invokespecial  #58; //Method denField:()I
       4: ireturn
    
    public int num();
      Code:
       0: aload_0
       1: invokespecial  #61; //Method numField:()I
       4: ireturn
    
    private void denField_$eq(int);
      Code:
       0: aload_0
       1: iload_1
       2: putfield       #15; //Field denField:I
       5: return
    
    private int denField();
      Code:
       0: aload_0
       1: getfield       #15; //Field denField:I
       4: ireturn
    
    private void numField_$eq(int);
      Code:
       0: aload_0
       1: iload_1
       2: putfield       #13; //Field numField:I
       5: return
    
    private int numField();
      Code:
       0: aload_0
       1: getfield       #13; //Field numField:I
       4: ireturn
    
    (中略)
    
    }
    
    $
    

    Scala は、総じては超ステキ言語なのですが、時たま、策士が策に溺れている感があります。そんなところも大好きですが。




    Fedora の Scala の RPM を CentOS でビルド

    Scala とだったら、JRE とうまくやっていけるんじゃないかという予感がしています。

    Fedora では、yum 一発で入ってくれるので助かります。

    [root@fedora ~]# yum install scala
    [root@fedora ~]#
    

    一方の CentOS-5.5 ですが、Scala が yum レポジトリ上にないのもさることながら、さすがに Java 関連のモジュールがいいかげん古くて、ビルドもままなりません。仕方ないので、Fedora のソースからビルドします。下記の各パッケージの情報ページ上部の “Build” の先から、新しい目の source rpm をいただいてきます:

    バージョンの上下でモグラ叩きになりますが、どうにか以下のような感じで順にビルドし、インストールします。ant は最低でも 1.7 でないと、scala のコンパイルに失敗します。よく見ていませんが、scala-2.8.0 は document まわりのコンパイルにコケていたので、2.7.7 を利用しています。充分ですよね?

    ant-contrib だけは EPEL のレポジトリから入れます。手順の最後に、標準以外のレポジトリから入れている ant-contrib と、標準パッケージと競合する ant-1.8 は remove しています。

    なお、下記は root による 実行例ですが、一般論として特権ユーザによる RPM の作成は危険ですので、普通は ~/.rpmmacros を用意して、一般ユーザで実行しましょう。それと、例によって Fedora の RPM キーは何か壊れているようで、MD5 のチェックが通りません。インストールには “–nomd5″ オプションが必要になっています。

    [root@node1 ~]# yum install -y rpm-build
    [root@node1 ~]# host=kojipkgs.fedoraproject.org
    [root@node1 ~]# wget \
     http://$host/packages/shtool/2.0.8/4.fc14/src/shtool-2.0.8-4.fc14.src.rpm \
     http://$host/packages/jline/0.9.94/0.6.fc14/src/jline-0.9.94-0.6.fc14.src.rpm \
     http://$host/packages/ant/1.8.1/6.fc15/src/ant-1.8.1-6.fc15.src.rpm \
     http://$host/packages/scala/2.7.7/1.fc13/src/scala-2.7.7-1.fc13.src.rpm
    [root@node1 ~]# rpm -i --nomd5 \
     shtool-2.0.8-4.fc14.src.rpm \
     jline-0.9.94-0.6.fc14.src.rpm \
     ant-1.8.1-6.fc15.src.rpm \
     scala-2.7.7-1.fc13.src.rpm
    [root@node1 ~]# topdir=/usr/src/redhat/
    [root@node1 ~]# rpmbuild -ba $topdir/SPECS/shtool.spec
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/shtool-2.0.8-4.noarch.rpm
    [root@node1 ~]# yum install -y ant junit
    [root@node1 ~]# rpmbuild --without maven -ba $topdir/SPECS/jline.spec
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/jline-0.9.94-0.6.noarch.rpm
    [root@node1 ~]# perl -p -i -e 's/(jpackage-utils) >= .*/\1/' \
     $topdir/SPECS/ant.spec
    [root@node1 ~]# rpmbuild --with bootstrap --without gcj_support \
     -ba $topdir/SPECS/ant.spec
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/ant-1.8.1-6.noarch.rpm \
     $topdir/RPMS/noarch/ant-nodeps-1.8.1-6.noarch.rpm
    [root@node1 ~]# rpm -Uvh $(printf \
     ftp://download.fedora.redhat.com/pub/epel/%s/%s/epel-release-*-*.noarch.rpm \
     $(rpm -q --qf "%{version}" $(rpm -q --whatprovides redhat-release)) \
     $(uname --hardware-platform) )
    [root@node1 ~]# yum install -y ant-contrib
    [root@node1 ~]# rpmbuild -ba -D "fedora %nil" $topdir/SPECS/scala.spec
    [root@node1 ~]# yum remove -y ant ant-nodeps epel-release
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/scala-2.7.7-1.noarch.rpm \
     $topdir/RPMS/noarch/scala-examples-2.7.7-1.noarch.rpm
    [root@node1 ~]# scala -e \
     'println(List("foo", "hoge").map((s: String) => s.length).mkString("\n"))'
    3
    4
    [root@node1 ~]#
    

    一度ビルドできれば、次からはバイナリだけ入れれば OK でしょう。SRPM 無修正という縛りプレイで行こうとしたのですが、残念なことに、jpackage-utils のバージョンだけ、静的に修正しています。

    Sun JDK の手順も、後で試しておきます。Hadoop の絡みで、OpenJDK では行けないケースもありますので。




    ソース斜め読み: pldebugger

    CentOS で PL/pgSQL デバッガ – Ayutaya.com」がうまく動かずに、パッチを作るのに結構手間がかかったので、ソースを斜め読みした結果を残しておきます。とりあえず、グローバル・ブレークポイントでのデバッグについてだけ書きます。ローカル・ブレークポイントについては、気が向いたら書きます。

    PL/pgSQL デバッガ (グローバル・ブレークポイント)

    # 手描き…

    上記の例では、pgAdmin-III から関数にブレークポイントを仕掛け、psql から関数を呼び出し、pgAdmin-III でデバッグを行なう、という流れで行きます。セッション間で AF_INET での IPC 通信をするところが分かれば理解できると思います。

    まず、pgAdmin-III から PostgreSQL へ、普通に接続します。普通にセッションプロセスが fork(2) します (図の、「通常」セッションプロセス)。

    関数にブレークポイントを設定するよう指示すると、pgAdmin-III は、上記の認証情報を用いて、もう一本、デバッガ用の接続を確立します (図の、「デバッガ」セッションプロセス)。PostgreSQL 拡張モジュール pldbgapi.so を用いて、デバッガセッションとして働きます。

    このデバッグ接続ですが、特権ユーザでしか接続できません。実際に何をしているのか (どんな SQL クエリを送っているのか) を見たくて log_min_duration_statement を 0 に設定してみたのですが、pgAdmin-III が「気を効かせて」、特権ユーザなのを良いことに、ログ出力を抑制してくれます。pgAdmin-III をリビルドして、何をしているのかをログに出すには、pgAdmin-III のソースの以下の部分を削除します:

    perl -p -i -e 's/.*SET log_min_messages TO fatal.*//' \
     ./pgadmin/debugger/ctlCodeWindow.cpp \
     ./pgadmin/debugger/dlgDirectDbg.cpp
    

    そういえば、g++-4.1.2 + wxGTK-devel で、配列添え字オペレータの演算子オーバーロードで、引数の int と size_t が曖昧でビルドできない、と言われるかも知れません。ヘッダでどちらかをコメントアウトすれば通ります。

    その結果出てくるのが、以下のようなログです (適宜行番号 & 改行追加):

    1: LOG:  期間: 1.226 ミリ秒  文: SELECT count(*) AS count, proname
        FROM pg_proc WHERE oid = 16446 GROUP BY proname
    2: LOG:  期間: 0.414 ミリ秒  文: set client_encoding to 'UNICODE'
    3: LOG:  期間: 1.437 ミリ秒  文: SELECT * from pldbg_create_listener()
    4: LOG:  期間: 1.398 ミリ秒  文: SELECT *, 'NULL' as pid
        FROM pldbg_get_target_info('16446', 'o')
    5: LOG:  期間: 0.439 ミリ秒  文: SELECT version();
    6: LOG:  期間: 0.610 ミリ秒  文: SELECT *
        FROM pldbg_set_global_breakpoint(1, 16446, NULL, NULL)
    

    pg_proc テーブルで、ターゲット関数を確認。pldbg_create_listener() で、デバッガセッションは 127.0.0.1 上に AF_INET でデバッグサーバのポートを開きます。このポートは、実際にデバッグモードに入った後で、デバッガとターゲットが独自のプロトコルで通信を行なうために用いられます。pldbg_get_target_info() でターゲットを指定し、デバッグセッションは、共有メモリ上にブレークポイントとポート番号の情報を書き込みます。最後に、pldbg_set_global_breakpoint() で、デバッグセッションはブロックし、待ちに入ります。

    pldbgapi.c:

    Datum pldbg_create_listener( PG_FUNCTION_ARGS )
    {
      debugSession * session =
       MemoryContextAlloc( TopMemoryContext, sizeof( *session ));
      initializeModule();
      // これ
      session->listener = allocateServerListener( &session->serverPort );
      session->serverSocket = -1;
      mostRecentSession = session;
      PG_RETURN_INT32( addSession( session ));
    }
    

    次に、psql(1) からサーバに接続し、新しいセッションプロセスを fork(2) します (図の、「ターゲット」セッションプロセス)。ここで働く plugin_debugger.so は、いわゆる普通の PostgreSQL モジュール (SQL から呼び出す) とは違って、PL/pgSQL の拡張モジュールです。ストアドプロシージャの各行の実行をフックすることができるので、これを利用してブレークポイントやステップ実行を実現します。

    plugin_debugger.c:

    // 関数の頭と、行の頭でフックがかかるようにする
    static PLpgSQL_plugin plugin_funcs =
     { dbg_startup, NULL, NULL, dbg_newstmt, NULL };
    
    void _PG_init( void )
    {
      PLpgSQL_plugin ** var_ptr =
       (PLpgSQL_plugin **) find_rendezvous_variable( plugin_name );
      reserveBreakpoints();
      *var_ptr = &plugin_funcs;
    }
    

    plpgsql.h:

    typedef struct
    {
      /* Function pointers set up by the plugin */
      void (*func_setup) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
      void (*func_beg) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
      void (*func_end) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
      void (*stmt_beg) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt);
      void (*stmt_end) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt);
    
      /* Function pointers set by PL/pgSQL itself */
      void (*error_callback) (void *arg);
      void (*assign_expr) (PLpgSQL_execstate *estate, PLpgSQL_datum *target,
       PLpgSQL_expr *expr );
    } PLpgSQL_plugin;
    

    dbg_startup() でブレークポイントを確認して PLpgSQL_execstate * estate に関数ごとにプラグインの固有情報を書きこみます。

    plugin_debugger.c:

    static void dbg_startup( PLpgSQL_execstate * estate, PLpgSQL_function * func )
    {
      if( !breakpointsForFunction( funcGetOid( func )) &&
       !per_session_ctx.step_into_next_func )
      {
        estate->plugin_info = NULL;
        return;
      }
      // estate->plugin_info 構造体が、dbg_newstmt() に渡ります
      initialize_plugin_info(estate, func);
    }
    

    その固有情報にもとづいて、dbg_newstmt() が行レベルデバッグを実現します。下記、attach_to_proxy() でデバッガセッションとの接続が確立されます (何かファイアウォールでこの接続が阻害されたりしてた? 確信無い)。

    static void dbg_newstmt( PLpgSQL_execstate * estate, PLpgSQL_stmt * stmt )
    {
      // ブレークポイントが無ければ即リターン
      if( frame->plugin_info == NULL )
        return;
      else
      {
        Breakpoint * breakpoint = NULL;
        // 該当行であればデバッグモードに入ります
        if(( dbg_info->stepping ) ||
         breakAtThisLine(
          &breakpoint,
          &breakpointScope,
          funcGetOid( dbg_info->func ),
          isFirstStmt( stmt, dbg_info->func ) ? -1 : stmt->lineno ))
        {
          dbg_info->stepping = TRUE;
        }
        else
          return;
        attach_to_proxy( breakpoint );
        // 以下、デバッガセッションとターゲットセッションのやりとり開始
      }
      return;
    }
    



    ソース斜め読み: java.util.regex

    java.util.regex の概要をおさえるために書いたメモです。あまり記事の体をなしていなくて申し訳ありません。

    関連項目:

    Pattern, Matcher 共に、パッケージ java.util.regex に属します。

    Pattern のインスタンス化は、static ファクトリメソッド java.util.regex.Pattern#compile() (public static Pattern compile(String regex)) が行なうので、Pattern のコンストラクタは private です。

    Matcher のインスタンス化は、ファクトリメソッド java.util.regex.Pattern#matcher() (public Matcher matcher(CharSequence input)) が行なうので、java.util.regex.Matcher のコンストラクタはパッケージ private です。

    NFA によるバックトラックでマッチを行ないます。DFA は作りません。NFA ならばバックトラックなので、たとえば選択 (“|”) の各項目で条件に重複があっても構わずバックトラックすれば良いのですが、DFA ではそうは行かないはずです (DFA では、特定の入力に対して、状態遷移先は 1 つに決まらなければならないので)。grep(1) などは DFA を作っているようですね、今度見てみます。

    “static class Pattern.Node” がなすツリーは、いわゆる Composite パターンで、Node.matcher() は、どのノードでも呼べます。正規表現の文字クラスを表す “private static abstract class Pattern.CharProperty extends Node” では CharProperty.isSatisfiedBy(int ch) がそうです。

    なお、「普通の」内部クラスは、「インスタンスの」内部クラス。「クラスの」内部クラスにする (static メソッドから使えるようにする) には、”static” をつける必要があります。”Node” クラスは static ファクトリでインスタンス化されますので、static である必要がありました。C++ にはローカルクラスなど無かったので、新鮮です。

    デバッグ出力が見づらくてかなわないです。こんな具合に書き換えれば良い?:

        /**
         * Used to print out a subtree of the Pattern to help with debugging.
         */
        private static String INDENT = "  ";
        private static void printObjectTree(Node node, String indent) {
            while(node != null) {
                if (node instanceof Prolog) {
                    System.out.println(indent + "**** start contents prolog loop");
                    System.out.println(indent + node);
                    printObjectTree(((Prolog)node).loop, indent + INDENT);
                    System.out.println(indent + "**** end contents prolog loop");
                } else if (node instanceof Loop) {
                    System.out.println(indent + "**** start contents Loop body");
                    System.out.println(indent + node);
                    printObjectTree(((Loop)node).body, indent + INDENT);
                    System.out.println(indent + "**** end contents Loop body");
                } else if (node instanceof Curly) {
                    System.out.println(indent + "**** start contents Curly body");
                    System.out.println(indent + node);
                    printObjectTree(((Curly)node).atom, indent + INDENT);
                    System.out.println(indent + "**** end contents Curly body");
                } else if (node instanceof GroupCurly) {
                    System.out.println(indent +
                     "**** start contents GroupCurly body" );
                    System.out.println(indent + node);
                    printObjectTree(((GroupCurly)node).atom, indent + INDENT);
                    System.out.println(indent +
                     "**** end contents GroupCurly body" );
                } else if (node instanceof GroupTail) {
                    System.out.println(indent + node);
                    System.out.println(indent + "Tail next is "+node.next);
                    return;
                } else if (node instanceof Branch) {
                    System.out.println(indent + "**** start contents Branch body");
                    for (Node atom: ((Branch) node).atoms) {
                        printObjectTree(atom, indent + INDENT);
                    }
                    System.out.println(indent + "**** end contents Branch body");
                } else {
                    System.out.println(indent + node);
                }
                node = node.next;
                if (node != null)
                    System.out.println(indent + "->next:");
                if (node == Pattern.accept) {
                    System.out.println(indent + "Accept Node");
                    node = null;
                }
           }
        }
    
        private static void printObjectTree(Node node) {
            printObjectTree(node, "");
        }
    

    “a+|b+” で、バララとこんな感じに出ました。だいぶ分かりやすくなった:

    **** start contents Branch body
      **** start contents Curly body
      com.ayutaya.java.util.regex.Pattern$Curly@276af2
        com.ayutaya.java.util.regex.Pattern$Single@1de3f2d
        ->next:
        Accept Node
      **** end contents Curly body
      ->next:
      com.ayutaya.java.util.regex.Pattern$BranchConn@5d173
      ->next:
      com.ayutaya.java.util.regex.Pattern$LastNode@1f9dc36
      ->next:
      Accept Node
      **** start contents Curly body
      com.ayutaya.java.util.regex.Pattern$Curly@e86da0
        com.ayutaya.java.util.regex.Pattern$Single@1754ad2
        ->next:
        Accept Node
      **** end contents Curly body
      ->next:
      com.ayutaya.java.util.regex.Pattern$BranchConn@5d173
      ->next:
      com.ayutaya.java.util.regex.Pattern$LastNode@1f9dc36
      ->next:
      Accept Node
    **** end contents Branch body
    ->next:
    Accept Node
    

    では。




    ブート時の jar はどこに記述が?

    JDK-1.5 のドキュメントの「クラスの検索方法」) を見ると、以下のようにあります:

    ブートストラップクラスは、Java 2 プラットフォームを実装しているクラスです。ブートストラップクラスは、jre/lib ディレクトリの rt.jar と他のいくつかの JAR ファイルに格納されています。これらのアーカイブは、システムプロパティ sun.boot.class.path に格納されているブートストラップクラスパスの値によって指定されます。このシステムプロパティは参照専用なので、直接修正しないでください。

    ブートストラップクラスパスの再定義が必要になることはほとんどありません。まれに、別のコアクラスのセットを使用する必要が生じた場合には、非標準のオプション -Xbootclasspath を使ってブートストラップクラスパスを再定義することができます。

    すると、初期化時に勝手に読まれるというブートストラップのアーカイブ名のリストというのは、一体どこに記述されているのでしょうか? 起動側のラッパ? JRE のランチャ? それとも JVM の中?

    以下は、CentOS-5.5 収録の OpenJDK-1.6.0 での話になります。

    とりあえずは、システムプロパティを表示してもらいます (参考: 「Javaのシステムプロパティをすべて表示するJavaコード – いろいろ解析日記」。Properties.list() を使うと、value の出力が省略されてしまう…)。

    import java.util.Properties;
    
    class SystemProps {
        public static void main(String[] args) {
            Properties properties = System.getProperties();
            for (Object key: properties.keySet()) {
                System.out.println(key + ": " + properties.get(key));
            }
        }
    }
    

    関係ありそうなのは、”sun.boot.class.path” だけです。以下に見られるように、明示的には何も指定しなくても、どこかで値が設定されています。

    $ java SystemProps
    java.runtime.name: OpenJDK Runtime Environment
    sun.boot.library.path: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386
    java.vm.version: 14.0-b16
    java.vm.vendor: Sun Microsystems Inc.
    java.vendor.url: http://java.sun.com/
    path.separator: :
    java.vm.name: OpenJDK Client VM
    file.encoding.pkg: sun.io
    sun.java.launcher: SUN_STANDARD
    user.country: JP
    sun.os.patch.level: unknown
    java.vm.specification.name: Java Virtual Machine Specification
    user.dir: /home/knaka/src/java
    java.runtime.version: 1.6.0_0-b16
    java.awt.graphicsenv: sun.awt.X11GraphicsEnvironment
    java.endorsed.dirs: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/endorsed
    os.arch: i386
    java.io.tmpdir: /tmp
    line.separator:
    
    java.vm.specification.vendor: Sun Microsystems Inc.
    os.name: Linux
    sun.jnu.encoding: UTF-8
    java.library.path:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/../lib/i386:
     /usr/java/packages/lib/i386:/lib:/usr/lib
    java.specification.name: Java Platform API Specification
    java.class.version: 50.0
    sun.management.compiler: HotSpot Client Compiler
    os.version: 2.6.18-194.8.1.el5
    user.home: /home/knaka
    user.zoneinfo.dir: /usr/share/javazi
    user.timezone:
    java.awt.printerjob: sun.print.PSPrinterJob
    file.encoding: UTF-8
    java.specification.version: 1.6
    java.class.path: .
    user.name: knaka
    java.vm.specification.version: 1.0
    java.home: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre
    sun.arch.data.model: 32
    user.language: ja
    java.specification.vendor: Sun Microsystems Inc.
    java.vm.info: mixed mode
    java.version: 1.6.0_0
    java.ext.dirs:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/ext:/usr/java/packages/lib/ext
    sun.boot.class.path:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/resources.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/rt.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/sunrsasign.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/jsse.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/jce.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/charsets.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/classes
    java.vendor: Sun Microsystems Inc.
    file.separator: /
    java.vendor.url.bug: http://java.sun.com/cgi-bin/bugreport.cgi
    sun.io.unicode.encoding: UnicodeLittle
    sun.cpu.endian: little
    sun.cpu.isalist:
    $
    

    “rpm -ql” から “xargs grep” しても無いようなので、ラッパや設定ファイルに記述されているわけではなさそうです。続いて、以下のようにランチャのデバッグ出力を見ても、それらしい記述はありません。

    $ _JAVA_LAUNCHER_DEBUG= java HelloWorld
    ----_JAVA_LAUNCHER_DEBUG----
    Command line Args:
            argv[0] = 'java'
            argv[1] = 'HelloWorld'
    JRE path is /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre
    jvm.cfg[0] = ->-client<-
        name: -client  vmType: VM_IF_SERVER_CLASS  server_class: -server
    jvm.cfg[1] = ->-server<-
    jvm.cfg[2] = ->-hotspot<-
        name: -hotspot  vmType: VM_ALIASED_TO  alias: -client
    jvm.cfg[3] = ->-classic<-
    jvm.cfg[4] = ->-native<-
    jvm.cfg[5] = ->-green<-
    jvm.cfg[6] = ->-cacao<-
    jvm.cfg[7] = ->-zero<-
    1 micro seconds to parse jvm.cfg
    pages: 258766  page_size: 4096  physical memory: 1059905536 (0.987GB)
    linux_i386_ServerClassMachine: false
    Default VM: client
    Does `/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client/libjvm.so' exist ... yes.
    ----_JAVA_LAUNCHER_DEBUG----
    Command line Args:
            argv[0] = 'java'
            argv[1] = 'HelloWorld'
    JRE path is /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre
    jvm.cfg[0] = ->-client<-
        name: -client  vmType: VM_IF_SERVER_CLASS  server_class: -server
    jvm.cfg[1] = ->-server<-
    jvm.cfg[2] = ->-hotspot<-
        name: -hotspot  vmType: VM_ALIASED_TO  alias: -client
    jvm.cfg[3] = ->-classic<-
    jvm.cfg[4] = ->-native<-
    jvm.cfg[5] = ->-green<-
    jvm.cfg[6] = ->-cacao<-
    jvm.cfg[7] = ->-zero<-
    1 micro seconds to parse jvm.cfg
    pages: 258766  page_size: 4096  physical memory: 1059905536 (0.987GB)
    linux_i386_ServerClassMachine: false
    Default VM: client
    Does `/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client/libjvm.so' exist ... yes.
    JVM path is /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client/libjvm.so
    1 micro seconds to LoadJavaVM
    JavaVM args:
        version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 4
        option[ 0] = '-Djava.class.path=.'
        option[ 1] = '-Dsun.java.command=HelloWorld'
        option[ 2] = '-Dsun.java.launcher=SUN_STANDARD'
        option[ 3] = '-Dsun.java.launcher.pid=9020'
    1 micro seconds to InitializeJVM
    Main-Class is 'HelloWorld'
    Apps' argc is 0
    1 micro seconds to load main class
    ----_JAVA_LAUNCHER_DEBUG----
    Hello, World!
    $
    

    bootclasspath オプションに存在しないパッケージを渡すとコケるので、明示的に指定されなかったら、JVM の中で、デフォルトのパッケージを読むのだと思われます。

    $ java -Xbootclasspath:/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/foo.jar \
     HelloWorld
    Error occurred during initialization of VM
    java/lang/NoClassDefFoundError: java/lang/Object
    $ java -Xbootclasspath:/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/rt.jar \
     HelloWorld
    Hello, World!
    $
    

    しかたがないので grep し倒してそれらしいところを探すと、どうやら os::set_boot_path() がそれです。スタックトレースをとってみます。

    $ gdb /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/bin/java
    (中略)
    (gdb) b os::set_boot_path
    Can't find member of namespace, class, struct, or union named "os::set_boot_path"
    Hint: try 'os::set_boot_path or 'os::set_boot_path
    (Note leading single quote.)
    Make breakpoint pending on future shared library load? (y or [n]) y
    Breakpoint 1 (os::set_boot_path) pending.
    (gdb) run HelloWorld
    Starting program: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/bin/java \
     HelloWorld
    (中略)
    [Switching to Thread 0xb7fefb90 (LWP 9549)]
    
    Breakpoint 1, os::set_boot_path (fileSep=47 '/', pathSep=58 ':')
        at /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/os.cpp:866
    866         const char* home = Arguments::get_java_home();
    (gdb) bt
    #0  os::set_boot_path (fileSep=47 '/', pathSep=58 ':') at \
     /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/os.cpp:866
    #1  0x00fbae77 in os::init_system_properties_values () at \
     /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/os/linux/vm/os_linux.cpp:332
    #2  0x00cf996f in Arguments::init_system_properties () at \
     /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/arguments.cpp:153
    #3  0x01058cb8 in Threads::create_vm (args=0xb7fef3a4, canTryAgain=0xb7fef30b) at \
     /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/thread.cpp:2820
    #4  0x00eba5f3 in JNI_CreateJavaVM (vm=0xb7fef3b8, penv=0xb7fef3b4, args=0xb7fef3a4) at \
     /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/prims/jni.cpp:3263
    #5  0x0804a088 in InitializeJVM (_args=0xbfffe078) at \
     ../../../../src/share/bin/java.c:1296
    #6  JavaMain (_args=0xbfffe078) at ../../../../src/share/bin/java.c:431
    #7  0x00b09832 in start_thread () from /lib/libpthread.so.0
    #8  0x001e1e0e in clone () from /lib/libc.so.6
    (gdb)
    

    C で書かれた「ランチャ」 (エントリポイントは jdk/src/share/bin/java.c) から走るスレッドで、JNI_CreateJavaVM() (libjvm.so 内の関数。この先は C++) の中から呼ばれています。

    os.cpp を見ると、以下のようなリストでハードコードされています。ホルダ “%” が、システムプロパティ “java.home” に置換されるのでしょう。

    bool os::set_boot_path(char fileSep, char pathSep) {
        ...
        static const char classpath_format[] =
            "%/lib/resources.jar:"
            "%/lib/rt.jar:"
            "%/lib/sunrsasign.jar:"
            "%/lib/jsse.jar:"
            "%/lib/jce.jar:"
            "%/lib/charsets.jar:"
            "%/classes";
        char* sysclasspath = \
         format_boot_path(classpath_format, home, home_len, fileSep, pathSep);
        if (sysclasspath == NULL) return false;
        Arguments::set_sysclasspath(sysclasspath);
    
        return true;
    }
    

    だいぶ構造が見えてきました。では。




    Java 標準ライブラリをハックする

    Java で、標準ライブラリのコピーをソースから作り、好きにイジる手順です。どなたか、他にもっと効率的な方法をご存知でしたら教えていただけるとありがたいです (その意味では Python は、自己責任において、アクセス指定関係なくクラス内部を触り放題なので、好もしい)。

    まずは、こんなソースを用意します (注: コンパイルできません):

    import java.util.regex.*;
    
    class RegExpDebug  {
        public static void main(String args[]) {
            Pattern pattern = Pattern.compile(args[0]);
            pattern.printObjectTree(pattern.matchRoot);
            Matcher matcher = pattern.matcher(args[1]);
            while (matcher.find()) {
                System.out.println("Matched: " + matcher.group());
            }
        }
    }
    

    引数に渡された文字列で正規表現マッチングを行なうだけの、簡単な Java のコードです。Pattern.compile() に渡された正規表現が内部でどのような構造になっているかを表示する Pattern.printObjectTree() を呼びたいのですが、パッケージプライベートのデバッグ用なので、こちらからは呼べません。当然、コンパイルできません:

    $ CLASSPATH=./ javac RegExpDebug.java
    RegExpDebug.java:6: java.util.regex.Pattern の matchRoot は public ではありません。
     パッケージ外からはアクセスできません。
            pattern.printObjectTree(pattern.matchRoot);
                                           ^
    RegExpDebug.java:6: printObjectTree(java.util.regex.Pattern.Node) は
     java.util.regex.Pattern で private アクセスされます。
            pattern.printObjectTree(pattern.matchRoot);
                   ^
    エラー 2 個
    $
    

    仕方が無いので、パッケージ名に適当なプレフィクス (ここでは “com.ayutaya”) をつけて、新しいパッケージとしてコピーを作り、コンパイル終了の際に Pattern.printObjectTree() を呼ぶようにライブラリ側を修正します。新しい呼び出し側は、以下のような感じです:

    import com.ayutaya.java.util.regex.*;
    
    class RegExpDebug  {
        pubelic static void main(String args[]) {
            Pattern pattern = Pattern.compile(args[0]);
            Matcher matcher = pattern.matcher(args[1]);
            while (matcher.find()) {
                System.out.println("Matched: " + matcher.group());
            }
        }
    }
    

    当然、まだパッケージがないのでコンパイルはコケます:

    $ CLASSPATH=./ javac RegExpDebug.java
    RegExpDebug.java:1: パッケージ com.ayutaya.java.util.regex は存在しません。
    (中略)
    $
    

    パッケージのコピーを作成します。下記の例は、CentOS-5.5 上でのものですので、適宜読み替えてください。まずは、ライブラリのソースを入手します。Red Hat 系の OpenJDK ですと、java-*-openjdk-src に入っていると思います。

    $ sudo yum install -y java-1.6.0-openjdk-src
    (中略)
    $ rpm -ql java-1.6.0-openjdk-src
    /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/src.zip
    /usr/share/doc/java-1.6.0-openjdk-src-1.6.0.0
    /usr/share/doc/java-1.6.0-openjdk-src-1.6.0.0/README.src
    $
    

    コピーを作ってからパッケージ名を変更し、修正・コンパイルします。

    $ mkdir -p com/ayutaya/
    $ pushd com/ayutaya/
    ~/src/java/regexp/com/ayutaya ~/src/java/regexp
    $ unzip /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/src.zip "java/util/regex/*"
    Archive:  /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/src.zip
       creating: java/util/regex/
      inflating: java/util/regex/ASCII.java
      inflating: java/util/regex/Matcher.java
      inflating: java/util/regex/MatchResult.java
      inflating: java/util/regex/Pattern.java
      inflating: java/util/regex/PatternSyntaxException.java
    $ cd java/util/regex/
    $ perl -p -i \
     -e 's/package java.util.regex;/package com.ayutaya.java.util.regex;/' *.java
    $ cp Pattern.java Pattern.java.orig
    $ vi Pattern.java
    $ diff -uNr Pattern.java.orig Pattern.java
    --- Pattern.java.orig   2010-09-12 00:31:46.000000000 +0900
    +++ Pattern.java        2010-09-12 00:32:29.000000000 +0900
    @@ -1503,6 +1503,7 @@
             groupNodes = null;
             patternLength = 0;
             compiled = true;
    +        printObjectTree(matchRoot);
         }
    
         /**
    $ javac *.java
    (中略)
    $ popd
    ~/src/java/regexp
    $
    

    今度は無事コンパイルが通り、実行すると、内部構造がダンプされるようになります。

    $ CLASSPATH=./ javac RegExpDebug.java
    $ CLASSPATH=./ java RegExpDebug "a+" "xxxaaaxxx"
    com.ayutaya.java.util.regex.Pattern$Curly@fa3ac1
    com.ayutaya.java.util.regex.Pattern$Single@276af2
    ->next:
    Accept Node
    **** end contents Curly body
    ->next:
    com.ayutaya.java.util.regex.Pattern$LastNode@1de3f2d
    ->next:
    Accept Node
    Matched: aaa
    $
    

    後は、やり放題です。では。




    正規表現で最長一致(する|しない)言語

    先日、Java の java.util.regex.Pattern のソースを読みながら、twitter で以下のようにつぶやきました:

    正規表現の大原則は最左最長一致だと思ってたんですが、Javaの”|”(Branch)のmatch()がatoms間の比較をしていない。どう見ても「最初一致」です。えー、これってOKなの?

    すると、@buri17 からツッコミが:

    @knaka perlでも正規表現の選択演算子は短絡評価だというのを昔どこかで読んだ気がするけど、ちがいましたかー。

    えっ? マジですか? 調べてみました。

    最長一致するもの

    grep:

    $ echo "XaabbX" | grep -o -E "a+|[ab]+"
    aabb
    $ echo "XaabbX" | grep -o -E "[ab]+|a+"
    aabb
    

    sed:

    $ echo "XaabbX" | sed -r -e 's/a+|[ab]+/foo/'
    XfooX
    $ echo "XaabbX" | sed -r -e 's/[ab]+|a+/foo/'
    XfooX
    

    gawk:

    $ echo "XaabbX" | gawk '{ sub("a+|[ab]+", "foo"); print }'
    XfooX
    $ echo "XaabbX" | gawk '{ sub("[ab]+|a+", "foo"); print }'
    XfooX
    

    ちなみに、秀丸エディタの正規表現検索でも最長一致します。正規表現ならば ED(1) だろって? すまん、使ったこともない。

    最長一致しないもの

    Perl:

    $ echo "XaabbX" | perl -n -e 's/a+|[ab]+/foo/; print'
    XfoobbX
    $ echo "XaabbX" | perl -n -e 's/[ab]+|a+/foo/; print'
    XfooX
    

    Python:

    $ python -c 'import re; print re.compile("a+|[ab]+").search("XaabbX").group(0)'
    aa
    $ python -c 'import re; print re.compile("[ab]+|a+").search("XaabbX").group(0)'
    aabb
    

    Ruby:

    $ ruby -e 'puts /a+|[ab]+/.match("XaabbX")'
    aa
    $ ruby -e 'puts /[ab]+|a+/.match("XaabbX")'
    aabb
    

    PHP:

    $ php -r 'preg_match("/a+|[ab]+/", "XaabbX", $m); echo $m[0] . "\n";'
    aa
    $ php -r 'preg_match("/[ab]+|a+/", "XaabbX", $m); echo $m[0] . "\n";'
    aabb
    

    grep で、Perl 正規表現オプションつき (PCRE?):

    $ echo "XaabbX" | grep -o --perl-regexp "a+|[ab]+"
    aa
    bb
    $ echo "XaabbX" | grep -o --perl-regexp "[ab]+|a+"
    aabb
    

    ついでに、Emacs の検索も最長一致しません。

    まとめると

    当たり前だと思っていた正規表現の最左最長一致を、新しいめのツール・言語では行なっていないことが分かります。たしかに最長一致を探そうとすると、もし選択の中でマッチが見つかっても、最長のマッチを探すために他の全ての項も試さなければなりませんから、実装的にも実行時にも高コストなのは分かりますが、気をつけねばなりませんな。

    古いめのツールでは行なうということは、これって POSIX の正規表現の規格にも反映されていたりするのか? と思って、PHP でも古い方の関数で試してみると、下記の通り、こちらは最長一致するんですよね。参考: 「PHP: POSIX 正規表現関数 – Manual

    PHP の POSIX 正規表現関数 (PHP >= 5.3 非推奨):

    $ php -r 'ereg("a+|[ab]+", "XaabbX", $m); echo $m[0] . "\n";'
    aabb
    $ php -r 'ereg("[ab]+|a+", "XaabbX", $m); echo $m[0] . "\n";'
    aabb
    

    そこで “POSIX 最長一致” でググると、こんな資料が → 「PHPの正規表現と
    最長一致, hanawa (a.k.a. id:hnw), y at hnw dot jp, 第29回PHP勉強会発表資料
    」 (PDF)。よく「最長マッチ」と誤訳される「Greedy-matching (繰り返しで最長まで食ってしまうマッチ)」と「Longest-matching (最長マッチ)」は別物よ、ということで、参考になりました。

    では。




    Ruby Mix-in で菱形継承

    Ruby はご存知のとおり、is-a 関係で継承を行なう「普通の」継承と、付加機能としてインターフェイスと実装を引き継ぐためのミックスイン継承とを備えています。通常継承では単一継承しか許していない Ruby ですが、通常の継承とミックスインとの多重継承、ミックスインとミックスインとの多重継承はできます。オーバーライドした際の同一シグネチャメソッドの優先順位がどうなっているのか気になったので、やってみました。

    予想としては、当然通常継承の方が優先度が高いでしょうし、ミックスイン間であれば、”include” などというくらいですから、当然後から include した方が上書きしそうです。実際そうでした。やるまでもなかったかも知れません。

    #!/usr/bin/ruby
    # -*- coding: utf-8 -*-
    
    module BaseMixin
      def initialize
        print "BaseMixin initialized\n"
      end
    end
    
    module DerivedMixin1
      include BaseMixin
      def initialize
        super
        print "DerivedMixin1 initialized\n"
      end
      def foo
        print "foo@DerivedMixin1 called\n"
      end
      def bar
        print "bar@DerivedMixin1 called\n"
      end
    end
    
    module DerivedMixin2
      include BaseMixin
      def initialize
        super
        print "DerivedMixin2 initialized\n"
      end
      def foo
        print "foo@DerivedMixin2 called\n"
      end
      def bar
        print "bar@DerivedMixin2 called\n"
      end
    end
    
    class Base
      include DerivedMixin1
      include DerivedMixin2
      def initialize
        super
        print "Base initialized\n"
      end
    end
    
    class Derived < Base
      include DerivedMixin2
      include DerivedMixin1
      def initialize
        super
        print "Derived initialized\n"
      end
      def bar
        print "bar@Derived called\n"
      end
    end
    
    o = Derived.new
    o.foo
    o.bar
    

    実行結果:

    $ ./diamond.rb
    BaseMixin initialized
    DerivedMixin1 initialized
    DerivedMixin2 initialized
    Base initialized
    Derived initialized
    foo@DerivedMixin2 called
    bar@Derived called
    

    Mix-in のコンストラクタは include した順(登録された順?)に実行され、その後で親クラスのコンストラクタが走ります。基底と派生とで include していたら、基底で include された方が優先で、派生の方での include は無視。これも妥当と思われる。

    名前かぶりは、後 include の Mix-in 優先、基底クラスがより優先、は予想通り。

    型も動的なので、菱形継承した基底の “BaseMixin” のインスタンスは一つしかありませんのね、C++ で言うところの “virtual public” 継承。初期化パラメータが両経路で異なったりすると混乱しそうなので、Mix-in の基底には複雑なことはやらせないようにしようと思います。Python も確かこんなだったような気がします。今度やってみます。

    では。




    ワイルドカード関数を書く

    こんなお題が出たことがあります:

    • ワイルドカードによるマッチングを行なう関数を書きなさい
    • 引数には、ワイルドカードパターンとマッチ対象文字列をとり、成否を返します
    • ワイルドカードのルールは以下のとおり:
      • “?” は、任意の 1 文字にマッチします
      • “*” は、0 文字以上の文字列にマッチします
      • それ以外の文字は、その文字自身にマッチします

    まず単純に 1 パスのループで回しながらマッチングをすることを考えて、すぐに “*” の処理ができないことに気づいて行きづまりました。そして、一足とびに、正規表現マッチングのコードを思い出してしまったために、頭はすっかり非決定性有限オートマトンから決定性有限オートマトンを生成する方法ってどうやるんだったっけな、と明後日の方向へ行ってしまいました。

    けれどもよく考えたら、この程度のワイルドカードならばパーサは必要ないわけです。一般化すれば、「少なくとも一つの解を見つければ OK」という典型的な問題なんですよね。

    そんなわけで、ちょっと書いてみました。キモは、1 つの “*” が、対象文字列の 0 文字から残り全部の文字までを食った場合のそれぞれごとに再帰してバックトラック、というのに気づけるかどうか。

    #include <stdio.h>
    
    const int Success = 1;
    const int Failure = 0;
    
    int
    match (
      const char * pattern,
      const char * haystack ) {
        while ((* pattern) || (* haystack)) {
            // 先にどちらか尽きたら失敗
            if ((! (* pattern)) || (! (* haystack))) {
                return (Failure);
            }
            // 今回の "*" が 0 文字から残り全ての文字を食い尽くした場合ごと
            //  に再帰
            if ((* pattern) == '*') {
                for (int i = 0; (* (haystack + i)); ++ i) {
                    if (match(pattern + 1, haystack + i) == Success) {
                        return (Success);
                    }
                }
                return (Failure);
            // 1 文字ずつのマッチは簡単
            } else if (
             ((* pattern) == (* haystack)) ||
             ((* pattern) == '?') ) {
                pattern ++;
                haystack ++;
            // 1 文字単位で不一致ならば失敗
            } else {
                return (Failure);
            }
        }
        // 共に尽きたら成功
        return (Success);
    }
    
    int
    main (
      int argc,
      char * * argv) {
        if ((match(argv[1], argv[2])) == Failure) {
            printf("Not matched\n");
            return (1);
        }
        printf("Matched\n");
        return (0);
    }
    

    精進せねば。