2009年9月 8日 (火)

SQL クエリのスタイル、もしくは、 なぜ SQL クエリは読みにくいのか

しばらくの間、 SQL で書かれた、様々な集計クエリを読んでいたのですが。どうも、クエリが読みにくくていけない、と思いまして、いまさらながらですが、 SQL クエリの書き方について考えました。

SQL クエリの読みにくさの原因というのは、いくつかあるのですが、一番大きいのは、 FROM 句と、 WHERE 句が離れてしまうことにあると思います。クエリを作るときには、テーブル/リレーションごとに条件を考えて、クエリを組み立てていくと思います。これを、 昔ながらのスタイルで SQL に書くと、 FROM 句にリレーションが並び、 WHERE 句にリレーションの条件が並ぶ、という、リレーションと条件とが分離した形になります。

クエリの読み手がクエリを読む際には、 クエリを作るときの手順を逆に行なうことになります。 WHERE 句に並んでいる条件が、 FROM 句のどのリレーションにかかっているのか、 FROM 句のリレーションごとに、 WHERE 句の条件を並べなおして、クエリからどのような結果が出力されるかを読み取ろうとするわけです。

このようなクエリは、例えば、以下のようなものになります 1

select
  a.A_ID,
  trim(a.A_LNAME) || ',' ||
    trim(a.A_MNAME) || ',' ||
    trim(a.A_FNAME) as a_name,
  ol.OL_QTY
from
  CUSTOMER c,
  ADDRESS ad,
  COUNTRY co,
  ORDERS o,
  ORDER_LINE ol,
  ITEM i,
  AUTHOR a
where
  ad.ADDR_ID = c.C_ADDR_ID and
  co.CO_ID = ad.ADDR_CO_ID and
  co.CO_NAME = 'Japan' and
  o.O_C_ID = c.C_ID and
  o.O_STATUS = 'SHIPPED' and
  ol.OL_O_ID = o.O_ID and
  i.I_ID = ol.OL_I_ID and
  i.I_PUB_DATE >= date '2007-08-01' and
  i.I_PUB_DATE < date '2009-09-01' and
  a.A_ID = i.I_A_ID and
  c.C_DISCOUNT >= 0.1
order by A_ID

このように、いちいち、 WHERE 句と、 FROM 句を頭の中で照合するのは、リレーションの数、条件の数が増えてきますと、やっておれなくなってきます。このため、別途、メモを作成して、リレーションごとに条件をまとめてみたりするわけですが、 無用な手間、という感じなんですよね。

そこで、私がこうしたクエリを書くときにやるのが、 WHERE 句にある条件を、すべて JOIN 句に書く、というやり方です。 さらに、 FROM 句の一番目のテーブルの条件だけは、 WHERE 句に残ってしまうため、あえて、サブクエリにします。

こうしますと、条件がリレーションごとにまとまりますので、かなり読みやすくなるのではないかと思います。

select
  a.A_ID,
  trim(a.A_LNAME) || ',' ||
    trim(a.A_MNAME) || ',' ||
    trim(a.A_FNAME) as a_name,
  ol.OL_QTY
from (
  select C_ID, C_ADDR_ID
  from CUSTOMER
  where C_DISCOUNT >= 0.1) c
inner join ADDRESS ad on
  ad.ADDR_ID = c.C_ADDR_ID
inner join COUNTRY co on
  co.CO_ID = ad.ADDR_CO_ID and
  co.CO_NAME = 'Japan'
inner join ORDERS o on
  o.O_C_ID = c.C_ID and
  o.O_STATUS = 'SHIPPED'
inner join ORDER_LINE ol on
  ol.OL_O_ID = o.O_ID
inner join ITEM i on
  i.I_ID = ol.OL_I_ID and
  i.I_PUB_DATE >= date '2007-08-01' and
  i.I_PUB_DATE < date '2009-09-01'
inner join AUTHOR a on
  a.A_ID = i.I_A_ID
order by A_ID

というわけで、 私は、 WHERE 句ではなく、 JOIN 句に条件を書く、というスタイルを推奨してみたいわけです。


1. データベース・スキーマは、 OSDL DBT-1 より。

| | コメント (0) | トラックバック (0)

2009年5月31日 (日)

計算機プログラムの詳細仕様書とは

どのようなものか知りたい人は、 自分の PC に、 TeX システム一式をインストールしてから、 tex.web に対して、以下のコマンドを実行すると良いと思います。

>weave tex.web
>tex tex.tex

これで、 tex.dvi というファイルが出来ます。 Donald Knuth 氏 による、tex プログラムの詳細仕様書です。

| | コメント (0) | トラックバック (0)

2009年5月10日 (日)

X = X + 1 ? 意味わからん

と、人生で初めてコンピュータ・プログラムというものをみたときに思いました。言語は、 MSX-BASIC でした。

しばらくプログラムを眺めているうちに、右辺の X が、”古い X ”で、左辺の X は、”新しい X ”を意味している、X は X でも、違う X を意味しているらしい、ということに気付いたのですが。

X = X + 1 というのは、一文で read-modify-write の 3 つの操作を表現していますから、コンピュータをよく知らない人間には、理解するのは難しい文なのかもしれませんね。さらに、等号というなじみのある記号を、「記憶域への代入」という、別の意味で使われている、というのも理解の妨げになるでしょう。

このことを思い出したきっかけは、「Java 並行処理プログラミング」にある、以下のサンプルコード。

public class Holder {
  private int n;

  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n != n) {
      throw new AssertionError("This statement is false.");
    }
  }
}

assertSanity メソッドを呼び出すと、 AssertionError を投げることがある、というものですが、一瞬、意味がわかりませんでした。

| | コメント (0) | トラックバック (0)

2009年4月26日 (日)

xyzzy から sqlite を使う

一応できたわけですが。

(setq test-home "\path\to\test")
=>"\path\to\test"
(setq test-db (merge-pathnames "test.db" test-home))
=>"\path\to\test\test.db"
(sqlite3 test-db "select date('now')")
date('now') = 2009-04-26
=>0
(sqlite3 test-db "create table tbl1(one varchar(10), two smallint);")
=>0
(sqlite3 test-db "insert into tbl1 values('hello!',10);")
=>0
(sqlite3 test-db "insert into tbl1 values('goodbye', 20);")
=>0
(sqlite3 test-db "select * from tbl1;")
one = hello!
two = 10
one = goodbye
two = 20
=>0

色々と危険なコードになってしまいました。良い子はまねしちゃいけませんね。

やっぱり、 C 言語でヘルパーを作るべきか。

(require "foreign")

(c:*define-c-type c:u_int c-pointer)

(c:*define-dll-entry
  (c:void *) memcpy ((c:void *) (c:void *) c:u_int)
  "msvcrt.dll" "memcpy")

(c:*define-dll-entry
  c:int sqlite3-open ((char *) ((c:void *) *))
  "sqlite3.dll" "sqlite3_open")

(c:*define-dll-entry
  (char *) sqlite3-errmsg ((c:void *))
  "sqlite3.dll" "sqlite3_errmsg")

(c:*define-dll-entry
  c:int sqlite3-close ((c:void *))
  "sqlite3.dll" "sqlite3_close")

;; int sqlite3_exec(
;;   sqlite3*,                                  /* An open database */
;;   const char *sql,                           /* SQL to be evaluated */
;;   int (*callback)(void*,int,char**,char**),  /* Callback function */
;;   void *,                                    /* 1st argument to callback */
;;   char **errmsg                              /* Error msg written here */
;; );
(c:*define-dll-entry
  c:int sqlite3-exec
  ((c:void *) (char *) (c:void *) (c:void *) ((c:void *) *))
  "sqlite3.dll" "sqlite3_exec")

(c:*define-dll-entry
  c:void sqlite3-free ((c:void *))
  "sqlite3.dll" "sqlite3_free")

(defconstant c-pointer-size 4)
(defconstant msg-size 256)

(defun make-c-pointer ()
  (si:make-chunk nil c-pointer-size))

(defun unpack-c-pointer (pp)
  (si:unpack-uint32 pp 0))

(defun get-c-argv (arg-pos argv)
  (let* ((v** (make-c-pointer))
         (rc (memcpy v**
                     (+ argv (* arg-pos c-pointer-size))
                     c-pointer-size)))
    (let* ((v* (si:make-chunk nil msg-size))
           (rc (memcpy v* (unpack-c-pointer v**) msg-size))
           (v (si:unpack-string v* 0 msg-size)))
      v
      )))

(c:*defun-c-callable
  c:int callback
  (((c:void *) NotUsed)
   (c:int argc)
   (((char *) *) argv)
   (((char *) *) azColName))
  (let ((rc 0))
    (handler-case
        (do ((i 0 (+ i 1)))
            ((>= i argc))
          (let ((colname (get-c-argv i azColName))
                (colv (get-c-argv i argv)))
            (format t "~A = ~A~%" colname colv)))
      (error (c)
        (format t "~A~%" (si:*condition-string c))))
    rc))

(defun sqlite3 (fname sql)
  (let* ((fname* (si:make-string-chunk fname))
         (db* (make-c-pointer))
         (rc (sqlite3-open fname* db*))
         (db (unpack-c-pointer db*)))
    (if (not (eql rc 0))
        (let* ((errmsg** (sqlite3-errmsg db))
               (errmsg* (si:make-chunk nil msg-size))
               (rc (memcpy errmsg* errmsg** msg-size))
               (errmsg (si:unpack-string errmsg* 0 msg-size)))
          (format t "Can't open database: ~A~%" errmsg)
          (let ((rc (sqlite3-close db)))
            rc))
      (let* ((sql* (si:make-string-chunk sql))
             (errmsg** (make-c-pointer))
             (rc (sqlite3-exec db sql* #'callback 0 errmsg**)))
        (if (not (eql rc 0))
            (progn
              (format t "Error: ~A~%" rc)
              (let* ((errmsg* (si:make-chunk nil msg-size))
                     (rc (memcpy errmsg* (unpack-c-pointer errmsg**) msg-size))
                     (errmsg (si:unpack-string errmsg* 0 msg-size)))
                (format t "~A~%" errmsg))
              (sqlite3-free (unpack-c-pointer errmsg**))
              (let ((rc (sqlite3-close db)))
                rc))
          (let ((rc (sqlite3-close db)))
            rc)
          )))))

| | コメント (0) | トラックバック (0)

2009年4月18日 (土)

Git はク○?

現実問題として、 Windows でも使えないと駄目ですよね。

Gitを作った人ってバカなの? - L'eclat des jours (2009-04-18)

とはいえ、 Subversion のマージのヘボさは、ブランチという概念を無意味にしている、というのも当たっていると思います。経験上、同一ファイルが競合したときには、ほとんどが失敗しますので。そのたび、 diff とって patch とかやってると、一体自分は何をやっているんだろう、という気分になってくる。

ということで。

DSAS開発者の部屋:Bazaarの紹介

私は、ここで紹介されている Bazaar も使っていたりします。 Windows と Linux 両方に入れてます。

Bazaar User Guide: 8.2 bzr-svn

ここにあるように、 trunk を Subversion にして、 Bazaar でローカルにブランチ作ってます。マージは、いまのところ失敗したことがない。結構快適。

Launchpad という、 GitHub のようなものもあるらしい。

Launchpad can host your project's source code using the Bazaar version control system.

Launchpad Code: Code in branches

Git のサポートもやろうとしているらしい。

BzrForeignBranches/Git - Bazaar Version Control

インストールは、まあカンタンでした。 Windows はインストーラで、 Linux は TarBall から。 Python を要求されるのを愛嬌と思えるかどうかですかね。 後、 Windows 版には TortoiseBazaar というのが付いてきますが、イマイチ使えないのでパスするのも愛嬌。

Tortoise 系は、 Windows エクスプローラにアドインされるのをメリットと考えるか、デメリットと考えるか微妙なところがありますね。使いやすいのは確かなので、 TortoiseSVN など、他の人には使うように薦めている一方、自分ではあまり使ってなかったりする。 Windows がなんか重くなるし、ファイル・ディレクトリがロックされたりするのがイヤだから。

| | コメント (0) | トラックバック (0)

CLAZY であるべきか

元は SICP です。

CLAZY: Lazy Calling in Common Lisp

お試しのソースコード:

(defmacro delay (expr) `(lambda () ,expr))
(defun force (thunk) (funcall thunk))
(defmacro cons-stream (head tail) `(cons ,head (delay ,tail)))
(defun head (lazy-stream) (car lazy-stream))
(defun tail (lazy-stream) (force (cdr lazy-stream)))

(defun make-plus-stream (x y)
  (cons-stream
   x
   (make-plus-stream (+ x y) y)))

(defun take-and-prn (stm b)
  (defun iter (s i)
    (if (>= i b) 'done
    (progn
      (format t "~A~%" (head s))
      (iter (tail s) (+ i 1)))))
  (iter stm 0))

こんな風に無限で遊べます:

(setq stm (make-plus-stream 1 1))
=>(1 . #<lexical-closure: (anonymous)>)
(take-and-prn stm 5)
1
2
3
4
5
=>done
(setq stm (make-plus-stream 1 2))
=>(1 . #<lexical-closure: (anonymous)>)
(take-and-prn stm 5)
1
3
5
7
9
=>done

| | コメント (0) | トラックバック (0)

そうか、xyzzy.ini があったか

これで色の設定がとれますね。

(require "foreign")
(require "wip/winapi")
(c:define-dll-entry
  winapi:DWORD GetPrivateProfileString
  (winapi:LPCSTR
   winapi:LPCSTR
   winapi:LPCSTR
   winapi:LPSTR
   winapi:DWORD
   winapi:LPCSTR)
  "kernel32" "GetPrivateProfileStringA")

(defun read-ini-file (ini-f sect key default)
  (let ((csize 256))
    (let ((ini-f* (si:make-string-chunk ini-f))
          (sect* (si:make-string-chunk sect))
          (key* (si:make-string-chunk key))
          (default* (si:make-string-chunk default))
          (ret-v* (si:make-chunk nil csize)))
    (let ((ret-api-v
           (GetPrivateProfileString
            sect* key* default* ret-v* csize ini-f*)))
      (si:unpack-string ret-v* 0 ret-api-v)))))

(defun get-user-config-file-path ()
  (let ((ini-f (si:getenv "XYZZYINIFILE"))
        (ini-f-def (merge-pathnames "xyzzy.ini" (user-config-path))))
    (if (not ini-f)
        ini-f-def
      (if (not (file-exist-p ini-f))
          (if (pathname-device ini-f)
              ini-f-def
            (progn
              (setq ini-f (merge-pathnames ini-f (user-config-path)))
              (if (file-exist-p ini-f)
                  ini-f
                ini-f-def)))
        ini-f)
      )))

(defun test-read-user-config ()
  (let ((ini-f (get-user-config-file-path)))
    (read-ini-file ini-f "Colors" "kwdColor1" "")))

実行結果:

(test-read-user-config)
=>"#ff0000"

| | コメント (0) | トラックバック (0)

2009年4月13日 (月)

命令的な Lisp

xyzzy で htmlize.el 的なものが欲しいと思いまして。

ソースコードを漁っていたのですけど、テキストの色を lisp から取得する方法はなさそうでした。それで、トークンの種類を取ることはできるようなので、これを使って色を付ければ良いかと考えました。

トークンの種類をとるコードを、試しに書いてみたわけですが。

色々と、 xyzzy lisp のソースコード読んでいるうちに、これは命令的に書く方が良いのかな、と思い始めまして。再帰の代わりにループと破壊的代入を使って、命令的 lisp で書いてみることにしました。

書いてみて、こういうコードは、なんか見ていてホッとするなあ、と思ったり。長年の習慣というのは、すっかり染み付いてますね。

(defun test-token (b)
  (interactive "bBuffer:")
  (with-open-stream (os (get-buffer-stream-o *foobar-output-buffer-name*))
    (set-buffer b)
    (save-excursion
      (let ((token nil)
            (s "")
            (pos 0))
        (while (< pos (point-max))
          (multiple-value-setq (token s pos) (get-token pos))
          (format os "~A ~A ~A~%" token s pos)
          )
        ))))

(defun get-buffer-stream-o (bname)
  (let ((b (get-buffer-create bname)))
    (erase-buffer b)
    (make-buffer-stream b)))

(defun get-token (pos)
  (let ((c (char-after pos)))
    (cond ((syntax-string-p c)
           (let ((s (string c))
                 (syn :string))
             (setq pos (+ pos 1))
             (setq c (char-after pos))
             (while
                 (and (< pos (point-max))
                      (eq syn :string))
               (setq s (concat s (string c)))
               (setq pos (+ pos 1))
               (setq c (char-after pos))
               (setq syn (parse-point-syntax pos)))
             (values 'string s pos)))
          ((or (syntax-word-p c)
               (syntax-symbol-p c))
           (let ((s ""))
             (while
                 (and (< pos (point-max))
                      (or (syntax-word-p c)
                          (syntax-symbol-p c)))
               (setq s (concat s (string c)))
               (setq pos (+ pos 1))
               (setq c (char-after pos)))
             (values 'word s pos)))
          (t (values 'other (string c) (+ pos 1))))
    ))

| | コメント (0) | トラックバック (0)

2009年4月 9日 (木)

Float 遊び

ソースコード:

/*
   float_play1.c

   see:
   http://msdn.microsoft.com/ja-jp/library/c9676k6h.aspx
*/

#include <stdio.h>
#include <float.h>

void print_fvalue(float v) {
  /*
    IEEE754 単精度
    ビット数: 符号 1 , 指数 8, 仮数 23
    仮数 先頭 1 ビットは省略される。
    指数 下駄(bias)は 127。
  */
  float *pfv = &v;
  unsigned int *piv = (unsigned int*)pfv;
  printf_s("[0] %x\n", *piv);
  printf_s("[1] %x\n", *piv >> 31);
  printf_s("[2] %d\n", (*piv << 1 >> 24) - 127);
  printf_s("[3] %x\n", *piv << 9 >> 8);
}

float get_test_fvalue() {
  float v;
  v = 0.1f;
  v = v * 10.0f;
  v = v / 10.0f;
  return v;
}

int test(unsigned int newControl,
         unsigned int mask) {
  unsigned int currentControl;
  errno_t err;
  float v;

  err = _controlfp_s(&currentControl, newControl, mask);
  if (err != 0) {
    printf_s("_controlfp_s failed!\n");
    return err;
  } else {
    v = get_test_fvalue();
    print_fvalue(v);
    if (v == 0.1f)
      printf_s("No problem?\n");
    else
      printf_s("Ouch!\n");
    return 0;
  }
}

int main() {
  int err;

  err = test(_RC_CHOP, _MCW_RC);
  if (err != 0) {
    return err;
  }
  err = test(_RC_UP, _MCW_RC);
  if (err != 0) {
    return err;
  }
  err = test(_RC_DOWN, _MCW_RC);
  if (err != 0) {
    return err;
  }
  err = test(_RC_NEAR, _MCW_RC);
  if (err != 0) {
    return err;
  }
  return 0;
}

ビルド:

>cl float_play1.c
Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

float_play1.c
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:float_play1.exe
float_play1.obj

実行:

>float_play1.exe
[0] 3dcccccc
[1] 0
[2] -4
[3] 999998
Ouch!
[0] 3dcccccf
[1] 0
[2] -4
[3] 99999e
Ouch!
[0] 3dcccccc
[1] 0
[2] -4
[3] 999998
Ouch!
[0] 3dcccccd
[1] 0
[2] -4
[3] 99999a
No problem?

アセンブリ リスト より:


 fld DWORD PTR _v$[ebp]
 fcomp QWORD PTR __real@3fb99999a0000000
 fnstsw ax
 test ah, 68     ; 00000044H
 jp SHORT $LN2@test

つまり:

/* ただそれだけのこと。 */

参考:

浮動小数点演算ではまった話 - bkブログ

| | コメント (0) | トラックバック (0)

2009年4月 7日 (火)

Packrat Parsing を調べてみる (その2)

遅延評価だから、一部が自分自身によって構築されるデータを、関数に渡しても無問題である、ということですかね。かつ、必要なデータは、その都度、芋づる式に評価されて構築されていくと。

例でいえば、最初に評価されるもの、”トリガー”になるのは、 先頭の dvAdditive でしょう。

つまり:

calc :: String -> Int
calc s = g $ parse s where
  g d = case dvAdditive d of
    Parsed v d' -> v

などとして:

*Main> calc "2*(3+4)"
14

これで、 parse 関数の、 add = pAdditive d の評価が開始します。

parse s = d
d = Derivs add mult prim dec chr
add = pAdditive d

pAdditive の中に入って、 dvMultitive d の評価が開始します。

add = pAdditive d
dvMultitive d

dvMultitive d は、 mult = pMultitive d ですから、 pMultitive の中にはいって、 dvPrimary d の評価が開始します。

add = pAdditive d
dvMultitive d
dvPrimary d

同様に、 dvDecimal d の評価が開始します。

add = pAdditive d
dvMultitive d
dvPrimary d
dvDecimal d

ここまでで、 dvChar d が 2文字分評価されて:

dvChar d = Parsed '2' d'
dvChar d' = Parsed '*' d''
dvDecimal d = Parsed 2 d'
dvPrimary d = Parsed 2 d'

pMultitive まで戻って、 ”2 *” までが読み込まれ、 再び pPrimary へ:

pMultitive d
dvPrimary d'' = pPrimary d''

ここで、 dvChar d'' が評価されて、 pAdditive へ:

dvChar d'' = Parsed '(' d'''
dvAdditive d''' = pAdditive d'''

再び pAdditive からの評価を通って:

dvChar d''' = Parsed '3' d''''
dvChar d'''' = Parsed '+' d'''''
dvDecimal d''' = Parsed 3 d''''
dvPrimary d''' = Parsed 3 d''''
dvMultitive d''' = Parsed 3 d''''
dvChar d''''' = Parsed '4' d''''''
dvDecimal d''''' = Parsed 4 d''''''
dvPrimary d''''' = Parsed 4 d''''''
dvMultitive d''''' = Parsed 4 d''''''
dvAdditive d''' = Parsed 7 d''''''

pPrimary まで戻って:

dvChar d'''''' = Parsed ')' d'''''''
dvPrimary d'' = Parsed 7 d'''''''

pMultitive まで戻って:

dvMultitive d = Parsed 14 d'''''''

最初の pAdditive まで戻る:

dvAdditive d = Parsed 14 d'''''''

なるほど〜。巧妙ですねえ。

| | コメント (0) | トラックバック (0)

2009年4月 4日 (土)

Packrat Parsing を調べてみる

このペーパーです。

Bryan Ford Packrat Parsing: Simple, Powerful, Lazy, Linear Time (PDF). ICFP, October 2002.

Haskell の実装例が載っているのですが、これがよくわからない。

parse :: String -> Derivs
parse s = d where
  d = Derivs add mult prim dec chr
  add = pAdditive d
  mult = pMultitive d
  prim = pPrimary d
  dec = pDecimal d
  chr = case s of
    (c:s') -> Parsed c (parse s')
    [] -> NoParse

なんじゃこりゃ?

どう評価されるんだろ? d のところの動きが謎ですね。

ペーパーには、以下のコードが載っています:

data Derivs = Derivs {
  dvAdditive :: Result Int,
  dvMultitive :: Result Int,
  dvPrimary :: Result Int,
  dvDecimal :: Result Int,
  dvChar :: Result Char}

data Result v = Parsed v Derivs | NoParse

pAdditive :: Derivs -> Result Int
pMultitive :: Derivs -> Result Int
pPrimary :: Derivs -> Result Int
pDecimal :: Derivs -> Result Int

pAdditive d = alt1 where
  alt1 = case dvMultitive d of
    Parsed vleft d' ->
      case dvChar d' of
        Parsed '+' d'' ->
          case dvAdditive d'' of
            Parsed vright d''' ->
              Parsed (vleft + vright) d'''
            _ -> alt2
        _ -> alt2
    _ -> alt2

  alt2 = dvMultitive d

この続きを書いてみる。

pMultitive d = alt1 where
  alt1 = case dvPrimary d of
    Parsed vleft d' ->
      case dvChar d' of
        Parsed '*' d'' ->
          case dvPrimary d'' of
            Parsed vright d''' ->
              Parsed (vleft * vright) d'''
            _ -> alt2
        _ -> alt2
    _ -> alt2

  alt2 = dvPrimary d

pPrimary d = alt1 where
  alt1 = case dvChar d of
    Parsed '(' d' ->
      case dvAdditive d' of
        Parsed v d'' ->
          case dvChar d'' of
            Parsed ')' d''' ->
              Parsed v d'''
            _ -> alt2
        _ -> alt2
    _ -> alt2

  alt2 = dvDecimal d

pDecimal d = pDi where
  pDi = case pDs "" d of
    Parsed "" d' -> NoParse
    Parsed v d' -> Parsed (read v) d'
    _ -> NoParse
  pDs cs d = case dvChar d of
    Parsed c d' ->
      if isD c
      then pDs (cs ++ [c]) d'
      else Parsed cs d
    _ -> Parsed cs d
  isD c = c `elem` "0123456789"

とりあえず。

>ghci
   ___         ___ _
  / _ \ /\  /\/ __(_)
 / /_\// /_/ / /  | |      GHC Interactive, version 6.4.2, for Haskell 98.
/ /_\\/ __  / /___| |      http://www.haskell.org/ghc/
\____/\/ /_/\____/|_|      Type :? for help.

Loading package base-1.0 ... linking ... done.
Prelude> :load packrat1.hs
Compiling Main             ( packrat1.hs, interpreted )
Ok, modules loaded: Main.
*Main> parse "2*(3+4)"

Top level:
    No instance for (Show Derivs)
      arising from use of `print' at Top level
    Probable fix: add an instance declaration for (Show Derivs)
    In a 'do' expression: print it

Show の instance がないと表示できません、か。

Haskell Hierarchical Libraries > Prelude > Converting to String (haskell.org)

を参考にして、 Show の instance を書きます。

instance (Show a) => Show (Result a) where
  showsPrec prec (Parsed v d) =
    showParen (prec > app_prec) $
      showString "Parsed " .
      showsPrec (app_prec + 1) v .
      showsPrec (app_prec + 1) d
    where app_prec = 10
  showsPrec prec NoParse =
    showParen (prec > app_prec) $
      showString "NoParse"
    where app_prec = 10

instance Show Derivs where
  showsPrec prec (Derivs add mult prim dec chr) =
    showParen (prec > app_prec) $
      showString "Derivs " .
      showsPrec (app_prec + 1) add .
      showsPrec (app_prec + 1) mult .
      showsPrec (app_prec + 1) prim .
      showsPrec (app_prec + 1) dec .
      showsPrec (app_prec + 1) chr
    where app_prec = 10

こんなんで良いのでしょうか。

*Main> parse "2*(3+4)"
Derivs (Parsed 14(Derivs (NoParse)(NoParse)(NoParse)(NoParse)(NoParse)))(Parsed
14(Derivs (NoParse)(NoParse)(NoParse)(NoParse)(NoParse)))(Parsed 2(Derivs (NoPar
se)(NoParse)(NoParse)(NoParse)(Parsed '*'(Derivs (Parsed 7(Derivs (NoParse)(NoPa
rse)(NoParse)(NoParse)(NoParse)))(Parsed 7(Derivs (NoParse)(NoParse)(NoParse)(No
... まだまだいっぱい。

うへえ。括弧地獄。

でも、一応 14 って答えが出てますね。

| | コメント (0) | トラックバック (0)

2009年3月29日 (日)

HTML + CSS で数式組版 (その16)

分数が組めるようになりました。

数式組版サンプル

といっても、水平方向の文字位置を調整する機能が入っていませんので、 2 次方程式の解の公式のように、中央寄せが必要な場合も、左寄せになってしまいます。

この組み方ですと、 CSS の text-align 属性が効かないみたいなのですよね。 もちろん、 div の align 属性も駄目。 とすると、 padding で中央に持っていくくらいしかないのですかねえ。

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版 (その15)

CSS の出力部分を作成します。

(defun gen-css-props (root ast)
  (let ((hs (get-elem-heights ast)))
    (gen-css-props1 root ast hs)))

(defun gen-css-props1 (root ast hs)
  (let ((mh (max-velem-n-height hs)))
    (gen-css-props2 root ast nil hs mh)))

(defun gen-css-props2 (root ast rest? hs mh)
  (cond ((atom ast) nil)
        ((atom (car ast))
         (let ((c1 (car ast))
               (c2 (cadr ast))
               (c3 (caddr ast)))
           (cond ((or (eq c1 'text) (eq c1 'argend)) nil)
                 ((eq c1 'frac)
                  (cons
                   (list (make-css-name root c1 c2)
                         (list 'float "left")
                         (list 'position "relative")
                         (list
                          'top
                          (format nil "~Aem"
                                  (* (- mh (cadr (caddr hs))) 1.1))))
                   (gen-frac-props root c3 hs)))
                 (t (cons
                     (list (make-css-name root c1 c2))
                     (gen-css-props1 root c3 hs))))))
        ((atom (caar ast))
         (let ((c1 (caar ast))
               (c2 (cadar ast)))
           (if (and (eq c1 'text) (not rest?))
               (cons
                (list (make-css-name root "b" c2)
                      (list 'padding-left "0.2em")
                      (list 'padding-right "0.2em")
                      (list 'float "left")
                      (list 'position "relative")
                      (list 'top (format nil "~Aem" (* mh 0.8))))
                (gen-css-props2 root (cdr ast) t (cdr hs) mh))
             (append (gen-css-props2 root (car ast) nil (car hs) mh)
                     (gen-css-props2 root (cdr ast) nil (cdr hs) mh)))))))

(defun gen-frac-props (root ast hs)
  (defun frac-arg (arg h pfn)
    (let ((c1 (car arg))
          (c2 (cadr arg))
          (c3 (caddr arg)))
      (cons
       (append
        (list (make-css-name root c1 c2)
              (list 'clear "left"))
        (funcall pfn))
       (gen-css-props1 root c3 h))))
  (let ((num (car ast))
        (den (cadr ast)))
    (append
     (frac-arg
      num
      (caddr (caddr hs))
      (lambda () nil))
     (frac-arg
      den
      (caddr (cadddr hs))
      (lambda ()
        (list (list 'border-top "solid 1pt")))))))

(defun output-css-classes (classes stm)
  (mapc
   (lambda (class)
     (let ((cn (car class))
           (ps (cdr class)))
       (format stm "~A {~%" cn)
       (mapc
        (lambda (p)
          (let ((n (car p))
                (v (cadr p)))
            (format stm "~A: ~A;~%" n v)))
        ps)
       (format stm "}~%")))
   classes))

(defun css-props-string (props)
  (with-output-to-string (out)
    (format out "~%")
    (output-css-classes props out)))

(defun math2html (str)
  (let ((ast (parse-string str 0)))
    (let ((elem
           `(html
             ((head
               ((style
                 (type "text/css")
                 (text
                  ,(css-props-string
                    (gen-css-props "math" ast))))))
              (body
               ((div (class "math")
                     ,(gen-html-elem ast))))))
           )
          (b (get-buffer-create *buffer-name*)))
      (erase-buffer b)
      (with-open-stream (stm (make-buffer-stream b))
        (output-html elem stm))
      b)))

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版 (その14)

式中の分数の最大高さを求めます。

(defun max-velem-height (elems)
  (defun velem-height (e)
    (let ((c1 (car e))
          (c2 (cadr e)))
      (if (eq c1 'v) c2 0)))
  (defun iter (es i)
    (cond ((eq es nil) i)
          ((listp (car es))
           (iter (cdr es)
            (max (velem-height (car es)) i)))
          (t (iter (cdr es) i))))
  (iter elems 0))

このように:

(max-velem-height
 (get-elem-heights
  (parse-string
   "x = \\frac{a}{b} + c"
   0)))
=>2
(max-velem-height
 (get-elem-heights
  (parse-string
   "\\frac{1}{1 + \\frac{1}{2}} + a + \\frac{\\frac{1}{2} + 1}{1 + \\frac{1}{2}}"
   0)))
=>4

式中の分数の分子の最大高さを求めます。

(defun max-velem-n-height (elems)
  (defun velem-n-height (e)
    (let ((c1 (car e))
          (c2 (cadr e)))
      (if (eq c1 'v)
          (let ((c3 (caddr e)))
            (cadr c3))
        0)))
  (defun iter (es i)
    (cond ((eq es nil) i)
          ((listp (car es))
           (iter (cdr es)
            (max (velem-n-height (car es)) i)))
          (t (iter (cdr es) i))))
  (iter elems 0))

このように:

(max-velem-n-height
 (get-elem-heights
  (parse-string
   "x = \\frac{a}{b} + c"
   0)))
=>1
(max-velem-n-height
 (get-elem-heights
  (parse-string
   "\\frac{1}{1 + \\frac{1}{2}} + a + \\frac{\\frac{1}{2} + 1}{1 + \\frac{1}{2}}"
   0)))
=>2

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版 (その13)

各要素の高さを求めます。

(defun text-height (str)
  (if (<= (length str) 1)
      (char-columns (schar str 0))
    (reduce
     (lambda (x y)
       (cond ((and (characterp x) (characterp y))
              (max (char-columns x) (char-columns y)))
             ((and (numberp x) (characterp y))
              (max x (char-columns y)))))
     str)))

(defun helem-height (h)
  (cond
   ((eq h nil) 0)
   ((numberp h) h)
   (t (cadr h))))

(defun get-helem-heights (hs)
  (let ((h (car hs)))
    (cond ((symbolp h) (cadr hs))
          ((<= (length hs) 1)
           (helem-height (car hs)))
          (t
           (reduce
            (lambda (x y)
              (max (helem-height x)
                   (helem-height y)))
            hs)))))

(defun get-elem-heights (elem)
  (if elem
      (cond
       ((atom (car elem))
        (let ((c1 (car elem))
              (c2 (cadr elem))
              (c3 (caddr elem)))
          (cond ((eq c1 'frac)
                 (let ((num (get-elem-heights (car c3)))
                       (den (get-elem-heights (cadr c3))))
                   (list 'v
                         (+ (get-helem-heights num)
                            (get-helem-heights den))
                         num den)))
                ((eq c1 'text)
                 (text-height c3))
                ((eq c1 'argbegin)
                 (let ((hs
                        (cons
                         (get-elem-heights (car c3))
                         (get-elem-heights (cdr c3)))))
                   (list 'h
                         (get-helem-heights hs)
                         hs)))
                (t 0))))
       (t (cons (get-elem-heights (car elem))
                (get-elem-heights (cdr elem)))))))

こうなります:

(get-elem-heights
 (parse-string "\\frac{\\frac{1}{2} + 1}{1 + \\frac{1}{2}}" 0))
((v 4
    (h 2 ((v 2 (h 1 (1 0)) (h 1 (1 0))) 1 1 0))
    (h 2 (1 1 (v 2 (h 1 (1 0)) (h 1 (1 0))) 0))))

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版 (その12)

垂直方向の位置を決める問題。

というのは、例えば以下のような式があったとしまして:

x = \frac{a}{b} + c

分数の前後にある文字の位置を、分母と分子の間まで下げたいわけです。

もっと、嫌らしい例を挙げますと:

\frac{1}{1 + \frac{1}{2}} + a + \frac{\frac{1}{2} + 1}{1 + \frac{1}{2}}

まず、真ん中の a をどれだけ下げればよいか、というのは、その後ろの分数の分子の高さで決まります。この分数は、分子にさらに分数が入っていますので、その高さはその分数の、 分子の高さ + 分母の高さ ということになります。

次に、先頭の分数を下げる量を考えます。この分数の分子には、分数が入っていませんので、後ろの分数の方が高さが大きくなり、その差は、後ろの分数の分子の分数の分子の高さ、ということになります。よって、先頭の分数は、これだけを下げることとなります。

この例からわかることをまとめますと:

  1. 分数でない要素を下げる量は、式中の分数の分子の高さの最大値
  2. 分数である要素を下げる量は、 1. より、その分数の分子の高さを引いた値

最後に、分数の分母分子の中に分数がある場合には、その分母分子の中についても同様に位置を下げる必要があります。

さて、そうすると、やらなければならない処理は:

  1. 分数の分母分子の高さを求める
  2. 分子の最大の高さを求める
  3. 分数でない要素を下げる量を 2. とする
  4. 分数である要素を下げる量を 2. から分子の差とする

と、いうことになりますかね。

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版 (その11)

parse-string 関数の誤りの訂正。 キーワードを見つけたときにも、下降しないと駄目でした。横着な実装は、やはり使えないみたいです。

(defun parse-string (str se)
  (if (> (length str) 0)
      (let ((m (match-token str)))
        (let ((mb (car m))
              (me (cadr m))
              (spec (caddr m)))
        (if mb
            (let ((se2 (+ se me)))
              (cond
               ((eq spec 'key) ; 下降させる。
                (let ((keyargs
                       (parse-keyargs
                        (substring str (+ mb 1) me)
                        (substring str me) se2)))
                  (let ((se3 (parse-end keyargs)))
                    (cons keyargs
                          (parse-string
                           (substring str (- se3 se))
                           se3)))))
               ((eq spec 'argbegin)
                ; parse-args -> parse-arg
                (let ((arg (parse-arg (substring str me) se2)))
                  (let ((se3 (parse-end arg)))
                    (cons arg
                          (parse-string
                           (substring str (+ (- se3 se2) 1))
                           se3)))))
               ((eq spec 'argend)
                (cons (list spec se2 (substring str mb me)) nil))
               ((eq spec 'text)
                (cons (list spec se2 (substring str mb me))
                      (parse-string (substring str me) se2)))))
          (parse-string (substring str 1) (+ se 1)))))))

(defun parse-keyargs (kw str se)
  (let ((sym (find-keyword kw *keywords*)))
    (if (eq sym nil)
        (error (format nil "keyword ~A not found." kw))
      (list sym se
            (parse-args str se)))))

(defun parse-args (str se)
  (if (> (length str) 0)
      (let ((m (match-token str)))
        (let ((mb (car m))
              (me (cadr m))
              (spec (caddr m)))
        (if mb
            (let ((se2 (+ se me)))
              (cond
               ((eq spec 'argbegin)
                (let ((arg (parse-arg (substring str me) se2)))
                  (let ((se3 (parse-end arg)))
                    (cons arg
                          (parse-args
                           (substring str (+ (- se3 se2) 1))
                           se3)))))
               (t nil))))))))

(defun parse-arg (str se)
  (let ((succ (parse-string str se)))
    (if (eq succ nil)
        (list 'argbegin se (list (+ se 1) nil))
      (list 'argbegin se succ))))

| | コメント (0) | トラックバック (0)

2009年3月22日 (日)

HTML + CSS で数式組版 (その10)

再度 HTML 出力に戻ります。 CSS に手を付けて気付きましたが、 span 要素が div 要素に入っていないのはまずい。位置指定ができなくなってしまいます。

(defun gen-html-elem1 (ast rest?)
  (cond ((atom ast) nil)
        ((and (atom (car ast)) (listp (caddr ast)))
         (list 'div
               (list 'class
                     (format nil "~A~A" (car ast) (cadr ast)))
               (gen-html-elem1 (caddr ast) nil)))
        ((and (atom (caar ast)) (eq (caar ast) 'argend))
         (gen-html-elem1 (cdr ast) nil))
        ((and (atom (caar ast)) (eq (caar ast) 'text))
         (if rest?
             (gen-html-elem1 (cdr ast) rest?)
           (cons
            (list 'div
                  (list 'class
                        (format nil "b~A" (cadar ast)))
                  (gen-html-elem2 ast))
            (gen-html-elem1 (cdr ast) t))))
        (t
         (cons (gen-html-elem1 (car ast) nil)
               (gen-html-elem1 (cdr ast) nil)))))

(defun gen-html-elem2 (ast)
  (cond ((atom ast) nil)
        ((atom (car ast))
         (cond ((eq (car ast) 'text)
                (list 'span
                      (list 'class
                            (format nil "~A~A" (car ast) (cadr ast)))
                      (list 'text (caddr ast))))
               (t nil)))
        ((and (atom (caar ast)) (eq (caar ast) 'text))
         (cons (gen-html-elem2 (car ast))
               (gen-html-elem2 (cdr ast))))
        (t nil)))

(defun gen-html-elem (ast)
  (gen-html-elem1 ast nil))

ということで、 span 要素は全部 div のなかに入れました。

(gen-html-elem (parse-string "x = \\frac{a}{b} + c" 0))
=>
((div (class "b1") ((span (class "text1") (text "x")) (span (class "text3") (text "="))))
 (div (class "frac9")
      ((div (class "argbegin10")
            ((div (class "b11") ((span (class "text11") (text "a"))))))
       (div (class "argbegin13")
            ((div (class "b14") ((span (class "text14") (text "b"))))))))
 (div (class "b17") ((span (class "text17") (text "+")) (span (class "text19") (text "c")))))

math2html を実行しますと:

(math2html "x = \\frac{a}{b} + c")
=>#<buffer: *math2html*>

このように:

<html>
<body>
<div class="math">
<div class="b1">
<span class="text1">x</span>
<span class="text3">=</span>
</div>
<div class="frac9">
<div class="argbegin10">
<div class="b11">
<span class="text11">a</span>
</div>
</div>
<div class="argbegin13">
<div class="b14">
<span class="text14">b</span>
</div>
</div>
</div>
<div class="b17">
<span class="text17">+</span>
<span class="text19">c</span>
</div>
</div>
</body>
</html>

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版 (その9)

CSS の出力にとりかかります。これが、このプログラムの一番のキモになる部分ですね。

(defun gen-css-props (root ast)
  (cond ((atom ast) nil)
        ((atom (car ast))
         (let ((c1 (car ast))
               (c2 (cadr ast))
               (c3 (caddr ast)))
           (cond ((eq c1 'argend) nil)
                 ((eq c1 'frac)
                  (cons
                   (list (make-css-name root c1 c2))
                   (gen-frac-props root c3)))
                 (t (cons
                     (list (make-css-name root c1 c2))
                     (gen-css-props root c3))))))
        (t (append (gen-css-props root (car ast))
                   (gen-css-props root (cdr ast))))))

(defun make-css-name (root c1 c2)
  (format nil ".~A .~A~A" root c1 c2))

(defun gen-frac-props (root ast)
  (append
   (gen-css-props root (car ast))
   (let ((c (cadr ast)))
     (let ((c1 (car c))
           (c2 (cadr c))
           (c3 (caddr c)))
       (cons
        (list
         (make-css-name root c1 c2)
         (list 'border-top "solid 1pt"))
        (gen-css-props root c3))))))

とりあえず、荒くスケッチを作ってみました。

(gen-css-props "math" (parse-string "x =\\frac{a}{b}+ c" 0))
=>
((".math .text1") (".math .text3") (".math .frac8")
 (".math .argbegin9") (".math .text10")
 (".math .argbegin12" (border-top "solid 1pt"))
 (".math .text13") (".math .text15") (".math .text17"))

| | コメント (0) | トラックバック (0)

2009年3月21日 (土)

HTML + CSS で数式組版 (その8)

HTML の出力です。

HTML を出力するだけならカンタン、と思いきや妙に手こずってしまいました。入出力は、関数プログラミングの鬼門かも。 Common Lisp は、モナドとかじゃなくて、普通に副作用で出力するのですけど。

変更箇所

(defun gen-html-elem (ast)
  (cond ((atom ast) nil)
        ((and (atom (car ast)) (atom (caddr ast)))
         (cond ((eq (car ast) 'text)
                (list 'span
                      (list 'class
                            (format nil "~A~A" (car ast) (cadr ast)))
                      (list 'text (caddr ast))))
               (t nil)))
        ((atom (car ast))
         (list 'div  ;; cons list -> list
               (list 'class
                     (format nil "~A~A" (car ast) (cadr ast)))
               (gen-html-elem (caddr ast))))
        ((and (atom (caadr ast)) (eq (caadr ast) 'argend)) ;; ignore argend.
         (cons (gen-html-elem (car ast))
               (gen-html-elem (cddr ast))))
        (t
         (cons (gen-html-elem (car ast))
               (gen-html-elem (cdr ast))))))

gen-html-elem 関数に、色々と忘れものをしていたので、変更を行いました。

追加箇所

(defun elem-childs (elem)
  (cond ((atom elem) nil)
        ((atom (car elem))
         (elem-childs (cdr elem)))
        ((atom (caar elem))
         (elem-childs (cdr elem)))
        (t (car elem))))

(defun output-attrs (attrs stm outfn)
  (cond ((atom attrs) nil)
        ((and (atom (caar attrs)) (atom (cadar attrs)))
         (let ((n (caar attrs))
               (v (cadar attrs)))
           (progn
             (funcall outfn n v stm)
             (output-attrs (cdr attrs) stm outfn))))
        (t (output-attrs (cdr attrs) stm outfn))))

(defun output-html (elem stm)
  (cond ((atom elem) nil)
        ((atom (car elem))
         (let ((c (car elem))
               (cs (cdr elem)))
           (format stm "<~A" c)
           (output-attrs
            cs stm
            (lambda (n v stm)
              (if (eq n 'text) nil
                (format stm " ~A=\"~A\"" n v))))
           (format stm ">~%")
           (output-attrs
            cs stm
            (lambda (n v stm)
              (if (eq n 'text)
                  (format stm "~A~%" v))))
           (output-html (elem-childs cs) stm)
           (format stm "</~A>~%" c)))
        (t (progn
             (output-html (car elem) stm)
             (output-html (cdr elem) stm)))))

(setf *buffer-name* "*math2html*")

(defun math2html (str)
  (let ((ast (parse-string str 0)))
    (let ((elem
           `(html ((body ((div (class "math")
                               ,(gen-html-elem ast))))))
           )
          (b (get-buffer-create *buffer-name*)))
      (erase-buffer b)
      (with-open-stream (stm (make-buffer-stream b))
        (output-html elem stm))
      b)))

最後の関数、 math2html を eval すると:

(math2html "x =\\frac{a}{b}+ c")
=>#<buffer: *math2html*>

バッファに HTML を出力します。長いので、 span エレメントの改行を消してます。実際には、エレメントごとに改行した状態で出力されます。

<html>
<body>
<div class="math">
<span class="text1">x</span>
<span class="text3">=</span>
<div class="frac8">
<div class="argbegin9">
<span class="text10">a</span>
</div>
<div class="argbegin12">
<span class="text13">b</span>
</div>
</div>
<span class="text15">+</span>
<span class="text17">c</span>
</div>
</body>
</html>

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版 (その7)

HTML エレメントに変換。

変更箇所

;(defun parse-string (str elems se)
(defun parse-string (str se)

重大なことに気付きました。 parse-string 関数の elems 引数は使っていません。ので、キャンセルということで。

たぶん、ここに破壊的代入で AST を構築するつもりだったのですね。で、戻り値で文字位置を戻すと。 命令的な書き方ですと、そういうインターフェイスになりそうです。

追加箇所

(defun gen-html-elem (ast)
  (cond ((atom ast) nil)
        ((and (atom (car ast)) (atom (caddr ast)))
         (cond ((eq (car ast) 'text)
                (list 'span
                      (list 'class
                            (format nil "~A~A" (car ast) (cadr ast)))
                      (list 'text (caddr ast))))
               (t nil)))
        ((atom (car ast))
         (cons (list 'div
                     (list 'class
                           (format nil "~A~A" (car ast) (cadr ast))))
               (gen-html-elem (caddr ast))))
        (t (cons (gen-html-elem (car ast)) (gen-html-elem (cdr ast))))))

gen-html-elem 関数は、 AST を受け取って、 HTML エレメントに変換します。

こんな感じ:

(gen-html-elem (parse-string "x =\\frac{a}{b}+ c" 0))
=>
((span (class "text1") (text "x")) (span (class "text3") (text "="))
 ((div (class "frac8"))
  ((div (class "argbegin9"))
   (span (class "text10") (text "a")) nil)
  ((div (class "argbegin12"))
   (span (class "text13") (text "b")) nil))
 (span (class "text15") (text "+")) (span (class "text17") (text "c")))

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版 (その6)

キーワード+引数部分の解析です。

変更箇所

(defun parse-string (str elems se)
  (if (> (length str) 0)
      (let ((m (match-token str)))
        (let ((mb (car m))
              (me (cadr m))
              (spec (caddr m)))
        (if mb
            (let ((se2 (+ se me)))
              (cond
               ((eq spec 'keyargs)
                (cons (parse-keyword (substring str mb me) se) ;; se2 -> se
                      (parse-string (substring str me) elems se2)))
               ((eq spec 'argbegin)
                (let ((args (parse-args (substring str me) se2)))
                  (let ((se3 (parse-end args)))
                    (cons args
                          (parse-string
                           (substring str (+ (- se3 se2) 1))
                           elems se3)))))
               ((eq spec 'argend)
                (cons (list spec se2 (substring str mb me)) nil))
               ((eq spec 'text)
                (cons (list spec se2 (substring str mb me))
                      (parse-string (substring str me) elems se2)))))
          (parse-string (substring str 1) elems (+ se 1)))))))

(defun parse-keyword (str se)
  (let ((mb (string-match *match-key* str))
        (me (match-end 0)))
    (let ((kw (match-string 1)))
      (let ((sym (find-keyword kw *keywords*)))
        (if (eq sym nil)
            (error (format nil "keyword ~A not found." kw))
          (let ((se2 (+ se me)))
            (list sym se2
                  (parse-string (substring str me) nil se2))))))))

これで、 parse-string 関数は一応完成と:

(parse-string "x = \\frac{a}{b} + c" nil 0)
=>
((text 1 "x") (text 3 "=")
 (frac
  9
  ((argbegin 10 ((text 11 "a") (argend 12 "}")))
   (argbegin 13 ((text 14 "b") (argend 15 "}")))))
 (text 17 "+") (text 19 "c"))

| | コメント (0) | トラックバック (0)

2009年3月20日 (金)

HTML + CSS で数式組版 (その5)

引数解析で、色々ハマりました^^)

変更箇所

(setf *match-argbegin* "{")
(setf *match-argend* "}")
(setf *match-text* "[^ \\{}]+")


(defun parse-string (str elems se)
  (if (> (length str) 0)
      (let ((m (match-token str)))
        (let ((mb (car m))
              (me (cadr m))
              (spec (caddr m)))
        (if mb
            (let ((se2 (+ se me)))
              (cond
               ((eq spec 'keyargs)
                (cons (parse-keyword (substring str mb me) se2)
                      (parse-string (substring str me) elems se2)))
               ((eq spec 'argbegin)
                (let ((args (parse-args (substring str me) se2)))
                  (let ((se3 (parse-end args)))
                    (cons args
                          (parse-string
                           (substring str (+ (- se3 se2) 1))
                           elems se3)))))
               ((eq spec 'argend)
                (cons (list spec se2 (substring str mb me)) nil))
               ((eq spec 'text)
                (cons (list spec se2 (substring str mb me))
                      (parse-string (substring str me) elems se2)))))
          (parse-string (substring str 1) elems (+ se 1)))))))

引数解析を行なう関係で、 parse-string 関数を色々と書き換えました。トークンの種類別に処理を行なう、というのと、解析対象の文字列をどこまで読んだか判るように、引数・戻り値にそれぞれ文字位置を追加しました。

追加箇所

(defun parse-end (ast)
  (cond ((atom ast) 0)
        ((atom (car ast)) (max (cadr ast) (parse-end (caddr ast))))
        (t (max (parse-end (car ast)) (parse-end (cdr ast))))))

(defun parse-args (str se)
  (let ((succ (parse-string str nil se)))
    (if (eq succ nil)
        (list 'argbegin se (list (+ se 1) nil))
      (list 'argbegin se succ))))

parse-end 関数は、解析結果を受け取って、文字列からどこまでトークンを読み込んだか、その最後の文字位置を返します。

ここは、 グローバル変数を使おうかと 悩みました。単に文字位置を取るためにリストを走査するっていうのがね。作り方間違ったかな? ま、とりあえずこうしておいて、後でまた考えるということで。

parse-args 関数は、 parse-string 関数が、引数のカッコを見つけたときに呼ばれる関数です。

こうなりました:

(parse-string "{a}{b}" nil 0)
=>
((argbegin
  1 ((text 2 "a") (argend 3 "}")))
 (argbegin
  4 ((text 5 "b") (argend 6 "}"))))

(parse-string "{a{b}}{c}" nil 0)
=>
((argbegin
  1 ((text 2 "a")
     (argbegin 3 ((text 4 "b") (argend 5 "}")))
     (argend 6 "}")))
 (argbegin 7 ((text 8 "c") (argend 9 "}"))))

(parse-string "{a{b{c}}}{d{e}}" nil 0)
=>
((argbegin
  1
  ((text 2 "a")
   (argbegin
    3
    ((text 4 "b")
     (argbegin 5 ((text 6 "c") (argend 7 "}")))
     (argend 8 "}")))
   (argend 9 "}")))
 (argbegin
  10
  ((text 11 "d")
   (argbegin 12 ((text 13 "e") (argend 14 "}")))
   (argend 15 "}"))))

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版 (その4)

変更箇所

(setf *match-keyargs* "\\\\[a-z]+\\({.+}\\)*")
(setf *match-text* "[^ \\]+")

(defun debug (msg)
  (let ((b (get-buffer-create "*debug*")))
    (let ((bp (buffer-size b)))
      (with-open-stream (stm (make-buffer-stream b bp))
        (format stm "~A~%" msg)))))

defvar を setf に変更しました。 defvar で一度値が束縛されてしまうと、再度評価しても値を変えられないことに、遅まきながら気付きまして。

match-text の仕様を変えようと思って、実際変えてみたら、再評価で値を変えられないということに気付きました。空白だけでなく、バックスラッシュでも、トークンを区切れるように変えてます。

debug 関数が追加されているのは、それで、ちょっとハマったから ^^)

追加箇所

キーワード解析部分の途中まで。

(setf *match-key* "\\\\\\([a-z]+\\)")
(setf *match-arg* "{\\(.+\\)}")
(setf *keywords*
      (list '("frac" frac)))

(defun find-keyword (str lst)
  (cond ((eq nil lst) nil)
        ((string= str (caar lst)) (cadar lst))
        (t (find-keyword str (cdr lst)))))

(defun parse-keyword (str)
  (let ((mb (string-match *match-key* str))
        (me (match-end 0)))
    (let ((kw (match-string 1)))
      (list
       (find-keyword kw *keywords*) (substring str me)))))

最後の関数 parse-keyword を eval すると、こうなります:

(parse-keyword "\\frac{a}{b}")
=>(frac "{a}{b}")

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版 (その 3 )

さて、プログラミングのお時間です。

開発環境

xyzzy の Common Lisp 処理系を使わせていただきます。理由は、私が愛用しているエディタだからです。以上。

入力

分数を組版することだけを考えることにします。数式は、以下のように入力します。何かに似ているような気がするかもしれませんが、気のせいです。

x = \frac{a}{b} + c

プログラム

まだ手をつけたところですが、何か長くなりそうなので、途中経過を載せてしまいます。あまり長くなると、ソースを載せるのが面倒になってしまいますので。

入力文字列を、トークンに分割するところまでです。

(defvar *match-keyargs* "\\\\[a-z]+{.+}*")
(defvar *match-text* "[^ ]+")

(defun match-token (str)
  (match-token-specs
   str
   (list `(,*match-keyargs* keyargs)
         `(,*match-text* text))))

(defun match-token-specs (str specs)
  (if specs
      (let ((mb (string-match (concat "^" (caar specs)) str))
            (me (match-end 0)))
        (if mb (list mb me (cadar specs))
          (match-token-specs str (cdr specs))))
    nil))

(defun parse-string (str elems)
  (if (> (length str) 0)
      (let ((m (match-token str)))
        (let ((mb (car m))
              (me (cadr m))
              (spec (caddr m)))
        (if mb
            (if (eq spec 'keyargs)
              (cons (list spec (substring str mb me))
                    (parse-string (substring str me) elems))
              (cons (list spec (substring str mb me))
                    (parse-string (substring str me) elems)))
          (parse-string (substring str 1) elems))))))

最後の関数を eval すると、こうなります:

(parse-string "x = \\frac{a}{b} + c" nil)
=>((text "x") (text "=") (keyargs "\\frac{a}{b}") (text "+") (text "c"))

parse-string 関数の、 (if (eq spec 'keyargs) ... のところ、同じ式を 2 回書いていますが、ここが、これから手をつけようとしている場所です。 キーワード+引数を見つけたら、どうにかするという処理を、これから書こうかと思っているところです。

| | コメント (0) | トラックバック (0)

2009年3月17日 (火)

イヤンの SQL

例えば、こういう "イベント型" のテーブルがあったとき :

select
  ev_id,
  ev_date,
  ev_str
from t_event
order by ev_date;

 ev_id |  ev_date   |  ev_str
-------+------------+----------
    23 | 2003-01-01 | event1
    24 | 2003-01-21 | event2
    25 | 2003-02-20 | event3
    26 | 2003-03-19 | event4
    27 | 2003-04-05 | event5
    28 | 2003-04-22 | event6
    29 | 2003-05-12 | event7
    30 | 2003-06-04 | event8
-- More  --

"前の日付" とか、 "前の前の日付" とかを一緒にとりたい場合があります。

すなわち :

  ev_date   |  ev_str  | p_ev_date  | pp_ev_date
------------+----------+------------+------------
 2003-01-01 | event1   |            |
 2003-01-21 | event2   | 2003-01-01 |
 2003-02-20 | event3   | 2003-01-21 | 2003-01-01
 2003-03-19 | event4   | 2003-02-20 | 2003-01-21
 2003-04-05 | event5   | 2003-03-19 | 2003-02-20
 2003-04-22 | event6   | 2003-04-05 | 2003-03-19
 2003-05-12 | event7   | 2003-04-22 | 2003-04-05
 2003-06-04 | event8   | 2003-05-12 | 2003-04-22
-- More  --

こんな SQL :

select
  e1.ev_date,
  e1.ev_str,
  e2.p_ev_date,
  max(e3.ev_date) as pp_ev_date
from t_event e1
inner join (
  select
    e1.ev_id,
    e1.ev_date,
    max(e2.ev_date) as p_ev_date
  from t_event e1
  left join (
    select
      ev_date
    from t_event) e2
    on e2.ev_date < e1.ev_date
  group by
    e1.ev_id,
    e1.ev_date) e2
  on e2.ev_id = e1.ev_id
left join (
  select
    ev_date
  from t_event) e3
  on e3.ev_date < e2.p_ev_date
group by
  e1.ev_date,
  e1.ev_str,
  e2.p_ev_date
order by
  e1.ev_date;

往々にして、この種のテーブルは、レコードが 1億件 とかあるんですよね。そのようなテーブルで、上のような SQL を実行しますと、テンポラリ領域がオーバー・フローします。

これこそ、非正規化して、 "前の日付" 、 "前の前の日付" をカラムに持たせるべきじゃないでしょうか?

と、ここで私の中のゴースト X が語りかけてきます。

「おいおい、データベースの中に linked list を作ろう ってのかい? INSERT 、 DELETE、 UPDATE でどうなるか考えてみなよ。」

こうなりますね :

INSERT 時
INSERT するレコードに、 その "前の日付" 、 "前の前の日付" を 代入。 INSERT するレコードの "次の" レコードの "前の日付"、 "前の前の日付" を 更新。 さらにその "次の" レコードの "前の前の日付" を更新。
DELETE 時
DELETE するレコードの "次の日付" のレコードを SELECT して、その "前の日付" 、 "前の前の日付" を、それぞれ、"前の前の日付" 、 "前の x 3 日付" にする。 さらにその "次の" レコードの (以下略)。
UPDATE 時
DELETE + INSERT 。

まさに linked list 。

"みんなの" データベース でそんなことしたら、どうなる? われらがレコードをまさにデータベースに突っ込まんとす、ってときに、他の奴が "前の" レコードを消しちまったら? RDBMS が java.util.ConcurrentModificationException を throw してくださるってのか?」

ですわな。

とすると、やっぱり、 テーブルはこのまま正規形にしておいて、読み取り側のアプリケーションに泣いてもらうのがベターですかね。 SQL 式でとろうとせずに、ホスト言語側でループするなり、 PL/SQL − PL/pgSQL か − を使うなりすれば、何とかなるでしょう。

| | コメント (0) | トラックバック (0)

2009年3月16日 (月)

HTML + CSS で数式組版 (その2)

もう少し頑張ってみました。

  x  =  
− b  ±                     
  b 2  −  4ac
2a

| | コメント (0) | トラックバック (0)

HTML + CSS で数式組版

数式の組版も組版には違いないわけですから、 HTML と CSS でも出来るんじゃね? と思いまして。実際、 CSS の仕様書なんて、まるで組版システムですしね。

Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification

実際やってみると、ブラウザによって、レンダリングが違いすぎるのがつらい。まあ、細かいところに目をつぶれば、それなりには出来る、とはいえるかも。

以下が、試しに作ってみたもの。

 

  x  =  
− b  ±  2  −  4ac
              2a              

| | コメント (2) | トラックバック (0)

2009年3月13日 (金)

Factory の私達

実装クラスを、適当なタイミングで差し替えたいわけですね。

>java Main Foo1Factory
class Foo1Factory
Foo1@9304b1

>java Main Foo2Factory
class Foo2Factory
Foo2@9304b1
public class Main {
  public static void main(String[] args) throws ClassNotFoundException {
    Class.forName(args[0]);
    Foo foo = FooFactory.createFoo();
    System.out.println(foo);
  }
}
public abstract class FooFactory {
  static FooFactory factory;
  public static Foo createFoo() {
    return factory.createFooImpl();
  }
  public abstract Foo createFooImpl();
}
public class Foo1Factory extends FooFactory {
  @Override
  public Foo createFooImpl() {
    return new Foo1();
  }
  static {
    FooFactory.factory = new Foo1Factory();
    System.out.println(Foo1Factory.class);
  }
}
public class Foo2Factory extends FooFactory {
  @Override
  public Foo createFooImpl() {
    return new Foo2();
  }
  static {
    FooFactory.factory = new Foo2Factory();
    System.out.println(Foo2Factory.class);
  }
}

しかし、クラスの数が増えてくると、似たような Factory のコードをいっぱい書かなければならないのが、面倒になってくるわけでして。”汎用ファクトリ”が欲しくなってきます。そこで、 Dependency Injection Container ( DI コンテナ) の登場、ということになります。

でも、外部ライブラリ ( DI コンテナ) を使いたくないってこともあります。ソース・コードだけで完結する、ということにも色々メリットがありますしね。そういったときは、 Factory パターンを使えばよいかと。

余談。

実装クラスを差し替えたい場合というのは、たいていは、リンク・エディット時に実装クラスを決めたいってことだったりします。ひとつのソース・ツリーから、2種類以上の実行ファイルを作り出したいとき。どのソース・ファイルがビルドに含まれるかによって、 どの実装クラスを使うのかが決まるようにしたいわけです。

でも、 Java には、スタティック・リンクという概念がないのですよね。結局、差し替えるのは、アプリケーション起動時になります。

アプリケーション起動時に実装が決まる、というのは、なにか中途半端に感じるのですよね。実装を決めたいのは、普通はビルド時じゃないかな。ソフトウエアを配布する前。

ソフトウエアの実行中(起動時にあらず)に実装を差し替える、という要件は、電話の交換機のソフトウエアなどではあるらしいですね。台数が多いから、いちいちデプロイ・再起動なんてやってられない、ってことですかね。全部自動で入れ替われ、と。

| | コメント (0) | トラックバック (0)

2009年3月 8日 (日)

回線速度の計測 ( その2 )

回線速度の計測 の続き。

Linux サーバ側で、ローカル・ループバックに送ってみました。

[localhost]$ java SpeedTest client localhost 9999
145
187
874
1507
7417
14891
73285
3615779.3103448274
4.485886631016043E7
4.798974828375286E7
5.566428666224287E7
5.654987191586895E7
5.63334094419448E7
5.723277614791567E7
4.603496258174721E7

50Mbps くらい。伝送効率 0.5 というのは低いですが、ありえない値という程でもないですね。

としますと、 Linux サーバ側に問題がある、という訳でもなさそうです。 Windows クライアント - Linux サーバ間の通信の問題と考えるべきでしょう。

Wireshark を使って、 Windows クライアントのパケットを除いてみます。


[TCP Dup ACK 27#1] distinct > saiseh [ACK] Seq=1 Ack=9653 Win=26280 Len=0 SLE=11113 SRE=12573
saiseh > distinct [ACK] Seq=22225 Ack=1 Win=65535 Len=1460
[TCP Dup ACK 27#2] distinct > saiseh [ACK] Seq=1 Ack=9653 Win=26280 Len=0 SLE=11113 SRE=14033
[TCP Fast Retransmission] saiseh > distinct [ACK] Seq=9653 Ack=1 Win=65535 Len=1460
[TCP Dup ACK 27#3] distinct > saiseh [ACK] Seq=1 Ack=9653 Win=26280 Len=0 SLE=11113 SRE=15493
saiseh > distinct [PSH, ACK] Seq=23685 Ack=1 Win=65535 Len=892
[TCP Dup ACK 27#4] distinct > saiseh [ACK] Seq=1 Ack=9653 Win=26280 Len=0 SLE=11113 SRE=16385
[TCP Dup ACK 27#5] distinct > saiseh [ACK] Seq=1 Ack=9653 Win=26280 Len=0 SLE=11113 SRE=17845
[TCP Dup ACK 27#6] distinct > saiseh [ACK] Seq=1 Ack=9653 Win=26280 Len=0 SLE=11113 SRE=19305
[TCP Dup ACK 27#7] distinct > saiseh [ACK] Seq=1 Ack=9653 Win=26280 Len=0 SLE=11113 SRE=20765
[TCP Dup ACK 27#8] distinct > saiseh [ACK] Seq=1 Ack=9653 Win=26280 Len=0 SLE=22225 SRE=23685 SLE=11113 SRE=20765
distinct > saiseh [ACK] Seq=1 Ack=20765 Win=29200 Len=0 SLE=22225 SRE=23685
[TCP Dup ACK 41#1] distinct > saiseh [ACK] Seq=1 Ack=20765 Win=29200 Len=0 SLE=22225 SRE=24577
saiseh > distinct [ACK] Seq=24577 Ack=1 Win=65535 Len=1460
[TCP Dup ACK 41#2] distinct > saiseh [ACK] Seq=1 Ack=20765 Win=29200 Len=0 SLE=22225 SRE=26037
[TCP Fast Retransmission] saiseh > distinct [ACK] Seq=20765 Ack=1 Win=65535 Len=1460

"Dup ACK" というのが一杯出てます。クライアントがパケットを送り出すスピードが速すぎて、サーバが受け取れない、ってことですかね。

以下を参考に、 Windows の TCP ウインドウ・サイズを、 64 KB から 17KB へと変えてみましたが、特に変化なし。

Windows 2000 および Windows Server 2003 の TCP 機能について - Microsoft Knowledge Base 224829

Java プログラムの書き込みバッファ・サイズを変更する、というのは効くみたいです。

   final int BUFFER_SIZE = 1460 * 4;
    System.out.print("Buffer: ");
    System.out.println(BUFFER_SIZE);

    Socket sock = new Socket(host, port);
    sock.setSendBufferSize(BUFFER_SIZE);
    sock.setReceiveBufferSize(BUFFER_SIZE);

    BufferedOutputStream out =
      new BufferedOutputStream(sock.getOutputStream(), BUFFER_SIZE);

でも、 これだけですと、 10Mbps くらいが限度みたいですね。

>java -client SpeedTest client 192.168.0.125
9999
Buffer: 5840
1982
1205
4013
8118
40793
81263
416608
264524.7225025227
6961500.41493776
1.0451791677049588E7
1.0333343187977334E7
1.0281920917804526E7
1.0322788969149552E7
1.0067747138797143E7
8383373.861174061

| | コメント (0) | トラックバック (0)

2009年3月 7日 (土)

PostgreSQL で 24:00 と 0:00 を扱う

問題の趣旨を理解できていないかもしれませんが。

これができないから、いつまでたっても、Date型とか使わずにcharやnumberな日付時刻カラムがなくならないんだよな。

BusinessDateTime型 - L'eclat des jours (2009-03-06)

夜勤帯が存在するような業務の場合、「当日の24:00」と「翌日の0:00」を区別したい、という話は出てきますね。

日付と時刻を分割するしかないんじゃないですかねえ。

PostgreSQL ですと、こんな感じになります。

create table t_event(
ev_id serial,
ev_date date,
ev_interval interval,
ev_str varchar(10),
primary key (ev_id) );

insert into t_event(
ev_date,
ev_interval,
ev_str)
values(
'2009-01-31',
'24:01:00',
'event1');

insert into t_event(
ev_date,
ev_interval,
ev_str)
values(
'2009-02-01',
'00:00:00',
'event2');

insert into t_event(
ev_date,
ev_interval,
ev_str)
values(
'2009-02-01',
'00:01:00',
'event3');

select *
from t_event
order by ev_date, ev_interval;

 ev_id |  ev_date   | ev_interval | ev_str
-------+------------+-------------+--------
     1 | 2009-01-31 | 24:01:00    | event1
     2 | 2009-02-01 | 00:00:00    | event2
     3 | 2009-02-01 | 00:01:00    | event3
(3 rows)

select
ev_date + ev_interval as normalize,
ev_str
from t_event
order by ev_date, ev_interval;

      normalize      | ev_str
---------------------+--------
 2009-02-01 00:01:00 | event1
 2009-02-01 00:00:00 | event2
 2009-02-01 00:01:00 | event3
(3 rows)

select
ev_date::varchar || ' ' || ev_interval::varchar as to_s,
ev_str
from t_event
order by ev_date, ev_interval;

        to_s         | ev_str
---------------------+--------
 2009-01-31 24:01:00 | event1
 2009-02-01 00:00:00 | event2
 2009-02-01 00:01:00 | event3
(3 rows)

select
ev_date - 1 as round_date,
ev_interval + '24:00:00' as round_interval,
ev_str
from t_event
where ev_id = 3
order by ev_date, ev_interval;

 round_date | round_interval | ev_str
------------+----------------+--------
 2009-01-31 | 24:01:00       | event3
(1 row)

| | コメント (0) | トラックバック (0)

2009年2月28日 (土)

Java SE 7 は .NET アセンブリの夢を見るのか

Sun JDK/JRE の素朴なクラスローダーの仕組みですと、多数の外部ライブラリに依存した、アプリケーションの配布はつらいと思うことは、確かにしばしばあります。

一方、2009年夏にリリース予定のJava SE 7は、モジュール型の開発/ディプロイメントを可能にする。これらの機能はJSR 294(Improved Modularity Support in the Java Programming Language)やJSR 277(Java Module System)をベースにしたものだ。JSR 277の目標は、JAM(Java Application Modules)フォーマットによるアプリケーション・パッケージングを促すことにある。

Java SE 7をベースとするJDK 7は、OSGi(Open Services Gateway initiative:サービス管理プラットフォーム仕様)に準拠したBundles(バンドル)をサポートする。「Java SE 7用のJavaモジュール・システムとOSGiバンドル間の相互運用性を定義する新仕様が完成した。これにより、OSGiバンドルを使うアプリケーションをJDK 7で作成し、それをそのままJDK 7で実行できるようになる」と、Sunのクライアント・ソフトウェア・グループでチーフ・アーキテクトを務めるダニー・カワード(Danny Coward)氏は説明する。

モジュラリティが特徴の次期Java SE、OSGi Bundlesをサポートへ (Computerworld.jp)

JAM やら、 OSGi Bundle やらは、 Microsoft の .NET アセンブリに相当するものと考えても良いのですかね。

アセンブリは .NET Framework アプリケーションのビルドブロックであり、配置、バージョン管理、再利用、アクティブ化のスコープの指定、およびセキュリティアクセス許可の基本単位となります。アセンブリは、相互に連携して 1 つの論理的な機能単位を形成するように構築された型やリソースの集合です。

共通言語ランタイムのアセンブリ (MSDN)

Java は、 機能面で .NET に先行されていると思うことが多いですね。コミュニティ・ベースで仕様を決めている Java と、 Microsoft 1社で仕様を決めている .NET。クロスプラットフォームの Java と、 Windows Only の .NET ということで、色々速度差が出てくるのでしょうが。

とにかく、 OSGi Bundle がサポートされるようになる、ということで、OSGi Framework の仕様書を読んでみたり。

The OSGi(tm) Alliance was founded in March 1999. Its mission is to create open specifications for the network delivery of managed services to local networks and devices. The OSGi organization is the leading standard for next-generation Internet services to homes, cars, mobile phones, desktops, small offices, and other environments.

OSGi Service Platform Core Specification (OSGi Service Platform Release 4, OSGi Alliance Specifications)

ネットワーク経由でコードを配布する、というのは、 Java の当初のコンセプトからあったわけですが。

The Java platform was initially developed to address the problems of building software for networked consumer devices. It was designed to support multiple host architectures and to allow secure delivery of software components. To meet these requirements, compiled code had to survive transport across networks, operate on any client, and assure the client that it was safe to run.

A Bit of History (The Java(tm) Virtual Machine Specification Second Edition)

このコンセプトは、当面、忘れた方がいいんじゃないか という気がします。とりあえずは、「より良いクラスローダー」で良いんじゃないでしょうか。

| | コメント (0) | トラックバック (0)

2009年2月26日 (木)

CREATE TABLE 文の「差分」をとる

JJTree を使って、 CREATE TABLE 文の「差分」をとるプログラムを作ってみました。2つの CREATE TABLE 文から、両者間で追加・変更・削除されたカラムを抜き出して、 ALTER TABLE 文を作る、という Toy プログラムです。

>SqlDDLDiff test\file001_1.sql test\file001_2.sql
ALTER TABLE author DROP COLUMN a_bio;
ALTER TABLE author ALTER COLUMN a_fname TYPE varchar(10);
ALTER TABLE author ADD COLUMN a_lname varchar(20);
/* file001_1.sql */
create table author (
a_id numeric(10),
a_fname varchar(10), /* 20 -> 10 */
a_lname varchar(20), /* add */
a_mname varchar(20),
a_dob date,
--a_bio varchar(500), /* drop */
primary key (a_id) );

/* file001_2.sql */
create table author (
a_id numeric(10),
a_fname varchar(20),
--a_lname varchar(20),
a_mname varchar(20),
a_dob date,
a_bio varchar(500),
primary key (a_id) );

JJTree を使ってみて、構文木を作るツールが流行らない訳が、なんとなく分かりました。

Parser -> 構文木 (AST) -> アプリケーション・データ

とまあ、汎用のデータ構造を経てアプリケーション独自のデータへと変換する訳ですけど、これって無駄に感じるのですよね。

Parser -> アプリケーション・データ

Parser からアプリケーション・データを直接作ればいいじゃない、となる訳です。

でも、 SQL の場合は、汎用の構文木を作る意味がありそう。 RDBMS ごとに独自の「方言」がありますからね。特に、 CREATE TABLE 文 のような、 DDL 系は、 RDBMS 実装による違いが出やすいところです。汎用の構文木を間に挟むことで、 Parser を RDBMS ごとに差し替え可能にする、という風に使えそうですね。

| | コメント (0) | トラックバック (0)

2009年2月21日 (土)

JJTree 構文木を作るツール

コンパイラ・コンパイラが、構文木まで作ってくれれば良いのに、と前々から思っていたのですが。そういうツールが、ちゃんと存在しているのですね。

JJTree is a preprocessor for JavaCC (tm) that inserts parse tree building actions at various places in the JavaCC source. The output of JJTree is run through JavaCC to create the parser.

JavaCC (tm): JJTree Reference Documentation

おなじみの「電卓」の例は、こんな感じです。

>java Calc
1+2*3-4/5
CalcExpr
 AddExpr:+,-
  Literal:1
  MulExpr:*
   Literal:2
   Literal:3
  MulExpr:/
   Literal:4
   Literal:5

構文生成規則は、以下のように入力します。

/* Calc.jjt */
options {
  STATIC=false;
  MULTI=true;
}

PARSER_BEGIN(Calc)
public class Calc {
  public static void main(String args[])
    throws ParseException {

    while (true) {
      Calc parser = new Calc(System.in);
      if (parser.CalcExpr() == 1) {
        ((SimpleNode)parser.jjtree.rootNode()).dump("");
      }
    }
  }
}
PARSER_END(Calc)

SKIP:
{
  " "|"\r"
}

TOKEN :
{
    < ADDOP  : "+" >
   |< SUBOP  : "-" >
   |< MULOP  : "*" >
   |< DIVOP  : "/" >
   |< LP     : "(" >
   |< RP     : ")" >
   |< LITERAL: (["0"-"9"])+ >
   |< NL     : "\n" >
}

int CalcExpr() :
{ }
{
      AddExpr() <NL>
        { return 1; }
     | <NL>
        { return 0; }
     | <EOF>
        { return -1; }
}

void AddExpr() #void :
{ Token t;
  int i = 0;
}
{
    (
      MulExpr() (
        (t=<ADDOP>|t=<SUBOP>)
          { jjtThis.addValue(t.image); }
        MulExpr())*
    ) #AddExpr(>1)
}

void MulExpr() #void :
{ Token t;
  int i = 0;
}
{
    (
      PrmExpr() (
        (t=<MULOP>|t=<DIVOP>)
          { jjtThis.addValue(t.image); }
        PrmExpr() )*
    ) #MulExpr(>1)
}

void PrmExpr() #void :
{ }
{
      Literal()
    | <LP> AddExpr() <RP>
}

void Literal() :
{ Token t; }
{
    t=<LITERAL>
      { jjtThis.addValue(t.image); }
}

Ant ビルドファイル build.xml は、以下のとおり。

<?xml version='1.0' encoding='utf-8' ?>

<project name="calc" default="build" basedir=".">

  <property name="javacc.home" value="/path/to/javacc"/>

<target name="clean">
  <delete>
    <fileset dir="src">
      <include name="ASTAddExpr.java"/>
      <include name="ASTCalcExpr.java"/>
      <include name="ASTLiteral.java"/>
      <include name="ASTMulExpr.java"/>
      <include name="Calc.java"/>
      <include name="Calc.jj"/>
      <include name="CalcConstants.java"/>
      <include name="CalcTokenManager.java"/>
      <include name="CalcTreeConstants.java"/>
      <include name="JJTCalcState.java"/>
      <include name="Node.java"/>
      <include name="ParseException.java"/>
      <include name="SimpleCharStream.java"/>
      <include name="SimpleNode.java"/>
      <include name="Token.java"/>
      <include name="TokenMgrError.java"/>
    </fileset>
  </delete>
</target>

  <target name="jjtree-gen">
    <exec dir="."
      failonerror="true" executable="cmd" >
      <arg value="/C" />
      <arg value="jjtree -OUTPUT_DIRECTORY:src -NODE_CLASS:CalcNode src/Calc.jjt" />
    </exec>
  </target>

  <target name="javacc-gen">
    <javacc target="src/Calc.jj" outputdirectory="src"
      javacchome="${javacc.home}"/>
  </target>

  <target name="compile">
    <delete dir="bin"/>
    <mkdir dir="bin"/>
    <javac srcdir="src" destdir="bin">
    </javac>
  </target>

  <target name="build" depends="clean,jjtree-gen,javacc-gen,compile">
  </target>

</project>

JJTree のオプションで、 -NODE_CLASS:CalcNode として、構文木のノードを指定しています。

/* CalcNode.java */
import java.util.ArrayList;

public class CalcNode extends SimpleNode {

  ArrayList<Object> valueList = new ArrayList<Object>();

  public CalcNode(int i) {
    super(i);
  }

  public CalcNode(Calc p, int i) {
    super(p, i);
  }

  public void addValue(Object value) {
    valueList.add(value);
  }

  public String toString() {
    StringBuilder sb = new StringBuilder(super.toString());
    int i = 0;
    for (Object value : valueList) {
      if (i == 0) {
        sb.append(':');
      } else {
        sb.append(',');
      }
      sb.append(value);
      i++;
    }
    return sb.toString();
  }
}

| | コメント (0) | トラックバック (0)

2009年2月14日 (土)

回線速度の計測

Java で簡単なプログラムを書いて、 LAN 回線速度の計測をしてみました。

最初に、サーバを立ち上げます。

>java -server SpeedTest server 9999

ローカル・ループバック・アドレスにデータを送ってみます。

>java -client SpeedTest client localhost 9999
30
131
420
851
4236
8493
42591
1.7476266666666668E7
6.403517557251909E7
9.986438095238096E7
9.857353701527615E7
9.901567516525024E7
9.87708465795361E7
9.847864572327487E7
8.231636109641485E7

一番最後の数字が、平均の bps 値です。おおよそ 100M bps に近い数字が出てます。

Windows クライアントから、 Linux サーバ (Vine 4.2) にデータを送ります。

>java -client SpeedTest client 192.168.0.125 9999
689
7018
33605
70920
344607
715351
3516913
760940.4934687953
1195298.945568538
1248119.029906264
1182826.8471517202
1217126.7559858041
1172656.2205127273
1192609.5413790445
1138511.1191389847

1Mbps くらいしか出てませんね。カーネル・パラメータのせいかな。後で調べる。

ソースコードは以下のとおりです。

import java.net.*;
import java.io.*;

public class SpeedTest {

  public static void main(String[] args) {
    if (args.length == 2 && args[0].toLowerCase().charAt(0) == 's') {
      try {
        startServer(Integer.parseInt(args[1]));
      } catch (IOException e) {
        e.printStackTrace();
      }
    } else if (args.length == 3 && args[0].toLowerCase().charAt(0) == 'c') {
      try {
        startClient(args[1], Integer.parseInt(args[2]));
      } catch (IOException e) {
        e.printStackTrace();
      }
    } else {
      System.out.println("usage: server port | client host port");
    }
  }

  public static void startServer(int port) throws IOException {
    final int STATUS_RCV = 0;
    final int STATUS_CMD = 1;
    final int STATUS_RST = 2;

    ServerSocket server = new ServerSocket(port);
    for (;;) {
      Socket sock = server.accept();
      System.out.println("accept: " +
        sock.getInetAddress().getHostAddress());

      BufferedInputStream in =
        new BufferedInputStream(sock.getInputStream());
      PrintWriter out =
        new PrintWriter(sock.getOutputStream(), true);

      int stat = STATUS_RCV;
      long startMs = System.currentTimeMillis();
      int ch;

      while ((ch = in.read()) != -1) {
        if (stat == STATUS_RST) {
          startMs = System.currentTimeMillis();
          stat = STATUS_RCV;
        }
        if (stat == STATUS_RCV && ch == '\\') {
          stat = STATUS_CMD;
        } else if (stat == STATUS_CMD) {
          if (ch == 's') {
            long ms = System.currentTimeMillis() - startMs;
            out.println(ms);
            System.out.println(ms);
            stat = STATUS_RST;
          } else if (ch == 'q') {
            break;
          } else {
            stat = STATUS_RCV;
          }
        }
      }
      out.close();
      in.close();

      System.out.println("quit: " + sock.getInetAddress().getHostAddress());
      sock.close();
    }
  }

  public static void startClient(String host, int port) throws IOException {
    final int[] sendSizes =
      {65536,     // 64KB
       1048576,   // 1MB
       5242880,   // 5MB
       10485760,  // 10MB
       52428800,  // 50MB
       104857600, // 100MB
       524288000  // 500MB
      };
    int[] recvMs = new int[sendSizes.length];

    Socket sock = new Socket(host, port);
    BufferedOutputStream out =
      new BufferedOutputStream(sock.getOutputStream());
    BufferedReader in = new BufferedReader(
      new InputStreamReader(sock.getInputStream()));

    for (int i = 0; i < sendSizes.length; i ++) {
      for (int j = 0; j < sendSizes[i] - 2; j++) {
        out.write('X');
      }
      out.write('\\');
      out.write('s');
      out.flush();
      recvMs[i] = Integer.parseInt(in.readLine());
      System.out.println(recvMs[i]);
    }
    out.write('\\');
    out.write('q');
    out.flush();
    in.close();
    out.close();

    double sendBpsSum = 0.0d;
    int sendBpsCount = 0;
    for (int i = 0; i < sendSizes.length; i++) {
      if (recvMs[i] > 0) {
        double sendBps = sendSizes[i] * 8000.0d / recvMs[i];
        System.out.println(sendBps);
        sendBpsSum += sendBps;
        sendBpsCount++;
      }
    }
    System.out.println(sendBpsSum / sendBpsCount);
  }

}

| | コメント (0) | トラックバック (0)

2009年2月11日 (水)

Ruby-pg を Windows にインストール

以下の環境でインストールしました。

  • Windows XP SP3
  • ActiveRuby 1.8.7.7
  • PostgreSQL 8.3.5
  • Microsoft Visual C++ 2008 Express Edition

1. PostgreSQL 側の準備

以下が前提条件となります。

  • C ヘッダー (include), ライブラリ (lib) がインストールされている
  • bin にパスが通っている ( pg_config.exe を使うため )

2. ruby config.h を修正

MSC のバージョンが限定されているので、これをはずします。

*** ruby-1.8/lib/ruby/1.8/i386-mswin32/config.h
--- ruby-1.8/lib/ruby/1.8/i386-mswin32/config.h
***************
*** 1,2 ****
! #if _MSC_VER != 1200
  #error MSC version unmatch: _MSC_VER: 1200 is expected.
--- 1,2 ----
! #if _MSC_VER < 1200
  #error MSC version unmatch: _MSC_VER: 1200 is expected.

3. ruby rbconfig.rb を修正

色々修正します。

  • CFLAGS: -MD (msvcrt dll にリンク) を -MT (lib にリンク)
  • DLDFLAGS: -debug を -release
  • LIBRUBY_A: 存在しないのでコメントアウト
*** ruby-1.8/lib/ruby/1.8/i386-mswin32/rbconfig.rb
--- ruby-1.8/lib/ruby/1.8/i386-mswin32/rbconfig.rb
***************
*** 24,26 ****
    CONFIG["PATH_SEPARATOR"] = ";"
!   CONFIG["CFLAGS"] = "-MD -Zi  -O2b2xg- -G6"
    CONFIG["CPPFLAGS"] = "  "
--- 24,26 ----
    CONFIG["PATH_SEPARATOR"] = ";"
!   CONFIG["CFLAGS"] = "-MT -Zi  -O2b2xg- -G6"
    CONFIG["CPPFLAGS"] = "  "
***************
*** 70,72 ****
    CONFIG["OBJEXT"] = "obj"
!   CONFIG["DLDFLAGS"] = "-link -incremental:no -debug -opt:ref -opt:icf -dll $(LIBPATH)"
    CONFIG["ARCH_FLAG"] = ""
--- 70,72 ----
    CONFIG["OBJEXT"] = "obj"
!   CONFIG["DLDFLAGS"] = "-link -incremental:no -release -opt:ref -opt:icf -dll $(LIBPATH)"
    CONFIG["ARCH_FLAG"] = ""
***************
*** 89,91 ****
    CONFIG["RUBYW_INSTALL_NAME"] = "rubyw"
!   CONFIG["LIBRUBY_A"] = "$(RUBY_SO_NAME)-static.lib"
    CONFIG["LIBRUBY_SO"] = "$(RUBY_SO_NAME).dll"
--- 89,91 ----
    CONFIG["RUBYW_INSTALL_NAME"] = "rubyw"
! #  CONFIG["LIBRUBY_A"] = "$(RUBY_SO_NAME)-static.lib"
    CONFIG["LIBRUBY_SO"] = "$(RUBY_SO_NAME).dll"

4. ruby gem でインストール

以下から gem install を起動します。

  • スタート メニュー\プログラム\Microsoft Visual C++ 2008 Express Edition\Visual Studio Tools\Visual Studio 2008 コマンド プロンプト
Setting environment for using Microsoft Visual Studio 2008 x86 tools.

C:\Program Files\Microsoft Visual Studio 9.0\VC>gem install pg
Building native extensions.  This could take a while...
Successfully installed pg-0.7.9.2008.10.13
1 gem installed
Installing ri documentation for pg-0.7.9.2008.10.13...
Installing RDoc documentation for pg-0.7.9.2008.10.13...

C:\Program Files\Microsoft Visual Studio 9.0\VC>

5. 動作確認

rdoc を参考に、以下のようなテストスクリプトを書いて実行します。

# pg_test.rb

require 'pg'

conn = PGconn.open(
:dbname => 'DBT1',
:hostaddr => '192.168.0.125',
:user => 'pgsql')
res = conn.exec('SELECT $1::int AS a, $2::int AS b, $3::int AS c',[1, 2, nil])
res.each { |t| p t }

動いてますね。

>ruby pg_test.rb
{"a"=>"1", "b"=>"2", "c"=>nil}

| | コメント (0) | トラックバック (0)

2008年10月29日 (水)

「変数のスコープは狭いほど良い」

「goto文禁止令」の類の話かと。

中途半端に優秀なプログラマが「正しいプログラミングテクニック」だと妄信しがちな3つポイント

比較的簡単なので1番目の「変数のスコープ」だけ。

その変数はどのくらい「変数」なのか?

「変数」の最大のポイントは、「代入」にあると思います。

public class Test {

  public static void main(String[] args) {
    if (args.length > 0) {
      String x = args[0];
      new Foo().bar(x);
    }
  }

}

上のソースで変数 x (つまり args[0]) って、どのくらい代入操作されますかね?

マルチスレッドなのか?

はてなブックマーク コメント よりお題をいただきました。

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TestServlet
  extends javax.servlet.http.HttpServlet
  implements javax.servlet.Servlet {

  static final long serialVersionUID = 1L;
  static HttpServletRequest request_;

  public TestServlet() {
    super();
  }

  protected void doGet(
    HttpServletRequest request,
    HttpServletResponse response)
    throws ServletException, IOException {

    request_ = request;
    try {
      Thread.sleep(5000);
    } catch (InterruptedException e) {
    }
    response.getWriter().println(request_.getParameter("p0"));
  }
}

2つのブラウザから、以下のリクエストを連続して投げること(5秒以内)。

GET /ServletTest/TestServlet?p0=A
GET /ServletTest/TestServlet?p0=B

一時期、この手のバグが話題になっていましたね。

とはいえ、 Servlet のユーザから見ると、 HttpServletRequest が「定数」に見えるのも確かです。 Servlet コンテナから見ると、「変数」なわけですけどね。

これは Servlet の仕様でしょうね。もともと、「軽量さ」が Servlet の売りであったことを思えば、意図的なデザインでしょう。

そのコードはどの程度「再利用」されるか?

使い捨てにするコードなら、別に気にしなくて良いですよね。広汎に使われるとなると、「想定外」の使われ方をする可能性が増えますので、より「防衛的に」プログラミングする必要があるかと。

| | コメント (0) | トラックバック (0)

2008年10月19日 (日)

固定小数点演算

基数変換誤差が問題となるのは、固定小数点だからなんですよね。浮動小数点演算では、計算値に対する精度が十分に大きければ、基数変換誤差を気にする必要はないわけでして。そう考えると、固定小数点と10進数演算は、常にセットで考えるものなのでしょう。2進固定小数点を見かけない理由でもあるわけですね。

さて、 Java の 固定小数点数 java.math.BigDecimal ではソースコードはこんな感じになります。

import java.math.*;

public class BigDecimalTest {

  public static void main(String[] args) {
    float fa = 0.4f;
    float fb = 0.3f;
    float fc = 0.1f;

    System.out.println(fa - fb - fc);

    final BigDecimal xa = new BigDecimal("0.4");
    final BigDecimal xb = new BigDecimal("0.3");
    final BigDecimal xc = new BigDecimal("0.1");
    final BigDecimal xd = xa.subtract(xb).subtract(xc);

    System.out.println(xd);
  }

}

実行結果:

-7.4505806E-9
0.0

確かに、うざったい感じですね。

精度、スケール、丸めの方向を java.math.MathContext 、および java.math.RoundingMode で細かく指定できるのは、良いと思いますけど。

忘れていましたが、 Microsoft .NET では、組み込み型で固定小数点がサポートされています。

ソースコードは以下のようになります。

using System;

namespace DecimalTest
{
    class Program
    {
        static void Main(string[] args)
        {
          float fa = 0.4f;
          float fb = 0.3f;
          float fc = 0.1f;

          Console.WriteLine(fa - fb - fc);

          decimal xa = 0.4m;
          decimal xb = 0.3m;
          decimal xc = 0.1m;

          Console.WriteLine(xa - xb - xc);

        }
    }
}

実行結果:

-7.450581E-09
0.0

精度は 128 ビットで固定されているようです。スケールを明示的に指定する方法はなさそうですね。丸めの方向も指定できなさそう。

組み込み型なのは良いのですが。 Java と良い点・悪い点が、ちょうど反対になっているのが面白いといいますか。

しかし、この .NET Framework マニュアルの説明はわけわからないですね。どこかでも話題になっていましたが。

Decimal 値型は、多数の有効な整数桁と小数桁を必要とし、丸め誤差を使用しない財務計算に適しています。

10 進数は、符号、値の各桁の範囲が 0 から 9 までの数値、数値の整数部と小数部を分ける浮動小数点の位置を示すスケール ファクタで構成される浮動小数点値です。

Decimal 構造体 (System)

「整数部と小数部を分ける ... スケール ファクタ」があるわけですから、固定小数点と呼ぶべきなのではないでしょうか。浮動小数点とは呼ばないのではないかと思うのですけど。

A decimal number is a floating-point value that consists of a sign, a numeric value where each digit in the value ranges from 0 to 9, and a scaling factor that indicates the position of a floating decimal point that separates the integral and fractional parts of the numeric value.

Decimal Structure (System)

小数点の桁位置を変えられることを、 ”floating-point value”, "floating decimal point" と言っているのですかね。かえって判りにくいような。

| | コメント (0) | トラックバック (0)

2008年10月17日 (金)

FORTRAN と COBOL の代替言語はないかも

以下の記事を読んでの感想。

「COBOLは現役バリバリだ。"COBOLは化石"などと口にするのはITとエンタープライズシステムが何たるかをわかっていない証拠」

「30年システムに携わっているが,COBOLは規格として長期間安定し,ビジネスロジックの書きやすさにおいては右に出る言語はない」

「COBOLは現役バリバリ」,東京海上日動がシステム全面再構築でCOBOLを選んだワケ (ITPro)

COBOL を使う最大の理由といえば、10進固定小数点演算が言語組み込みでサポートされている点がまず思い浮かびました。

事務処理用及び管理分野用とされるCOBOLにおいては会計・経理処理などのように通貨を対象とした正確な数値計算が特に要求されるため、数値に対する10進数から2進数への基数変換時に計算誤差の発生しない2進化10進数による数値型(固定小数点数)を用いることができる。

COBOL (Wikipedia)

会計計算では、計算規則は、人間が手で計算することを前提に作られているはずですから、10進固定小数点で計算しなければ、計算結果は「正しく」ならないでしょうね。保険関係の計算を行なうのに COBOL を使うのは、合理的な選択に思えます。プログラミング言語の組み込み型に、10進固定小数点がなければ、プログラムを書くのは、かなりつらくなるでしょう。

他方、浮動小数点演算はどうかといえば、未だ FORTRAN がデファクト・スタンダードであったりします。

十数年前、C言語 が普及し始めた当時、C言語の浮動小数点演算機能について、確か以下のようなことが言われていたと思います。

  1. 式を評価する際、浮動小数点数が double にキャストされる仕様がふざけている
  2. 標準ライブラリの数学関数の引数がすべて double になっているのがふざけている
  3. 複素数型がないのは馬鹿にしている

C言語 は、 FORTRAN より退化している、という評価がもっぱらであったかと思います。

以下の数を扱える。

  • 整数(範囲は機種依存、4バイトであればその範囲)
  • 単精度実数(整数部+小数部、指数表示可能、精度は機種依存)
  • 倍精度実数(単精度の倍の精度を持つ)
  • 複素数(単精度または倍精度の実数の組み合わせ。2つの数字を()でくくる)

    ......

    組込み関数

    組込み関数は、同じ機能でも引数の型と関数の型によって名前が異なる。関数名の先頭がI〜Nのものが整数型、Dが倍精度実数型、Cが複素数型、それ以外が実数型である。

    FORTRAN (Wikipedia)

なお、 C言語 浮動小数点で批判が多かった、 「式がすべて double になる」は、その後改善されています。

ここで注意すべきは、式の中の float が自動的に double に変換されるのではないことである。これは C のもとの定義に比べて代わった点の一つになっている。しかし一般に <math.h> にあるような数学関数では倍精度が使われよう。 float を使う主な理由は、大きな配列の記憶容量を減らすことであり、またそうあることではないが、倍精度の算術がとくに高くつくような計算機で計算時間を短くする点にある。

B.W.カーニハン/D.M.リッチー著, 石田晴久訳, 『プログラミング言語C 第2版』, 共立出版, 1989, p.55

単精度を使う理由が効率の問題でしかない、というのは少し乱暴な話ですね。

Java言語では、このC言語の仕様を、ほぼそのまま受け継いでいます。

Conversions and Promotions (Java Language Specification Third Edition)

Math (Java 2 Platform SE 5.0)

で、やっぱり批判されていると。

Java's floating-point arithmetic is blighted by five gratuitous mistakes:

  1. Linguistically legislated exact reproducibility is at best mere wishful thinking.
  2. Of two traditional policies for mixed precision evaluation, Java chose the worse.
  3. Infinities and NaNs unleashed without the protection of floating-point traps and flags mandated by IEEE Standards 754/854 belie Java's claim to robustness.
  4. Every programmer's prospects for success are diminished by Java's refusal to grant access to capabilities built into over 95% of today's floating-point hardware.
  5. Java has rejected even mildly disciplined infix operator overloading, without which extensions to arithmetic with everyday mathematical types like complex numbers, intervals, matrices, geometrical objects and arbitrarily high precision become extremely inconvenient.

    W. Kahan and Joseph D. Darcy, "How Java's Floating-Point Hurts Everyone Everywhere"1

C言語のときからすると、要求のハードルは大分高くなっているようにも思えますけど。5番目は、10進固定小数点演算 java.math.BigDecimal についても当てはまりますね。

FORTRAN と COBOL というのは、コンピュータが「計算機」であった時代に生まれたプログラミング言語であるわけでして、数値計算に関しては、未だこれに代わる言語というのは存在していないように思いますね。C言語がプログラミング言語のメインストリームになってからは、数値計算というのはプログラミング言語の進化から忘れられた存在なのかもしれません。


1. http://www.cs.berkeley.edu/~wkahan/JAVAhurt.pdf

| | コメント (0) | トラックバック (0)

2008年8月19日 (火)

OOPL - 命令型 - 関数型 = ?

この分類をJavaが手近なので当てはめてみる。ServletやActionについてはモジュールだ。一見、ある特定Webアプリケーションに特化したように見えるが、ユーザーに対しては何もしないインフラのみのクラスにfeatureを加えているのは明らかだ。このタイプは実装継承が有効に働く。

では、型と言えるのは何だろう? ...... 思いつかない。

これが、犬猫OO解釈がだめになった理由ではないか? 少なくともJavaで構築するタイプのアプリケーションあるいはサービスにおいてのOOの利用はモジュールの側だからだ。

拡張と特殊化 (L'eclat des jours)

型といえる場合、というもので、私が典型的に思い浮かべるのは、「複素数型」ですかね。『プログラミング言語 C++』で例として取り上げられていたと思います。あるいは、 Ruby ですと、 Numeric 型を継承した Integer 型、 Float 型など。

OO の「クラス」を、ある種の代数的構造として使おうとする場合でしょうね。 C++ での、演算子オーバーロード、 friend 指定 など、こうした方向性を持っていたと思います。

関数型プログラミング言語 Haskell では、

  • 型: 値の集合
  • クラス: 型に対する関数の集合

と、かなり明確な形で、代数的構造を作るための機構が用意されているようです。

この場合、「値の集合」であるところの型というのは、「継承」されたりはしないわけでして、「継承」されるのは、「関数の集合」としての「クラス」だけですね。

こうした風に、 OO の「クラス」を使うというのは、やはり、中途半端な感があるわけでして、 OO 「ならでは」、という感じではないですよね。 OO の機能を使って、関数プログラミングの真似事をしているみたいです。

とすると、やはり、 OO 独自の特徴としましては、

  • オブジェクトが状態を持ち、メソッドの副作用でオブジェクトの状態が変わる
  • オブジェクトの状態を継承できる

といったあたりにあるのであろう、と思うわけでして。俗にいう、「実装の継承」というのは、ほぼ「状態の継承」を含んでいるのではないかと思いますね。

たしかに継承は便利機能に過ぎなくて,本来不要なものかもしれない。でも, 実際すごく便利なんだ。 C++ で継承を避けて合成・包含を優先するように設計を行っていくと,最終的に酷く面倒なことになるケースが多い。継承を避けるのはストイックで安全なやり方かもしれないけれど,なぜ楽にできることを楽にしないのだろうという,妙な矛盾と直面することになる。

継承を禁忌すること (Radium Software)

「悪しき習慣であるが、すごく便利だ」というのは、 OOPL のプログラミング言語としての位置付けを端的に言い表しているのではないでしょうか。論理的な明快さを優先するなら、関数型が勝るはずであるわけでして、 OOPL の存在意義というのは、まさにその悪しき便利機能にあるのではないですかね。

すぐに思い出せるだけでもWadlerとかXiとか、「オブジェクト指向は駄目」と言っている(のを私が聞いたことがある)プログラミング言語研究者は少なくありません。

オブジェクト指向と私 (sumiiの日記)

プログラミング言語のアカデメイアでは、 OOPL はあまり好まれてはいないそうで。

| | コメント (0) | トラックバック (0)

2008年7月11日 (金)

Celestia スクリプト ― カメラの位置と方向 (Part 2)

簡単に解説をします。

座標系 "Ecliptical (Follow)"

”Celestia .CEL Scripting Guide"1 に説明があります。

The X axis points away from the Sun in the direction of the Julian 2000.0 vernal equinox. The Y axis is normal to the ecliptic with positive Y north. The Z axis completes the right-handed coordinate system.

”Celestia's Coordinate Systems”, p.12

すなわち :

  • X軸は春分点の方向
  • Y軸は黄道面の法線で、北の方向が正
  • Z軸は右手系をなすように定める

XZ 平面が黄道面になる黄道座標みたいですね。

位置

”Celx Objects and Methods”2 によりますと :

  newposition (x, y, z)
      Creates a new position object, from numbers or from URL-style Base64-
      encoded values.
        x, y, z: The components of the new position. Either as numbers (unit
                 is microlightyears), or as x, y, z string-values taken from
                 a cel-style URL.

ここで、”microlightyears” とはいかなる単位なのかといいますと、以下のようにあります。

MLY used as a DISTANCE:
   One MLY = 9,466,411.842 km. Define a constant of this value (ie. KM_PER_MLY
   = 9466411.842) and use it in your scripts where you need to convert from/
   to km/MLY.

方向

同じく、以下のようにあります。

 newrotation (axis, w) -OR- (w, x, y, z)
      Creates new rotation object (i.e. a quaternion).
           axis: Vector describing the axis of this rotation.
              w: The angle of this rotation (for details check out
                 quaternions).
      Or ...
        w,x,y,z: Number values for this quaternion.

ベクトル・角度(ラジアン)か、もしくは、 四元数(しげんすう、quaternion;クオータニオン) で回転/方向を指定するようです。 方向を指定する (setorientation) 場合、ゼロ度は Z 軸の反対方向、 XZ 平面に平行となるようです。

以下のスクリプトを実行して、結果を確かめます。 X 軸上に太陽の光が当たっていること、 Z軸上では角度がゼロとなること、などに着目していただければ。

-- test02.celx

KM_PER_MLY = 9466411.842

myearth = celestia:find("Earth")
celestia:select(myearth)
distance = myearth:radius() * 10.0 / KM_PER_MLY

myobserver = celestia:getobserver()
myframe = celestia:newframe("ecliptic", myearth)
myobserver:setframe(myframe)
myobserver:follow(myearth)

deftim = celestia:gettime()
defort = myobserver:getorientation()
defpos = myobserver:getposition()

celestia:print("test02", 3)
wait(3)

-- 春分
celestia:settime(celestia:tojulianday(2008, 3, 20, 0, 0, 0.0))

-- x軸上にカメラ移動
mypos = celestia:newposition(
  distance,
  0.0,
  0.0)
myobserver:gotolocation(mypos, 3)
wait(3)

celestia:print("shot 1", 3)
wait(3)

-- 地球中心方向へカメラ向き設定
myrot = celestia:newrotation(celestia:newvector(0.0, 1.0, 0.0), math.pi * -0.5)
myobserver:setorientation(myrot)
wait(1)

celestia:print("shot 2", 3)
wait(3)

-- z軸上にカメラ移動
mypos = celestia:newposition(
  0.0,
  0.0,
  distance)
myobserver:gotolocation(mypos, 3)
wait(3)

celestia:print("shot 3", 3)
wait(3)

-- 地球中心方向へカメラ向き設定
myrot = celestia:newrotation(celestia:newvector(0.0, 1.0, 0.0), 0.0)
myobserver:setorientation(myrot)
wait(1)

celestia:print("shot 4", 3)
wait(3)

-- y軸上にカメラ移動
mypos = celestia:newposition(
  0.0,
  distance,
  0.0)
myobserver:gotolocation(mypos, 3)
wait(3)

celestia:print("shot 5", 3)
wait(3)

-- 地球中心方向へカメラ向き設定
myrot = celestia:newrotation(celestia:newvector(1.0, 0.0, 0.0), math.pi * 0.5)
myobserver:setorientation(myrot)
wait(1)

celestia:print("shot 6", 3)
wait(3)

-- 元に戻す
celestia:settime(deftim)
myobserver:setorientation(defort)
myobserver:setposition(defpos)

celestia:print("fin", 1)
wait(1)

実行結果は以下のとおりです。

Test0200

Test0201

Test0202

Test0203

Test0204

Test0205

Test0206

| | コメント (0) | トラックバック (0)

2008年7月 9日 (水)

Celestia スクリプト ― カメラの位置と方向 (Part 1)

天文シミュレータ Celestia では、スクリプトを使ってカメラを移動させることができます。使えるスクリプトが2種類ありまして:

  • CELスクリプト (.cel)
  • CELXスクリプト (.celx)

前者のCELスクリプトというのは、コマンドを逐次に並べるような形になっており、プログラミングの機能はないようです。ただ、書いたコマンドが順に実行されるだけです。

後者のCELXスクリプトの方は、 Lua 言語エンジンが使われていて、変数、制御文、関数など、一通りのプログラミング機能が使えます。

CELXスクリプトで、カメラの位置と方向の設定を実験してみました。

-- test01.celx

KM_PER_MLY = 9466411.842

myearth = celestia:find("Earth")
celestia:select(myearth)
distance = myearth:radius() * 10.0 / KM_PER_MLY

myobserver = celestia:getobserver()
myframe = celestia:newframe("ecliptic", myearth)
myobserver:setframe(myframe)
myobserver:follow(myearth)
defort = myobserver:getorientation()
defpos = myobserver:getposition()

-- カメラ移動
mypos = celestia:newposition(
  distance,
  0.0,
  0.0)
myobserver:gotolocation(mypos, 3)
wait(3)
celestia:print("shot 1", 3)
wait(3)

-- カメラ向き設定
myrot = celestia:newrotation(celestia:newvector(0.0, 1.0, 0.0), math.pi * -0.5)
myobserver:setorientation(myrot)
wait(1)
celestia:print("shot 2", 3)
wait(3)

-- 元に戻す
myobserver:setorientation(defort)
myobserver:setposition(defpos)

celestia:print("fin", 1)
wait(1)

結果は以下のようになります。

Test0100

Test0101_2

Test0102_2

| | コメント (0) | トラックバック (0)

2008年6月15日 (日)

「かぐや(SELENE)」 on Celestia - Part4

ようやくプログラミングへと辿り着きました。

が、実際のところ、やることはほとんどないのですよね。色々と調べてわかったのは、「かぐや」軌道データは、ほぼそのまま Celestia で使える、ということでして。プログラミングといっても、「かぐや」軌道データファイルを Celesita の XYZファイルへと変換するだけです。

>ruby traj2xyz.rb KAGUYA_Traj.txt >selene_jaxa.xyz

traj2xyz.rb は、以下のとおりです。

require 'date'

ARGF.each do |line|
  if line =~ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\S+\s+(\S+)\s+(\S+)\s+(\S+)/
    dt = DateTime.new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i)
    printf "%#.5f %#.4f %#.4f %#.4f\n", dt.ajd.to_f, $7.to_f, $8.to_f, $9.to_f
  end
end

KAGUYA_Traj.txt は「かぐや」軌道データを時間順にすべてマージしたものでして、例えば Windows コマンドプロンプト では以下のように作成します。

>type KAGUYA_Traj\0709140216-0709142013.txt >KAGUYA_Traj.txt
>type KAGUYA_Traj\0709142013-0709152300.txt >>KAGUYA_Traj.txt
>type KAGUYA_Traj\0709152300-0709180000.txt >>KAGUYA_Traj.txt
>type KAGUYA_Traj\0709180000-0709190056.txt >>KAGUYA_Traj.txt
【以下略】

| | コメント (0) | トラックバック (0)

「かぐや(SELENE)」 on Celestia - Part3

「かぐや」軌道データについてまとめます。

データ形式

「かぐや」軌道データの形式については、JAXAより以下のように情報が提供されています。

CCSDS勧告文書 ”CCSDS 502.0-B-1 RECOMMENDATION FOR SPACE DATA SYSTEM STANDARDS”1 をみますと、以下のようにあります。

3.2 OPM CONTENT

The OPM shall be represented as a combination of the following:

a) a header; b) metadata (data about data); c) optional comments (explanatory information); and d) data.

CCSDS 502.0-B-1 Page 3-1

OPM というのは、 ”ORBIT PARAMETER MESSAGE” の略です。ヘッダ、メタデータ、コメント、そしてデータ行から成るものであるようです。

Keyword Description Units Obligatory
EPOCH Epoch of state vector & optional Keplerian elements n/a Yes
X Position vector X-component KM Yes
Y Position vector Y-component KM Yes
Z Position vector Z-component KM Yes
X_DOT Velocity vector X-component KM/S Yes
Y_DOT Velocity vector Y-component KM/S Yes
Z_DOT Velocity vector Z-component KM/S Yes

データ行は、時点、位置ベクトル [x, y, z]、 速度ベクトル [x', y', z'] から成るものであるようです。

「かぐや」軌道データ

「かぐや」軌道データの内容について調べます。メタデータの部分をみると、データ行の意味がわかるようになってるようです。

まず時刻(EPOCH)ですが、[かぐや」軌道データでは、以下のようになっています。

TIME_SYSTEM = UTC

UTC というのは、 ”Coordinated Universal Time” の略であるようです。

It is tied to TAI by an offset of integer seconds (called seconds (called 'leap seconds'), which is regularly updated to keep UTC in close agreement with UT1 (within 0.9s).

”CCSDS 500.0-G-2 NAVIGATION DATADEFINITIONS AND CONVENTIONS” Page 4-102

いわゆる、世界時(Universal Time) であると考えて良さそうです。

座標系について、「かぐや」軌道データでは、以下のようになっています。

CENTER_NAME = EARTH
REF_FRAME = EME2000

EME2000 というのは、 ”Earth Mean Equator and Equinox of J2000” の略であるようです(CCSDS 502.0-B-1 Page 3-3)。この座標系がどんなものか、説明した資料を色々と探したのですが、適当なものがみつかりません。 Google で検索するとメモ的なものは出てきます。

The Earth Mean Equator and Equinox of Epoch J2000 inertial reference system is a right-handed Cartesian set of three orthogonal axes chosen as follows:

  • +ZEME2000 is normal to the Earth mean equator at epoch J2000
  • +XEME2000 is parallel to the vernal equinox of the Earth mean orbit at J2000
  • +YEME2000 completes the right-handed system.

The epoch J2000 is the Julian Ephemeris Date (JED) 2451545.0.

JPL Interoffice Memorandum 15 July 1999 312.B/015-993

ユリウス暦2000年時点で、

  • z方向は赤道面からの北極への垂線
  • x方向は赤道面から春分点への平行線
  • y方向は右手系による

ということのようですね。

原点はどこになるか

さて、以上でだいたいのところは調べたのですが、最後に「かぐや」軌道データの原点がどこにあるのか、確認してみたいと思います。おそらく、地球の中心であると思うのですが、資料上でははっきりしませんでしたので。

「かぐや」軌道データの一番最初の時点での、位置ベクトルの長さを計算してみます。

>ruby vectsize.rb 3.381801563861468E+03 -4.822172015202145E+03 -3.237785869454149E+03
6721.09976848816

地球の半径が、 6378.142 km ですから、この時点で地上から 300 km ほどのところにいることになります。原点を地球中心にとっても良さそうですね。

なお、 vectsize.rb は以下のとおりです。

include Math

x = ARGV[0].to_f
y = ARGV[1].to_f
z = ARGV[2].to_f

puts sqrt(x**2 + y**2 + z**2)

ついでに、1時間ごとに、位置ベクトル、速度ベクトルそれぞれの長さを計算してグラフにしてみました。

07

月と地球の間の距離は 38万4,400km だそうです。4 地球のまわりを2回周り、段々と遠ざかりつつ、月へと向かう様子がわかります。また、地球に近づくほど速度が上がるのは、万有引力の法則どおりですね。


1. http://public.ccsds.org/publications/archive/502x0b1.pdf

2. http://public.ccsds.org/publications/archive/500x0g2.pdf

3. http://ntrs.nasa.gov/archive/nasa/casi.ntrs.nasa.gov/20010056226_2001089749.pdf

4. 月 (Wikipedia)

| | コメント (0) | トラックバック (0)

2008年6月14日 (土)

「かぐや(SELENE)」 on Celestia - Part2

Celestia に天体の軌道データをどう入力するか、という点についてまとめます。「かぐや」の軌道データを入力するために、必要最低限のことだけを調べました。

Celestia のデータファイル

以下の種類があるようです。

  • STC (STar Catalog) files tell Celestia where to draw stars.
  • DSC (Deep Space Catalog) files tell Celestia where to draw Nebulas, OpenClusters and Galaxies.
  • SSC (Solar System Catalog) files tell Celestia where to draw planets.
  • XYZ and Spice trajectory files tell Celestia where to draw SSC or STC objects. They're an alternative to using EllipticalOrbit definitions.

    A (not so) Brief Introduction to Celestia Addons (3rd edition)

太陽系内の天体については、 ”SSC (Solar System Catalog) files” を使えば良いようですね。

SSCファイル

Celestia includes its own SSC files in its \data\ directory to define the solar system: all of the planets and many of their moons, along with asteroids, comets and a few spacecraft. The solar system's planets and moons are defined in the file named solarsys.ssc in Celestia's data directory.

A (not so) Brief Introduction to Celestia Addons (3rd edition)

SSCファイルは、 %CELESTIA_HOME%\data 以下にあるようです。例えば、国際宇宙ステーション(ISS)は、 spacecraft.ssc 内で、以下のように定義されています。

"ISS" "Sol/Earth"
{
        Class "spacecraft"
        Mesh "iss.3ds"
        Radius 0.040
        Beginning           2451138    # Zarya module launched 20 Nov 1998
        # Ending ????

        EllipticalOrbit {
                Period          0.064176392
                SemiMajorAxis   6767
                Eccentricity    0.0016886
                Inclination      51.5684
                AscendingNode   343.1518
                ArgOfPericenter 346.2476
                MeanAnomaly      13.8216
                Epoch           2452028.18381755
        }

        UniformRotation
        {
        Inclination    51.5684        #
        MeridianAngle  90             # orientation corrections by Matt McIrvin
        AscendingNode 343.1518        #
        }

        Albedo        0.10
}

”SSC-Scripting Guide (Version 1.0)”1 によりますと、 ”EllipticalOrbit” 要素により、地球中心からの極座標で、天体の位置・速度が定義されているようです。なお、地球中心が原点となるのは、先頭で "Sol/Earth" が指定されているからで、地球以外の惑星や太陽を原点とすることもできるようです。

「かぐや」軌道データは、 [x, y, z] のデカルト座標系によって与えられています。この場合、 ”EllipticalOrbit” 要素の代わりに、 ”XYZ and Spice trajectory files” が使えるようです。

spacecraft.ssc 内より例を探すと、 ”Cassini” の ”SampledOrbit” 要素に XYZファイルが指定されています。

"Cassini" "Sol"
{
        Class "spacecraft"
        Mesh "cassini.3ds"
        Radius 0.011

        InfoURL "http://saturn.jpl.nasa.gov/home/index.cfm"

        Beginning 2450736.893877314 # 1997 Oct 15 09:27:11

        SampledOrbit    "cassini.xyz"

        FixedRotation { }
}

”Radius”要素は、天体の半径をキロメートル単位で指定するようです。”Beginning”要素は、天体軌道の開始時点を、ユリウス日で指定するようです。

XYZファイル

cassini.xyz ファイルを見ると、以下のような形式となっています。

2450736.8939 138193070 56107309 -424
2450736.8946 138192732 56108444 -386
2450736.9008 138189870 56119338 35
【以下略】

カラム一番目がユリウス日で、二番目以降は、 [x, y, z] とデカルト座標系での天体の位置をキロメートル単位で指定したものであるようです。

  • The xy plane is the planet's equatorial plane at the reference epoch J2000 (For the Earth it is the orbital plane on Jan 1, 2000).
  • The x axis points at the ascending node of the equatorial plane on the ecliptic.
  • The z axis is perpendicular to the xy plane and points to the planet's north pole .
  • The y axis completes the right handed coordinate system.

    Guide to Celestia Data Files by Thomas Guilpain 2

座標系は、ユリウス暦2000年時点で考え、

  • xy平面は赤道面
  • x方向は、惑星中心より赤道と黄道が交わる点(春分点)へ向かう方向
  • z方向は、惑星中心より北極方向
  • y方向は、右手系をなすようにとる

となるようです。


1. http://www.celestiamotherlode.net/creators/bhegwood/SSCGuide.zip

各種のドキュメントは、以下にあります。

http://www.celestiamotherlode.net/catalog/documentation.html

2. 以下ですが、広告がうるさいです。間違って広告をクリックしないよう気をつけてください。

http://members.fortunecity.com/guilpain/Fichiers%20xyz_uk.htm

| | コメント (0) | トラックバック (0)

2008年6月13日 (金)

「かぐや(SELENE)」 on Celestia - Part1

Celestia というのは、オープンソースの3D天文シュミレータでして、以下のサイトからダウンロードできます。

Celestia Home

JAXA のサイトで、月周回衛星「かぐや(SELENE)」の軌道データが公開されています。このデータを使って、 Celestia で「かぐや」の軌道をシミュレートしてみよう、と思い立ちました。

月周回衛星「かぐや(SELENE)」の軌道データの提供について (JAXA宇宙教育センター)

作ったものはこちらです。 Celestia の Add on の形になっています。

selene_jaxa.zip (833.9K)

Celestia のインストール

インストールといっても、 上記サイトからダウンロードした、Windows用のインストーラを使ってインストールするだけです。ただ、 Celesita は、3Dの描画を OpenGL ライブラリで行なっており、この点は注意が必要です。つまり、インストール先PCに搭載されている、ビデオカードとそのドライバが、 Celestia の要する OpenGL ライブラリ機能をサポートしていない可能性があります。

私の試した環境は、以下のとおりです。

バージョン Celestia 1.5.1 (Windows)
OS Windows XP
PC TOSHIBA dynabook Qosmio E10/1KLDEW
ビデオカード NVIDIA GeForce FX Go5200

最初に上記環境で、 Celesita を起動しますと、 "Driver component sizes mis-match" と、 OpenGL ドライバのエラーが出て、起動できませんでした。予想がつきますが、ビデオカード関係は、FAQのいの一番であるようです。

Q1:

Celestia crashes, what it draws is messed up or it's extremely slow. What can I do?

A1: Celestia makes use of the most advanced features of OpenGL that the graphics driver claims to support. Unfortunately, many OpenGL implementations have serious bugs in the new code for those advanced features.

Therefore, the very first thing to do is to

a) Upgrade to the most recent drivers for your graphics card.

A preliminary Celestia User's FAQ

ということですので、 ”the very first thing to do” の ”a)” に従い、ドライバのアップデートを行なうことにしました。

dynabook Qosmio E10/1KLDEW, E10/1KCDE ディスプレイドライバ(NVIDIA GeForce FX Go5200)のアップデート

私の環境では、これで起動するようになりました。

「かぐや」軌道データ のインストール

”selene_jaxa.zip” ファイルをダウンロードし、展開しますと、 ”selene_jaxa” というフォルダになります。このフォルダを、(その中身ごと)以下のファイルパス以下にコピーします。

%CELESTIA_HOME%\extras

なお、"%CELESTIA_HOME%"は、 Celestia をインストールしたフォルダです。

フォルダをコピーした後、 Celestia を起動すると、 「かぐや」軌道データが読み込まれます。 メニュー ”Navigation” -> ”Solar System Browser” を実行して、 ”Solar System Objects” -> "Earth" を展開すると、 "Selene" というオブジェクトが見つかります。これが「かぐや」です。

01

スクリーンショット

「かぐや」軌道データは、 2007年9月14日 02:16(UTC) より始まります。日本時間では、9時間プラスですから、11:16 ですね。ちなみに、打ち上げ時刻は、 10:30 です。

以下はこの時点の画面です。わかりにくいかもしれませんが、 中心にある赤い丸が 「かぐや」です。

02

太陽電池パドル展開。 2007年9月14日 11:44 (日本時間)。

03

周期調整マヌーバ1回目。 2007年9月19日。

04

月周回軌道投入マヌーバ。 2007年10月4日 06:20 (日本時間)。

05

月周回軌道への投入。 2007年10月19日。

06

| | コメント (0) | トラックバック (0)

2008年5月24日 (土)

動的言語 vs 静的言語

海の向こうでも議論が続いているようですね。

Debate and more Insights on Dynamic vs. Static Languages (InfoQ)

思うに、「プログラミングの問題」というのは、基本的に計算機の性能の問題なんですよね。例えば、並列処理は何のためにあるのか? といえば、計算機をより効率的に使うためにあるわけでして。動的言語と静的言語というのも、計算機の性能とのトレードオフというのが、根底にはあるわけです。

コンパイル時に型をチェックして、かつ、記述が煩雑にならないよう、型指定を省略できる、でも指定したいときには指定もできる、というような言語を作ることも理屈上は可能でしょう。それで、かつコンパイル時間と、目的コードの実行時間も、”実用上十分なほど”早くするとなると、実現は難しくなってくるわけで、ここで、言語機能の何かをトレードオフしなければならなくなってくるわけですね。何をトレードオフしたか、ということで”動的言語”と”静的言語”の分類が生じているのでしょう。

実際に色々な言語でプログラミングをしていると、型指定や型チェックが欲しいと思うこともありますし、型をいちいち指定するのが煩わしいと思うこともあります。もちろん、達成したい性能目標との兼ね合いもあります。

These languages are all useful, for different things. A good programmer uses his common sense to provide the best value possible. That includes choosing the best language for the job. If Ruby allows you to provide functionality 5 times faster than the equivalent functionality with Java, you need to think about whether this is acceptable or not. On the one hand, Java has IDEs that make maintainability easier, but with the Ruby codebase you will end up maintaining a fifth of the size of the Java code base. Is that trade off acceptable? In some cases yes, in some cases no. In many cases the best solution is a hybrid one.

両方の特徴を兼ね備えた言語が1つあれば、理想的ではあるわけですが。それがかなわない以上、上の意見が妥当でしょうね。シーンに応じて、言語を使い分けるしかないでしょう。

| | コメント (0) | トラックバック (0)

2008年5月22日 (木)

スケジュールの重なりを判定する問題

以下のように、作業A と 作業B があり、 それぞれに、開始日、終了日のペアが与えられている場合に、この2つの作業のスケジュールが重なるか否かを判定する問題を考えます。

作業 開始日 終了日
A 5/1 5/20
B 4/1 5/3

 

解答

2つのスケジュールが 重ならない 場合というのは、以下の式になりますね。

終了日B < 開始日A ∨ 終了日A < 開始日B

よって、2つのスケジュールが重なる場合は、上の否定になります。これを Ruby で書いたものが以下です。

def overlapped?(x1, x2, y1, y2)
  not (y2 < x1 or x2 < y1)
end

Aの 開始日, 終了日 を、 x1, x2 、 Bの 開始日, 終了日 を y1, y2 としています。

検証

以前、以下のエントリで作成しました、Rubyスクリプトを使用します。

順序関係列挙と事前条件

まずは、今回の問題で起こり得る論理式を、すべて列挙してみます。

irb(main):021:0> i = 0; apply_cond(gen_norder([:x1, :x2, :y1, :y2]), [:x1, :x2,:y1, :y2], ['x1<=x2', 'y1<=y2']).each {|v| print("#{i += 1}:"); p v }; nil
1:[:x1, :x2, :y1, :y2]
2:[:x1, :y1, :x2, :y2]
3:[:x1, :y1, :y2, :x2]
4:[:y1, :x1, :x2, :y2]
5:[:y1, :x1, :y2, :x2]
6:[:y1, :y2, :x1, :x2]
7:[[:x1, :x2], :y1, :y2]
8:[:y1, [:x1, :x2], :y2]
9:[:y1, :y2, [:x1, :x2]]
10:[[:x1, :y1], :x2, :y2]
11:[[:x1, :y1], :y2, :x2]
12:[:y1, [:x1, :y2], :x2]
13:[:x1, [:x2, :y1], :y2]
14:[:x1, :y1, [:x2, :y2]]
15:[:y1, :x1, [:x2, :y2]]
16:[[:y1, :y2], :x1, :x2]
17:[:x1, [:y1, :y2], :x2]
18:[:x1, :x2, [:y1, :y2]]
19:[[:x1, :x2, :y1], :y2]
20:[:y1, [:x1, :x2, :y2]]
21:[[:x1, :y1, :y2], :x2]
22:[:x1, [:x2, :y1, :y2]]
23:[[:x1, :x2, :y1, :y2]]
=> nil

全部で23個ありますね。

上の論理式で表される、各々のケースについて、 overlapped? を評価した結果を付与したいと思います。

def gen_implication(src_a, cond_a)
  res_a = []
  src_a.each do |exp|
    res_a.push([exp, cond?(exp, cond_a)])
  end
  res_a
end

ついでに、シンボルの配列として表現されている論理式を、文字列に変換するメソッドも用意しておきます。

def print_exp(exp)
  res = ''
  exp.each do |term|
    res += ' < ' if res != ''
    if term.instance_of?(Array)
      res += "#{term.join(' = ')}"
    else
      res += "#{term}"
    end
  end
  res
end

以下を実行して、評価結果を得ます。

irb(main):022:0> i = 0; gen_implication(apply_cond(gen_norder([:x1, :x2, :y1, :y2]), [:x1, :x2, :y1, :y2], ['x1<=x2', 'y1<=y2']), ['overlapped?(x1, x2, y1, y2)']).each {|v| puts "#{i += 1} : #{print_exp(v[0])} : #{v[1]}" }; nil
1 : x1 < x2 < y1 < y2 : false
2 : x1 < y1 < x2 < y2 : true
3 : x1 < y1 < y2 < x2 : true
4 : y1 < x1 < x2 < y2 : true
5 : y1 < x1 < y2 < x2 : true
6 : y1 < y2 < x1 < x2 : false
7 : x1 = x2 < y1 < y2 : false
8 : y1 < x1 = x2 < y2 : true
9 : y1 < y2 < x1 = x2 : false
10 : x1 = y1 < x2 < y2 : true
11 : x1 = y1 < y2 < x2 : true
12 : y1 < x1 = y2 < x2 : true
13 : x1 < x2 = y1 < y2 : true
14 : x1 < y1 < x2 = y2 : true
15 : y1 < x1 < x2 = y2 : true
16 : y1 = y2 < x1 < x2 : false
17 : x1 < y1 = y2 < x2 : true
18 : x1 < x2 < y1 = y2 : false
19 : x1 = x2 = y1 < y2 : true
20 : y1 < x1 = x2 = y2 : true
21 : x1 = y1 = y2 < x2 : true
22 : x1 < x2 = y1 = y2 : true
23 : x1 = x2 = y1 = y2 : true
=> nil

感想とか考察とか

このネタはずいぶん前に仕込んでいたのですが、これって何か意味あるのかなあ、と悩んでしまいまして。つまり、「解答」と「検証」の間に、何か本質的な違いがあるのかと。同じことの、単なる言い換えにすぎないようにも思えるのですよね。

しかしまあ、同じことを違う表現にしてみて、結果が同一になることを確かめる、というのは検証の本質ですかね。例えば、テストなんてのは、抽象化された形で表現されたプログラムに対し、具体的な入力出力の組で表現されたテストケースによって、プログラムの意図と、テストケースが一致するかを確かめるわけですしね。その意味では、上で行なっている検証というのは、具体的な値を使ったテストケースと、プログラムそのものとの中間表現にあたりますかね。

あまり便利という感じがしないのが残念ですが。結局、上のプログラムも検証も、”重なり”という概念の定義を確かめる以上のものではないわけでして、これ以上の簡単化はなさそうにも思えますね。

| | コメント (0) | トラックバック (0)

2008年5月14日 (水)

Java言語の位置

もともと、Java言語が狙っているのは、 C/C++ のポジションだと思うのですよね。確か、当初は組み込み分野で使うことを考えていたのではなかったか、と以下の記事を読んでいて思い出したわけですが。

Real-time response is a requirement in Java application domains such as banking, online collaboration, and games development, as well as for safety critical applications used in hospitals, manufacturing, aviation, and emergency response systems. Unfortunately, the Java platform has long suffered from its erratic response time. Java applications are known to freeze because the garbage collector has "stopped the world." Incremental garbage collection (-Xincgc) improves this situation, but it does not completely eliminated the nasty "full GC" pauses associated with the Java platform.

To further complicate things, Java program execution also can suffer from unexpected delays caused by Just-In-Time (JIT) compilation, class initialization, or standard utility collections internally resizing.

Realistically real-time: Real-time Java application development using multicore systems (Java World)

言語設計者の意図に反して、Java言語は、むしろ、エンタープライズ分野やWebアプリで広く使われるようになったわけですが。Java言語が目指している位置は、システムプログラミング言語であって、エンタープライズ分野とWebアプリは、Java言語が目指していた方向とは違うのではないか、と思うわけです。

こんなことは Java ではいたるところに存在していて、たとえばテキストファイルひとつ読み込むのも FileInputStream と InputStreamReader (と BufferedReader) を組み合わせて書かないといけない。だいたい、なんで FileReader では文字コードを指定できないのか、理解に苦しむ。

「怠慢はプログラマの美徳」というけれど (kwatchの日記)

FileReader で文字コードが指定できない理由というのは、 もともと”文字を読む”という発想がなかったからでしょうね。 『プログラミング言語 Java 第4版』 ”20.3.3 文字ストリームと標準ストリーム” によりますと:

標準ストリーム System.in、 System.out、 System.err は、文字ストリームが導入される前から存在しています。したがって、論理的には文字ストリームであるべきですが、標準ストリームはバイトストリームです。

p.448

もともと、Java API には、文字ストリームは存在せず、バイトストリームしかなかったみたいですね。このあたりに、InputStream と Reader との関係が煩雑になった事情がありそうです。 言語設計者の意識は、テキスト・データより、バイナリ・データの方に向いているように思えますね。

Java言語の設計者は、エンタープライズ分野やWebアプリには、あまり興味はなさそうな感じですよね。今後も、Java言語が、エンタープライズ分野やWebアプリに特化する、ということはないでしょうね。やはり、システムプログラミング言語を目指し続けるのではないでしょうか。

| | コメント (0) | トラックバック (0)

2008年5月10日 (土)

ベンチマークなんて必要ですか?

適当な意見だなー、そのシンタックスシュガーとやらをなくすとコンパイル時間が何ミリ秒速くなるんだよ。絶対ベンチ取らずに思いつきで言ってる

http://twitter.com/todesking/statuses/806986776

まあ、適当なことを言っている、というのは否定しませんけど。

言語仕様がシンプルな方が、コンパイル時間を短くできる、というのは一般論としてあるわけでして、別に私のオリジナルな思いつき、というものではないですよ。

翻訳過程での込み入った複雑さを少なくできる唯一の方法は、明確に定義され構造が整っているソース言語を選択することである。

二クラウス・ヴィルト 著, 滝沢徹・牧野祐子訳, 『コンパイラ構成法』, アジソン・ウェスレイ, 1997, p.1

原始プログラムを効率のよい目的プログラムに翻訳したいだろうし、翻訳の過程そのものも効率化したいだろう。どちらの場合にも、言語の設計が、それらの計算がどの程度簡単に行なえるかということに影響を及ぼし得る。

A.V.エイホ ・ J.D.ウルマン著, 土居範久 訳, 『コンパイラ』, 倍風館, 1986, P.25

確か、C言語の設計思想について、もっと明確に、コンパイラに楽をさせるためである、としていた本があったと思うのですが。ベル研究所の関係者が書いたもので。すみませんが、何だったか、思い出せないし、今本棚探しても見つかりません。

ベンチマークなのですが。言語設計の違いが、コンパイル時間に有意な差をもたらすかどうか、などというような研究は、私の手には負えません。といいますか、個人の力では無理でしょう。ですので、以下に示すものも”超適当”なもので、何の証拠にもなりませんよ、と前おきさせていただきまして。

Java言語の拡張for文と、従来のfor文で、コンパイル時間に差がつくか、を試してみました。もともと、Java言語には、シンタックス・シュガーなるものはほとんど存在しないわけですが(それが設計思想だと思います)、『Java言語仕様 第3版』によりますと:

拡張for文の意味は、基本for文へと変換することによって与えられる。

p.343

とありますので、拡張for文はシンタックス・シュガーである、と考えて良さそうです。

でまあ、以下のような Rubyスクリプトで、10,000個のソースファイルを2セット作りました。 Test1 は拡張for文、 Test2 は基本for文です。

#genTest1.rb
(1..10000).each do |i|
File.open("01/src/Test#{i}.java", "w") do |out|
out.print <<EOS
public class Test#{i} {

  public void test() {
    int[] a = new int[101];
    for (int i = 0; i < a.length; i++)
      a[i] = i;
    int sum = 0;
    for (int i : a)
      sum += i;
    System.out.println(sum);
  }

}
EOS
end
end

#genTest2.rb
(1..10000).each do |i|
File.open("02/src/Test#{i}.java", "w") do |out|
out.print <<EOS
public class Test#{i} {

  public void test() {
    int[] a = new int[101];
    for (int i = 0; i < a.length; i++)
      a[i] = i;
    int sum = 0;
    for (int i = 0; i < a.length; i++)
      sum += i;
    System.out.println(sum);
  }

}
EOS
end
end

こうやって作ったソースファイルを、Antを使ってコンパイルしてみます。 使った build.xml は以下のとおり。

<project name="MyProject1" default="compile" basedir=".">

<target name="clean">
<delete>
  <fileset dir="bin" includes="*.class" />
</delete>
</target>

<target name="compile">
  <javac srcdir="src" destdir="bin" />
</target>

</project>

javac と ant のバージョンは以下のとおり。

>javac -version
javac 1.5.0_14
>ant -version
Apache Ant version 1.7.0 compiled on December 13 2006

Test1(拡張for文) の結果です。

01>ant
Buildfile: build.xml

compile:
    [javac] Compiling 10000 source files to 01\bin

BUILD SUCCESSFUL
Total time: 1 minute 40 seconds

Test2(基本for文) の結果です。

02>ant
Buildfile: build.xml

compile:
    [javac] Compiling 10000 source files to 02\bin

BUILD SUCCESSFUL
Total time: 1 minute 30 seconds

ということで、拡張for文の方がコンパイル時間が10秒ほど遅くなりました。上の結果を出すのに、一応、”ウォーム・アップ”はしました(つまりそれぞれ2回ずつ実行した)。まあ、統計とって分析したわけではありませんので、この結果にたいした意味はないでしょうけど。

| | コメント (0) | トラックバック (0)

2008年5月 8日 (木)

言語の冗長さはコンパイラのためにある

もう、人間が機械にあわせる、という発想は時代遅れなのかもしれませんけど。

冗長さが除去されて言語が簡便になれば大規模開発にも役立ちます (矢野勉のはてな日記)

小クラス主義をとりますと、コンパイル単位を小さく出来る、という利点があります。コンパイル単位が小さくなれば、コードのどこかに手が入った際、他の依存するコンパイル単位も再コンパイルが必要、という事態を最小限に抑えることが期待できます。

コンパイラに型を推測させず、たとえ文脈から明らかな場合でも、型を人間が指示することで、コンパイラの負担を軽くすることができます。シンタックス・シュガーを用意せずに、人間がいちいちその”展開形”のコードを書くことで、コンパイラが余計な処理をせずにすむようになります。

大規模ソフトウエア開発では、ビルド・プロセスに何時間もかかるような、巨大なソフトウエアを開発するわけですよね。ある程度の負担を人間が引き受けることで、コンパイラの負担を軽くして、ビルドにかかる時間を短縮したい、というニーズはあるんじゃないですかね。例えば、JDKなり、Eclipseなりのソースコードを全てコンパイル/ビルドするのは、何時間かはかかる”重い”プロセスでしょうし、コンパイルが早くなれば嬉しいでしょう。

言語処理系の”性能”というのは、コンパイル後コードの実行時の性能だけではなく、コンパイル時の性能というのもありますから。コンパイラにシンタックス・シュガーを処理させるなら、その時間をコード最適化に使って欲しいというプログラマもいるでしょうね。

| | コメント (0) | トラックバック (0)

2008年3月24日 (月)

順序関係列挙と事前条件

さらに続きです。

4変数の順序関係はいくつあるか?

順序関係を列挙する

順序関係の列挙に、事前条件の加味を加えます。以下のように、事前条件を満たさないものを、列挙から除きます。

irb(main):002:0> i = 0; apply_cond(gen_norder([:x1, :x2, :x3, :x4]), [:x1, :x2,:x3, :x4], ['x1<x2', 'x2<x3', 'x3<x4']).each {|v| print("#{i += 1}:"); p v }; nil
1:[:x1, :x2, :x3, :x4]
=> nil

apply_cond メソッドは、第1引数に順序関係の配列、第2引数に結果に必ず含まれるべき変数の配列、第3引数に事前条件の配列を指定して呼び出します。

def apply_cond(src_a, req_a, cond_a)
  res_a = []
  src_a.each do |exp|
    if required_exist?(exp, req_a) &&
      cond?(exp, cond_a)
      res_a.push(exp)
    end
  end
  res_a
end

required_exist? メソッドは、順序関係 src_exp の中に、配列 req_a のシンボルがすべて存在するかをテストします。

def required_exist?(src_exp, req_a)
  target_a = src_exp.flatten
  req_a.each do |v|
    if !target_a.include?(v)
      return false
    end
  end
  true
end

cond? メソッドは、ちょっと強引ですが。まず、順序関係 src_exp の各変数に、配列の添字 + 1 を、 eval で代入します。その後、与えられた条件の配列 cond_a を順に eval します。

def cond?(src_exp, cond_a)
  init_exp = []
  src_exp.each_index do |i|
    v = src_exp[i]
    if v.instance_of?(Array)
      init_exp.push("#{v.join('=')}=#{i+1}")
    else
      init_exp.push("#{v}=#{i+1}")
    end
  end
  eval(init_exp.join(';'))
  cond_a.each do |cond|
    begin
      if !eval(cond)
        return false
      end
    rescue NameError
    end
  end
  true
end

事前条件 x1 < x3, x2 < x4 を与えてみます。

irb(main):003:0> i = 0; apply_cond(gen_norder([:x1, :x2, :x3, :x4]), [], ['x1<x3', 'x2<x4']).each {|v| print("#{i += 1}:"); p v }; nil
1:[:x1, :x2, :x3, :x4]
2:[:x1, :x2, :x4, :x3]
3:[:x1, :x3, :x2, :x4]
4:[:x2, :x1, :x3, :x4]
5:[:x2, :x1, :x4, :x3]
6:[:x2, :x4, :x1, :x3]
7:[[:x1, :x2], :x3, :x4]
8:[[:x1, :x2], :x4, :x3]

...

44:[:x1, :x2]
45:[:x2, :x1]
46:[[:x1, :x2]]
47:[:x4]
48:[:x3]
49:[:x2]
50:[:x1]
51:[]
=> nil

大体、条件をつけない場合の3分の1くらいでしたね。

| | コメント (0) | トラックバック (0)

2008年3月22日 (土)

順序関係を列挙する

先日のエントリ 4変数の順序関係はいくつあるか? の続きです。

順序関係を列挙するプログラムを、Ruby で作成してみます。

順列と組合せ

教科書のタイトルみたいですが。これが基本操作になりますので、初めに作成します。

順列を作るメソッド permutate を定義します。このメソッドは、配列 src_a と整数 rib_i を受け取って、順列の配列を戻します。これは、src_a から rib_i 個をとる順列を作ります。順に要素をひとつ取得し、取得した要素を除いた配列からまた取得、とすればよろしいかと思います。

def permutate_it(src_a, dst_a, rib_i, res_a)
  if rib_i == 0 || src_a.size == 0
    res_a.push dst_a
  else
    src_a.each do |src|
      permutate_it(
      src_a - [src], dst_a + [src], rib_i - 1, res_a)
    end
  end
  res_a.size
end

def permutate(src_a, rib_i)
  res_a = []
  permutate_it(src_a, [], rib_i, res_a)
  res_a
end

以下のように実行します。

irb(main):007:0> permutate([:x1, :x2, :x3], 2)
=> [[:x1, :x2], [:x1, :x3], [:x2, :x1], [:x2, :x3], [:x3, :x1], [:x3, :x2]]
irb(main):008:0> permutate([:x1, :x2, :x3], 2).size
=> 6

組合せを作るメソッド combine を定義します。順列メソッドとほぼ同じですが、組合せの場合、取得した要素よりも前の位置にある要素からは取得しません。常に次以降の要素から取ります。

def combine_it(src_a, dst_a, rib_i, res_a)
  if src_a.size < rib_i
    # nothing.
  elsif rib_i == 0 || src_a.size == 0
    res_a.push dst_a
  else
    src_a.each_index do |i|
      combine_it(
      src_a[i + 1, src_a.size - i - 1],
      dst_a + [src_a[i]], rib_i - 1, res_a)
    end
  end
  res_a.size
end

def combine(src_a, rib_i)
  res_a = []
  combine_it(src_a, [], rib_i, res_a)
  res_a
end

以下のように実行します。

irb(main):009:0> combine([:x1, :x2, :x3], 2)
=> [[:x1, :x2], [:x1, :x3], [:x2, :x3]]
irb(main):010:0> combine([:x1, :x2, :x3], 2).size
=> 3

不等号、等号、不存在

それぞれのメソッドを定義します。不等号の場合 case_inequal は、単に順列を作成するだけです。等号の場合は case_equal は、2個以上の組合せをまず作成し、元の配列からその要素を除き、1要素として追加した後、不等号の場合を適用します。不存在の場合 case_exist は、1個以上の組合せを作成し、元の配列から除いた後、不等号の場合、等号の場合をそれぞれ適用し、足し合わせます。

def case_inequal(src_a)
  permutate(src_a, src_a.size)
end

def case_equal(src_a)
  res_a = []
  (2 .. src_a.size).each do |r|
    combine(src_a, r).each do |c|
      p_a = [c] + src_a - c
      res_a += case_inequal(p_a)
    end
  end
  res_a
end

def case_exist(src_a)
  res_a = []
  (1 .. src_a.size).each do |r|
    combine(src_a, r).each do |c|
      p_a = src_a - c
      res_a += case_inequal(p_a)
      res_a += case_equal(p_a)
    end
  end
  res_a
end

すべてを列挙

等号、不等号、不存在の各メソッドを順に適用し、足し合わせます。

def gen_norder(src_a)
  res_a = case_inequal(src_a)
  res_a += case_equal(src_a)
  res_a += case_exist(src_a)
  res_a
end

以下が実行結果です。

irb(main):012:0> gen_norder([:x1, :x2, :x3, :x4]).size
=> 144
irb(main):013:0> i = 0; gen_norder([:x1, :x2, :x3, :x4]).each { |v| print("#{i += 1}:"); p v }
1:[:x1, :x2, :x3, :x4]
2:[:x1, :x2, :x4, :x3]
3:[:x1, :x3, :x2, :x4]

...

133:[[:x1, :x4]]
134:[:x1, :x3]
135:[:x3, :x1]
136:[[:x1, :x3]]
137:[:x1, :x2]
138:[:x2, :x1]
139:[[:x1, :x2]]
140:[:x4]
141:[:x3]
142:[:x2]
143:[:x1]
144:[]

配列の中に配列があるものは、その要素が”等しい”の意になります。

| | コメント (0) | トラックバック (0)

2008年2月22日 (金)

JDK API リファレンスのEclipseヘルプ化

とりあえずやってはみたのですが、便利かどうかは微妙です。

Eclipse_help_01

 

toc.xml の生成 (Java TOC Doclet)

JavaTOC ドックレットと Javadoc を実行すると、Java API リファレンス・マニュアル、目次 (TOC) ナビゲーション、そしてプラグイン構造を生成することができます。あるいは JavaTOC ドックレットだけを実行し、開発者から提供された既存のマニュアルから TOC ナビゲーションを生成することも可能です。

JavaTOC ドックレットを使って生成する Eclipse Javadoc API リファレンス構造 (IBM developerWorks)

”%JDK_HOME%\src.zip"を展開して、APIのソースコードより、Eclipseヘルプのplugin.xmlファイル、toc.xmlファイルを生成します。JDK APIのJavadocドキュメントは、Sunのサイトよりダウンロードしておきます。

コマンドは以下のようにしました。

javadoc -J"-Xmx512m" @config @options @package-list

config ファイル :

-doclet com.ibm.malup.doclet.config.TOCDoclet
-docletpath C:\JavaTOC\bin\TOCNavDoclet.jar

options ファイル :

-sourcepath ./src
-d ./output/java.doc.api
-overview ./docs/api/overview-summary.html
-version "1.5.0" -provider "Sun Microsystems"

package-list ファイル:

JDK API ドキュメントの"docs\api"フォルダにあるものを使用。

plugin.xml, toc.xml の編集

生成されるファイルなのですが、微妙に違っていて、そのままでは使えません。何箇所か手を入れます。

1. plugin.xml に、生成された toc.xml をすべて追加

Eclipseヘルプシステムは、plugin.xml に記述された toc.xml だけを認識するようです。生成された当初は、 primary.plugin.toc.xml しかありませんので、toc.xml をすべて追加します。

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.0"?>

<plugin>

  <extension point="org.eclipse.help.toc">

    <toc file="primary.plugin.toc.xml" primary="true"/>
    <toc file="java.applet.toc.xml" />
    <toc file="java.awt.color.toc.xml" />
    <toc file="java.awt.datatransfer.toc.xml" />
    <toc file="java.awt.dnd.toc.xml" />
    <toc file="java.awt.event.toc.xml" />

    <!-- 中略 -->     <toc file="org.xml.sax.ext.toc.xml" />
    <toc file="org.xml.sax.helpers.toc.xml" />
    <toc file="org.xml.sax.toc.xml" />
  </extension>

</plugin>

2. primary.plugin.toc.xml に ”anchor” 要素 を追加

id="java.packages" として ”anchor” 要素 を追加 します。

<?xml version="1.0" encoding="UTF-8"?>
<?NLS TYPE="org.eclipse.help.toc"?>

<toc label="Java 2 Platform Standard Edition 5.0 API Specification">
   <topic label="Overview" href="topics/overview-summary.html" />
   <topic label="Packages">
    <anchor id="java.packages" />
   </topic>
</toc>

3. "link_to" 属性のパスを編集

toc.xml ファイルの "toc"要素 "link_to"属性にあるパスを修正します。例えば、"java.applet.toc.xml"なのですが、なぜかパスが一段上になっています。パッケージのトップレベルに該当するファイルは全部こうなってますので、修正します。

<?xml version="1.0" encoding="UTF-8"?>
<?NLS TYPE="org.eclipse.help.toc"?>

<toc label="java.applet Package" link_to="../primary.plugin.toc.xml#java.packages">

<!-- 中略 --> </toc>

以下のように、パスを揃えます。

<?xml version="1.0" encoding="UTF-8"?>
<?NLS TYPE="org.eclipse.help.toc"?>

<toc label="java.applet Package" link_to="primary.plugin.toc.xml#java.packages">

<!-- 中略 --> </toc>

4. "build.properties" ファイル

"bin.includes"に必要なファイル・フォルダを含めます。

bin.includes = META-INF/,\
               plugin.xml,\
               primary.plugin.toc.xml,\
               java.applet.toc.xml,\
               java.awt.color.toc.xml,\

               ; 中略

               org.xml.sax.ext.toc.xml,\
               org.xml.sax.helpers.toc.xml,\
               org.xml.sax.toc.xml,\
               topics/

"topics"以下に、JDK API ドキュメントの"docs\api"以下をコピーします。

5. Eclipseプラグイン・プロジェクトでテスト・ビルド

Eclipseプラグイン・プロジェクトでテストを行なった後、 jar ファイルをエクスポートして完成です。

| | コメント (0) | トラックバック (0)

2008年2月16日 (土)

モンティ・ホール問題

モンティ・ホール問題(Monty Hall problem)というのは、確率の読み物では、必ずといっていいほど、取り上げられる問題のようですね。

この問題は「Let's Make a Deal」(1960年代〜1970年代に人気を博した)というクイズ番組で、よくあった場面として紹介されているが、今日のクイズ番組でもまだ見られる状況である。「Let's Make a Deal」の司会者はモンティ・ホールで、問題の名前の由来となっている。

クイズ番組の筋書きとして、問題は次のように進行する。まず、モンティが解答者に3つのカーテンを見せる。モンティはそれぞれのカーテンの後ろに何があるかを知っていて、そのうちの1つに新車(当たりの商品)が隠されていることを説明する。残り2つのカーテンは 無価値の商品 (モンティは zonks と呼ぶ)である。ロバや巨大なロッキングチェアのようなものや、まったく役に立たないようなものがZonkとして用意されている。モンティは解答者にカーテンから1つを選ばせ、解答者はカーテンに隠されたものを獲得する。仮に、カーテンAを選んだとすると、モンティは選ばれなかったカーテン(例:カーテンB)を開けて、それがZonkだったことを示す。ここでモンティは、出場者が最初に選んだカーテンAを、最後に残ったカーテンCに変更してもよいと言うが、変更すべきだろうか?

Bruce Frey 著, 西沢 直木 訳, 『Statistics Hacks』, オライリー・ジャパン, p.152

最初に選択したものを変更しない場合、当たりを得る確率が 1/3(約33%) となり、変更した場合には、 2/3(約67%) となります。つまり、”変更すべきである”というのが答えとなります。直観的には納得し難い答えですよね。実際、この問題がとりあげられた当時、哲学者・数学者によって、相当数の反論があったとか。

私もどうも不思議の感が捨てきれないので、実際に試してみることにしました。以下のRubyプログラムは、このゲームを10万回実行して、最初に選択したものを変更しない場合の当たり数、および変更した場合の当たり数を出力します。

# monty_hall.rb
first_count = 0 second_count = 0 doors = [0, 1, 2] (1 .. 100000).each do |i|
  hit = rand(3)
  first_sel = rand(3)
  other_doors = doors - [first_sel]
  door_open = other_doors - [hit]
  door_open.delete_at(rand(2)) if door_open.size > 1
  second_sel = (other_doors - door_open)[0]

  first_count += 1 if first_sel == hit
  second_count += 1 if second_sel == hit
end puts "#{first_count}:#{second_count}"

以下が実行結果です。

>ruby monty_hall.rb
33365:66635

実際に試してみても、ちゃんと 33% と 67% になりますね。でもやっぱり不思議。

・・・

Statistics Hacks ―統計の基本と世界を測るテクニック Book Statistics Hacks ―統計の基本と世界を測るテクニック

著者:Bruce Frey
販売元:オライリー・ジャパン
Amazon.co.jpで詳細を確認する


 

| | コメント (5) | トラックバック (0)

2008年2月15日 (金)

テスト駆動開発はユニット・テストを駄目にする?

InfoQ の記事より。

Peter Ritchie recently  raised concern about what he considers a tendency for adherence to TDD and BDD to keep practitioners from writing good unit tests. In particular, it is the mantra of "interaction testing" that he suggests has the effect of creating incomplete unit tests; tests that fail to show proof that a unit - an object - works under any conditions it could potentially be used.

InfoQ: TDD/BDD Leading To Incomplete Unit Tests?

テスト駆動開発 Test Driven Development(TDD)/Behavior Driven Development(BDD)が奨めるプラクティス − 最初に、オブジェクトのインターフェイスに対してユニット・テストを書き、テストが失敗するのを確認し、オブジェクトを実装し、テストが成功するのを確認する − は、ちゃんとしたユニット・テストを作るのを妨げている、という話のようですね。

確か、”Behavior Driven Development” というのは、”テスト駆動”という言葉では誤解を招く、TDDで作成しているのは、ユニット・テストではなく、ソフトウエア・インターフェイスの”実行可能な仕様(要求)”である、という主張であったと思います。テスト駆動で開発することのメリットとして、インターフェイス仕様を明確にできる、というのは最近よく耳にする主張かと思います。ソフトウエア・デザイン的な部分を強調する方向性ですね。

元記事(の元記事になりますが)が主張しているのは、そうして作成した、一連のユニット・テスト群は、それがテストであることを明確に意識しないで作成されるがために、 テストとしては無価値になるのではないか、ということのようでして。逆に”テストとしての価値”を問い直しているようです。

TDD派からはいろいろ突っ込みが入りそうです。ちょっと、無理筋な感は否めませんけども(「それとこれとは話が別」とかね)、 ユニット・テストというものの位置付けを考える上で、必要な視点だと思います。それがテストでないなら、テストはどこでやるのか、開発工程上どう位置づけるのか、”仕様としての”ユニット・テストと、”テストとしての”ユニット・テストは、それぞれ別と考えるべきなのか、同じと考えて良いのか、といったことです。テストとプログラミングを同時にやろうとすると、注意が散漫になり、結局テストが中途半端になる、というのはわかるような気もします。

しかし、元記事の元記事で、 Wikipedia BDDエントリ から以下のサンプル・コードを取り上げていますけど、これは、あまりフェアじゃないですね。

public class PrimeNumberCalculatorTests extends junit.framework.TestCase {
   public void testIfPrimeAfter100() {
      PrimeCalculator calculator = new EratosthenesPrimesCalculator(100);
      int result = calculator.nextPrime();
      assertEquals("First prime after 100 should be 101 but is " + result, 101, result);
   }

   public void testIfFirstPrime() {
      PrimeCalculator calculator = new EratosthenesPrimesCalculator();
      int result = calculator.nextPrime();
      assertEquals("First prime should be 2 but is " + result, 2, result);
   }

   public void testIfPrimeAfter683() {
      PrimeCalculator calculator = new EratosthenesPrimesCalculator(683);
      int result = calculator.nextPrime();
      assertEquals("First prime after 683 should be 691 but is " + result, 691, result);
   }

   public void testFirst10Primes() {
      PrimeCalculator calculator = new EratosthenesPrimesCalculator();
      int[] primes = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 };
      for (int i = 0; i < primes.length; i++) {
         int result = calculator.nextPrime();
         assertEquals("Expected prime: [" + primes[i] + "], got: [" + result "]", primes[i], result);
      }
   }
}

このサンプル・コードに対して、以下のように書いてます。

The EratosthenesPrimesCalculator constructor interface accepts (or seems to) a signed integer.  The tests detailed only test 13 of 4,294,967,296 possibilities.  These tests may very well test the expected behaviour of one system, but don't really test EratosthenesPrimesCalculator as a unit.

Testing the Units (Peter Ritchie's MVP Blog)

4,294,967,296 の可能な入力に対して、たった 13 の入力しかテストしてない、って... まあ、そりゃ、40億くらいの入力なら全部テストできなくはないでしょうけどね。 素数表を使って、テストコードを書けば、簡単にできるでしょうし。入力が大きすぎるなら、ランダム入力でも良いかもしれません。でも、しょせんは、サンプル・コードなんですから、それじゃ、BDDのサンプル・コードとしてはねえ、といいたくなります。

ただ、このサンプル・コードのテスト・ケースが 適当すぎる のも確かですねえ。私なら、テスト・ケースはこんな感じにしますかね。

1. プログラムが処理可能な最大の素数

2. 最大の素数 + 1

3. 最大の素数 - 1

4. 適当な大きさの素数

5. 適当な大きさの素数 + 1

6. 適当な大きさの素数 - 1

7. 2, 1, 0, -1

8. プログラムが処理可能な最大の正の整数

9. プログラムが処理可能な最小の負の整数

・・・

おまけ

テストに関する本には、あまり載っていないのですが(おそらくは、あまりに初歩的すぎるから)。テストケースの作成に関する基本の基本について。

問題

IF A THEN B

上のようなプログラムがあったとして、テストケースはどのように作るべきでしょうか?

解答

まずは、

命題: Aならば、Bである

が真となることを確かめます。つまり、Aを入力してBが出力されるのを確認します。

次に、この命題の対偶命題が真となることを確認します。

対偶命題: Bでないなら、Aではない。

つまり、出力がBとならないような入力で、A以外のものを試します。なお、これは”not A”であるとは限りません(裏命題は必ずしも真ならず)。

| | コメント (0) | トラックバック (0)

2008年2月 9日 (土)

Eclipseのヘルプシステム

Eclipse のヘルプ・システムについての覚え書きです。

Eclipseのヘルプ・システムに(Help > Help Contentsで)アクセスすると、実際には埋め込まれた Apache Tomcatサーバーを起動します。次にWebブラウザに基づいたウィンドウが開き、サーバー上にある適切なページを指します(図1)。文書は縮小表示できる目次が左側にあり、右側にHTML文書がある形で提供され、(ApacheのLuceneサーチエンジンのおかげで)いつでも検索ができるのです。

Eclipseのヘルプ・システムを使ってプロジェクトを文書化する (IBM developerWorks Japan)

Webサーバを裏で立ち上げていたとは... どおりでやたらと重いと思いました。非力なマシンだと、画面が固まりますからねえ。改めて、ヘルプを立ち上げてみると、ステータスバーに、

”http://127.0.0.1/何たらを開いています”

とメッセージが、ちゃんと表示されていますね。

試しに、Eclipseのヘルプを起動して、Webブラウザから、

http://127.0.0.1:61973/help/index.jsp?topic=/org.eclipse.help.base/doc/help_home.html

などと打ち込んでみると、Webブラウザでもヘルプ画面が表示されました。なお、URLのパスはEclipseのバージョンによって違うようです。ポート番号もおそらく、空いた番号を適当に使っているのだと思います。URLはEclipseヘルプの方で調べました(使っているブラウザによりますが、例えば、右クリックでメニューを出して、文書プロパティなどを調べれば分かります)。

このヘルプ・システム、単体でサーバとして起動できるみたいです。infocenter と呼ぶらしいですね。

How to start or stop infocenter from command line

The org.eclipse.help.standalone.Infocenter class has a main method that you can use to launch infocenter from a command line. The command line arguments syntax is:

-command start | shutdown | [-eclipsehome eclipseInstallPath] [-data instanceArea] [-host helpServerHost] [-locales localeList] [-port helpServerPort] [-dir rtl] [-noexec] [platform options] [-vmargs JavaVMarguments]

Installing the help system as an infocenter (dev.eclipse.org)

例えば、"c:\eclipse"にEclipseをインストールしていて、ポート 8081 で listen したい場合には、コマンドラインから以下のコマンドを実行します。クラスパスに入れている jar の名前、"org.eclipse.help.base" 何たらは、Eclipseのバージョンによって違いますので、自分の Eclipse インストール先の中をみて、実際の名前を調べました。

java -classpath C:\eclipse\plugins\org.eclipse.help.base_3.3.1.v20070813_33x.jar org.eclipse.help.standalone.Infocenter -command start -eclipsehome C:\eclipse -port 8081

これでブラウザにURL

http://localhost:8081/help/index.jsp?topic=/org.eclipse.help.base/doc/help_home.html

を入力してアクセスしてみると、ヘルプの内容がブラウザに表示されます。

サーバを停止するには、以下のように、” -command shutdown” を指定します。

java -classpath C:\eclipse\plugins\org.eclipse.help.base_3.3.1.v20070813_33x.jar org.eclipse.help.standalone.Infocenter -command shutdown -eclipsehome C:\app\eclipse -port 8081

Eclipseのメニュー、”Window”・”Preferences”・”Help”・”Content”の箇所に、”Include help content from a remote infocenter" という項目があり、 "Location"でサーバを指定するようになっています。サーバ上にヘルプを置いて、それを、各クライアントマシンのIDEから使えるようにする仕組みのようですね。

| | コメント (0) | トラックバック (0)

2007年12月14日 (金)

Windows で Emacs Muse を試してみる

Emacs Muse。Windows版 GNU Emacs 22.1 で試してみました。

Emacs Muse is an authoring and publishing environment for Emacs. It simplifies the process of writings documents and publishing them to various output formats. Muse uses a very simple Wiki-like format as input.

こういうシステムって何て言えばいいんでしょうかね。MarkdownやWikiのような記法で書いたテキストを、HTMLその他で出力できるシステムです。

インストール

以下を参考にさせていただきました。

emacs-muse のインストールと設定

MuseのREADMEによりますと。

Run `make' as a normal user, if you haven't done so already.

Run `make install' as the root user if you have chosen installation locations that require this.

とありまして、makeを使ってインストールするようになっています。おそらく、Makefileの中身はファイルのコピーと、Emacsを使ったelispのバイト・コンパイルなのでしょう。私はCygwinを使って、実行しました。Makefile.defsというファイルにインストール先等の情報を、以下のように入力して、”make"、"make install"を実行します。

Makefile.defs

EMACS    = /cygdrive/c/emacs-22.1/bin/emacs.exe
SITEFLAG = --no-site-file
PREFIX   = /cygdrive/c/emacs-22.1
ELISPDIR = $(PREFIX)/site-lisp/muse
INFODIR  = $(PREFIX)/info

EMACS に、Emacsの実行ファイルをフルパスで指定。PREFIXには、Museの各種ファイルコピー先のルートとなるディレクトリをフルパスで指定します。

設定(.emacs)

以下、 .emacsファイルの設定です。

.emacs

(add-to-list 'load-path "c:/emacs-22.1/site-lisp/muse")

(require 'muse-mode)     ; load authoring mode
(require 'muse-html)     ; load publishing styles I use
(require 'muse-latex)
(require 'muse-texinfo)
(require 'muse-docbook)

(require 'muse-project; publish files in projects
(require 'muse-wiki)

(setq muse-html-encoding-default 'japanese-shift-jis-unix)
(setq muse-html-charset-default "shift_jis")

(setq
muse-mode-hook
'(lambda ()
    (setq coding-system-for-write 'japanese-shift-jis-unix)
    (setq coding-system-for-read 'japanese-shift-jis-unix)))

どうも、MuseはCRLFを改行と認識してくれないようです。それで改行をLFにしました。

htmlize.el のインストール

私が一番試してみたかったのが、”ソースコードの色づけ”機能だったのですが。これは、Museの機能ではなく、htmlize というelispの機能だったようで。

If you have htmlize.el version 1.34 or later installed, you can publish colorized HTML for source code in any major mode that Emacs supports by using the <src> tag.  If not publishing to HTML, the text between the tags will be treated like an <example> tag.

 Muse Quick Start

以下から、  htmlize.el を入手しました。

[http://fly.srk.fer.hr/~hniksic/emacs/htmlize.el]

.emacs

(add-to-list 'load-path "c:/path/to/htmlize")
(require 'htmlize)

試してみる

というわけで、このエントリを、Museを使って書いてみました。改行コードと文字コードで、少しハマりましたが、何とかうまくいったようです。各所で引用したソースコードには、ちゃんと色づけがされていると思います。プロジェクトとか、他の機能はまだ試していませんが、その辺はおいおいやってみようかと思っています。

| | コメント (0) | トラックバック (0)

2007年10月20日 (土)

互いにパラサイトしあえるのなら(パラサイト・プログラマ)

"パラサイト・プログラマ", その様式を "パラサイト・プログラミング" と呼ぶことにしよう. 以下 PP と省略.

PP は仕事がはやく片づく. 検索結果から大量のハズレページを眺める時間, 見当外れのステップ実行をする時間はとても長い. (くたびれてサボる時間も長い.) これらをスキップできるから仕事は速やかだ. 一部には, こうした "ハズレ" の作業を貴重な経験と重視する向きもある. PP の価値観からすると, これは無駄なばかりか悪習ですらある. 10m の距離に音速で到達できる正解があるのに, 何ホップも遠くのサーバや数千数万行のコードを探るのは不合理に思える. 私はそんなスパルタを好まない. ラクできる時はラクをしたい. 同僚が知らないことは結局自分で調べるのだし.

”パラサイト・プログラミング”, steps to phantasien t(2007-04-16)

面白い話だと思います。チーム内で知識を持ち寄り、自分の知らない分野について、互いにコンサルトできるのであれば良いですね。

ただ、誤解を招きそうな記事でもありますね。元記事でも、ちゃんと指摘されていますが。

「助け合う、ってのはお前、最低限のことができる人間同士が集まって、それで初めて意味のあることじゃねえのかい。嬢ちゃんの気持ちはわかるが、できる人間ができない人間をただ助ける一方なのは、助け合うとは言わねえ。荷物を抱えるってんだ」

小野不由美 著, 「図南の翼 十二国記」, 講談社, 1996

また、”教育”という観点では、あえて自分で解決しようとする姿勢も必要だと思います。

ささだ
    後輩の指導は、どんなふうにされてるんですか?

まつもと
    いや、してません。何か聞かれたら、それに答える。

前田
    まつもとさんは、「教育でプログラマーが育つ」っていうことに、否定的ですよね。

まつもと
    うん。自分で勝手にやれ。自分でできない人は、教えてもできるようにはならない。

Rubyist Magazine - Rubyist Hotlinks 【第 1 回】 まつもとゆきひろさん

まつもとゆきひろ氏の意見は、いくらか極論ではあると思いますけどね。

| | コメント (0) | トラックバック (0)

2007年6月23日 (土)

CodeGearのRuby on Rails統合開発環境

CodeGearデベロッパーキャンプには、残念ながら参加しなかったのですが、この統合開発環境(IDE)には注目しています。

Rubyのような動的型付け言語でも、ソース内で型が定義されていれば、その型情報を利用して、コード補完、リファクタリングを行なうことは当然可能なのでしょうけど、このIDEではそれ以上のことを実現するみたいですね。

「Ruby IDEは、エキスパートにとっても生産性を上げられるツール。コードコンプリーション、リファクタリング、タイプブラウザ、ナビゲーションなどの機能を提供します」。これまでのIDE開発で培ってきたノウハウを生かしつつも、「動的スクリプト言語のサポートには、静的な言語とは異なる難しさがあります。例えばコードジェネレーションのように、まだ実装していないメソッドについてもコードコンプリーションを行う必要があります」。

CodeGear、Ruby統合開発環境の詳細を明らかに, @IT

----

CodeGearが提供するRuby on Railsテクノロジーの主な機能は以下のとおりです。

  • コード補完、リファクタリング、タイプブラウザとナビゲーションなどの高度な機能を利用できるRuby and Railsをフルサポートした完全な統合開発環境
  • コマンドライン開発のパワーと、IDEやウィザード、CodeInsightなどの使いやすさと生産性を独自にブレンドした革新的な新機能「Commander」
  • リレーションシップやロケーション、フォーマットが異なってもすべてのリソースに対してシームレスに働く「依存性の可視化とナビゲーション」
  • Ruby、Rails、Gems、データベースを含む、開発から配布までのフルセットモジュール

CodeGear、アジャイルWeb 2.0開発をサポートするRuby on Rails向け開発環境の概要を公開, CodeGear Developer Network

----

ビデオ - 「CodeGearのRuby開発環境プレビュー」, CodeGear Developer Network

「実装していないメソッドについてもコードコンプリーション」というのをどう実現したのか、詳細はないようですが、Railsフレームワークのconventionを利用して予想する、という方向のようですね。

動的に実装された機能も含み、RubyおよびRails固有のコード補完とリファクタリングをサポートしている点も大きな特徴。「Rubyは動的言語なので、IDEを作る人間にとっては”地獄”のはず。にもかかわらず、コード補完を実現しているのがすごい」(まつもとゆきひろ氏)。動的型付けの言語は実行されるまでどのメソッドが実行されるか予想が難しいという特性があり、コード補完の実装は一般的に困難だ。Shelby Sanders氏は「Railsは規約があるので、これを頼りにがんばった」とする。

Matz:コード補完を実現しているのがすごい--CodeGear Ruby IDE, ZDNet Japan

コード補完とリファクタリングというのは、馬鹿にできない機能だと思います。Javaの開発に、Eclipse JDTを使っていて、一番感心するのが、このあたりの機能なのですよね。いちいちマニュアルを参照しなくても、うろ覚えでコードが書けてしまうのは、すごいと思います。

”Java vs Ruby”の比較で、よく話題に出るのが、Rubyの開発環境にはコード補完とリファクタリングの機能がない、という点ですから、この点がクリアされると、Rubyも新しい段階へと進みそうですね。

| | コメント (0) | トラックバック (0)

2007年3月13日 (火)

kill dash nine (kill -9)

というラップ・ミュージックだそうです。

Geekなぺーじ:kill -9 ラップ

「殺す」だの「死ぬ」だの「ゾンビ」だの...物騒ですよね。他業種の方から見ると、相当おかしいらしいです。

以前、機械系のエンジニアの方が、PCの電源を入れることを、

パソコンに火をいれる

とおっしゃっていて、面白いと仲間内で話題になったことがありましたけど、まあ、他の方のことを言える立場ではなかったりします。

| | コメント (0) | トラックバック (0)

2006年9月28日 (木)

Javaの次に来るもの

Javaの時代は終わった?

"Beyond Java"という書籍は初めて知りましたが、面白そうですね。有名なPaul Graham氏のエッセイ、 「普通のやつらの上を行け」 でいうところの、「普通のやつらの上」が、近いうちに「普通」になる、というようなことですかね。読んでみたいと思います。

本を読んでないので、内容については何もいえないのですけども、「習得に何年もかかるようになってしまった、というJavaの複雑性」という点について、少し考えてみたいと思います。

--
1) 溢れる慣用句、ギミック
これは、C/C++/Javaの各言語共通の特徴だと思います。言語仕様をシンプルにした結果、ある処理を書きたいとき、自然な形で書く方法がなく、これをやるための「慣用句」が、言語仕様の外に大量に生まれるという。

2) 静的型付けとデザインパターン
「静的型付け」、つまり、コンパイル時にコンパイラはオブジェクトの型を知らなければならない、というのは、実のところ、オブジェクト指向とは、あまり相性がよくないのでしょう。ある種の「デザインパターン」-Abstract Factoryとか-あるいは、Dependency Injection(依存性注入)などは、この「静的型付け」による制約を何とか回避するためのトリックのようにも見えます。実際、Javaのフレームワークは、「デザインパターン」で溢れていますし。

3) オブジェクト参照型と組み込み型
Java言語を初心者に教えるとき、おそらく一番の壁は、「オブジェクト参照型」の概念ではないでしょうか。これは結局、「スマートポインタ」の一種だと思うのです。C/C++での、「ポインタ」が学習の難度を上げている構造が、そのまま持ち込まれていると思います。もちろん、ポインタよりは「スマート」で「安全」なのですけど。

実のところ、Rubyも「オブジェクト参照型」のアイデアを全面採用しており、この面では差はないです。ただし、Rubyが「全てがオブジェクト(参照型)」であるのに対し、Javaには別に「組み込み型」というのがあります。これが、型によって変数の扱いを変えなければならないという、煩雑さをもたらしている面はありそうです。
--

Javaのような「重い言語」に比べれば、Rubyでなくとも、PerlとかPHPとか、あるいはLisp(!)などの「軽い言語」の方が、生産性が高いのは、当たり前という気もしますけど。もちろん、どのようなプログラムを開発するかにもよりますが。

「オブジェクト指向」という枠内、あるいはJavaの代替となるもの-より生産性の高いJava-という視点では、「Rubyが最良」というのは頷けます。

JVMの何がそんなに「イケているのか」は、ちょっと想像がつきませんでした。本を買って読んでみます。

--

ハッカーと画家 コンピュータ時代の創造者たち Book ハッカーと画家 コンピュータ時代の創造者たち

著者:ポール グレアム
販売元:オーム社
Amazon.co.jpで詳細を確認する

| | コメント (4)

2006年9月22日 (金)

自分色に染めてしまいたい衝動

小野和俊のブログ:人のプログラムを自分色に染めてしまいたくなる衝動 より。

かのビル・ゲイツが、その昔、自社のBASICインタプリタのプログラム・ソースを、全部書き直していた、という逸話を思い出しました。

私にも身に覚えがあります。昔、外注先から納品されたソース全てに手を加えたりしていたこともありましたね。最近はできるだけ気にしないことにしています。プログラムの「実装部分」については。インターフェイス部分は、未だに自分色を押し通していますけど。

--
レベル1)
いわゆるプログラムのスタイル(書法)レベルですね。これは気にしないようにしてます。典型的には、「インデント論争」でしょう。タブかスペースか、2個か4個か8個(!)か。ちなみに、私は「スペース2個」派なんですけど。この論争は決着不能なようですので、自分が最初に書くときは自分色ですが、すでにソースがあって手を入れるときは、そのソースを書いた人のスタイルを踏襲します。本当は、プロジェクト内で統一した方が望ましいのですけども。一度試みてあきらめました。

レベル2)
これは、結構やってるかも...
自分が手を入れるときには、場合によっては、原型を留めないくらいに変えることがありますね。それで、別のバグを混入させてしまうこともありますけど。長い目で見れば、変えておいた方が良いコードというのもあり、状況によってはやります。

レベル3)
うーん、逆にそれでちゃんと動いているのなら、ある意味すごいですけど。まだリリースしておらず、稼動実績がないのであれば、「捨ててやり直し」ですかね。ただ、最近のプロジェクトは人的・時間的リソースがひどくタイトですので、一発勝負でやり直し不可となってしまってます。ですので、初めて一緒にやるメンバーの場合、最初にユーティリティ的な、ちょっとしたクラスなりメソッドなりを作ってもらい、それで、その人を使うか使わないか、使うならどの箇所か、を判断しています。
--

もう一つ、これは「レベル1」だと思いますが、論理式の書き方をどうするか、というのもあります。
例えば、

not ((a==b) or (c==d))

と書くか、あるいは、

(a!=b) and (c!=d)

と書くか、ですね。基本的に、私は下の方が「好み」なのですけども。人によっては、上の方を好むようです。

| | コメント (2)

2006年9月21日 (木)

jMockを使うとより「文芸的プログラミング」になる?

Javaのユニットテストを書く際、jMock を使うと、より「文芸的」でコードが読みやすくなるよ("the code more literate")、という記事のようですが。

We can use constraints to construct more flexible assertions.

assertThat(a, eq("3"));

is more readable and understandable than

assertEquals("3", a);

Tom White's Blog: Literate Programming with jMock

パッと見て、下の方がわかりやすいと思ってしまいました...
JUnitでは、下の例が「通例」ですので、「見慣れている」から「わかりやすい」と感じたのでしょうかね。

英語の構文としては、上の方がより「自然」なんですね。下の"assertEquals"ですと、動詞のassertとequalが連続しているので、英文として不自然で、上の"assertThat"は、動詞("assert")+関係代名詞("that")+主語("a")+動詞("eq")+目的語("3")の順に並ぶので、自然だということのようです。

でも、これは日本人には微妙ですねえ。プログラミング言語を「英語」だと認識している人って、あまりいないような気がします。プログラミング言語は、あくまでプログラミング言語として見ているような。あるいは「暗号」の一種として。

まてよ、むしろ、日本語として読むと下の方が自然なのかしら。「aは"3"に等しいと表明する」だから、下の"assertEquals"の例を逆順に読むと、日本語の語順になるような...

ちなみに、ハンガリー語の文法は日本語と非常に類似しているそうで。ハンガリー語は、「ハンガリアン記法」の例にあるように、英語圏の人間にとっては、「読みにくい」の代名詞。でも、日本人には、その方が読みやすいという話なのでしょうかねえ。

| | コメント (0)

2006年9月 6日 (水)

複合キーの必要性はなし?

先ごろ開かれたRailsConfでは、オープニングキーノートにおいてPragDaveが「Railsでは解決できない事項」に焦点をあてていた。その中にはエンタープライジーなことも含まれていた。たとえば、複合キーを持つような、様々なデータ構造を扱うことが必要だというのだ。

これに対するDHHの反応は、この上なく痛烈な拒絶であった。

Martin Fowler's Bliki in Japanese - エンタープライズRails

ここでいう「複合キー」が何を指すのか、はっきりとは書いてありませんが。リレーショナル・データベースのテーブルが持つ、「複数列で構成される主キー」ということにして、複合キーの必要性について、考えてみます。

まず、複合キーが「必要とされる」状況を、3点挙げてみます。

---

1) 階層構造を表現したい

これはすぐに否定できますね。
単一の主キーと外部キーの関連を使って、階層構造は容易に表現できます。

2) パーティショニングを行ないたい

例えば、複数拠点にデータベースを分散配置し、それぞれの拠点で、テーブルにデータを追加したい場合ですね。さらに、拠点で入力したデータを、「中央」に集めるような場合です。

この場合は、「パーティショニング・キー」として、拠点のIDを使用し、さらに、拠点のIDと、拠点で入力の際に付けるIDを、組み合わせて「複合キー」とします。

この場合も、「パーティショニング・キー」=「主キー」である必要はなく、「主キー以外の候補キー」を、「パーティショニング・キー」とすることができます。主キーの値は、データベースごとに独自に作れば良いでしょう。

「パーティショニング・キー」=「主キー」とすることで、単純なデータベース・レプリケーションによって、データベースを分散させることができる、というのはありそうですが。いわゆる”Auto Number”機能をうまく使えば、何とかなりそうな気もします。

3) データベースをわかりやすくしたい

単なる連番で作られるような、人工的なキーよりも、より自然な「複合キー」の方が、理解しやすい、という場合があります。これはおそらく、エンドユーザが、「直接」データベースを見ることを想定してのことですね。

Martin Fowler氏の主張する、 「アプリケーション・データベース」 -データベースを単一のアプリケーションの「ストレージ」として考える-のようなものを想定すれば、こういった「わかりやすさ」は必要なくなるかもしれません。

もっとも、私の経験では、エンドユーザは「人工的なキー(の必要性)」というものを、意外に理解してくれます。あるいは、そんな「技術的なこと」には関心をもたないか、ですね。逆にプログラマの方が、こういったやり方に首をひねることが多いような気がします。おそらく、かって「階層型データベース」を使っていた経験などから、先入観を持ってしまっているのだろう、と推察しますけど。

---

Ruby On Railsでの、「全ての主キーを単一の自動連番とする」というプラクティスは、とてもシンプルで、望ましいやり方だと思いますね。「主キーの選定」は、間違えるとひどいことになりますけど、この方法なら、そういったことは起こりませんし。「主キーの選定」に頭を悩ます時間も節約できます。

私もデータベース設計では、つい複合キーを使いがちではあったのですが。最近は、可能な限り単一キーとする方が良いと考えています。こう考えるようになったのは、Railsの影響が大きいですね。

ちなみに、「T字形ER手法」でも、「主キー(「認知番号」)の複合構成は認めない」という記述があり、同じ考えを持っているようです。

---

 

データベース設計論 T字形ER―関係モデルとオジブェクト指向の統合をめざして Book データベース設計論 T字形ER―関係モデルとオジブェクト指向の統合をめざして

著者:佐藤 正美
販売元:ソフトリサーチセンター
Amazon.co.jpで詳細を確認する

| | コメント (0)

2006年9月 5日 (火)

「コメント率50%」の謎

先日、とあるSIerの方とお話した時に、上記のような考えで、ソースコード中のコメント率は20%を切るべき、コメントなど書かなくてもわかるようなコードこそ美しい、という話をしたところ、怪訝な顔をされてしまったことがあった。彼にはちゃんと理由を説明して納得してもらったのだけれども、もしコメント率50%と主張している人がいたら、もう一度その必要性を振り返って考えてみてもよいのではないだろうか。

小野和俊のブログ:ソースコードのコメント率は20%を切ることが望ましい

少なくとも、「いまどきのプログラミング言語」を前提にしますと、「コメント率50%」はほとんど「逐語訳」に近いわけでして。確かに異常な数字だと思いますね。

「コメント率50%」がどこから来たのか、理由をいくつか考えてみました。

---

1) 昔の習慣を引きずっている

「昔の言語」-FORTRANやCOBOL-の場合、そもそも、わかりやすくコードを記述するのが不可能だったりします。変数やプログラムの名前の長さも相当制約されており、多くは"PG010001"といった、「意味不明」の名前が付けられていたりします(今どきの言語を使っているのに、今だにこういった名前を好んで使う人もいますが)。

これらの「暗号」を解読するには、ほぼソース1行あたり1行くらいの割合で、「平文」が必要になります。

2) 「英語だとわかりません」

ソースコードは英語で書かれています。英語というより、「英単語」程度なんですけどね。しかし、これでも「日本語訳が欲しい」という向きがあるようです。

3) 「プログラムが読めません」

ちょっと信じられませんが、SI業界には、「プログラムを読めないプログラマ」は存在するらしいです。コメントで解決する問題ではないと思いますが。

---

コメントを多くつけると、ソースコードがわかりやすくなる、というのは幻想にすぎないと思います。確かに、「的確なコメント」がついていれば、幾分、わかりやすくなることはあるかもしれません。が、「的確なコメント」が出来るなら、「的確なソースコード」を書く能力はあるでしょうし、結局、わかりにくいコードに、わかりにくいコメントをつけて、さらに解りづらくしている、というのが私の印象です。

| | コメント (5)

2006年9月 4日 (月)

Rakeを使ってSQL-DDLファイルを作る

Rake(Ruby Make) は、make、antに代表される、「ビルド・ツール」の一つです。「タスク(task)」と、タスク間の依存関係を定義すると、その依存関係に従って、タスクを実行してくれます。もちろん、makeにあるような、「ソース」ファイル、「ターゲット」ファイルのタイムスタンプを比較して、ターゲットがソースより古ければタスクを実行、ということもできます。

Rakeを特徴づけているのは、これが、Rubyの「ライブラリ」-より正確には「言語内DSL」 -となっている点です。この特徴により、Rubyを使用してタスクをプログラミングすることができます。

私は、テーブル仕様書-データーベース・テーブルのレイアウトを書いたドキュメント-から、DDL文(CREATE TABLE文)が入っているファイルを自動生成する、といった仕事に、このソフトウエアを使用しています。これがどんな感じなのか、以下で記述したいと思います。

--

1) テーブル仕様書(Excel)をテキストファイルにする

テーブル仕様書は、Excelワークシートとして作成しています。1シート1テーブルで記述しています。1つのワークシートを、1つのテキストファイルにします。テキストファイルの名前は、テーブル名にして、中身は、ワークシートをそのままコピーしてます(タブ区切りのテキストになりますよね)。一応、この作業を自動化するマクロを書いてます。

2) 上記テキストファイルからDDL文を生成するRubyプログラムを作成する

上記のテキストファイル-定義ファイルと呼んでます-から、DDL文を生成する、Rubyプログラムを作成しています。これは、以下のような感じです。

 

# sqlgen.rb:
# テーブルDDL文の生成。
def generateTableDDL(src_file, target_file,
  schema = '', data_ts = '', index_ts = data_ts,
  pkey_extract = /^#{schema}_/)
  
  ddl = TableDDL.new(
    File.basename(src_file, '.def'),
    schema, pkey_extract)
  
  # DDLオブジェクトを構築。
  ddl.load_from_file(src_file)
  
  # DDL文をファイルに書き出す。
  ddl.generate_sql(target_file, data_ts, index_ts)
end

3) 「DDL文生成タスク」を定義する

定義ファイルから、DDL文ファイルを生成するタスクを定義します。これは、以下のような感じです。

 

# sqlgen-task.rb:
# テーブルスクリプト生成タスクの作成。
def make_sqlgen_table_task(src_dir, dst_dir,
  task_symbol,
  schema = '', data_ts = '', index_ts = data_ts,
  pkey_extract = /^#{schema}_/)
  
  Dir.glob(File.join(src_dir, '*.def')) do |file|
    target = File.join(dst_dir, 
      File.basename(file, '.def') + '.sql')
    file target => [file] do |t|
      generateTableDDL file, target, schema,
        data_ts, index_ts, pkey_extract
    end
    task task_symbol => target
  end
end

ディレクトリ"src_dir"にある定義ファイル"*.def"全てについて、対応するDDL文ファイルをディレクトリ"dst_dir"に作成するタスクです。

4) Rakefileを作成する

"Rakefile"は、Rakeがデフォルトで使用する、タスク定義ファイルです。つまり、Rakefileのあるディレクトリで、

>rake

とコマンドを打つと、このファイルにある「デフォルト・タスク」
(":default")が実行されます。

Rakefileは、以下のような感じです。

 

# Rakefile:
# テーブル作成スクリプト生成タスク。
# tabledef->createtable.sql
make_sqlgen_table_task(
  File.join(SCHEMA_DIR, TABLE_DEF_DIR),
  File.join(SCHEMA_DIR, CREATE_TABLE_DIR),
  :generate_create_table,
  SCHEMA_NAME,
  DATA_TABLESPACE,
  INDEX_TABLESPACE)
# タスク指定なしのrakeコマンドで実行するタスク。
task :default => [:generate_create_table]

--

RDBMSはOracleを使用しています。インデックス、権限付与、シノニムなどのDDL生成も、これで行なっています。こうした、「共通」の部分に加え、さらに、案件特有のものも追加したりしてまして、例えば、テーブルの「管理項目」を入力するトリガーとか、削除データを別テーブルに保存するトリガーなども、生成したりしています。

--

Using the Rake Build Language

Martin Fowler氏による、Rakeの解説です。これだけ詳しい解説があれば、十分だと思います。

| | コメント (0)

2006年8月24日 (木)

「文芸的プログラミング」を試みる。

Documentation and source code are written into one source file. Both the complete source code and its documentation can be extracted from this file with specific utilities. The information is written and presented in a reading order suitable for human consumption with detailed explanations. The code is automatically rearranged for ordinary processing by other computer tools, such as compilers or interpreters.
「Literate programming」(Wikipedia)

Donald Knuthが提唱した「文芸的プログラミング」のコンセプト-ソースコードとドキュメントを一つのものとして扱う-は、Java言語のJavaDoc や、Perl言語のPOD などの「埋め込みドキュメント」に受け継がれています。

私は、プログラムの「詳細設計書」のようなもの、つまり、プログラムの呼び出しインターフェイス仕様を書いたり、プログラムの処理を書いたりするドキュメントを、今まで、必要に応じて作成していたのですが、やはり、これは、「ソースコードそのもの」を、そのまま仕様とするようにすべきだと思いました。結局、プログラマは、ドキュメントをソースにコピーするような作業をやりますから、初めからソースファイルに書く方が、無駄がないですよね。

よく、OracleデータベースのPL/SQL言語(ストアド・プロシージャ)を使用して、アプリケーション・ロジックをサーバに置く、ということをやっています。このインターフェイス仕様を書くのに使えるツールはないか、とネットを探し回った結果、PLDoc というソフトウエアを見つけました。

以下のように、日本語も、ちゃんと使えるようになっています。
(ver0.8.3です。ver0.8.3.1では使えなくなってしまってます。)

pldoc -doctitle \"Samples\" -overview overview1.html -d Samples -inputencoding Shift_Jis sample*.sql

エンコードで、"UTF-8"だけでなく、"Shift_Jis"も使えるのは有難いですね。HTMLをUTF-8にするのは良いのですが、ソースコードは、OSネイティブな文字コードを使いたいので。

しばらく、このツールを使って、インターフェイス仕様を書いているのですが、今のところは、順調にいっているようです。「紙にしにくい」というのは、難点かなと思っていますが、プログラマ以外は読まない類のドキュメントですので、問題はなさそうです。

ちなみに、数ある「埋め込みドキュメント」のツールの中では、RDOC(Ruby Documentation System) が秀逸だと思います。ソースコードまで見られるのは、素晴らしいですね。

---

Book 文芸的プログラミング

著者:ドナルド・E. クヌース
販売元:ASCII
Amazon.co.jpで詳細を確認する

| | コメント (4)

2006年4月 2日 (日)

Rails実行環境をWindowsマシンに構築

まずは、Ruby on Railsを触ってみるために、
Rails実行環境を、手持ちのWindowsXPノートPCに、
インストールしてみます。

Windowsの場合は、Rails実行環境をパッケージにした、
Instant Rails もあります。
こちらは、インストーラひとつで、
必要なもの全てがインストールできます。
しかし、Apache、PHP、MySQLなどの環境を、
Rails以外でも使いたい場合には、少々不便かもしれません。
私も最初はInstant Railsを使ったのですが、
結局、全部入れなおしました。

1 Rubyをインストール

Rubyの処理系をインストールします。
推奨バージョンは、1.8.4で、1.8.2でも"fine too"だそうです。

One-Click Ruby Installer for Windowsをダウンロードして、
インストーラを実行します。

2 MySQLをインストール

Railsで使用するデータベースエンジンとして、
MySQLをインストールします。
推奨バージョンは、4.1または5.0とあります。

ダウンロードページに移動して、"Windows (x86)"を、
ダウンロードし、インストーラを実行します。

3 Ruby MySQLドライバをインストール

Ruby MySQLドライバ(mysql.so)をインストールして、
RubyからMySQLデータベースを操作できるようにします。

Windows用のパッケージは存在しないようです。
RubyForApacheに同梱されているので、
ここからインストールせよ、とあります。
RubyForApacheは、ApacheのRuby拡張(mod_ruby)のようですね。

欲しいのは、mysql.soだけで、
Railsを使用するのに、Apacheは必ずしも必要ないわけですが。
この際なので、Apacheをインストールするか、
もしくは、インストーラに適当なディレクトリを、
Apacheのディレクトリだと教えて、騙しましょう。

4 Rakeをインストール

最新バージョンのRake(Ruby Make)をインストールします。
現時点での最新バージョンは、0.7のようです。

インストールは、Rubyに付属のパッケージシステム、
gemを使用します。
コマンドプロンプトから、次のコマンドを実行します。

gem install rake

5 Railsをインストール

さて、ようやくRailsのインストールです。
現時点での最新バージョンは、1.1です。
コマンドプロンプトから、次のコマンドを実行します。

gem install rails --source http://gems.rubyonrails.org

6 動作確認

以上で、Railsのインストールは完了です。
Rolling with Ruby on Railsにあります、
cookbookサンプルアプリケーションを作成して、
ちゃんと動作するかを確認します。

MySQLを操作するUIとして、phpMyAdminをインストールすると、
作業がはかどるかもしれません。
phpMyAdminを動作させるには、Apacheと、PHPが必要になります。

付記 MySQLのパスワード問題について

Client does not support authentication protocol

MySQLでは、バージョン4.1以降で、
パスワードの扱いが変更されたそうです。
このため、古いバージョンのクライアント
(PHP組込みのMySQLドライバが該当します)が、
古いパスワードを使用して、MySQLサーバにログインしようとすると、
上記のエラーとなります。

上記ドキュメントにもありますが、
この問題に対処するひとつの方法は、
古い形式のパスワードを使用し続けることです。

1) 作成済みのユーザ・パスワードは、古い形式に変換する。

mysql> SET PASSWORD FOR
-> 'some_user'@'some_host' = OLD_PASSWORD('newpwd');

または

mysql> UPDATE mysql.user
      ->SET Password = OLD_PASSWORD('newpwd')
      -> WHERE Host = 'some_host' AND User = 'some_user';
mysql> FLUSH PRIVILEGES;

2) MySQLサーバを、--old-passwordsオプション付きで起動する。

my.iniに、次の一行を追加。

old-passwords

2)を行うと、今後作成されるパスワードは、すべて古い形式となります。

Railsでは、"be sure to use new-style password hashing"と、
新しい形式を推奨していますが、とりあえずはお遊びなので、
PHPとMySQLを共用したい場合は、古い形式でも良いでしょう。

| | コメント (0)

Ruby on Railsとの邂逅

Ruby on Railsは、オブジェクト指向スクリプト言語Rubyを使用した、Webアプリケーションフレームワークです。

次のような特徴があります。

1) Model-View-Control パターンのアーキテクチャをサポート。

2) ActiveRecordパターンとなるO/Rマッピングをサポート。

3) DSL的な各種機能サポート。

まだ、私も勉強中ですので、あまり適切な説明は出来そうにないのですが、一番目立つ特徴は、2)のO/Rマッピングのサポートだと思います。少なくとも、私にとっては、そうでした。

従来のO/Rマッピング(例えばHibernate)は、XML形式の設定ファイルに、RDBMS内のテーブルと、ホスト言語のオブジェクトとの対応を記述し、ホスト言語側でオブジェクトのソースコードを用意した上で、O/Rマッピングフレームワークで、両者を結び付ける、というものでした。

Railsでは、テーブルとオブジェクトとの対応(通常、設定ファイルに記述)の部分と、オブジェクトのソースコードを用意する部分が不要となっています。つまり、極端な話、DBにテーブルを追加すれば、そのテーブルにO/Rマッピングするオブジェクトが、プログラミングなし、設定ファイルなしで出来上がります。

私は、Railsを見たときに、これこそ真のO/Rマッピングだと思いました。従来のO/Rマッピングフレームワークでは、本当に狭義のマッピング、つまり、オブジェクトをDBのテーブルに代入する部分と、DBのテーブルをオブジェクトに代入する部分しか、やってくれないように思うのです。正直、これでは、フレームワーク導入の手間には見合わないのではと考えていました。しかし、Railsの場合は、ここまでフレームワークがやってくれるのであれば、フレームワークを導入する価値は十分すぎるほどあるように思います。

ただし、ソフトウエアの真価は、使ってみなければわかりませんから。しばらくは、Railsを触ってみようと思っています。

---

RailsによるアジャイルWebアプリケーション開発 Book RailsによるアジャイルWebアプリケーション開発

販売元:オーム社
Amazon.co.jpで詳細を確認する

| | コメント (0)