メイン

PC一般 アーカイブ

2005年08月25日

インターネットラジオ(1)

RealPlayer対応の無料のインターネットラジオのチャンネル(サイト)を探すのに手間取るのは私だけだろうか。
Windows MediaPlayerなら、"Radio Tuner"を押せば、索引ページ出てきて、すぐにいくつか見つけることができる。しかし、フリーのRealPlayerの「Realガイド」を押して探しても、「プレミアムコンテンツ」という有料チャンネルばかりで、なかなか無料のコンテンツが見つけられない。
しかし、とても見つけにくいが、きちんと「Realガイド」にも無料チャンネルの索引ページがあるのだ。
"RealGuide Home"の"Radio"を押し、出てくる"Free Radio"の枠のピアノの鍵盤の写真を押せば、右に書いてあるチャンネルが再生されると同時に、索引ページが出てくる。これはわからん!

続きを読む "インターネットラジオ(1)" »

2005年10月11日

IEのオートコンプリートの項目削除

Internet Explorerを使っていて、オートコンプリートで出てくる候補に不要なものが残って、邪魔に思う時は無いだろうか。入力欄に何か入れたら、シンプルなウィンドウで過去に入力した候補が出るあれである。

あるログイン名の入力が必要なページを開いて、いつもは1文字入力してオートコンプリートの候補を選んで入力完了だったのに、ある時、間違えて違うログイン名を入力してしまった。それが1文字目が正しいログイン名と同じものだったので、それからは1文字入力すると2つの候補が出るようになって、毎度どっちだったか一瞬悩むようになってしまった。
また、google検索でもオートコンプリートされるので、人に見られたくない検索語はオートコンプリートの履歴から削除したいこともあるだろう。

実はオートコンプリートの項目は削除できるのだ。
方法は、オートコンプリートの候補が表示された時に、消したい候補をカーソルキーで選んでDELキー。

たまたま操作して見つけて知ってる人も多いだろうが、知らない人も多いのではないだろうか。

2005年11月18日

SD-Jukeboxアップデートのトラブル

SD-Jukeboxをアップデートしようとして、SD-Jukebox V5.0L60 Light Edition をインストールしたら、起動時に問答無用の
「エラーが発生したため、SD-Jukeboxを終了します。プログラムをもう一度開始する必要があります。
エラーログを作成しています。」
というエラーが出て、一切起動しなくなってしまった。
その後、SD-Jukeboxをアンインストールして、古いバージョンを再インストールしても同じエラーが出た。
もはやWindowsを再インストールするまでSD-Jukeboxを使えないのか!?
と思って色々がんばったら、復旧することができた。

続きを読む "SD-Jukeboxアップデートのトラブル" »

2006年04月07日

VMWare PlayerでISOイメージのOSインストール

OS上で仮想マシンを実現するVMWareは、もはやご存知の方も多いと思う。これを使えば、Windows上でLinuxを動かしたり、Linux上でWindowsを動かしたりすることができる。そのVMWareの機能限定版、VMWare Playerが昨年無償で公開された。

VMWare Playerには、仮想マシンのイメージを新規作成したり、仮想マシンの構成を変更する機能が無く、VMWareで作成するか、仮想マシンのイメージ(色々なサイトで用意されている)をダウンロードする必要がある。

VMWare無しで、VMWare Playerだけで仮想マシンに好きなOSをインストールする方法は、既に色々なサイトで紹介されている。
(参考リンク)
http://yamashita.dyndns.org/blog/343
http://blog.livedoor.jp/hakin/archives/50168211.html
http://blog.yasaka.com/archives/2005/10/vmware_playervi.html


今回、VMWare Player(for Windows)以外に何もインストールせずに、CD-ROMドライブ無しで、ISOイメージのRedHat9.0をインストールすることに成功したので、報告する。

LinuxやFreeBSDなどは、インストールCDのISOイメージをFTPサイトからダウンロードすることができる。これらをVMWare Playerで仮想マシンにインストールするには、daemon toolsを使ったり、CD-ROMに焼いたりして、仮想マシンでインストーラーをCD起動する方法が主だ。

だが、daemon toolsをインストールしたくなかったり、CD-ROMドライブが無かったりすることもあるだろう。そんな場合に、仮想マシンの構成ファイルをテキストエディタで編集して、ISOイメージを仮想マシンのCD-ROMとする方法がある。(その方法は後の方を参照)
その時に問題になるのが、インストール中にCDの入れ替えがある場合だ。

RedHat9.0もCD3枚組なので、インストール中にCDの入れ替えが発生する。CDの入れ替えの画面になったら、仮想マシンをsuspendして、構成ファイルを編集してISOイメージファイル名を変更して、仮想マシンをresumeすると、なんとなくCDの入れ替えが成功した感じになるのだが、なぜか途中でそのCDにファイルが無いというエラーが出てしまう。てっきりダウンロードしたISOイメージがおかしいのかと思ったら、daemon toolsでmountするとそのエラーは出なかった。
色々試してみた所、CDの入れ替えの時に、構成ファイルを編集して一旦物理CD-ROMドライブを使う設定にし、物理ドライブにアクセスしてCDが見つからないというエラーを出した後、入れ替え先のISOイメージを設定すると、きちんとCDの入れ替えが行われることが判明した。

RedHat9.0以外ではまだ試していないが、おそらくどれでも同じだと思う。
この方法により、VMWare Playerと仮想マシンイメージとインストールCDのISOイメージさえあれば、好きなOSをインストールした仮想マシンを作成できると思われる。

続きを読む "VMWare PlayerでISOイメージのOSインストール" »

2006年04月08日

Lunascape3.0Liteは快適

私のPCのCPUはCeleron(Pentium2相当)の433MHzだ。最近のアプリケーションをインストールすると、まともな速度では動かない。
先月からブラウザとしてNetscape8.1を使ってきたが、洒落にならない遅さだ。起動に1分はかかるし、ボタンを押しても1秒くらい反応したのかどうかわからなかったりするし、どの操作も0.5秒以上のウェイトが入る。リンクを2つくらい同時に新しいタブで開くと、ブラウザが10秒くらい固まったりする。パスワードマネージャ等、色々便利な機能があるので使い続けていたが、昨日ブログを更新していると、その遅さに我慢できなくなってしまった。会社でなら「生産性が悪い」と言うやつだ。

それで、前に使っていたFireFoxの最新バージョンをインストールしたが、前からそうだったのだが、どうも私の手が覚えている操作とFireFoxのキーバインドの相性が悪く、面倒なことが発生するのだ。例えば、ブックマーク画面で、新しいタブでブックマークを開くつもりで、ブックマークを右クリックして"T"を押すと、ブックマークが削除されるのだ。すぐに気付けばブックマークの貼り付けで元に戻せるのだが、キーが入らなかったのかと思って同じ操作を繰り返すと、もう1つブックマークが削除されて、先に削除されたものが回復不能になるのだ。何でHTMLのリンクは"T"でタブで開くのにブックマークは"T"で開かないんだろう。UIのバグだと思う。以前にこれで痛い目に会って、FireFoxを使わないようにしていた。

という訳で、前から気になっていたLunascape3.0 Liteを試してみた。まだ使い始めだが、今の所トラブル無く、すこぶる軽快だ。リンクをばんばん開く時などは、IEよりも軽いかも知れない。
ロボフォームも、まだ使い方がきちんとわかっていないが、かなり便利だ。Netscape8.1のパスワードマネージャより操作少なく、パスワード認証ありのサイトにログインできる。
Lunascape3.0 Liteのお陰で、まだまだこのPCを不満なく使えそうだ。

2006年06月12日

NAT越えの方法

我が家の自宅HTTPサーバー公開で何度も苦労の種になっているNAT超えであるが、ナンセンスな静的NAT/NAPT変換だけではなく、もっとスマートな方法が色々考えられているようだ。

1. UPnP (Universal Plug&Play)
Microsoftを含むグループが提唱した方式で、LAN内の接続先機器がブロードキャストすることにより、UPnP対応ルーターが外部からのアクセスの適切な転送先を把握する方式。Windows XPはUPnPに対応しているらしい。UPnP対応ルーターが必要だが、既に多くのUPnP対応ルーターが出ており、現時点でのデファクトスタンダードか。

2. STUN (Simple Traversal of UDP through NATs)
IETFによる、RFC3489として公開されているプロトコルを使う方法で、LAN内の接続先機器からインターネット上のSTUNサーバーにUDPでアクセスして、インターネットから見た接続先のアドレスとポート(外側のアドレスとポート)を取得する方式。外側のアドレスとポートが接続毎に変わることがない、「完全Corn型」のNATでしか使えない。ポピュラーな方式であり、現時点ではよく使われているような気がする。

3. 拡張STUN
「Corn型」でなく「Symmetric型」のNATでも使えるとされるSTUN。実際に出回っているSymmetric NATは外側のアドレスとポートに規則性がある場合が多いらしく、その規則性を調べることにより外側のアドレスとポートを知るという、強引かつ奇妙な方式。

4. B2BUA(Back-To-Back User Agent)
やはりSymmetric NAT越えのために考えられている方式。B2BUAとはある種のSIPプロトコルの転送サーバーの一般名称であり、具体的にどういうB2BUAを用意してどうやるとNAT越えが実現するのかは調べ切れなかった。参考ページから推測すると、LAN内の接続先からインターネット上のB2BUAにSIP接続しておき、接続元からB2BUAにSIP接続することにより、接続先の、外側のアドレスとポートを知るということだろうか。

5. TURN (Traversal Using Relay NAT)
IETFによる方式で、インターネット上にTURNサーバーを設置し、LAN内の接続先からTURNサーバーにアクセスして接続を確保し、LAN内の接続先へのアクセス(データ送受信含む)は全てTURNサーバーを介するという方式。LAN内のサーバーから見ると接続元が同じなので、Symmetric NATでも使用可能だと思われる。NAT越えTCP接続も可能。但し、全ての通信がTURNサーバーを介するので、遅延が大きくなる。

6. ICE (Interactive Connectivity Establishment)
IETFによる方式で、STUN、TURNを含む多くのプロトコルを使う、SIPベースの複雑な方式。インターネット上のSIPサーバーを使用してNAT越しにセッションを確立し、LAN内のサーバーへの接続は確立したセッション上で行う。データ送受信はそのセッション上で行う。SIP的には美しいが、えらく複雑だ。

7. IPv6
NAT越えの話題には、NAT不要論というのが常に伴うらしい。確かに、そこまで面倒な話なら、IPv4+NATを諦めて、必要な所だけIPv6にすればいいという気がする。

続きを読む "NAT越えの方法" »

2006年06月21日

WindowsでGPG

本日、急にWindowsでPGPを扱う必要が生じ、PGPのソフトをインストールしたので、そのことについて記録する。PGPのソフトは、商用のものもフリーのものも複数あるが、ソフト名が単に"PGP"のものが多くて紛らわしい。

まず、以前に使ったことがあるPGPソフトを紹介する。
・SOURCENEXT/McAfeeの"PGP personal privacy"(有料)
機能が豊富でかつ直感的にわかりやすく、とても便利だった。
ファイルシステムごと暗号化する「PGPディスク」機能も、リムーバブルメディアには欠かせなかった。
このページにあるフリーソフト
動作がちょっと不安定だった。特に、Becky!のPGPプラグインと併用した時に、ダイアログが連発して困ったことになることが多かった。

今回は、手持ちのソフトが無く、至急だったので、フリーソフトで対応することにしたが、上記のフリーソフトは以前に失敗だったので、以下の2つのどちらかを使うことにした。
The International PGPのもの
GnuPG(GPG)
時代はGPGのような気がするし、ここはやはりGPGを試してみたい。Becky!やDatulaといったメールソフトのプラグインも存在し、メールの暗号化/復号も手軽なので、申し分ない。

●GPGのインストール手順
1. GPGのダウンロード
上記GnuPGのサイトからWindows用バイナリのダウンロードができるらしいが、私はうまくいかなかったので、ミラーサイトからダウンロードした。上記サイトの"Mirrors"をクリックすると、ミラーサイトがたくさん出てくが、日本のサイトならRingサーバーがお勧めだ。
今回は、gnupg-w32cli-1.4.3.exeをダウンロードした。

2. GPGのインストール
上記exeファイルを実行するとインストーラーが起動し、特に考えること無くインストールが完了する。
しかし、環境変数PATHが自動的には書き換わらないので、手動で設定する必要がある。WindowsXPの場合、コントロールパネルの「システム」を開き、「詳細」タブ→「環境変数」を選択し、環境変数PATHの値の最後に";C:\Program Files\GNU\GnuPG"を追加する。

以下、""で囲まれるコマンドは、コマンドプロンプトで実行することを仮定している。

●PGP keyの準備
・既にPGP keyがある場合
"gpg --import (鍵ファイル名)"として、GPGへの取り込みを行う。
例:"gpg --import key.asc"

・新規にPGP keyを作成する場合
"gpg --gen-key"を実行すると、色々質問され、その後keyができる。

・作成した鍵の公開鍵(public key)を配布する方法
"gpg --export (鍵) > ファイル名"で、公開鍵がファイルに保存できるので、そのファイルを配布する。
「(鍵)」の部分は、鍵に関連付けられたメールアドレスの一部で良い。
例:"gpg --export ynomura > ynomura.asc" → ynomura.ascを配る

・秘密鍵をファイルに保存する方法
"gpg --export-secret-keys (鍵) > ファイル名"とする。

●コマンドラインからの暗号化/復号
・暗号化
"gpg -e -r (公開鍵) (暗号化するファイル名)"を実行すると、暗号化されたファイルが別に作られる。(公開鍵)の部分は、公開鍵に関連付けられたメールアドレスの一部で良い。
例:"gpg -e -r ynomura file.doc" →file.doc.gpgができる

・復号
"gpg (暗号化されたファイル名)"を実行する。適切な秘密鍵を持っていれば、パスワードの入力を求められ、入力すると、復号されたファイルが別に作られる。
例:"gpg file.doc.gpg" →file.docができる

●メールソフト用プラグイン
・Becky!用プラグイン
このページからダウンロードできる。インストールは自動で、インストール後の設定は不要だった。

・Datula用プラグイン
このページからダウンロードできる。インストールは手動だが、ファイルを1つ名前を変えて移動するだけ。

2006年07月12日

MozillaへのJava plugin設定

先月の話だが、思い出したので記録しておく。

RedHat9 Linuxにjdk1.5をインストールした。MozillaにJavaプラグインを設定しようとして、
/usr/java/jdk1.5.0_07/README.html
からSunのサイトのマニュアルを辿っていって、
http://java.sun.com/j2se/1.5.0/manual_install_linux.html
にある通りに、
ln -s /usr/java/jdk1.5.0_07/jre/plubin/i386/ns7/libjavaplugin_oji.so /usr/lib/mozilla/plugins/
とやって、Mozillaを起動して、「ヘルプ」→「Plug-inについて」を開いたら、認識されていなかった。
Installed plug-insの画面に、Javaプラグインが表示されないのである。当然、/usr/java/jdk1.5.0_07/demo/applets.htmlから何かJavaアプレットを選択しても起動しない。

2時間くらい色々調べた結果、「RedHat9のgccは3.xだから、当然Mozillaもgcc 3.xでコンパイルされてるんだろう」という思い込みが問題の原因だったことが判明した。
ln -s /usr/java/jdk1.5.0_07/jre/plubin/i386/ns7-gcc29/libjavaplugin_oji.so /usr/lib/mozilla/plugins/
("ns7"を"ns7-gcc29"に変更)が正解だったようで、これによりMozillaがJavaプラグインを認識した。

結局この1行でプラグインのインストールは完了だったのに、つまらない思い込みで、えらい時間を潰してしまった。

2007年04月20日

(VMWare)HDDに空きが無くてもshrink成功

中身が少ないのに肥大化してしまったVMWareの仮想ディスクをスリムにする(shrink)するためには、その肥大化した仮想ディスクと同じサイズの空きがHDDに必要だということが、VMWareサイトのページに書いてある。

Shrinking requires free disk space on the host equal to the size of the virtual disk you are shrinking.

しかしながら、先日、割合的にほとんどHDDに空きが無い状態にもかかわらずshrinkに成功したので、諦めずに試してみる価値ありということで報告する。

環境とshrink前の状態は以下の通り。
VMWare: 5.5.3 build-34685 (日本語版)
Host OS: Windows XP Home Edition
Guest OS: Windows 2000 SP4
Virtual disk: 190GB (NTFS, splitted by 2GB, 200GB at max, 115GB used)
  (ホストOSで見ると「現在のサイズ190GB」、ゲストOSで見ると115GB使用中)
Host HDD: 232GB, NTFS, <8GB Free

手順はVMWareのページの通り。
1. ゲストOSでデフラグ
2, ゲストOS終了
3. ホストOSで「仮想マシン設定」画面を開き、「デバイス」のハードディスクを選択して「ディスクの最適化」実行
4, ゲストOS起動
5. コントロールパネルから「VMWare Tools」→「仮想ディスク圧縮」を開き、shrinkするパーティションにチェックを入れて「圧縮の準備」実行
6. 「圧縮しますか?」という感じのダイアログが出るので、OKする

続きを読む "(VMWare)HDDに空きが無くてもshrink成功" »

2007年08月10日

忘れやすいC++の仕様(1/3)

私は10年ほど前にC++の仕様を隅から隅まで覚えたことがあるが、それから10年、全く触っていない。Cは時々接するのでANSI Cの仕様はまだ結構覚えているが、C++の仕様は全般的にうろ覚えだ。
そんな状態で、久々にC++の仕様を眺めていると、すっかり忘れていることがいくつもあったので、ここに記す。

(1) 構造体内の構造体定義の参照

struct Outer{
	struct Inner{
	int i;
	} j;
}
というコードのInnerを参照する場合、Cだと
struct Inner i;
とできるが、C++だと
struct Outer::Inner i;
としないといけない。

(2)int値のenum型への変換
Cでは

enum Color {RED=1, GREEN, BLUE};
enum Color c;
c = 1;
ができるが、C++ではできない。
c = Color(1);
c = (Color)1;
のようにする必要がある。
蛇足だが、Cで"c = Color(1);"とすると、コンパイルエラーとはならず、Color()という関数が無いというリンクエラーになる。

(3)template文はクラス定義以外にも使える

template<class T> T square(T x)
{
	T y;
	y = x * x;
	return y;
}
のような関数定義もできる。
ひょっとしてtemplateをクラス定義にしか使えないと思い込んでいる人は少数だろうか。

(4)通常型引数と参照型引数の多重定義
C++では引数の型を変えて同じ名前の関数を多重定義できるが、

void foo(int i) {}
void foo(int &i) {}
の2つを同時に定義することはできない。
よく考えると、これらの関数を呼び分けられないので当然とも思える。
同様に
void foo(int i) {}
void foo(const int &i) {}
の組み合わせも不可だが、
void foo(int &i) {}
void foo(const int &i) {}
の2つは同時に定義できる。
なお、g++では重複定義自体はエラーにならず、呼び出し文にてコンパイルエラーとなった。

2007年08月13日

忘れやすいC++の仕様(2/3)

続いて、C++のクラスの仕様に関する、忘れていたことを書き留める。

(1) 定数オブジェクトは定数メソッドしか使えない
例:

class CLS{
    int i;
public:
    CLS(int i = 1){this->i = i;}
    int get() const {return i;}  /* (a) */
    void set(int i) {this->i = i;}
};

int main()
{
    CLS c1;
    const CLS c2(2);
    int i;

    c1.set(3);
    c2.set(4);    /* (b) */
    i = c1.get();
    i = c2.get();    /* (c) */
}
このソースをコンパイルすると、(b)でエラーになる。
もし(a)の行のconstを外すと、(c)もコンパイルエラーになる。コンパイル時にreadonlyとwritableの区別はメソッド呼び出し毎に厳密になされる言語仕様らしい。
定数オブジェクトが出現する可能性を考えると、クラス定義においてメソッドの定数定義は重要だ。

(2)デフォルトコンストラクタが無いと配列を定義できない

class CLS{
    int i;
public:
    CLS(int i){this->i = i;}    /* (d) */
};

int main()
{
    CLS c[2];    /* (e) */
}
このコードは、(e)がコンパイルエラーになる。
C++では、配列の定義時に引数のあるコンストラクタが呼べないので、引数なしで呼べるコンストラクタ(デフォルトコンストラクタ)が存在しないと、そのクラスの配列は定義できない。
従って、CLS::CLS()やCLS::CLS(int i=0)のようなメソッドが必要になる。

なお、このコードの(d)を削除すると、コンパイルが通る。明示的なコンストラクタ定義が1つも無ければ、自動的にデフォルトコンストラクタが作成されるからだ。

(3)単項演算子の定義方法
単項演算子には前置(++obj等)と後置(obj++等)があり、それぞれ定義方法が異なる。
クラス名をCLS、演算子を@とすると、前置はCLS::operator@() / operator@(CLS&)、後置はCLS::operator@(int) / operator@(CLS&, int)で定義する。呼び出し時には、int引数には0が入る。

また、単項演算子関数が返す型は、CLSでもCLS&でも良い。だから、通常は異なる値を生成する(一時的なインスタンスを作って返す)マイナス演算子が何もしないなら、

CLS& CLS::operator-() {return *this;}
と参照を返すこともできる。

(4) コピーコンストラクタと代入演算子の共通化
コピーコンストラクタを定義する必要がある場合、代入演算子もそっくりな内容で定義する必要がある場合が多い。
そんな代入演算子を定義しようとして、

class CLS{
private:
    …
public:
    CLS(const CLS& c){…}
    const CLS& operator=(const CLS& c){CLS(c); return *this;}  /* (f) */
};
のようにコピーコンストラクタを普通のメソッドと同じ感覚で呼び出そうとすると、コンパイルエラーになる。なぜなら、上記の"CLS(c);"は"CLS c;"と同じ意味(!)、つまり新たなインスタンスの定義になるからだ。
コピーコンストラクタと代入演算子の処理を共通化するには、次のように別な関数を用意する必要がある。
class CLS{
private:
    …
    inline void copy(const CLS& c){…}
public:
    CLS(const CLS& c){copy(c);}
    const CLS& operator=(const CLS& c){copy(c); return *this;}
};

(参考文献:JIS X 3014)

2007年08月14日

忘れやすいC++の仕様(3/3)

1つ前のエントリーの続きである。

(5) 純粋仮想関数の定義方法
クラス定義内で、"virtual void foo() = 0;"のように、メソッドのプロトタイプにて、関数=0;とする。
関数の宣言と同時に、関数のポインタの値を0に決めてしまうという感じだろうか。独特の風味だ。

この"=0"を外すと、派生クラスに関数の実体があっても、基底クラスの仮想関数の実体が無いということでコンパイルエラーになるし、"=0"じゃなく"{}"とすると、その基底クラス(仮想クラス)のインスタンスを作ることが可能になってしまう。

(6) デストラクタはvirtualにすると派生クラスのデストラクタも呼ばれる

class CLS{
    …
public:
    virtual ~CLS() {…}  /* (g) */
};

class DRV : public CLS{
    …
public:
    ~DRV() {…}
};

int main()
{
    CLS* p = new DRV;
    delete p;  /* (h) */
}
このコードにおいて、(h)の基底クラスのポインタを使ったdeleteを実行すると、派生クラスのデストラクタDRV::~DRV()と基底クラスのデストラクタCLS::=~CLS()が順に呼び出される。もし(g)のvirtualが無いと、CLS::=~CLS()しか呼び出されない。
従って、基底クラスのデストラクタはvirtualにすべしと言えるだろう。

蛇足だが、派生クラスのポインタを使ってdeleteすると、基底クラスのデストラクタをvirtualにしていなくても、派生クラスのデストラクタの後に基底クラスのデストラクタが呼び出される。

(7) フレンド関数の1つ目の引数は型変換される
フレンド関数の1つ目の引数については、フレンド関数の他に適用できるクラスメソッドや関数やが無く、型変換により適用可能であれば、自動的に型変換されて適用される。
クラス外で定義されるクラスメソッドは、1つ目の引数が型変換されて適用されることは無い。従って、フレンド関数はクラスメソッドよりも適用範囲が広いと言える。

次のコードは、1つの演算に適用できるクラスメソッドとフレンド関数との両方がある例である。

class CLS1{
    friend class CLS2;
    int i;
public:
    CLS1(int i=0){this->i = i;}
    CLS1& operator+(double d){i += (int)d; return *this;}  /* (k) */
};

class CLS2{
    double d;
public:
    CLS2(){d = 0.0;}
    CLS2(CLS1 &i){d = (double)i.i;}  /* (l) */
    friend CLS2& operator+(CLS2, double);  /* (m) */
    double get() const {return d;}
};

CLS2& operator+(CLS2 c2, double e)  /* (m) */
{
    c2.d += e;
    return c2;
}

int main()
{
    CLS1 c1(3);
    CLS2 c2 = c1 + 2.5;  /* (n) */
}
このコードにおいては、(n)の演算において(k)のクラスメソッドがc1.operator+(2.5)という形で呼び出され、演算結果は5.0になるが、もし(k)の行が存在しないと、(n)の演算において、(l)による型変換が自動的に適用されて、(m)のフレンド関数がoperator+(CLS2(c1), 2.5)という形で呼ばれ、演算結果は5.5になる。

2007年10月28日

Module::Starter::PBP v0.0.3のエラー

ITProのページ「Webプログラミング実力アップ Part1 正しいPerl/CGIの書き方」を参考に、Module::Starter::PBPをインストールして、

perl -MModule::Starter::PBP=setup

を実行して、module-starterを実行すると、
Unknown placeholder <MAIN PM FILE> in Makefile.PL

というエラーが出た。

何かCPANのモジュールが足りないのか、1つ目のコマンドで生成された、~/.module-starter/configの内容が悪いのか、などと思って色々試したが、全く改善しなかった。
仕方なく、インストールされたModule::Starter::PBPの本体であるPBP.pmを解析した所、同じディレクトリのSimple.pmと整合していないことが判った。どうやら、CPANにてModule::Starter::Simpleがバージョンアップされて(今回インストールされたのはv0.0.3のPBP.pmとv1.44のSimple.pm)不整合が発生したようだ。

結局、PBP.pmのsub Makefile_PL_gutsとsub Build_PL_gutsの最初の方を以下のように修正した。
(#の後が元々の記述、#の後が無い行は新しく追加した行)

sub Makefile_PL_guts {
my $self = shift;
my $main_module = shift; #
my $main_pm_file = $self->_module_to_pm_file($main_module); #
my %context = (
'MAIN MODULE' => $main_module, #shift,
'MAIN PM FILE' => $main_pm_file, #shift,

sub Build_PL_guts {
my $self = shift;
my $main_module = shift; #
my $main_pm_file = $self->_module_to_pm_file($main_module); #
my %context = (
'MAIN MODULE' => $main_module, #shift,
'MAIN PM FILE' => $main_pm_file, #shift,

これにより、module-starterがエラー無く動くようになった。

それから、上記ITProのページに書かれている通りに、module-starterを実行した直後の状態の.pmに対して"perl Build test"までを実行すると、

Global symbol "$VERSION" requires explicit package name ...

というエラーが出る。どうせ雛形の中の書き換える部分のエラーなので放っておいても良いのだが、これを回避するには、同じくPBP.pmの中に書かれているModule.pmのテンプレートの
use version; $VERSION = qv('0.0.3');

use version; our $VERSION = qv('0.0.3');
に書き換えておけばよい。(書き換えた後は~/.module-starterを作り直す必要があるので注意)

続きを読む "Module::Starter::PBP v0.0.3のエラー" »

2008年01月03日

データベース用語和英対応表

6~7年前に買ったSQLの入門書を、捨てる前に読み返している。この入門書を使って1回SQLを勉強したのだが、実際に使うことが無かったため、全く身に付かず、歳のせいで記憶にも残っていないのだ。実際、MySQLを触りながら復習しようとして、mysqlを起動すると、"select * from (テーブル名);"以外の文はそらでは全く書けなかった。

従って、SQLの入門書とMySQLのマニュアル(MySQL info)とを見ながらSQLを試しているのだが、SQLの入門書が日本語でMySQLのマニュアルが英語であり、筆者にデータベースの基礎知識がないため、日本語と英語の対応が取れない用語がいくつか発生した。

そこで、出くわしたデータベース用語の日本語と英語の対応表を作ることにした。

日本語英語
階層型データベースhierarchical database
ネットワーク型データベースnetwork structure database
リレーショナルデータベースrelational database, RDB
データ定義言語Data Definition Language, DDL
データ操作言語Data Manipulation Language, DML
データ制御言語Data Control Language, DCL
主キーprimary key
候補キーcandidate key
代理キー(代替キー)alternate key
複合キー(連結キー)composite key
外部キーforeign key
非キー属性non-key attribute
正規化normalization
非正規化denormalization
非正規形non-first normal form. NF2
第1正規形first normal form, 1NF
第2正規形second normal form, 2NF
第3正規形third normal form, 3NF
ボイス-コッド正規形Boyce-Codd normal form, BCNF
第4正規形forth normal form, 4NF
第5正規形fifth normal form, 5NF
射影-結合正規形projection-join normal form, PJNF
関係代数relational algebra
集合演算set operation
関係演算relational operation
sum
difference
product
quotient
直積(デカルト積)direct product(Cartesian product)
選択selection
射影projection
結合join
交差結合cross join(Cartesian join)
等結合equi-join
自然結合natural join
内部結合inner join
外部結合outer join
左外部結合left outer join
右外部結合right outer join
自己結合self-join
相関サブクエリーcorrelated subquery
集計関数aggregate function
参照整合性制約referential integrity constraint
一意性制約unique constraint
(2008/1/4 更新)

続きを読む "データベース用語和英対応表" »

2008年01月05日

第3正規形とBoyce-Codd正規形

久々にSQLの入門書を読んで復習しているが、これまでRDBの第2正規形や第3正規形をよく理解していなかったので、この機会にきちんと理解しようとしている。
この入門書には簡単な例だけで正規形の正確な意味が書かれていないので、ネットで調べながら勉強していると、ボイス-コッド正規形というものがあることを知った。

第1~3正規形とボイス-コッド正規形の意味を非常に浅く理解した後、ボイス-コッド正規形は必ず第3正規形の条件を満たすというのを読んで、理解するのに結構な時間がかかったので、忘れないように、自分なりの理解で図を書いてみた。

第2正規形とは、「主キーの値が決まれば、他の列の値が決まる」と書いてあることがあるが、私の理解では「主キーまたはいずれかの候補キーの値が全て決まらない限り、他の列の値はどれも決まらない」と書かないと意味が取れないと思う。
第3正規形は、第2正規形でかつ「候補キーでない列の値よって他の列の値が決まることがない」と理解した。
つまり第2正規形は主キーに対して制約がある形、第3正規形は非キー属性にも制約がある形と言えるのではないだろうか。

Boyce-Codd正規形は、「属性間の自明でない依存関係X→Yの全てについて、Xは候補キーかそれを含む集合」ということなので、「候補キーを含まないキーによって定まる属性はない」と理解したが、これだけでは第2~3正規形との関係がよくわからないので、図を書いて考えてみる。

2_3_bcnf.png

上の図において、A~Eは属性(列)で、ABが主キー、BCは(主キー以外の)候補キーであり、第1正規形である(第1正規形の条件を満たす)とする。
ABの値が決まればD,Eの値が決まるので、AB→D、AB→Eの依存関係がある。

B→Dの依存関係があると、候補キーの値が揃わなくても非キー属性の値が決まってしまうので、第2正規形でない。
D→Eの依存関係があると、候補キー以外の属性(非キー属性)によって決まる非キー属性があることになるので、第3正規形でない。(または、AB→D、D→Eで非キー属性が推移的に候補キーに従属するので、第3正規形でない)
C→Dの依存関係は、非キー属性によって決まる非キー属性があることにはならないので、第3正規形の条件を破らないが、候補キーを含まないキーによって定まる属性があることになるので、Boyce-Codd正規形の条件は満たさない。

続きを読む "第3正規形とBoyce-Codd正規形" »

2008年03月30日

自己出力プログラム

Perlで自分自身を出力するプログラムが作れるという話を聞いたので、考えてみた。

print文1行だけではどう考えても作れないので、コードを

C1; print C2;

(C1,C2は何らかのコード)という形だと仮定する。当然C2は"C1; print C2;"に展開されるコードということになるが、何も無い所から"print"なんて文字列を含む値を生成するモジュールは無いので、これを実現するには、媒介変数を使って"print"を複製するしかない。従って、コードを改めて
C1; $x = "C2printC3"; print "C4$xC5$xC6";

という形に仮定し直す。このコードで表示される文字列は
C4C2printC3C5C2printC3C6

となるが、ここで、C3の部分がダブルクォーテーションで始まり、「$x = "…"」ではなく「$x = q()」または「$x = qq()」を使わないと厳しいことがわかる。従って、コードをさらに
C1; $x = q(C2printC3); print "C4$xC5$xC6";

という形に仮定し直す。このコードで表示される文字列は、やはり
C4C2printC3C5C2printC3C6

となる。

「C4C2」の部分は「C1; $x = q(C2」に一致しないといけないので、C4=「C1; \$x = q(」(C4はダブルクォーテーションの中にあるので"$"はエスケープが必要)となる。
「C3C5C2」の部分は「C3); 」に一致しないといけないので、「C5C2」=「); 」ということになるが、C2はq()の中にあるため")"は使えないので、C2は空、C5=「); 」となる。
「C3C6」の部分は「 "C4\$xC5\$xC6";」に一致しないといけないが、両辺にC6があり、C6の末尾が「C6";」となることから、C6は空ということになる。

仮定したコードからC2,C4,C5,C6を消して書き直してみる。

C1; $x = q(printC3); print "C1; \$x = q($x); $x";

「C3C6」=「 "C4\$xC5\$xC6";」より、C3=「 "C1; \$x = q($x); $x";」となるので、コードは
C1; $x = q(print "C1; \$x = q($x); $x";); print "C1; \$x = q($x); $x";

となる。「C1; 」の部分は""による展開に気をつければ何でも良いということがわかる。

例えばC1を「$a = 0」にすると、コードは

$a = 0; $x = q(print "\$a = 0; \$x = q($x); $x";); print "\$a = 0; \$x = q($x); $x";

となり、「C1; 」を空にすると、コードは
$x = q(print "\$x = q($x); $x";); print "\$x = q($x); $x";

となる。これらのコードを実行すると、コード自身と全く同じ文字列を出力する。


続きを読む "自己出力プログラム" »

2008年04月19日

MySQLと日本語文字

MySQLで日本語文字を使うために調べたことを記録する。

●基本事項
サーバーではデータベース毎に文字コードセット(コーディング)を決めることができる(表毎、列毎の指定も可能)。またサーバー~クライアント間のデータ送受信時に使用する文字コードセットは随時変更することができる。
従って、DB上の文字列の文字コードに関わらず、クライアントから所望の文字コードセットで文字列を送受信することが可能である。

MySQLはコンパイル時(configure時)にデフォルトで全ての文字コードセットに対応している訳ではないので、日本語文字コードセットに対応するようにコンパイルする(configure実行時に--with-charsetオプションを付ける)必要がある。

動作中のMySQLで使用できる文字コードセットは、クライアントにて

SHOW CHARSET;
で見ることができる。

●サーバー側の文字コードセット指定
デフォルトの文字コードセットは、my.cnfの[server]エントリーのdefault-character-setで指定できる。
特定のDBに対しては、CREATE DATABASEまたはALTER DATABASE文にてCHARACTER SETオプションで指定できる。

●クライアント側の文字コードセット指定
クライアントで使用する文字コードセットは、

SET NAMES '文字コードセット名';
で指定できる。EUCは'ujis'、UTF-8は'utf8'、Shift-JISは'sjis'である。
実際には、これによって関係する複数の環境変数が同時に更新される。
文字コードセットに関係する環境変数の値は、
SHOW VARIABLES LIKE 'character%';
とすると全て見ることができる。

なお、MySQLのinfoを含め色々な所にmy.cnfの[client]エントリーのdefault-character-setにてデフォルトの文字コードセットを設定できるようなことが書かれているが、筆者の環境(FreeBSD 4.11+MySQL 5.0.27)ではその設定は反映されない。Webを見ると、他でも同じ現象が少なからず発生しているようだ。my.cnfにてこれを設定する場合は、1回mysqlを起動して環境変数が意図通りに変わっているかどうか確認しておくべきだと思う。

●LOAD DATA INFILEで使われる文字コードセット
MySQLのLOAD DATA INFILEでファイル読み出し時に使われる文字コードはcharacter_set_databaseの値であり、上述のSET NAMESでは更新されない。従って、対象ファイルにてデフォルトでない文字コードが含まれる場合は、先に

set character_set_database=文字コードセット名;
のようにして設定する必要がある。
なお、文字コードによっては区切り文字の判定に曖昧さが出る可能性があるため、日本語文字列は""(ダブルクォート)で括ってLOAD DATA INFILE文にFIELDS ENCLOSED BY '"'(シングルクォート2つの間にダブルクォート1つ)を指定するなどした方が良いと思う。
(参考:http://www.hirohama.biz/mysql/2008/01/31-100131.html

2008年05月24日

数式画像作成CGI

TeXの数式をPNG画像に変換するCGIを作ってみた。

例:\sum_{k=0}^{n-1}x^k = \frac{1-x^n}{1-x}

2008年06月08日

ImageMagickで半透明画像を作る

前のエントリーに貼っている数式画像は、このサイトの数式画像作成CGIで作った画像を、ImageMagickで白黒反転して半透明にしている。
今回実際に行ったコマンドを書き記す。

convert -negate -resize 75% original.png tmp_nega.png
convert tmp_nega.png -channel alpha -fx "g" tmp_blur.png
convert tmp_blur.png -fx "g>0" result.png

1行目が白黒反転と縦横75%縮小、2行目が各ピクセルのα値(非透過率)の設定、3行目が背景でない部分を白にするコマンドだ。2~3行目では白の度合いを緑のレベルで代用しているが、元画像は白黒なので、赤や青を使っても同じだ。

白さに比例したα値を設定した後に色を白にするのは、そうしないと輝度が下がりすぎてしまうからである。例えば輝度50%のピクセルに50%の透過率を与えると、黒地だと輝度が25%に下がってしまう。黒地のモノクロ画像を半透明にするには、その白さをα値にして、白で塗り潰せば良いのである。
しかし、それだと、暗色の背景で透過合成表示してくれない画像ビューワで見ると真っ白になってしまうため、3行目では白の度合いが0のピクセルだけは黒にしている。"g>0"を"1"にすれば白で塗り潰すことになる。

なお、この例では変換の度に異なる画像ファイルを作るようにしているが、上書きで良いなら同じファイル名を指定しても良い。

参考:
convertの使い方ImageMagickのサイト内)

続きを読む "ImageMagickで半透明画像を作る" »

2008年06月21日

C言語の悪習

ある所で、C言語のソースに

if (NULL == pointer){...
という表記があるのを見て、好ましくない書き方だ、と言ったら、一緒にいた2人に、この書き方には理由がある、この書き方もありだ、というようなことを言われた。
このC言語特有の"=="の左側に定数を置く書き方は、知ってると通っぽい、知ってることを自慢したくなる、不思議な匂いを漂わせるようだ。プロはみんなこう書いている、という錯覚を起こさせることもあるようだが、事実は全く逆で、まともな書籍には書かれない、最もわかってる人々は使わない書き方だ。著名なオープンソースではマイナーである。
しかし、これが好ましくない書き方であることを簡潔に説明することは難しく、また100%誤りと言えるものではなく、一応利点もあることから、根強い人気があり、まるで宗教論争のように平行線の議論が繰り返される。

C言語のスタイルでよく議論になってるのを見かけるトピックは他にもある。それらと合わせて、個人的な見解を書いてみる。

(1) 定数==変数
"NULL == pointer"に限らず、

0 == integer
とか
true == boolean
というのも見かける。
肯定派の根拠はもちろん、変数==定数という書き方だと、if文などの条件式で間違って(変数=定数)と書いた場合にコンパイルエラーにならないため、バグの元になりやすいというものだ。
しかし、それが唯一の利点であることが、逆に好ましくない理由を示している。もしその利点が無ければ、標記のような書き方は誰もしないだろう。その理由は、読みにくいからである。
その利点には、可読性を犠牲にするまでの価値があるだろうか。

(計算式=定数)だと定数を左にしなくてもコンパイルエラーになるし、(変数=変数)だとコンパイルエラーにする類似の手段はない。まさかそういう間違いをコンパイルエラーにするために普段から(0+変数==変数)と書く人は居るまい。救われるのは"=="と"="の書き間違い全てではなく、(変数=定数)のみである。
それも、現在広く使われているコンパイラでは、条件式の部分が(変数=定数)になっているとワーニングを出してくれるので、さほど問題ではない。それに、そもそもそんな書き間違いは稀であろう。

変数と定数の比較の時だけ比較対象を左に書き、それ以外の比較では比較対象を右に書く、というのは、コーディングスタイルとして整合性に欠けるのではないか。それとも、定数==変数と書く人は、必ず比較対象を左に書くのだろうか?
C言語以外の、比較対象の定数を比較演算子の右に書ける言語では、定数を左に書く人は居ないだろう。C言語の限られた比較演算の場合だけ、役立つかどうか不明な些細な利点のために、敢えてわかりにくい書き方をするべきだろうか。

(2) (char)NULL

char c = (char)NULL;
というやつである。もちろん正しくは
char c = '\0';
だ。
これの支持者は'\0'より(char)NULLの方が「ヌルキャラクタ」として理解しやすいし、(メジャーな誤用のため)よく見かけるし、(char)NULLが'\0'と異なる値になる処理系はまず存在せず誤動作しないので、間違いだと気付かないのだろう。
しかし、ANSIの言語仕様でNULLは「ヌルポインタ」であり、いかなるオブジェクトも指さないポインタの値と決まっている。C言語的には(int)NULLは0でなくても良いのだから、(char)NULLを'\0'の意味で書くのは誤りである。
それでも、実際に問題が起こらないなら読みやすい方がいいだろう、と言われれば、返す言葉が見つからない。

(3) プログラム終了時でもmalloc()したメモリは必ずfree()すべし
コーディングスタイルやコーディングルールとして、malloc()には必ず対になるfree()を書く、というのがある。習慣的に静的にメモリリークを防ぐためには、基本的には好ましいことであるが、main()を抜ける直前やexit()の直前のfree()は無駄である。争点は、そのようなfree()も書くべきかどうかだ。
free()に限らず、ソケットのclose()やファイルポインタのfclose()など、exit時にOSに返されると規定されているリソース全てについて同じ話が当てはまる。

終了時もfree()すべし派の言い分は、
・その方がプログラムの構造として美しい
・free()して害になることはほとんど無い
・exit()後に処理があると、free()を怠ることにより問題が生じる可能性がある
というのがあったのを記憶している。ソースコードの静的解析ツールを使うと警告が出てしまう、というのもあったかも知れない。3つ目はatexit()やon_exit()のことを言っているが、これは世界にそんなコードが1つあるか無いかというオーダーのレアケースであろう。

そりゃ本当に無害なら書いた方が良い、終了直前のfree()は省略できるというのは知らなくてもいい無駄な知識、だとは思うが、終了直前のfree()も書くとコンパイルされてマシン語コードになってしまうのである。例えばfree()1つくらいなら10~20バイトくらいかも知れないが、それも10個になると100~200バイトにもなる(PowerPCで試した1つの例だと120バイトになった)。それだけ実行ファイルのサイズが無駄に大きくなってしまうのである。

例えばソースコードの読みやすさやメンテナンス性を犠牲にしてまで実行ファイルのサイズが小さくなる書き方を選ぶべきかと問われれば、よほどサイズがクリティカルなプロジェクトで無い限りNoであろうが、終了直前のfree()を省略して害になることはほとんど無かろう。ゴミを作り出すコードを埋め込んでまで、ある種のプログラムの対称性を重視する価値があるだろうか。

自分が書くコード、目の前にあるコードに整っていることを求める気持ちはよくわかる。例えばもし、C言語のソースファイルの最後の閉じ括弧は書かなくても良くて、書かない方がオブジェクトコードが小さくなる、というコンパイラがあったとしたら、個人的には最後の閉じ括弧を書かないのは気持ち悪すぎてできない。書いてコメントアウトするのも同様である。読む用とコンパイル用に閉じ括弧ありのソースとなしのソースを別に作るとしても、コンパイル用のソースの閉じ括弧を消して保存する操作が違和感の極みであり、コードを見ながらの手作業ではかなり後味が悪いだろう。

それでも、私はやはり実利を取るべきであり、ゴミを生むコードは書くべきでないと思う。終了直前のfree()は、どうせまとめて焼却されるゴミは分別するコストが無駄、というアナロジーででも割り切って省くべきではないだろうか。

2008年06月22日

ビット数を数えるルーチン

32bitの立っているビット数を高速に数えるアルゴリズムとして、

int bitcount(unsigned long n)
{
    n = ((n&0xaaaaaaaa) >> 1) + (n&0x55555555); 
    n = ((n&0xcccccccc) >> 2) + (n&0x33333333); 
    n = ((n&0xf0f0f0f0) >> 4) + (n&0x0f0f0f0f); 
    n = ((n&0xff00ff00) >> 8) + (n&0x00ff00ff); 
    n = ((n&0xffff0000) >> 16) + (n&0x0000ffff); 
    return n;
}
こういうのが良く知られている。1行目で2ビットずつ足し合わせることで2ビットの中の立っているビット数を求め、2行目でそれらを2つずつ合計して4ビットの中の立っているビット数を求め…というのを32bit使って並列にやっている訳だ。ポイントを理解すれば単純明快、1ビットずつ32回評価するより段違いに速いのは明らかである。

数年前にこれを初めて見た時、独学の計算量節約プログラミングを長年続け、私の頭の中で完成しつつあった独自のプログラム理論は、一気に消し飛んだ。

そんな記憶も彼方の昨日、ネットサーフィンしていて、下のようなコードを見つけた。HAKMEM 169と呼ばれるアルゴリズムらしい。ネット上の色々なC言語実装を参考に勝手に整形した。

int bitcount2(unsigned long n){
    n = n - ((n >> 1) & 033333333333) - ((n >> 2) & 011111111111);
    return ((n + (n >> 3)) & 030707070707) % 077;
}
短い!
1つ目の例と同じ香りが漂うが、演算子の数が少ない。もしかして最強コードか?と思って解読しようとすると、数学的に大変ややこしい。
1行目は、3bitの整数m内の立っているビット数がm - (m>>1) - (m>>2)で計算できる(※1)ことを利用して、3bitずつ並列に、立っているビット数を計算する。2行目は、3bitずつのビット数を2つずつ足し合わせて6bitずつのビット数にしてから(※2)、bが6bitの整数、kが6の倍数の時にb*2^kを6bitの111111で割った余りがbになる(※3)ことを利用して、6bitずつの合計を求めている。

よく読むと、剰余演算を使ってるので、1つ目の例の方が速い。
参考:http://infolab.stanford.edu/~manku/bitcount/bitcount.html
("Parallel"が1つ目の例、"MIT"が2つ目の例に相当する。"Parallel"より速いのはビット数のテーブルを予め用意している。)

元々のMITのHAKMEMのコードは剰余演算のあるアセンブラで書かれており、アセンブラとしては目を疑うほど短い。HAKMEM 169は最速ではないが、コードのコンパクトさと合わせて称えるべきアルゴリズムなのだろう。

続きを読む "ビット数を数えるルーチン" »

2008年07月03日

乗除を使わずに円を描く

三角関数や平方根はもちろん、×や÷をも使わずに円を描くアルゴリズムがあることを知った。通称ブレゼンハムのアルゴリズム(Bresenham's circle algorithm)と呼ばれるらしい。

さぞかし恐ろしく複雑な反復演算をするんだろう、乗除を使った方が速いというオチではないか、と思いながらウェブ上で調べてみると、どのサンプルコードを見てもとてもシンプルだった。ここ「伝説のお茶の間」)とかここ(Wikipedia(英語))とかに図解入りの説明とサンプルコードがある。これは速そうだ。

それらのページの図を見て基本的な原理はすぐに理解できたが、説明とソースコードが理解できなかったので、自分で考えてみた。
Javaのデモページへのリンク
ソースコード

基本的な原理は、例えば格子上のXY平面(X座標とY座標は整数のみ)で(x,y)=(r,0)の点から45°分の円弧を描く場合、同じY座標でX軸方向に複数の点を描く必要は無いので、yを1ずつ動かしながら、適当な時にxを1つ動かして、点を描いていけば良い。45°分の円弧が描ければ、そのX軸対称、Y軸対称や90°回転を使って360°分の円弧が作れる。

yを+1していく時、いつxを-1すれば良いだろうか。
円の式はx2+y2=r2であるが、x,y,rを整数に限ると、もちろんこの等式は成立せず、なるべく誤差の小さいxを選んでいくことになる。ここでは左辺と右辺の差e(x,y)=x2+y2-r2を誤差として考える。
仮にyを固定してxを-1すると、誤差は(x-1)2+y2-r2になるので、誤差の差分は2x-1である。つまり、誤差eが2x-1より大きい時は、xを-1してもまだ(x,y)は円の外側なので、xを-1する必要がある。
yが1増えるとeが2y+1増えるので、eに2y+1を足していって2x-1を超えればxを-1すれば良い、と考えられる。そう考えてとりあえず書いたのがサンプルコードのmethod1()である。

なるほど、こういう手があったか。
要領が解ったので、もう少し真剣に考えてみる。

上の方法では点が外側に行き過ぎないようにしてるだけなので、円の内側に点を取った方が円弧に近い場合も、円弧より遠い外側の点を取ってしまう。
そこで、次に(x,y)と(x-1,y)の中点が円弧の内側にあるか外側にあるかで点を選ぶことが考えられる。そのためには、円弧とその中点との誤差e(x,y)=(x-1/2)2-y2-r2が正か負かを判定すれば良い。
このeはyが+1すれば2y+1増え、xが-1すれば2x-2減る。(r,0)から始めると初期値が-r+1/4なので、eは整数にならない。従って、e=0になることはなく、e>0かe<0である。yを+1していって、e>0の時は中点が円の外側にあるから、内側の点(x-1,y)を取れば(xを-1すれば)良く、逆にe<0の時は外側の点(x,y)を取れば(xをそのままにすれば)良い。
ということで書いたのが、サンプルコードのmethod2()である。
なお、このコードは上記のWikipediaにあるコード(通称Bresenham's circle algorithm)と意味的にはほぼ同じであり、全く同じ円を描くことを確認した。


MSXを使っていた頃、マシン語で書いてもSIN()やCOS()や乗除が遅いのに対して、BASICのCIRCLE文の円てどうやってこんなに速く描いてるんだろう、と不思議に思っていたものだが、内部でこういうのが動いてたんだなと思うと、感慨深い。

続きを読む "乗除を使わずに円を描く" »

2008年07月20日

整数演算のみで楕円を描く

前のエントリーでブレゼンハムのアルゴリズムを使って乗除を使わずに整数演算だけで円を描いてみたが、同じ原理を応用して、乗除を使わずに整数演算で楕円を描けないだろうか、と考えてみた。
Javaのデモページへのリンク
ソースコード

横幅が2a、縦幅が2bの楕円を考え、円の時と同様に、(a,0)や(0,b)から始めて、楕円の外側か内側かを判定してポインターを1ずつ縦横斜めに動かしながら、点を描いていく。第2象限~第4象限(90°~360°)はx,yの符号を変えながら第1象限(0°~90°)と同じ曲線を描けば良いので、第1象限だけを考える。

楕円の式は(x/a)2+(y/b)2=1であるが、整数演算のために割り算を避けて、b2x2+a2y2=a2b2とする。
まず、(x,y)=(a,0)からyを+1していって、xは(x,y)と(x-1,y)の中点が実際の楕円の外側になったら-1する(前のエントリー参照)。それを、xの動きがyの動きより大きくならない所、すなわち接線の傾きが135°になる所まで繰り返す。
次に、(x,y)=(0,b)からxを+1していって、同様に(x,y)と(x,y-1)の中点が楕円の外側になったら-1する、というのを、接線の傾きが135°になる所まで繰り返す。

式で書くと、(x,y)と(x-1,y)の中点は(x-1/2,y)なので、楕円の式に当てはめて、b2(x-1/2)2+a2y2 > a2b2なら中点が外側ということになる。E(x,y)を左辺-右辺とすると、E(a,0)=b2(-a+1/4)であり、E(x,y)はyが+1するとa2(2y+1)増え、xが-1するとb2(2x-2)減る。このE(x,y)の1回の増減分をそれぞれdEdy, dEdxと置くと、dEdyはyが+1すると2a2増え、dEdxはxが-1すると2b2減る。a2やb2は先に計算しておけばいいので、これで乗算を使わずに点を動かしていけることがわかる。
(x,y)と(x,y-1)の中点については、今の話のxとyを置き換えれば良い。

ということで書いたのが、サンプルコードのdraw1()である。コード上では上記のa,bはWx,Wyとしている。whileループの中では乗算は使っていない。
デモでは、赤線で描いているのがそれである。


さて、デモでは、比較のために青線でjava.awt.Graphics#drawArc()で楕円を描いて、その上にdraw1()で同じ大きさの楕円を赤線で描いているが、Java VMにも依るだろうが、青線と赤線が結構ずれている。原因として1つ間違いないのは、X軸、Y軸をy=0, x=0に取って上下対象、左右対称にしているので、楕円の横幅や縦幅が偶数の時も奇数しか書いていないことだ。-1<=y<=1で書くと縦幅は3、-2<=y<=2で書くと縦幅は5である。実は前のエントリーのブレゼンハムの円描画のアルゴリズムでも偶数幅の円が描けないという同じ問題があるのだが、コードの簡潔さを重視して敢えて触れなかった(巷のサンプルコードでも大概無視してるし)。
今回はI/F仕様をjava.awt.Graphics#drawArc()に合わせて楕円のバウンディングボックス(外接矩形)のサイズを入力するようにしているので、それに見合った動作をさせたいものである。

横幅が偶数の場合、-a < x < a-1の範囲で描くとすると、横方向の対称軸はx=-0.5となる。x=0とx=-1とでY座標が同じになるのだが、対称軸がx=0の楕円のx=0.5, x=-0.5の時のyをX方向に-0.5ずらして描くと、yの最大点、最小点が無くなってしまう。実際、a,bをそのままにして(x,y)=(a-1/2,0)や(0,b-1/2)からプロットすると、楕円の上下左右の端が十分に膨らまず、少し四角に近くなってしまった。
そもそも、バウンディングボックスを中心に考えると、x=-a, x=a-1の点はそれぞれ必ずxの最小点と最大点なのであり、中心軸がx=-0.5なので、X軸方向の半径がa-1/2の楕円を描かないといけないのであり、半径がaの円を平行移動する方針ではきれいな楕円にならないようだ。

そこで、曲線描画中は、楕円の横幅や縦幅が偶数の時は径を1/2減らして計算する方向で考えてみた。2aや2bが偶数の場合、楕円の式は(b-1/2)2(x-1/2)2+(a-1/2)2y2 = (a-1/2)2(b-1/2)2となり、(x,y)=(a-1,0)から始めてxを-1、yを+1していく時、(x-1,y)と(x,y)の中点が楕円の外側かどうかを判定する式は(b-1/2)2(x-1)2+(a-1/2)2y2-(a-1/2)2(b-1/2)2 > 0 となる。以下、左辺を4倍して考える。左辺の初期値はy=0なので(2b-1)2(-x+1/4)であり、yが+1すると(2a-1)2(2y+1)増え、xが-1すると(2b-1)2(2x-1)減る。以下略。結局、辛うじて乗算を使わずに点を動かしていける。

ということで書いたのが、サンプルコードのdraw2()である。コード上では上記の2a,2bがWx,Wyに対応する。
デモでは、比較のために青線でjava.awt.Graphics#drawArc()で楕円を描いて、その上にdraw2()で同じ大きさの楕円を緑色の線で描いている。JRE 6.0だと、緑線と青線はほとんど差が無い。

続きを読む "整数演算のみで楕円を描く" »

2008年12月04日

Windows XPでのかな入力のトラブル記

今年の春、会社で異動になり、転勤先で新しいパソコンを買ってもらった。仕事柄、メールをよく打つようになった。秋には仕事にも慣れてきて、横の席の人がメールを打っている音が耳に入ってくるようになった。すごい速さでキーボードをヒットしている。画面上に日本語が何文字か増える間にも、やたらキーボードを叩いている気がした。
すると、自分のタイピングも気になり出した。何せローマ字入力は指が完全に覚えているし、文字数に対して打鍵数が多い。「させて頂きます」は"SASETEITADAKIMASU"、「申し訳ございません」は"MOUSIWAKEGOZAIMASENN"、「よろしくお願い致します」は"YOROSHIKUONEGAIITASHIMASU"である。我ながらよくこんなにたくさんキーを打ってるものだ、と思った。1つのメールを書く間の打鍵量は、数えてみたらすごいことになるのではないか。
そんなことを意識すると、キーボードのJIS配列のかなキーに目の焦点が合うようになってきた。9割以上の人がローマ字入力で日本語を打つこの時代に、まだあったのか。

よく見ると、我が家のパソコンのキーボードにも、アルファベットキーや数字キーを中心に、キーの1つ1つにひらがなが刻印されている。数字キーには「ぬふあうえおやゆよわ」、アルファベットキーの1段目はUキーから左に読んでいくと「なんかすいてた」である。

20年以上前に買ったMSX2パソコンにも、JIS配列が書かれていた。「12345」が「アイウエオ」、「QWERT」が「カキクケコ」であるアイウエオ配列も書かれていたので、JIS配列で打つ訳が無かった。
大学で触ったパソコンには、ローマ字かな入力があり、実質それが標準だったので、私もローマ字入力で日本語を打ち始めた。同時に電子メールも使い始めたので、指が完全にローマ字入力になった。
以後、キーボードにJIS配列の刻印があっても、まず意識することは無かった。


1日の日本語入力量が増えて、ますますキーボードのひらがなの刻印にピントが合うことが増えた。子音、母音、子音、母音、子音、子音、母音、子音、子音、子音、母音。日本語入力中もローマ字入力のために頭の中はアルファベットが流れ続けている。指が押し続けているアルファベットにはひらがなが書いてある。

私には、かな入力を試さないことは不可能だった。


家で練習を始めてから1ヶ月ちょっと経ったが、時間に余裕がある時に会社でかな入力を試し始めると、やはり打つのに時間がかかる。指が配列を十分に覚えていないことも、英数字や記号を打つのが面倒なのも要因だが、致命的な問題が、時々勝手にかな入力モードから英数入力モードに切り替わってしまうことだ。
会社で使っているパソコンはWindows XPで、日本語入力に関しては何も設定を変更していない。ALT+ひらがなキーを押してローマ字入力とかな入力を切り替えているだけである。それでいて、漢字変換しながらひらがなを入力し続けているだけで、ふと画面を見ると「スケジュール4a30p」と表示されていたりする。間違って英数入力モードに切り替えてしまったのかと思って、反射的に「半角・全角/漢字」を押してもう一度「うちあわせ」と打ち込むと、画面には「4a30p」と出る。
突然かな入力モードから全角の英数入力モードに切り替わるのである。しかも、言語バーの表示は「A」ではなく「あ」のままなので、いつ英数入力モードに切り替わったのかは打ってみないとわからない。よく起こる時は1分に1回くらい起こる。何度も何度もそんなことが起こると、時間のロスになるだけでなく、平常心を保てなくなる。使い物にならない。

1つのパターンとして、Internet Explorerの入力フォームで、かな入力して、スペースを何度か叩いて漢字変換の候補枠を出して、カタカナを選んで確定して、数字キーに割り当たっているひらがなを打ち込もうとすると、100%の確率で発生することが判った。「スケジュール」を候補窓で確定して入力し、「う」キーを押すと全角英数に切り替わってしまうのだ。これを防ぐ方法は、候補窓でカタカナを確定した後は「半角・全角/漢字」を2回押してリセットすることしか見つけていない。しかも、この現象が起こるのはその手順だけではない。
不思議なことに、ほとんど同じ状況を家のPCで再現しても、家のPCでは全く起こらない。OSはどちらもWindows XP、日本語入力FEPはどちらもデフォルトのMS-IMEである。

Webで調べ回った限りでは、正確な原因には辿り着けなかったが、同様の症状に遭った人は少なからず居るようで、MS-IMEのバグというのが定説のようであり、確実にこれを回避する方法はただ1つ、コントロールパネルの「地域と言語のオプション」で「詳細なテキスト サービスをオフにする」を有効にすることなのだそうである。

確かに、その設定をすることにより、上記の問題は全く起こらなくなった。しかし、この設定をすると、言語バーが貧弱になり、半透明にならなくなったり、タスクバーに入れても出しても見えなくなったりして、とても悲しくなる。なんともはや、という感じである。JIS配列は至る所のキーボードに刻印されていても、既に見捨てられているのだろうか。

参考:http://www.atmarkit.co.jp/fwin2k/win2ktips/630ctfmon/ctfmon.html
本件のバグに関する直接的な記述は無いが、色々バグがあるようだ。

2009年03月01日

Java servletをPerl CGIに移植してみる

Javaのservletは、HTTPリクエストを受けてテキストを返す場合(HttpServlet)に限ると、役割はCGIと同じである。実行プロセスが常駐している以外は、CGIと大差が無い。CGIでも、例えばApache+mod_perlを使えばPerlのモジュールをapacheのプロセスに常駐させることができ、さらにJava servletと差が無くなる。従って、Java servletにすべきかCGIか、またCGIならどの言語を選ぶべきかは、動作速度と作り易さにかかっているのではないか…と思っていた。

先日、3択首都当てクイズのservletのコードを見直していて、文字列処理等でJava特有の面倒臭さがあり、PerlのCGIならかなり楽に書けるんじゃないか、と思った。
筆者はPerlは結構昔から使っているのだが、PerlのCGIを真面目に作ったことが無かったので、CGI.pmの勉強を兼ねて、3択首都当てクイズのサーバーサイドプログラムのCGI版を作ってみた。

CGIのソースコード
対応するクライアント(Java applet)側コード
対CGI版首都当て3択クイズの起動ページ

確かに幾分楽に書けたが、常駐型ではないことが前提になっている。当然、極端に遅い。速度を確保するには常駐型にする必要があるが、作ってみて思ったが、これを常駐型にするのは厄介である。
筆者はPerlのCGIを常駐型にする方法としてApache+mod_perlしか知らないので、これを前提に書くが、PerlのCGIはJavaと違って使用メモリ量を制限するのが容易ではない。このwebサーバーにもmod_perlを導入しているが、メモリに関しては結構苦労があった。適切なタイミングでガベッジコレクションさせる術が無いため、何回か毎にリセットするしか無いのである。1つのコンテキストで処理していると、リセット中のスループットが低下するため、2つのコンテキストで処理するようにしたいが、次にデータベースとの接続の管理方法が問題になる。もしTomcatのようなコネクションプールの仕組みが使えるとしても、そこまでしてマルチスレッドにしたいか?と悩んでしまう。
筆者はApache+mod_perlは重要な選択肢の1つだと考えているが、CGIをmod_perlでインターネットに公開するのは、怖くてできない。一般論として、CGIを常駐型で動かすのは、強引で裏技的な手段であり、好んで採用するものでは無いと思う。やはり、常駐型ならJavaのサーブレットで、非常駐ならCGIで作るのが良いのだろう、と考え直した。

続きを読む "Java servletをPerl CGIに移植してみる" »

2009年11月22日

PMXで楽譜作成

PMXとは、

こういう楽譜を作るツールである。フリーウェアであること、出力される楽譜が綺麗であることもさることながら、入力データをテキストファイルで書けるのが魅力である。

表示されている楽譜の上にマウス操作で音符を乗せていくようなソフトは多数あるが、入力し易いものに出会ったことが無い。筆者が細かいマウス操作が苦手なこともあるが、五線譜の上から2番目と3番目の線の間に音符を持っていって、次の音符は2番目の線の上に乗せて…という作業は苦痛である。ドラッグしているその音符をあと半音上に動かす必要がある時、今マウスが止まってるので、マウスが反応するぎりぎりの速度を狙ってゆっくりとマウスを前に動かし、マウスが反応したらすぐ手を止めないといけない。手を止めるのが少し遅くて目的地より半音上に行ってしまったら、今度は同様に慎重にマウスをバックさせてカーソルが動いた瞬間に手を止めないといけない。そうこうしていると、ボタンを押す指が疲れて一瞬震え、上か下かに半音ずれた所で音符が確定しまう。それを思い出して文章にしてるだけでイライラしてくる。
昔、MSXの「MUE」(ハル研究所)という、マウスが無かったので当然(パソコンの)キーボードで音符を入力する作曲ソフトを買ったことがあるが、そっちの方がずっと入力し易かった。音符はキーボードで入力できる方が便利なのである。

そこでMusiXTeXである。TeXのマクロであり、当然入力はテキストファイルなので音符はキーボード操作で入力できる。そして、フリーソフトであるにも関わらず、出力される楽譜は出版物並みに綺麗である。
MusiXTeXの書式は大変難しいが、そのプリプロセッサであるPMXM-Txを使うと、かなり容易である。M-Txは楽譜無いに歌詞も書けるようにPMXを拡張したものだが、若干PMXと書式が異なる。M-Txを使うにはPMXの知識も必要になるので、歌詞が要らないならPMXだけで充分と思う。

上の楽譜は、次のソースファイルから生成されたものである。

2 1 3 4 3 4 1 0
0 8 20 0.0

bt
./

It144ipipi
Ap

r4 rp ( a82 e+ a ) r r4 ( e82 e+ gs ) r r4 ( a82 e+ a ) r r4 /
( e85 ds e ds e b dn c a4 ) r8 ( c- e a b4 ) r8 ( e- gs b c4 ) r2 /


"./"までの部分は、PMXファイルとして必須の、楽譜全体に関する設定(ヘッダ)であり、"r4"より下の部分が各小節のデータ(ブロックの集まり)である。最低それらが含まれていれば、PMXのソースファイルとして成立する。
このファイルをforelise.pmxとして保存し、PMXがインストールされている環境で
pmx forelise.pmx

または、
pmxab forelise
musixflx forelise
musixtex forelise

とすると、TeXでお馴染みのDVIファイルができる。dvipsでPSファイルに、dvipdfでPDFファイルに変換できる。上の楽譜画像は、DVIファイルをdvipsでPSファイルにしてImageMagickでPNGに変換したものである。
1つ目のpmxコマンド一発でコンパイルする方法だと、ソースファイルに"I"コマンドがあると、ついでにMIDIファイルもできる。

上のソースファイルを解説する。
●ヘッダーの先頭から12個の数字(必須)


通称上での値意味
nv2パートの数
noinst1楽器の数
mtrnuml3(小節の境目の判定に使う)何分の何拍子の分子
mtrdenl4(小節の境目の判定に使う)何分の何拍子の分母
mtrnump3(実際の表示に使う)何分の何拍子の分子
mtrdenp4(実際の表示に使う)何分の何拍子の分母
xmtrnum01弱起(最初の小節が不完全)の場合の最初の小節の拍子数
isig0五線譜の左端の調を決める#や♭の数(負だと♭)
npages0出力ページ数、0は自動
nsyst8五線譜の段数(全ページ分)npages=0の時は1段当たりの小節数
musicsize20音符のサイズ
fracindent0.0五線譜の1段目のインデント量

●ヘッダーのそれ以降の行(必須)

項目上での値内容
楽器名 (noinst)行分の、楽器名(ここでは空行)
音符記号bt各パートの、ト音記号(t)かヘ音記号(b)かの設定(下のパートから)
出力先./出力先ディレクトリ("./"はカレントディレクトリ)

●楽譜全体の設定
"I"で始まる行はMIDIファイル出力の設定
 "t144"はテンポ
 "i...."は(nv)個分の楽器("pi"はピアノ)
"Ap"はMusiXTeXの「Type Kスラー」(またはPostscriptスラー)という綺麗なスラーを使う設定(要インストール)
●楽譜本体
・下のパートから書く
・パートの切り替えは"/"
・音符と音符の間はスペースで区切る
・音符について
 cdefgabはドレミファソラシ、rは休符
 音符内の数字は、音の長さ(1,2,4,8,16,32分音符はそれぞれ0,2,4,8,1,3)とオクターブ(4がト音記号の普通の所)
  長さを省略すると1つ前の音と同じ長さ
  オクターブを省略すると1つ前の音に一番近い高さ(c b(ド、シ)ならシは1つ下のオクターブのシ)
 +/-はオクターブの上下移動
 "s"は#(♭なら"f")
・()はタイ/スラー
・rpは全休符
・小節の区切りは自動的に計算される


もう1つサンプルを作ってみた。解説は省略する。
csikospost.pmx(PMXソースファイル)
csikospost.pdf(できた楽譜)
csikospost.mid(できた音楽ファイル)

ついでにもう1つ公開する。
Nukenin1.pmx(PMXソースファイル)
Nukenin1.pdf(できた楽譜)
Nukenin1.mid(できた音楽ファイル)

参考資料:マニュアル(英語、PDF)

viやEmacsで楽譜を入力する。TeXで楽譜を作る。何とエレガントな響きであることか。

続きを読む "PMXで楽譜作成" »

2010年02月27日

Emacs Lispでシューティングゲームを作ってみた

・画面イメージ

|                                        |
|                                        |
|                                        |  score: 25
|                                        |
|    G         GG        GG         G    |
|     F        FF        FF        F     |
|      E      E  E      E  E      E      |
|       D    D    D    D    D    D       |
|        C  C      C  C      C  C        |
|         BB        BB        BB         |
|                                        |
|                                        |
|         ^                              |
|                                        |
|          ^                             |
|                                        |
|                                        |
|                                        |
|                                        |
|           A                            |

・プログラム tinyshoot.el

・使い方 Emacsで、M-x load-fileとして、このプログラムをロードする。

続きを読む "Emacs Lispでシューティングゲームを作ってみた" »

2010年03月28日

Script-Fuで迷路を描いてみた

今月、GIMPのScript-Fuを試したくなったことがあって、少しやってみたので、忘れない内にメモしておく。ついでに、使い道が考えにくいが、迷路を描かせてみた。

・サンプルスクリプト:maze.scm

上のリンク先のスクリプトを、GIMPのユーザースクリプトディレクトリ(UNIX系だと~/.gimp-2.?/scripts/、WindowsだとC:\Documents and Settings\(username)\.gimp-2.?\scripts、
Macだと~/Library/Application Support/Gimp/scripts/等)に置いて、GIMPを起動して、"File"→"Create"→"Maze"→"in new image"を実行すると、迷路の画像が作られる。または、GIMPで画像ファイルを開いて、"Filter"→"Maze"→"in new layer"を実行すると、その画像上に半透明の迷路のレイヤーが追加される。

original → original

Script-Fuは、SchemeというLisp系の言語をベースにした、GIMPに自動処理をさせるためのスクリプト言語である。Schemeの命令と、ファイルを開くとかレイヤーをコピーするとかファジー選択するとかの一通りのGIMPの操作に対応する、GIMPの関数が使える。Script-Fuコンソール上でGIMPの一通りの操作を行うことも、がんばればきっと可能である。
GIMPの関数の一覧は、"Help"→"Procedure browser"またはScript-Fuコンソールの"browse"ボタンを押すと出てくるProcedure browserで調べられる。
Schemeの命令については、GIMP 2.4以降ではSchemeの処理系がTinySchemeというものらしいが、資料が少ないので、以前のGIMPで使われていたSIODの資料を探すと良さそうである。gimp.orgTinyScheme移行に関するページにはR5RSを見ると良いと書かれているが、例えば実際にScript-Fuで動くrandom関数はR5RSには見当たらない。Script-Fuのlanguage referenceやAPI referenceが整うまでは、Script-Fuコンソールで試しながら手探りするしか無さそうである。

Script-Fuのスクリプトの作り方は、GIMPのマニュアルの"A Script-Fu Tutorial"の項に書いてある。作った.scmファイルに
(script-fu-register)
(script-fu-menu-register)
の記述があると、GIMP起動時または"Script-Fu"→"Refresh scripts"をした時にメニューに追加され、登録した関数がメニューから起動できるようになる。
例:

; 登録する関数((script-fu-register)や(script-fu-menu-register)より上の方に置く)
(define (script-fu-create-maze-image width height size …) …)

(script-fu-register
"script-fu-create-maze-image"  ; 登録する関数の名前
"in new image"   ; メニュー上の名前
"Creates a maze in new image"  ; メニュー項目のヘルプ文
"Y. Nomura"   ; オーサー
"(C)Osaka penguin consortium"   ; コピーライト
"2010.3.22"   ; デート
""    ; カラーモデル(""は指定なしを意味する)
SF-VALUE "width" "12"   ; 1つ目の引数の定義(型、ダイアログ上の名前、初期値)
SF-VALUE "height" "12"   ; 2つ目の引数の定義(同上)
SF-VALUE "cell size" "20"  ; 3つ目の引数の定義(同上)
 …   ; …
)

(script-fu-menu-register "script-fu-create-maze-image"
"/File/Create/Maze") ;メニューの追加先:"File"の下

フィルター形式のスクリプト、すなわち今操作している画像(アクティブイメージ)や今操作しているレイヤー(アクティブレイヤー)に対して処理を行うスクリプトの場合は、(script-fu-register)のスクリプトに与える引数のリストの最初をSF-IMAGEにし、アクティブレイヤーに対する操作ならそれに加えて2つ目の引数をSF-DRAWABLEにすると、アクティブイメージやアクティブレイヤーのIDが追加した関数に渡されるようになる。
例:

; 登録する関数((script-fu-register)や(script-fu-menu-register)より上の方に置く)
(define (script-fu-create-maze-layer image drawable width height …) …)
; imageにアクティブイメージのID、drawableにアクティブレイヤーのIDが入る

(script-fu-register
"script-fu-create-maze-layer"  ; 登録する関数の名前
"in new layer"   ; メニュー上の名前
"Creates a maze in new layer"  ; メニュー項目のヘルプ文
"Y. Nomura"   ;ザ・クリエーター
"(C)Osaka penguin committee" ; コピーライト・ディスクリプション
"2010.3.22"   ; ザ・クリエーション・デート
""    ; カラーモデル(任意)
SF-IMAGE "Image" 0   ; 1つ目の引数、アクティブイメージが入る
SF-DRAWABLE "Drawable" 0 ; アクティブレイヤーのIDも必要な場合
SF-VALUE "width" "12"  ; これ以降の引数は実行時にダイアログで入力する
SF-VALUE "height" "12"  ; 型、ダイアログ上の変数名、初期値(""で括るのが無難)
…   ; …
)

(script-fu-menu-register "script-fu-create-maze-layer"
"/Filters/Maze") ;メニューの追加先:"Filter"の下

デバッグの手段は、エラーコンソールが頼みの綱である。エラーコンソールを開いていないと、エラーメッセージがダイアログで出てしまうので、まずはエラーコンソールを開いておく。
構文エラーや実行時エラーに対するGIMPのエラーメッセージは大変シンプルであるが、(tracing TRUE)としておくと、長めのバックトレースが表示されて、助かることがある。
いわゆるprintfデバッグをしたい場合は、コード中で(gimp-message)を呼ぶと、エラーコンソールに任意の文字列を表示することができる。数字を表示したい時は、(number->string)で文字に変換する。
例:

(gimp-message (number->string x))
(gimp-message
(string-append
"x=" (number->string x)
", y=" (number->string y)))

続きを読む "Script-Fuで迷路を描いてみた" »

2010年07月25日

(Emacs)カーソル位置の単語を取得するマクロ

とうとう、我が部屋は、日光が入らないようにカーテンを閉めてエアコンを付けっ放しにしても、35℃を下回らなくなった。
この部屋がアパートの最上階の西の端の部屋で、建物がコンクリート造りのため、部屋自体が熱を持ってしまっているらしい。床や壁を触ると、ぬくいのである。天井は触っていないが、おそらくヒーターと化しているであろう。
クーラーを動かしっ放しにしてやっと外と同じくらいの気温である。どちらかというと、殺す気かという感じである。
いや、元々殺さない気はないのであろう。一般論として、建物というものが人の生命を守るとは限らない。部屋も凶器になり得るはずである。

従って、頭が働かず、材料が揃わないので、ブログが更新できない。
でも何か書きたくなったので、軽めに、最近やったEmacsのカスタマイズについて書くことにする。

Emacs Lispを書くようになって、C-h fのdescribe-function(関数の説明を見るコマンド)やC-h vのdescribe-variable(変数の説明を見るコマンド)をよく使うようになった。カーソルをelispの関数名や変数名の上に置いてC-h fとすると、デフォルトのでその関数名や変数名が候補になるのが便利である。それに慣れると、カーソル位置にある単語をキルリング(Xで言うセレクション、Windowsで言うクリップボード)にコピーするキー操作が無いことに気付いた。
WindowsやMacだと大抵マウスのダブルクリックでそれができるので、結構無意識にやりたくなる操作である。

単語の先頭に移動するキーはあるので、
M-b (単語の先頭にジャンプ)
C-SPC (マークセット)
M-f (単語の末尾にジャンプ)
M-w (コピー)
とすると一応できるのだが、この8キーは結構打ちにくい。

筆者はこれをよく行うので、そういうことをする関数をキーに割り当てようとしたのだが、そういう関数が見つからなかった。そんな訳で、次のような設定を追加した。

(defun kill-ring-save-current-word ()
"Save current word to kill ring as if killed, but don't kill it."
(interactive)
(save-excursion
(forward-char)
(backward-sexp)
(let ((pos (point)))
(forward-sexp)
(kill-ring-save pos (point)))
))
(global-set-key "\C-xw" 'kill-ring-save-current-word)

これで、C-x wでその位置にある単語をコピーできるようになる。
カーソルが移動することもないので、例えば、これをコピーして別の所に貼り付けたいが、コピーした後に今のカーソル位置で少し編集したい、という時にも邪魔にならない。

続きを読む "(Emacs)カーソル位置の単語を取得するマクロ" »

2010年08月09日

Macのターミナルからtelnetして日本語入力する方法

Mac OS X(バージョン10.5.8)の「ターミナル」をEUC-JPモードにしてUNIXにtelnetでログインすると、日本語が入力できないことに気付いた。sshではできていたので、支障がないので放ってあったのだが、今回、sshでは入力できることを忘れていて、調べまくってしまった。

リモート側でod -xして入力を16進表示すると、7bit目が落ちていた。ありがちなパターンである。
しかし、Macの「ターミナル」の設定の「非アスキー入力をエスケープする」はOFFにしてあるし、telnet接続後にstty allとすると、ちゃんと"cs8","-parenb","-istrip"になっていた。念のため"stty pass8"とやったが、変わらなかった。「ターミナル」の問題でもttyの問題でもなく、telnet接続の問題のようである。

そこで、man telnetしたら1ページ目に出てきた、-8オプションを使ってみたが、状況は変わらなかった。ところが、

telnet> set binary
telnet> open ...

すると、日本語が入力できるようになった。それに代えて、telnet接続してからCtrl+]でtelnetモードに入って"set binary"としても日本語が入力できるようになった。しかし、それらと同じ意味のように読める"telnet -8"ではやはりできなかった。

仕方なく、しばらくの間、~/.telnetrcに

(hostname)
 set binary ←先頭はスペース

と書いて回避していたが、その後も気になって複数の接続先に対して色々試している内に、いつの間にか"telnet -8"でも日本語入力できるようになっていた。何だったのだろう??

2010年08月29日

[WinXP] 2回再起動でCoregaの無線LAN接続不能な場合

Windows XPのセットアップをしていて、Corega製の無線LANアダプターを導入したら、ドライバーをインストールして一度だけ再起動した後はアクセスポイントに繋がるのに、もう一度再起動すると、ワイヤレスネットワークの状態が「限定または接続なし」になり、インターネットには繋がらないという状態に出くわした。
物理層、データリンク層では接続されていたが、DHCPのIPアドレスが取得できない模様であった。
ドライバーを再インストールして、対アクセスポイントの設定を変えたりして、インターネットアクセスに成功して「よっしゃ!」と思って、再起動するとLANにも繋がらなくなったり、再起動しても繋がるようになって「よっしゃ!」と思って、Windows Updateするとネットワークに繋がらなくなったりする。解決したと思って暫くした後に否定されるのを繰り返されるのは、必要以上に疲れるものであることを思い知らされた。

筆者は基本的に無線LANを使わないのであるが、昔ある所に住んでた時、配線の都合で無線LANにせざるを得なかったので、半年だけ無線LANを使ったことがある。その時も最初は結構苦労したが、そのことをすっかり忘れて、同じ器具を使って同じことを繰り返してしまった。

まず、USBのWLANアダプターCG-WLUSB2GSのドライバーをインストールして、1回目の再起動後は繋がったが、2回目の再起動以後はIPアドレスが取得できない状況になった。
アクセスポイント側の設定もかなり疑った。アクセスポイントのIPアドレスがDHCPサーバーから与えられるアドレスと衝突することは無いか、DHCPサーバーは同じMACアドレスを受けているか、DHCPサーバーが持ってるエントリーを使い切ってはいないか、等々。
このCG-WLUSB2GSは昔から、使っているとすごく熱くなって、2〜3時間もするとオーバーヒートして動かなくなったので、使わなくなった物である。やはりハードウェア不良なのだろうと判断して、昔も結局追加購入したPCIのWLANアダプタCG-WLPCI54GLを持って行って、PCに差したが、やはり1回目の再起動後は繋がったが、2回目の再起動以後は繋がらなくなった。CG-WLUSB2GSのハードウェア不良が原因ではなかったようだが、前回も安定動作したCG-WLPCI54GLを使用することにした。

何回もドライバーのアンインストールとインストールを繰り返してやっと、インストールした直後の再起動後の「ネットワークユーティリティ」の画面と、2回目以降の再起動後の「ネットワークユーティリティ」の画面が全然違うことに気付いた。ステータスバーのWLANアイコンをクリックすると、1回目の再起動後は「コレガ無線LANユーティリティ」が起動し、それを操作してアクセスポイントに接続できるが、2回目以降はコントロールパネルの「ワイヤレス ネットワーク接続」が起動するようになって、アクセスポイントへの接続は何回待たされても成功しなかった。
ネット上のいくつかの情報を整理した結果、「ワイヤレス ネットワーク接続」の「Windowsでワイヤレスネットワークの設定を構成する」のチェックを外すと、「ワイヤレス ネットワーク接続」が起動しなくなり、再起動しても無線LANが正常に使える状態に戻った。これはわかりづらい…


さて、完全に解決したと思った爽快感に浸りながら、Windows Updateの指示に従い、WinXPのService Pack 3をインストールすると、

プロシージャエントリポイントapsSearchInterfaceがダイナミックリンクライブラリwlanapi.dllから見つかりませんでした

というエラーが出るようになり、またアクセスポイントに繋がらなくなった。
これは結構メジャーな問題らしく、何とも凶悪なことに、マイクロソフトのページには、ドライバを更新するしかないという意味のことが書かれている。そして、CG-WLPCI54GLは既にサポート対象外らしく、CoregaのWebページには、SP3対応のドライバはリリースされていなかった。
このカードは昔もWinXPで使用していたが、当時はSP3は存在しなかったので、使用実績は無い。さすがにこの状況はどうしようも無かろう、SP3をアンインストールするしかないな、と思ったが、しかしSP3のアンインストールってのはそもそも可能なのだろうか…と悩んだので引き続きネットで情報を探しまくっていたら、Coregaのサポートページのドライバのインストーラーを使わずに「新しいハードウェアの検索ウィザード」を使ってドライバをインストールすること、という情報と、CG-WLPCI54GLのドライバの代わりにCG-WLCB54GLのドライバを使ったら成功したという情報を見つけた。半信半疑で試してみた所、確かにCG-WLCB54GLのドライバを(「コレガ無線LANユーティリティ」無しで)インストールすると、使えるようになった。

続きを読む "[WinXP] 2回再起動でCoregaの無線LAN接続不能な場合" »

2010年10月11日

フリーのUMLツールを使ってみる

少し前からUMLを復習していて、UMLを書くためのフリーソフトを探している。
UMLの要素は、丸や四角のような基本図形の組み合わせで書けるので、専門のソフトでなくてもUMLは書ける。むしろ、Visioのような作図ツールや、簡単な図形描画ができるWordやPowerPointで書かれる場合も少なくない。筆者も10年近く前、会社でUMLを書く必要があった時、好んでVisioで書いていた。周囲でもVisioが多数派だったと思う。その後、部署の方針で少数派のRhapsodyだったかRational Roseだったかを使うことになったが、短期間だったし、使いこなせなかったので、全く覚えていない。だから、Visioが手元に無い今、それに近いDiaでUMLを書くのは悪くない選択肢だと思っている。

しかし、ここはやはり、UMLそのものの復習をするなら、UMLの要素を図形でなくUMLの要素として扱う、専門のツールを使ってみたい。四角は四角でなく、クラスかインターフェースかオブジェクトかを区別して管理してほしいのである。それに、専門のツールであれば、UMLとして誤った書き方を避けられる可能性も高い。

商用のソフトなら、Rational製品かEnterprise Architectが有名どころであるが、そんな高いものを買う気はさらさらない。Visual Paradigmというのもすごそうだが、やはり高価なのでパスである。今は無償のソフトでいいのがあれば使いたいという程度である。

今回は次のような条件で探してみた。
・フリーソフト
・UMLの要素をUMLとして管理できること
・UML以外の書き方を許さないこと
・なるべくUML 2.0対応
・WindowsでもMacでも使用可能(最近主にMacBookを使っているため)

触ってみたソフトと、使ってみた感想をまとめる。
1. ArgoUML
最初に興味をそそられたのがArgoUMLであった。正統派というか、見た目より内容重視というか、UMLツールの理想形を目指している、思想的に最も優れたソフトのように思えるのである。
ファイルフォーマットは多分に漏れずXMLベースであるが、当然だと言わんばかりに自動的に圧縮されるのもポイントが高い。
【利点】
・UMLのポリシーに忠実に作られている
・作成した図に対して、UMLとして不適切な箇所を指摘してくれる
・Javaで実装されており、クロスプラットフォームである
・複数言語のソースコードの出力が可能
【欠点】
・UML 2.0に対応していない(UML 1.4のみ)
・全体的にUIに癖があり、最初はコツを掴まないと全然思い通りにならない
・GUIは貧弱で、お世辞にも使いやすいとは言えない
・正しく使わないとおかしな状態になりやすく、しかも落ちずにいつの間にか回復不可能な状態に陥ることも多い
・日本語化されていない
・ドキュメントが不十分

ArgoUML sample image

2. astah* community(旧JUDE community)
日本製ということで、UMLが書けるツールとしては、国内では最も有名であろう。
しかし、名前がAstahになってから、無償版の機能制限が多くなってしまったようだ。
今回、無償版をダウンロードして使ってみたが、トップメニューから製品版の案内が出る上、メニューに有償版のみ有効になる灰色の項目が多すぎて非常に鬱陶しく、イラッと来たので、やめた。
【利点】日本語である
【欠点】何かにつけ、調べた挙句、フリー版ではできないことを知る羽目になる

3. Omondo EclipseUML
Eclipseのプラグインとしては定番のものである。
作成した図を画像ファイルとしてexportする方法がわからなかったので、やめた。

4. eUML2
これもEclipseのプラグインである。
EclipseのSoftware Updateで"UML"で検索したら出てきたので、使ってみた。
また、個人的にSwingのVisual Editorで馴染み深いSoyatec製だということにも興味を惹かれた。(Visual Editorの開発は止まってしまったようだが)
【利点】
・UML2に対応している(が、作成できる図の種類は限られる)
・Eclipseと親和性が高く、極めればEclipseでいろんなことができそう
【欠点】
・動作はとても不安定、よくJavaのエラーが出る(特に

Unhandled event loop exception
java.lang.StackOverflowError
は頻出、筆者の環境ではコンポジット構造図でClass間にnavigableでないAssociationの線を引くだけで出る)
・というか、まだまだバグだらけ
・できる図に違和感を感じることが多い(下のサンプルでは、ポートが要素の枠線上に無い、要求インターフェースの○を消せない(ソケット(半円)のみが正しい)、ボール(丸)とソケットがずれている)
・追加したのに表示されない要素、消すにはXMLを編集するしかない要素がある
・全体的に、XMLの編集を余儀なくされることが多い
eUML2 sample image

5. AmaterasUML
やはりEclipseのプラグインである。(そればかり狙った訳ではないのだが…)
まだまだ開発途上で、作成できる図の種類が少なすぎるので、やめた。

6. Topcased
なぜかEclipseのプラグインである。
Eclipse上で動くが、Eclipseっぽい動き方をしない。Eclipse上で独自の世界を構築してるようである。UMLの使い方としても独特のものがあるように感じる部分がある。
動作は安定しているが、思い通りの図を書くには後少しの所でつまずくことが多い。また、思い通りに要素を配置できない時に何が悪いのかわからないことも多い。
【利点】
・UML2に対応している
・まあまあ綺麗な図が書ける
・動作が安定している
・使い始めは覚えやすい
【欠点】
・Eclipseとの親和性が低い
・完全に思い通りのUMLを書くのは難しい
・というか、たぶん書けないものがある(コンポジット構造図のパートの内部構造など)
・特殊すぎて、UMLを学ぶという用途には適さない気がする
Topcased sample image
(ソケットはGIF形式でexportすると消える、PNG形式なら残るが色数が落とされて汚い)


…どうも、これに決まり、というものが無いなあ。

続きを読む "フリーのUMLツールを使ってみる" »

2010年12月19日

Emacsでドットエディターを作ってみた

ちょっとドット絵を作りたくなったが、適当なツールが思い付かなかった。Windowsの「ペイント」しか思い付かなかったが、それだけのためにWindowsを起動するのは負けのような気がした。
手元で動いているのはMacBookである。「ペイント」に相当するくらいのアプリケーションはプレインストールされているような気がするが、筆者はMacに疎いのである。少なくとも「アプリケーション」の中を眺める限り、それだとわかるものは無い。GIMPはインストールしているのだが、マウスが無いのでしんどそうだし、それだけのために重いGIMPを起動するのも負けのような気がした。

目の前ではCarbon Emacsが動いていた。ふと、Emacs Lispで(if (looking-at ...))とかやれば、1キーで白黒反転するマクロが簡単に書けるのではないか…と思って、やってみたら、思ったより使い易かったので、もう少し進めてみた。

doteditor.el(漢字コードはEUC)

■起動方法
M-x load-fileまたは(load-file)で上のファイルを開き、M-x dot-editor-mode

■キーバインド
SPC: カーソル位置の□と■の反転
M-r: 選択領域の□と■の反転
M-c: 指定サイズの矩形領域を□で作成
M-e: 選択領域をビットマップとみなして16進数に変換したものを、選択領域の直後に挿入
M-d: 選択領域の16進数を□と■のビットマップに変換したものを、選択領域の直後に挿入
M-p: 選択領域の16進数をPBM形式の画像データにしたものを、tmp.pbmというバッファに作成 & image-modeが使用可能ならそれでプレビュー

■使用例1
1. M-cとし、width, heightに32, 24を入力
 → □が32x24個挿入される
2. 編集

□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
□□□□□□□□□□□□■■■■■■■■□□□□□□□□□□□□
□□□□□□□□□■■■■■■■■■■■■■■□□□□□□□□□
□□□□□□□□■■■■■■■■■■■■■■■■□□□□□□□□
□□□□□□□■■■■■□■■■■■■□■■■■■□□□□□□□
□□□□□□■■■■■■■■■■■■■■■■■■■■□□□□□□
□□□□□□■■■■■■■■■■■■■■■■■■■■□□□□□□
□□□□□■■■■■■■■■□□□□■■■■■■■■■□□□□□
□□□□■■■■■■■■■■■■■■■■■■■■■■■■□□□□
□□□□■■■■■■■■■■■■■■■■■■■■■■■■□□□□
□□□■■■■■■■■■□□□□□□□□■■■■■■■■■□□□
□□□■■■■■■■■□□□□□□□□□□■■■■■■■■□□□
□□■■■■■■■□□□□□□□□□□□□□□■■■■■■■□□
□■■■■■■■□□□□□□□□□□□□□□□□■■■■■■■□
□■■■■■■■□□□□□□□□□□□□□□□□■■■■■■■□
□■■■■■■□□□□□□□□□□□□□□□□□□■■■■■■□
■■■□■■■□□□□□□□□□□□□□□□□□□■■■□■■■
■■■□■■■□□□□□□□□□□□□□□□□□□■■■□■■■
■■■□■■■□□□□□□□□□□□□□□□□□□■■■□■■■
■■□□□■■■□□□□□□□□□□□□□□□□■■■□□□■■
■■□□□■■□■■■■■■□□□□■■■■■■□■■□□□■■
□□□□□□□■□□□□□□■■■■□□□□□□■□□□□□□□
□□□□□□□□■■■■■■□□□□■■■■■■□□□□□□□□

3. ビットマップ全体を選択し、M-e
00000000
00000000
000ff000
007ffe00
00ffff00
01f7ef80
03ffffc0
03ffffc0
07fc3fe0
0ffffff0
0ffffff0
1ff00ff8
1fe007f8
3f8001fc
7f0000fe
7f0000fe
7e00007e
ee000077
ee000077
ee000077
c70000e3
c6fc3f63
0103c080
00fc3f00

4. 16進データを選択し、M-pで表示確認

5. 16進データを保存

■使用例2
1. 16進テキストのビットマップデータを読み込む

00, 00, 00, 00,
00, 07, f8, 00,
00, 1f, fe, 00,
00, 3f, ff, 00,
00, 7f, ff, c0,
00, ff, e7, f0,
01, ff, ff, fc,
01, ff, ff, e0,
03, ff, ff, e0,
03, ff, ff, d0,
03, ff, ff, d0,
07, ff, ff, 10,
07, ef, be, 08,
07, ef, be, 08,
07, df, bc, 08,
07, df, 78, 08,
07, df, 78, 08,
0f, de, f8, 10,
0f, dd, f0, 10,
1f, dd, f0, 10,
1f, ed, f0, 20,
3d, f3, e0, 40,
38, f3, e1, e0,
60, 3f, ff, f0,

2. データ全体を選択し、M-d
(0-9, a-f, A-F以外の文字は無視されるのでカンマはそのままで良い、"0x"があるとその"0"が展開対象になるので注意)
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
□□□□□□□□□□□□□■■■■■■■■□□□□□□□□□□□
□□□□□□□□□□□■■■■■■■■■■■■□□□□□□□□□
□□□□□□□□□□■■■■■■■■■■■■■■□□□□□□□□
□□□□□□□□□■■■■■■■■■■■■■■■■■□□□□□□
□□□□□□□□■■■■■■■■■■■□□■■■■■■■□□□□
□□□□□□□■■■■■■■■■■■■■■■■■■■■■■■□□
□□□□□□□■■■■■■■■■■■■■■■■■■■■□□□□□
□□□□□□■■■■■■■■■■■■■■■■■■■■■□□□□□
□□□□□□■■■■■■■■■■■■■■■■■■■■□■□□□□
□□□□□□■■■■■■■■■■■■■■■■■■■■□■□□□□
□□□□□■■■■■■■■■■■■■■■■■■■□□□■□□□□
□□□□□■■■■■■□■■■■■□■■■■■□□□□□■□□□
□□□□□■■■■■■□■■■■■□■■■■■□□□□□■□□□
□□□□□■■■■■□■■■■■■□■■■■□□□□□□■□□□
□□□□□■■■■■□■■■■■□■■■■□□□□□□□■□□□
□□□□□■■■■■□■■■■■□■■■■□□□□□□□■□□□
□□□□■■■■■■□■■■■□■■■■■□□□□□□■□□□□
□□□□■■■■■■□■■■□■■■■■□□□□□□□■□□□□
□□□■■■■■■■□■■■□■■■■■□□□□□□□■□□□□
□□□■■■■■■■■□■■□■■■■■□□□□□□■□□□□□
□□■■■■□■■■■■□□■■■■■□□□□□□■□□□□□□
□□■■■□□□■■■■□□■■■■■□□□□■■■■□□□□□
□■■□□□□□□□■■■■■■■■■■■■■■■■■■□□□□

3. 編集
以下略

続きを読む "Emacsでドットエディターを作ってみた" »

2011年01月18日

(Emacs) Faceを使って部分的に見た目を変える

Emacsのfaceとは、フォント名や太字/斜体/下線といったフォント属性や文字色といったフォント設定をまとめたものであり、バッファ内のテキストの一部分に適用する事ができる。Emacsでソースコードを開くと、予約語やコメント部分や文字列部分に色が付くが、これは、そういう色で文字を表示するというfaceがそれぞれ定義されており、それらの部分にそれぞれのfaceが適用されている状態である。

Emacsでフォントの設定をしていると、時々faceという単語が出てくる。意味がわからないままでもフォントの設定はできたが、特にX resourcesでフォントセットの定義をすると、何に使われるのかわからなくても、default, bold, italic, bold-italicの4つ分を定義させられる。これらのフォントも、テキストにそういうfaceが適用されると使用されるが、そういうfaceが適用されないと、使われる事がない。
筆者はC, Java, Perl, Pythonなどのプログラム言語のソースコードをEmacsで開くことがあるが、キーワードや特定パターンに色が付くのはよく見るが、太字や斜体になるのは滅多に見ない。目にするのは、helpやinfoを開いた時くらいである。最も多く編集するのはテキストファイルであるが、text-modeでは色も付かない。

せっかく設定したものがほとんど使われず、しかもどうやれば使えるのかがわからないのは、気になる。そこで、faceの使い方を少し調べることにした。


Emacsで開いているテキストの一部分のfaceを変える方法としては、主に
・facemenu-*関数を使う
・Font Lock modeを使う
・Hi-lock modeを使う
・Highlight Changes modeを使う
がある。他にも、Emacs23ならtext-scale-*関数があったり、色々あるようであるが、Emacs22のinfoですぐ見つかるのはこれくらいである。(10年くらい前にEmacsを使っていた時はhilit19.elというのにお世話になったが、今ではobsoleteのようである)


■facemenu.elを使う
範囲選択してfacemenu-set-*関数を実行すれば、その範囲にfaceを適用できる。

これは、Carbon Emacs 22.3で3文字ずつM-o o(facemenu-set-face)してみた例である。適用したfaceは順に、highlight, lazy-highlight, link, match, query-replace, custom-button, custom-button-pressed, custom-face-tagである。加えて、"abc"はM-o bでboldに、"def"はM-o iでitalicにしてある。


これは、後述のfacify-iroha-regionマクロで、1文字ずつ異なるfaceを適用してみたものである。

キャラクターベースのリモート端末でも、これくらいカラフルになる。

(EmacsはDebianのemacs21-nox、ターミナルソフトはPuTTY、設定はほぼvt100のdefault)

しかし、Font Lock modeがONになっていると使えない。昨今のPCの性能では、もはやFont Lock modeをOFFにして使用することは少ないと思うので、あまり使えなさそうである。
また、これによって設定されたfaceはバッファローカルであり、編集したテキストと共に保存される訳ではないので、あまりこれによるテキスト装飾を手作業でがんばる意味は無い。失われることを前提に一時的に装飾するのに使うものである。
ただ、上のようにマクロで好き勝手に装飾したい時は、使えそうである。


■font-lock-mode.elを使う
何らかのソースコードを開いて自動的に色が付くなら、まず間違いなく、Font Lock modeが有効になっている。予め設定されたルールに従って自動的にfaceが適用されるモードである。
大体、c-modeとかperl-modeとかの言語毎のモードに入ると、自動的にその言語特有のルールが追加される。
有効でない場合は、.emacs等に

(global-font-lock-mode t)
と書けば有効になる。

追加ルールの設定については、長くなったので、次のエントリーに書く。


■Hi-hock modeを使う
長くなったので、次の次のエントリーに書く。


■Highlight Changes modeを使う
変更部分に自動的にfaceが適用されるモードである。ファイルを開いてからM-x highlight-changes-modeとするか、.emacs等に

(global-highlight-changes 1)
と書くと有効になる。

次の画像は、テキストファイルの編集にHighlight Changes modeを使った画面の例である。

黒以外の部分が、M-x highlight-changes-modeを実行してから変更した部分である。色違いは、highlight-changes-rotate-facesで世代を切り替えたものである。
highlight-changes-previous-changeやhighlight-changes-next-change等の関数を使うと、前の変更部分や次の変更部分にジャンプできるようである。
facemenu.elを使った場合と同様、これによる色はファイルに保存される訳ではない。

保存されていない変更があるかどうかはmode lineの**でわかるし、変更履歴として保存される訳でもないので、使い所が難しい気がする。Font Lock modeを使っていると、編集中の部分に本来の着色がされなくなり、スペルミスの点検ができなくなったりするので、却って不便である。Font Lock modeの効果が少ないText modeで長文の一部を修正する時は便利かも知れない。

続きを読む "(Emacs) Faceを使って部分的に見た目を変える" »

(Emacs) Font Lockのルールを追加する

前のエントリーからの続きである。

Font Lock modeの追加ルールを設定するには、font-lock-add-keywordsという関数を使う。ルールは、正規表現のパターンとそれにマッチする部分に適用するface、の組み合わせとして定義する。モード毎の設定が基本だが、現在のバッファのみの設定もできる。

font-lock-add-keywords関数の書式は、

(font-lock-add-keywords モード名 ルールのリスト)
である。モード名をnilにすると、現在のバッファに対する設定になる。
ルールの書式はいくつかの種類がある。
  • a. MATCHER
  • b. (MATCHER . SUBEXP)
  • c. (MATCHER . FACENAME)
  • d. (MATCHER . HIGHLIGHTER)
  • e. (MATCHER HIGHLIGHTER1 HIGHLIGHTER2 ...)
  • f. (eval . FORM)
(参考:font-lock-add-keywordsのhelpと、Emacs LispのinfoのFont Lock ModeのSearch-based Fontificationの項)
a.とb.はfaceを指定しない場合(font-lock-keyword-faceが使われる)、c.はfaceのみを指定する場合(MATCHERにマッチする部分全体に適用される)、f.は正規表現の代わりにfaceの変更対象を検索する関数を別途定義する場合に使用するもので、a.〜c.はd.で代用でき、d.はe.で代用でき、f.は特殊なので、1つ覚えるならe.の形式が良いと思う。

HIGHLIGHTERの書式は、主に(SUBEXP FACENAME [OVERRIDE])である。SUBEXPは正規表現に()を使う場合の何番目の()の部分かの意味で、全体なら0とする。OVERRIDEは別のルールが適用済でも適用するかどうかである。

例えば、次のようにすると、java-modeの時に、TABの部分がunderline、2バイトスペースや行末の空白部分がtrailing-whitespaceというface(これらはfaces.elに定義されている)になる。

(font-lock-add-keywords 'java-mode '(
  ("\t" . 'underline)
  (" " . 'trailing-whitespace)
  ("[ \t]+$" . 'trailing-whitespace)
))
実行前

実行後

次のようにすると、java-modeに限らず、全モードで有効になる。

(defadvice font-lock-mode (before my-font-lock-mode ())
  (font-lock-add-keywords
   nil   ;現在のバッファのみ
    '(("\t" 0 'underline append)
      (" " . 'trailing-whitespace)
      ("[ \t]+$" . 'trailing-whitespace)
     )))
(ad-enable-advice 'font-lock-mode 'before 'my-font-lock-mode)
(ad-activate 'font-lock-mode)
なお、上の3行目の部分をnilでなくmajor-modeと書いている例をよく見かけるが、そのようにすると、バッファが開く度にfont-lock-add-keywordsが実行され、font-lock-keywords-alistが肥大化してしまうので、nilの方がいいと思う。


HIGHLIGHTERの書式には、他に、MATCHERがマッチした後の行末までの部分を対象に、別のパターンの検索を行う、anchoredな形式がある。
ANCHORED-MATCHERを使う場合は、HIGHLIGHTERの書式が(ANCHORED-MATCHER PRE-FORM POST-FORM SUBEXP-HIGHLIGHTERS...)となる。PRE-FORMは、このANCHORED-MATCHERの検索が始まる前に実行され、POST-FORMは、行末までこのANCHORED-MATCHERが探された後に実行される。これを使うと、以下のようなことができる。

■コメント部分の特定キーワードの表示を変える
筆者は試行錯誤中のコードをコメントアウトして残す癖があり、作成中のコードには使用中のコードの3〜4倍のコードがコメントに存在することが普通にあるが、デフォルトのルールでは大体それらがコメント色1色になってしまうので、たまに痛い時がある。

(font-lock-add-keywords nil '(
 (";"
  ("face\\|frame"
   nil  ;PRE-FORM
   (goto-char (match-end 0))  ;POST-FORM: pointを";"の直後に戻す
   (0 font-lock-type-face t))
  ("default"
   nil  ;PRE-FORM
   nil  ;POST-FORM
   (0 font-lock-builtin-face t))
  )))
実行前

実行後(コメント中の"face", "frame", "default"に色が付いている)

■CやJavaの"if"の後の"="を警告表示にする

(add-hook 'c-mode-common-hook
  '(lambda ()
    (font-lock-add-keywords major-mode '(
      ("\\<if\\>"
       ("[^<>=]\\(=\\)[^=]" nil nil (1 font-lock-warning-face))
       )))
))
実行前

実行後

過去にそんなミス滅多に無いだろ、みたいなことを書いたが、筆者はExcel VBAを書いた直後だとやってしまうことに気付いた。

■"\x1b\x24\x42"〜"\x1b\x28\x42"とその間の16進数に着色する

(font-lock-add-keywords nil '(
 ("\\\\x\\(1b\\)\\\\x\\(24\\)\\\\x\\(42\\).*?\\\\x\\(1b\\)\\\\x\\(28\\)\\\\x\\(42\\)"
  (1 font-lock-function-name-face t) ;"1b"の部分
  (2 font-lock-function-name-face t) ;"24"の部分
  (3 font-lock-function-name-face t) ;"42"の部分
  (4 font-lock-constant-face t)  ;"1b"の部分
  (5 font-lock-constant-face t)  ;"28"の部分
  (6 font-lock-constant-face t)  ;"42"の部分
  ("[0-9a-f]"  ;ANCHORED
   (progn  ;PRE-FORM
    (goto-char (match-end 3)) ;3つ目の後に移動
    (match-beginning 4))  ;4つ目の先頭までに限る
   nil  ;POST-FORM
   (0 font-lock-type-face t))
)))
実行前

実行後

続きを読む "(Emacs) Font Lockのルールを追加する" »

(Emacs) Hi-lock modeを使う

前のエントリーからの続きである。

Hi-lock modeは、Font Lock modeのバッファ毎の追加ルールを手軽にインタラクティブに設定できるI/Fである。
Font Lock modeを有効にしていると、C-sで検索すると、マッチする部分がハイライト表示される。それが便利なので、ハイライト表示するためだけにいちいちC-sで検索することがあるのは筆者だけではないであろう。検索を抜けるとハイライトが解除されてしまい、またハイライトするために検索してしまう。そんな時はHi-lock modeである。

M-x highlight-regexp(既にHi-lock modeならC-x w h)を実行して、ハイライトするパターンを入力すると、マッチする箇所がハイライトされる。何かマッチすると、自動的にHi-lock modeが有効になる。もしFace Lock modeがOFFだと、自動的にFace Lock modeもONになる。

・実行前(Hi-lock modeでない)

・"x"をhighlight-regexpした状態(Hi-lock mode、mode-lineに"Hi"が追加されているのに注目)

検索パターンを単にxとするのでなく\<x\>とすると、単語の一部でなく単独で現れるxを対象にできる。
・"\<x\>"をhighlight-regexpした状態

続けてC-x w hして別のパターンをハイライトすることもできる。
・さらに"\<y\>"をhighlight-regexpした状態

C-x w l(highlight-lines-matching-regexp)を使うと、パターンを含む行全体をハイライトできる。
・さらに"draw"をhighlight-lines-matching-regexpした状態

1つのパターンのハイライトを解除するには、C-x w r(unhighlight-regexp)を実行する。全て解除するなら、M-x hi-lock-modeとしてHi-lock modeをOFFにすれば良い。

C-x w b(hi-lock-write-interactive-patterns)とすると、そのモードに定義されたコメント形式で、現在のハイライト設定が挿入される。
・上の状態から続けてhi-lock-write-interactive-patternsを実行した状態

c-modeなら、/*〜*/で括られたものが挿入される。これを残しておくと、次にこのファイルでHi-lock modeがONになった時に同じハイライトがなされる。

ハイライトを解除した後で、コメントに書かれたHi-lock設定を読み込むには、C-x w i(hi-lock-find-patterns)とする。

コメント形式のHi-lock設定はFont Lock modeのルールと同じなので、これを書き換えると、結構複雑な設定も書ける。
・"draw"のルールを書き換え中("draw_に続く単語を赤太字、"draw_"より後ろの"r"をピンクでハイライトするつもり)

・C-x w i(hi-lock-find-patterns)を実行した状態

続きを読む "(Emacs) Hi-lock modeを使う" »

2011年03月29日

StrategyパターンとVisitorパターンの使い分けに関する考察(1)

今更ながら、かの著名なGoFのデザインパターンを復習している。

10年前に居た会社にGoF本の日本語版があり、たまに何とかパターンという言葉を使う人が居たので、その本を一通り流し読みしたことがあるが、その時はそれの重要性に気付かず、23種類のパターンのそれぞれについてなんとなくわかった気になったら、1行に要約して控えておいただけだった。
結局、その部署で耳にしたパターン名は「オブザーバーパターン」だけだったので、それ以外のパターンは全て忘れてしまった。

Observerパターンは要するに状態変化時のコールバック関数を登録する設計のことであり、特にGUIのイベントハンドラーを定義する仕組みでは頻出なので、忘れようが無いし、「コールバックモデル」とか「イベントハンドラー」とか、Javaなら「リスナー」とか言えば通じるので、もはや「オブザーバーパターン」という言葉を耳にすることも無い。もしソフトウェア開発の場で耳にしたら、何をカッコつけとんねん、と言いたくなるだろうし、「何かあったら電話して」と言われた時に「オブザーバーパターンですね」と答えるような用途は考えられないことは無いが、よほどのマニアでない限り、その洒落が通じることは無いであろう。

その後、デザインパターンへの興味を失ってしまったのだが、オブジェクト指向とは何かということに興味を持ち、時々図書館でそれ系の本を借りて読むようになると、GoFのデザインパターンがよく引用されているので、また目にするようになった。
というか、GoFのデザインパターン以外に、これぞオブジェクト指向設計と思うような例はほとんど目にしない。オブジェクト指向設計になっていても、実際に動作するアプリケーションになっていると、そのアプリが提供する機能に目を奪われてしまうからだろうか。

そこで、もう一度GoFのデザインパターンを復習した。

半年かかった。

23種類ってのは、1つ1つ取り組むと、思ったより多かった。
当初は1つ1つのパターンについてオリジナルなサンプルコードを作ることを目標としていたが、結構大変であることに気付いたので、方針変更して、興味を持ったテーマについてだけ書くことにした。


今回は、StrategyパターンとVisitorパターンの使い分けについて考える。全然違うもののようだが、特定の処理を交換可能にするという用途に限ると、似たような効果が得られるようである。

Strategyパターンとは、特定の処理を外部のクラスに置くことにより、既存のクラスを変更せずにアルゴリズムを交換可能にするパターンである。次のUML図は、StableClass以下を変更せずに、合計の計算方法を変更することを可能にする、Strategyパターンの適用例である。

これをJavaで実装したサンプルコードを、下の方に添付している。
StableClassを、計算対象のデータを保持する、変更不可なクラスとする。
StableClassが抽象的なSumStrategyへの参照を持っており、合計の計算はそれを介して行われる。具体的なSumStrategyは実行時に決まるので、StableClassを変更することなく、合計の計算方法を追加することができる。合計の計算方法として、通常の足し算でなく、掛け算を選択することができるように変更するには、新たなSumStrategyであるMultiplicationクラスを追加するだけで良い。

Visitorパターンとは、任意の処理を持つオブジェクトを受け付ける口を設けることにより、既存のクラスを変更せずに、様々な処理を後から追加できるようにするパターンである。
次のUML図は、上と同じことをVisitorパターンでやってみた例である。

Visitorパターンでは、visitorが操作対象となるクラス(以下visitee)をvisitすることをそのクラスがacceptするという表現が用いられる。
操作対象のクラスは、visitorのvisitメソッドを呼ぶだけの、acceptメソッドを用意する。抽象的なVisitorが提供するI/Fが呼ばれると、visitorはvisiteeの具体的なクラスを知らないまま、自身を引数にしてaccept()を呼び、visiteeのaccept()は、visitorの具体的なクラスを知らずに自身を引数にしてvisit()を呼ぶ。(accept()の呼び出し時にStableClassの具体的なクラスの特定が行われ、visit()の呼び出し時にVisitorの具体的なクラスの特定が行われる)
このdouble dispatchと呼ばれる2段階の呼び出しを経由することにより、StableClassとVisitorの結合は呼び出し時まで抽象化される。つまり、visitorとvisiteeのクラスの組み合わせによって決まる、実際に実行される処理は、実行時まで未確定にすることができるので、StableClassを変更せずに、新たなVisitorを追加できるのである。
従って、これによっても、新たな合計の計算方法を追加するには、図中のMultipleのようなクラスを追加するだけで良い。

一般的にはVisitorはVisiteeの全ての具象クラスをvisitできる必要は無いとされるため、上図のように、抽象Visiteeクラスにaccept()が無く、VisiteeからVisitorへの関連は、accept()を持つ具象Visiteeクラスから抽象Visitorへの関連とされる(accept()を持たない具象クラスからの関連は無い)ようである(GoF本は手元に無いので未確認)。しかし、今回のように、デフォルトの処理がVisitorとして存在する場合は、全てのVisiteeはvisitableであるとしても問題無さそうなので、StableClassにaccept()を設けてみる。そうすると、StableClassからVisitorへの関連は抽象クラス同士になり、Strategyパターンに近い構図になる。

これをJavaで書いたコードを、下の方に貼っている。

本題の考察に入る前に、もう少し大きなサンプルを作ってみる。
(次のエントリーに続く)

続きを読む "StrategyパターンとVisitorパターンの使い分けに関する考察(1)" »

2011年04月10日

StrategyパターンとVisitorパターンの使い分けに関する考察(2)

StrategyパターンとVisitorパターンの応用例として、簡単な引力モデルのシミュレーションを作ってみる。引力の計算方法は外部クラスに存在させ、既存クラスを変更せずに追加/変更可能にする。
なるべく簡単にするため、次のような制限をつける。
・全物体の質量は同じ
・空間は2次元
・引力は2物体間の距離だけで決まる

まず、Strategyパターンを応用する例を考える。
引力の計算方法についてStrategyパターンを適用し、クラス構成は、次のようにする。

class diagram

PhysicalObjectクラスの階層(緑部分)が物体の位置や速度を持ち、GravityStrategyクラス/インターフェースの階層(水色)が引力の計算処理を持つ。抽象レベルでPhysicalObjectがGravityStrategyに関連づけるため、PhysicalObjectの基底クラスがGravitystrategyへの参照を持つ。
NormalGravityは距離の2乗に反比例する引力、Repulsionは距離の2乗に反比例する斥力、NoGravityは引力なしである。

GravityStrategyが定義するメソッドは、計算結果を返すようにする方が自然だが、今回の例では、そのまま計算結果をPhysicalObjectに反映するようにしている。これは、Visitorとの類似性を追究する目的のため、そのようにした。その為、 PhysicalObjectの値を更新する為のaccessor(VelocityControllerインターフェース)が必要になっている。

Strategyパターンとして、そういう実現方法は許されるのかという問題があるが、一般に計算結果が1つの値とは限らないし、Strategyに委譲する処理が複雑になるほど、戻り側で必要な値が増える可能性があるし、出力する値の個数が決まっていると拡張する範囲が限られてしまう。
また、入力としてオブジェクト丸ごとでなく、最低限のパラメーターだけを渡す方がいいという考え方もあるが、やはりそのパラメーターの数が決まっていると拡張性が制限されてしまう可能性がある。
それに、入力のデータ型を新たに定義せずに委譲元のクラスそのものとするのであれば、出力のデータ型についてだけ、戻り値を格納できる新たなデータ型を定義するのはすっきりしないし、一般にStrategyの計算結果は中間データなので、委譲元のクラスには格納できない。
よって、Strategyへの入力を委譲元のオブジェクトそのものとし、併せてそれへのaccessorを渡すことにより、計算結果の反映までをStrategyに委譲する方法は、1つの妥当な方法だと考えられる。
Strategyパターンの本質は、クラスの一部の処理を差し替え可能な形で別のクラス(Strategy)に委譲することにより、既存クラスを変更せずに処理を追加できるようにすることにあるので、Strategyクラスに対して既存クラスを隠蔽することは優先されないと、筆者は考える。

Strategyを呼び出すのはPhysicalObjectクラスのupdateVelocityメソッドで、このメソッドは存在する自分自身以外の全ての物体についてStrategyを呼び出すことにより、他の物体からの引力作用を自身の速度に反映させる。
物体の形状は、PhysicalObjectが、形状そのものではなく、形状の描画方法として定義する。抽象的なPhysicalObjectは形状を持たないので、具体的な描画方法はPhysicalObjectの各サブクラスが持つ。

ついでに、描画時の色をStrategyパターンで切り替えられるようにもしてみる。
物体の色は、物理的特徴の1つとしてPhysicalObjectに持ち、ColorStrategyによって標準色から変更されるとする。ColorStrategyがnullなら、標準色とする。

Strategyパターンの実装として、Strategyを無設定にすることが許されるかという問題があるが、よくわからないので、そういう問題があることをメモしておく目的で、ここでは無設定を許すとする。
おそらく、デフォルトの処理を本体に持つかStrategyに持つかという問題に関係していると思う。デフォルトの処理が原始的で変更される可能性が無く、本体にあって自然なら、Strategyとしてnullが許されるのではないだろうか。
また、Strategyが計算結果を返すなら、Strategyがnullな状態を許すと、それに対応する計算結果と同じ型の値が必要になるので、違和感があるような気がする。
GravityStrategyの方は、無重力とは引力作用が無いということではなく、ゼロの引力が働いているものとして扱うため、明示的にクラス化している。

以上の方針によって実装したのが、次のリンク先のページのJavaアプレットである。(ソースコードへのリンクもあり)
・StrategySample2のアプレットとソースコードのページ
フィールド上をクリックすると、何らかの物体が置かれる。
"Disc", "Wired Star", "Star"のいずれかのボタンを押すと、クリックして置かれる物体が切り替わる。
"Switch gravity strategy"を押すと、次に置かれる物体の引力のルールが切り替わる。
フィールドは、表示されている領域の3x3倍の大きさがあり、それより外に出た物体は削除される。
標準(デフォルト)の引力と色は次の通り。

無指定時の引力無指定時の色
Disc引力水色
Wired Star無重力マゼンタ
Star斥力黄色

あとはソースコード参照。

次のエントリーでは、大体同じものをVisitorパターンで作ってみる。
(続く)

続きを読む "StrategyパターンとVisitorパターンの使い分けに関する考察(2)" »

2011年04月17日

StrategyパターンとVisitorパターンの使い分けに関する考察(3)

前のエントリーでStrategyパターンで作った引力モデルのシミュレーションとほぼ同じものを、今度はVisitorパターンで作ってみる。


class diagram

Strategyパターンの場合との全体的な構造上の違いとして、本体のクラスに切替可能な処理(またはアルゴリズム)への関連が無く、処理を実装するクラスから本体のクラス階層への関連がある。具体的には、PhysicalObjectクラスにVisitorのメンバが無く、Visitorの具象クラスから本体の具象クラスへの依存がある。

切替可能な処理の構造の違いとしては、Strategyパターンでは本体の具象クラス毎の既定の処理をStrategyでなく本体側に入れることが可能であったのに対して、VisitorパターンではそれらをVisitor側に定義する必要がある。もし本体側に実装しても、それを呼び出す仕組みが必要になるので、Visitor側にクラスを定義するのは避けられない。
また、Strategyパターンでは処理が無い場合は無処理のStrategyを定義する必要があった(特にStrategyが値を返す場合)が、Visitorパターンでは無処理ならVisitorを定義する必要が無い。
具体的には、各PhysicalObjectの標準の処理の実装のために、DefaultGravity, DefaultColorizerというクラスを定義しており、無重力の場合は引力計算の処理の必要が無いので、NoGravityApplierはのようなクラスを定義していない。

なお、ColorVisitorのサブクラスにて各PhysicalObjectの標準色を使う場合があるため、
標準色の設定処理はColorVisitorクラスに実装するようにし、DefaultColorizerは、ColorVisitorをそのまま引き継ぐ、インスタンス生成可能にするためだけの空のクラスとしている。ColorVisitorを抽象クラスでなく具象クラスにすればDefaultColorizerクラスが不要になるように思えるが、そうすると、ColorVisitorが標準色の設定の役割を兼ね、ColorVisitorのサブクラスが標準色の設定の役割を持つColorVisitorとして振る舞えることになってしまい、いわゆるリスコフの置換原則に反してしまうので、好ましくない。

以上の方針で実装したのが、下のリンク先のページのJavaアプレットである。(ソースコードへのリンクもあり)
・VisitorSample2のアプレットとソースコードのページ

StrategyパターンのサンプルではボタンによるStrategyの切替がそれ以後に追加される物体のみに有効になるのに対し、Visitorパターンのサンプルでは、フィールド上にある全ての物体に対して切り替わる。
WiredStarについては、同じVisitorでもvisit先の具象クラスによって処理を変更する例として、全てのGravityVisitorのvisit()を空にし、常に無重力の状態にしている。

オブジェクト毎に別々のVisitorを適用するようにしていないのは、そうするためにはアプリケーション側でオブジェクトとVisitorとの対応を管理しないといけなくなり、Visitorパターンのメリットが大きく損なわれるからである。

次のエントリーで、StrategyパターンとVisitorパターンの例に対して、本体と切替可能処理のそれぞれのクラス階層にクラスを追加してみる。(続く)

続きを読む "StrategyパターンとVisitorパターンの使い分けに関する考察(3)" »

2011年04月25日

StrategyパターンとVisitorパターンの使い分けに関する考察(4)

これまで[1][2]にStrategyパターンとVisitorパターンそれぞれで作った引力モデルのシミュレーションに対して、以下のような要素を追加してみる。

  1. 物体の種類として、Square
    Squareの特徴:
    • 縦か横にしか動かない(縦方向か横方向かどちらか速度の大きい方に動く)
    • 但し速度は2次元(一方が空回り)で、全方向の引力の作用を受ける
    • 標準の引力は通常の距離の2乗に反比例する引力
    • 標準色は緑
  2. 引力計算の種類に、引力の方向が相手の方向より少しずれるCurlGravityを追加する
  3. 色設定の種類に、Blinkを追加する
それぞれ、PhysicalObject, GravityStrategy/Visitor, ColorStrategy/Visitorのクラス階層へ何かを追加するという例である。

Strategyパターンによるものに対する変更は、次のようになる。
class diagram
元のクラス図に対して、黄色で示されたBlink, Square, CurlGravityの3つのクラスを追加している。

これに基づいて変更したソースコードとJavaアプレットを、次のリンク先のページに置く。
・StrategySample3のアプレットとソースコードのページ
ソースコードの差分は、詳しいdiffの出力を末尾に載せているが、 Square.java, CurlGravity.java, Blink.java の3ファイルが新規に作られた他は、変更されたファイルは SampleApp.java の1つのみであり、その変更も、新たに追加したSquare, CurlGravity, Blinkの3つが選択できるようにGUIのボタンを追加したりボタンの動作を変更したりするものだけであり、それらを使わないアプリケーションには必要の無い変更である。
つまり、本体側のサブクラスの追加もStrategyの追加も、既存のクラスに全く手を入れることなく行うことができる(†追記部分に補足あり)ということであり、いずれの場合も、オブジェクト指向設計の基本であるであるOpen-Closed Principleを満たしていることが明らかである。

一方、Visitorパターンによるものに対する変更はかなり多い。
class diagram
元のクラス図に対して、黄色で示されたBlink, Square, CurlGravityの3つのクラスを追加した他に、Visitorの階層のGravityVisitor, DefaultColorizerを除く全ての既存のクラスに変更が入っている。

これは、Visitorインターフェースにvisit(Square)を追加した為、Visitorインターフェースを実装する全てのクラスにvisit(Square)を追加する必要があるからである。もちろんVisitor階層で実装を継承できる場合は追加不要だが、Visitorパターンがその効果を発揮する用途ではVisitor同士は役割が類似しないことが多い(後述)というよりむしろ全く異なることが多い為、Visitorインターフェースが変更されても変更せずに済むのは、親クラスを僅かに拡張をするようなクラスに限られる。
また、Visitorインターフェースにvisit(Square)を追加しなくても良いのは、SquareがVisitorを一切acceptしない(accept()が何もしないか例外を投げる)場合のみである。(accept()でのvisit(this)はコンパイルエラーになる)

これに基づいて変更したソースコードとJavaアプレットを、次のリンク先のページに置く。
・VisitorSample3のアプレットとソースコードのページ
ソースコードの差分は、同様にdiffの出力を末尾に載せているが、 Square.java, CurlGravity.java, Blink.java の3ファイルが新規に作られた他に、 SampleApp.java, ColorVisitor.java, DefaultGravityApplier.java, Gradation.java, NegativeColorizer.java, NormalGravityApplier.java, RepulsionApplier.java, Visitor.java の8つのファイルが変更されている。
SampleApp.javaの変更は、Strategyパターンの場合と同様、使用者側が新たなクラスを使う為の変更であるが、その他は既に実装されているメソッドをcopy&pasteしたような怠惰な追加である。末尾のdiffの出力は先頭が+の行が追加された行であり、変更箇所の前後3行が参考の為に表示されているが、SampleApp.javaを除く前述の7ファイルの追加内容は、それらのすぐ上に同一または類似の処理があることが見て取れると思う。いかにも冗長である。

ただ、これら7ファイルは全て、PhysicalObjectの階層にSquareクラスが追加されたことに対応してvisit(Square)が追加されたものであり、CurlGravityやBlinkの追加に伴って発生した変更ではない。よって、VisitorパターンはVisitorの追加に対しては変更箇所が閉じている(Open-Closed Principleを満たしている)ことがわかる。
繰り返しになるが、visit(Square)を追加しなくて良いのはSquareがVisitorをacceptしない場合のみであり、それはVisitorパターンの適用外ともいうべきもので、そういう場合には変更に閉じていると言えるものではない。

以上より、StrategyパターンとVisitorバターンの違いとしては、次のようなことが見られる。

  • Visitorパターンの場合、本体側のクラス階層にサブクラスが追加されると、Visitor階層全体に変更が及ぶ。
    Visitorパターンの宿命のようである。
  • Visitorパターンでは、いずれのVisitorクラスも、本体側の全ての具象クラスについて別々のメソッドを持つ。
    具象クラス毎に処理が異なることが少ない場合は、適しているとは言い難い。一方、Strategyパターンでは、具象クラス毎に処理が異なるStrategyのみ、具象クラス毎にStrategyのクラスを別にすることが可能である。
  • Visitorパターンでは、本体側のクラスが主体となってその処理を実行することができない。
    accept()が呼ばれるまではVisitorが実装する処理を実行できない。その為、例えばメソッドの途中でVisitor側にある処理を呼ぶことは困難である。(抽象Visitorクラスがイベント発行要求を受け付けて、Visitorインスタンスを管理するクラスに通知して、acceptを呼ぶような、トリプルディスパッチが必要かも知れない)
  • Strategyパターンが本体側のクラスのインスタンス毎にStrategyを持つのに対して、Visitorパターンは特定のインスタンスとの関連が無い。
    切替可能な処理を繰り返し実行するのであれば、Visitorパターンの場合はVisitorのインスタンスを別に管理する必要が生じる。
    切替可能な処理を1回だけ、複数のオブジェクトに対して実行するのであれば、Strategyパターンの方が処理が煩雑になる。全オブジェクトに(抽象化された)同じ処理を1回だけ適用したい場合には、Visitorパターンが有利である。
  • Strategyパターンの場合は、Strategyの使われ方が決まっているので、追加できるStrategyの自由度は限定的だが、Visitorパターンの場合は任意と言うに近い自由度で処理が追加できる。

そもそも、Visitorパターンが有効な場合は相当限られると思う。
適用されるクラス群は抽象化されているのだからそれなりに共通の特徴を持つだろう が、
・Visitorから見るとクラス毎に処理が違うくらいにはサブクラスは異なる特徴を持ち、
・Visitorが行う抽象的な処理の種類はStrategyとして共通の特徴を持たないくらいには異なる、
ような場合ということになる。筆者がこれまでに見たことがある例は

  • オブジェクトダンプのようなデバッグ表示(toString+printのようなもの)
  • ファイル保存などのための、オブジェクトのフォーマッティング
くらいであるが、他にはなかなか思い付かない。ファイルハンドルのように、ストレージはファイルシステムかHDDかデータベースかネットワーク越しの何かかわからないようなものに対する処理を追加できるようにしておくような場合だろうか。

続きを読む "StrategyパターンとVisitorパターンの使い分けに関する考察(4)" »

2011年04月29日

アジャイルセミナー傍聴録

いつも返却ポストとして利用している図書館で、参加費無料のアジャイルソフトウェア開発のセミナーをやるというので、今借りてる本の返却期限はまだであるが、自転車を30分漕いで行ってきた。

【内容】

  1. Kent Beck氏からのビデオレター上映
    被災した日本の皆様にお悔やみ申し上げますって感じの20秒くらいのもの。
    再生後、日本語訳が説明され、それを踏まえてということで、もう一度再生された。
  2. テクノロジックアートの長瀬氏による講話
    後述
  3. ThoughtWorks社のDavid Joyceという人のオーストラリアからのテレ講演
    System thinkingとかLean開発とか
  4. パネル討論会
    後述

【長瀬氏のお話】
これまでは、アジャイル開発をやってきた人はオブジェクト指向のプロフェッショナルだったりして、アジャイル開発でうまく行ったという話も、アジャイル開発でなくてもうまくできるような人がやってた。それに対して、最近はウォーターフォールをやってた所がやり始めている。

アジャイル開発を始めるには、まずそこのカルチャーを見極めないといけない。きちんとドキュメントを作ってきた所なのか、インターネット系でオープンソースをばりばり使ってるのか、新しい技術が好きで次々に取り込んでるのか、若い人中心でゲーム感覚で作っているのか。

日本ではアジャイル特有の言葉はあまり使わない方がいい。既存の概念に囚われない目的でそうするものだが、既存の概念を捨て去るのは合わない。
全員がプログラムも設計もできる、というようにするのは難しい。それらは分業することになるが、そうするとオペレーション(イテレーションの周期)が2週間では厳しい。4週間とかになるだろう。

アジャイルにはオブジェクト指向設計が大事。最適な設計はオブジェクト指向設計から。やったことがない所ならオブジェクト指向の教育をみっちりと。

アジャイル導入には、まず手順書を作ること。テストを作ってから中身を作って、とか、それぞれの人の役割をはっきりするとか。それにはじっくり1ヶ月はかかる。

テスト駆動開発はユーザーの要求を漏れなく反映できるかにかかっている。欧米ではそのためのしっかりしたツールがあるが、欧米ではユーザー(顧客)が技術に明るいことが多く、ツールの作りもそれが前提になっている。それに対し、日本のユーザーは漠然とした要求しか出さないケースが多いので注意が必要。
ツールを使ったら使ったで、その内毎日のCI(Continuous Integration)つまり結合と自動テストが夜中に終わらないということも起こることがある。考えるべきことは多い。
拠点が分散しているとツールを使わざるを得ない。しかし、日本ではこれまでアジャイルは1つの拠点で小規模にされることが多かったので、アジャイル特有のツールが広まっていない。

【パネル討論会】
テーマ:アジャイルは失敗するって本当ですか?
パネラー:長瀬さん(T)、細谷さん(H)、永田さん(N)
司会:前川さん(M)

M: 本当です。
アジャイルは今第2次ブーム。
アジャイル宣言から10年、最初は技術者が主体だったが、ちょっと下火になった時期があって、今は経営陣が興味を持つようになってまたブームになってきた。
それ故、十分な知識が持たれないことが増えてきた。

(以下、Qは会場からの質問、Aは会場からの回答、Cは会場からのコメント)
M:パネラーの皆さんにとって、Agileとは、一言で言うと?
H:開発をマネジメントする為の有効なフレームワークの1つ。
N:"interplay"
(JAZZの用語、誰かがピアノを弾いてる所に、阿吽の呼吸で合わせていくようなこと。
営業でも開発でも、メンバーの誰かが課題やアイデアを持ったら、他のメンバーがそれに合わせて動く、のような意味合い。)
T:日本のソフトウェア開発を崩壊させるかも知れないもの。
それについていけないと、日本のソフトウェア開発が無くなってしまうもの。
M: 皆さんにとって、何をもってAgileが成功したと言えるか?
H: お客様が満足するか、プロジェクトが計画通りに進むこと。
N: Deliveryを満たすこと。
T: 日本では、イテレーションが回るようになったこと、開発が楽しそうであることを言うのではないか。
お客様が要求をはっきり言わず、具体的な要求は変化するので、機能追加型で開発できないといけない。その為には、リファクタリングもできないといけない。そこまでできて、イテレーションが完結すると言える。
Q: アジャイルを始める動機は何が多いか?Waterfallに失敗したから?
T: Waterfallが失敗したから、と言って相談に来る人はいない。このままではダメになるのではないか、Waterfallだけではだめなのではないか、という感じの漠然とした危機感を持っている人が多い。
Q: 欧米では、組み込み系と業務用システムで、どちらの方がAgileが成功しているか?
(中略)
H: ドメインによって違いはあると思う。
ハードウェアまで作り込むとか難しい物理的な制御をするなら、最初にある程度きちんと作らないと、全然動かない。
業務用システムを作るなら、最初は簡単に作って見てもらうことも可能。
T: 欧米では、開発が相当早い。
CIが活用されていて、毎回3,000のビルドが行われたりしている。
ツールチェーンが確立していて、ストーリーカードから実装、CIまでが繋がっている。
ビジネスアナリストからエンジニアまで、速く作れる体制が整っている。
ビジネス分析者がエンジニアと同じ稼ぎ。
ソフトウェア工学的には、日本はかなり遅れていると思わざるを得ない。
Q: 管理するツールという面で、Redmineはどうなのか?
A(阪井さん): 何の為に使うかによる。ツールはプロセス改善のために導入されるべきもの、何の為にツールを使うかを明確にすることが大事。
H: Redmineでは、メンバーにある程度マネージメントの権限を委譲できるのが良い。
ガントチャートを更新するというのは結構大変な作業。
但し、より上位のスケジュールとの差異をどう管理するかが問題。
M: Redmineの失敗事例として、ただチケットを発行して現場任せにして、遅れがあれば催促する、ということをやって、現場の担当者がチケット恐怖症になり、自分からはチケットを発行しないようになったということがある。
T: ツールを使うのが目的になるのではなく、マニュアル(手作業)でやってみて問題があったらツールを使う、というのが正しい。
欧米ではツールとマニュアルと両方やっている。ツール使いながらストーリーカードが壁に貼ってある。
M: PDCAを回す(計画を直しながら進める)のとタイムボックスを守るのとの違いは?
N: Agileはプロセスなのか、プロセス改善なのかという話があった。
Engineeringから見ると、Agileはなかなか大変だと思う。
H: 不安感が生産性に影響すると思っている。漏れ聞こえる噂に左右される。
タイムボックス化の効果は、2週間は邪魔が入らず集中できること。
M: 結局、マネージャーが、このメンバーで何が作れるか、どう成長するかをはっきりと予想できるかどうかにかかっているような気がする。
H: そこまでわかってたらWaterfallでできるのではないか?
C: 2002年くらいの論文では、Waterfallは見直されていて、2回回せばいいという論文も出ている。
?: Waterfallの知識が無いとAgileはできないと思う。Waterfallは、プロジェクト検証の過程が無いのが問題なだけではないか。
H: WaterfallでもAgileでも、実行可能だと大多数の人が思っている計画しかうまくいかないというのが真実ではないかと思っている。スキルが上がると、Waterfall力がついてくる。
Q: Agileって儲かるのか?
T: 儲からないと思う。
ユーザーから見て、Waterfallは途中が見えないから博打、Agileは見えるから安心ではある。開発とユーザーとの信頼関係は上がる。しかし、Agileは失敗したらユーザーの責任になることがあまり理解されていない。
M: これからAgileを始めたい人に一言
H: 失敗する所はちゃんとわかってやらないから失敗する、という話もあるが、興味あるならやってみれば良いと思う。
N: 海外の人は、AgileはEngineeringだという意識が強い。だから進んでいる。
Lean開発の目的がqualityになってるのがやっぱり凄いと思った。
T: 大手は大体そのどこかではやっていて、失敗した経験も持っているので、今後は失敗は減っていくだろう。
最初から失敗したくなかったら、コンサルタントを雇うとか、金が無かったら、こういうセミナーで擬似的にやってみるとか。
(その他の会場から出た質問で答えが出なかったもの)
Q: 外注さんが含まれる場合、どうお願いすればいいのか?
Q: 詳細設計ってどうやるのか?

続きを読む "アジャイルセミナー傍聴録" »

2011年11月11日

SVGで半透明図形の外側にドロップシャドーを付けるには?

暫くこのweblogを更新してないので、リハビリの為に何か書く。
これまで、代わりにTwitterとかをやってた訳ではなく、weblogとかを書きたく無くなった訳でもなく、ネタはあって細かいことは色々やってはいたのだが、何をやっても成果が出ないのである。考え事のネタだけが溜まっていく。物書き用のMacBookが不調で筆無精になってたり、部屋の地デジ化に時間を奪われたり、肺炎に罹ったりしたこともあったが、土日もクタクタで、そもそも頭を使うことに取り組む時間が激減してるので、トシなのだろう。

話を戻して、SVGでドロップシャドーを付けるには、SVG 1.1のドキュメント§15.1のサンプルコードのように、feGaussianBlurを使ってぼかしてずらしたものを先に描けば良いのだが、影を付ける対象が半透明だと、半透明の部分に自身の影が透けて暗くなってしまう。例えば、この方法で
dropshadow0.svg(注:この画像はJPEG、クリックするとSVGファイルが開きます)
この四角の上にある半透明の円に影を付けると、
dropshadow1.svg(注:リンク先はSVGのfilterに対応していないブラウザでは影が出ません)
このように、円の内側が暗くなってしまう。もし、円の外側にだけ影を付けたい場合、どうすれば良いだろうか。
手前に光源があれば、円の内側の半透明部分も影を作り出すんだから、円の内側に影が入り込むのは当たり前だろ、と言われればその通りである。この所、ブログの記事を書き始めてから問題設定に問題があることに気付いて、草稿をボツにすることが度々発生するのだが、今日はこのまま書き続ける。

上のSVGでは、SVG 1.1のドキュメントのサンプルコードに従って、

<filter id="dropShadow1" width="150%" height="150%">
<feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="5"/>
<feOffset in="blur" result="offsetBlur" dx="10" dy="10"/>
<feBlend in="SourceGraphic" in2="offsetBlur" mode="normal"/>
</filter>

このようなフィルターを定義しており、元の絵(SourceGraphic)と影との合成にfeBlendを使っているが、これをfeMergeに替えて
<filter id="dropShadow1" width="150%" height="150%">
<feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="5"/>
<feOffset in="blur" result="offsetBlur" dx="10" dy="10"/>
<feMerge>
<feMergeNode/> <!-- blurred and offset image -->
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>

にしても結果は同じである(feMergeはnormalモードでfeBlendするのと同じ)。

SVGでは、オブジェクトにα値がある限り、オブジェクトの重ね合わせにはα合成が発生するので、下の絵を消すかのようにα値ごと上書きすることはできない。<filter>〜<filter>の中では、feBlendやfeComposite等を使うことによりブレンドモードを変えることができ、特に<feComposite operator="arithmetic" k1="0" k2="0" k3="1" k4="0" />とすると重ねる絵で完全に上書きすることができるが、これらは矩形でしか行えないので、重ねる絵の何も無い部分(α=0のピクセル)も上書きして、下の絵を消してしまう。従って、

<filter id="shadow_normal" width="150%" height="150%">
<feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="5"/>
<feOffset in="blur" result="offsetBlur" dx="10" dy="10"/>
</filter>

これによってできる
dropshadow2.svg
こういう影の画像に元の絵を重ねるのではなくて、
dropshadow3.svg
こういう影の画像を作らないといけない。このdropshadow3.svgでは
<filter id="shadow_out" width="150%" height="150%">
<feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="5"/>
<feOffset in="blur" result="offsetBlur" dx="10" dy="10"/>
<feComposite in="SourceAlpha" in2="offsetBlur" operator="arithmetic"
k1="0" k2="-100" k3="1" k4="0" />
</filter>

のようにして、元の画像のα値を-100倍して重ねることにより、元の画像のα > 0.01の部分のαを0にしている。これで目的が達成できているように見えるが、元の画像を重ねると
dropshadow4.svg
このように、少し隙間ができてしまう。これは、元の画像の輪郭の、輪郭を外れた少し外側に、アンチエイリアシングによって生じる微小なαを持つピクセルがあるからだと考えられる。上の例では右下に影を付けているので、影を左上にずらせば隙間を隠せるのではないかとも思うが、
dropshadow5.svg
この円の右上のように、隙間は右下にできるとは限らない。

次に、フィルターではなくクリップやマスクを使って影をカットすることを考える。まず問題になるのは、<clipPath>によって作られるクリッピング領域はパスの内側を残すクリップだということである。SVGではクリップやマスクを反転させる術は無いように思える。パスの外側を塗り潰すことが難しいのと同様に、パスの外側を残すクリップを作るのも難しいのである。
しかし、clip-ruleにevenoddを指定することによって穴開き領域を作ることができることを利用して、十分に大きな領域の内側に消したい領域の穴を開けたクリップを作ることは可能である。
そう思って、

<clipPath id="does_not_work" clip-rule="evenodd">
<rect x="0" y="0" width="200" height="200"/> <!-- 十分大きな領域 -->
<circle cx="80" cy="80" r="50"/> <!-- 消したい部分 -->
</clipPath>

のようにしてクリッピング領域を作ってみたが、これはrect部分からcircle部分が抜き取られるのではなく、rect部分とcircle部分の和になってしまった。evenoddは1つのパスで完結しないと働かないようである。
そこで、上記のrectとcircleをpathに置き換えて連結してみた。
<clipPath id="shadow_clip" clip-rule="evenodd">
<path d="M0,0 H200 V200 H0 V0
M130,80
C130,107.6 107.6,130 80,130
C52.4,130 30,107.6 30,80
C30,52.4 52.4,30 80,30
C107.6,30 130,52.4 130,80
Z"
/>
</clipPath>

これによって影を切り取ると
dropshadow6.svg
このようになり、元の絵を重ねると、
dropshadow7.svg
となり、所望の外側だけのドロップシャドーが得られた。

とりあえず目的は達成できたが、問題点として、
(1) SVGファイル内に似たようなパスデータが何度も現れる
(2) 影をつける対象は必ず1つのパスで書けなければならない
がある。(1)は似たようなパスでクリップと影と本体を別々に書かないといけないことによる。影と本体は、次のdropshadow8.svgのようにxlinkとuseを使ってパスを共通化できるが、クリップとの共通化は難しい。(2)とも共通する課題だが、複数のパスデータをclip-rule="evenodd"が利くように繋げる手段や、複数の要素をclip-rule="evenodd"で並べる手段が見つからないのが原因である。
dropshadow8.svg(xlinkを使って少しまとめたもの)

やはり問題設定が適切でないということだろうか。

続きを読む "SVGで半透明図形の外側にドロップシャドーを付けるには?" »

2012年03月25日

Mac間のデータ移行

物書き用に使っていた白いMacBook MB402J/Aは、使い心地も外見もとても気に入っていたのだが、半年くらい前から、トラックパッドのボタンが固くなって、クリックする度に、引っ掛かった金属片が弾かれたようなガチッガチッという音がするようになり、さらに、たまに固くて押せない(押す位置を変えると押せる、押せないのはどの位置でも起こる)こともあり、ストレスを感じるようになってしまった。
MacBookのバッテリーパックが膨らんで、真上にあるトラックパッドを押し上げて異常が出るというのはよくあることらしく(参考リンク [1] [2] [3] [4])、筆者のMacBookもそれが原因のようで、バッテリーを外して使うと問題が起こらなかったのだが、MacBookは安全設計で電源プラグが軽く外れるようになっているので、バッテリー無しで使うのは危険すぎる。かといって、トラックパッドでクリックしにくい問題だけのために1万円以上かけてバッテリーを交換する気にはなれない。デスクトップ用の外付けのトラックパッドであるMagicTrackpadを購入するという方法もあるが、6,800円するし、もう少しバッテリーが膨らむと全くクリックできなくなるらしいので、そうなったらMagicTrackpadを購入したことを後悔するだろう。マウスを買うのも同じリスクがあるし、筆者は元々マウスというデバイスが苦手なのである。

半年悩んだ末、悩む時間が勿体なく思うようになったので、携帯性を高めるため、自分への誕生日プレゼントと自分に言い訳して、ついにMacBook Air MC969J/Aを買ってしまった。本当はMC968J/Aを買おうとしたのだが、誕生日の前日に慌てて買いに行った日本橋のどこにも在庫が無かった。

新しいMacの電源を入れたら、セットアップウィザードの数画面先で、別のMacからのデータ移行のメニューが出てきた。ネットワーク経由で別のMacから直接コピーするか、外付けHDDのTimeMachineバックアップから取り込むかを選択できたが、ネットワーク経由でコピーし始めると「残り8時間」とか出てきたので、中断してTimeMachineバックアップから取り込んだ。30分くらいで終わった。すると、アプリケーションからDashboardやDockメニューの配置からFinderのショートカットから「最近使ったフォルダ」に至るまで、前のMacの環境が完璧に再現された。起動したら、多用しているスティッキーズのメモが前のMacと全く同じ位置に現れて、感動した。暫くEmacsやらEclipseやらを触っていたが、キーボードとトラックパッドの違い以外、全く違和感を感じなかった。

しかし、1つだけ問題が起こった。iTunesを起動すると、データも設定も全く引き継がれていなかった。DRM(著作権管理)か何かの都合で、パソコン間で自動的にコピーされるのは問題があるのだろうか、と思ったが、ネットで調べてもそんな情報は見つからない。むしろ、どこを見ても、iTunesライブラリもiPodの設定も全て自動的に引き継がれた、「移行アシスタント」完璧すぎる、と書いてある。最初のネットワーク経由の移行アシスタントを電源断で中断したのが悪かったのだろうか、と、Command+Rを押しながら電源ONでMacOSXユーティリティを起動してTimeMachineバックアップからのリストアをやり直したり、Lionの再インストールをやり直したりしたが、同じだった。

5回目の移行アシスタントにはネットワーク経由でユーザーアカウントを移行させたら、iTunesのデータが完璧に引き継がれた。とりあえず全て移行できたので、そういうもんか、と思って、それ以上追究するのを止めたが、数日後、同じ外付けHDDを新しいMacのTimeMachineバックアップにしようとして、原因がわかった。外付けHDDの容量が少ないので、前のMacでも、~/Music/iTunesをTimeMachineバックアップの対象外にしていたのだった。

続きを読む "Mac間のデータ移行" »

2012年04月01日

MacPortsでPython対応Diaをインストールするには

先週、今更ながらPythonでDiaのプラグインが作れることを知ったので、早速試してみようと思ったら、MacPortsでインストールしたDia-0.97.2ではPythonが使えないことがわかった。
ソースコードからDiaをmakeする時に、configureスクリプトに--with-pythonを渡す必要があるのだが、MacPortsのDiaのPortfileがそうなっていないのである。そのようにするMacPortsのパッチは既に作られているが、これはかなり昔のMacPortsに対するパッチで、現在ではそのまま当てられないのはもちろん、それに従って手でパッチを当てても、configureがエラーになるだけだった。

エラーログを見ると、

:info:configure checking for libpython2.7.a... not found config
:info:configure configure: error: could not find files required to build python plugin

というように、どうもインストールされたpy27-gtkにうまく対応できないようだ。

試行錯誤の末、かなり強引だが、以下のようにすると成功した。
(このブログが提供するPortfile-dia.diffpatch-configure.diffが~/Downloadsにあるとする)
1. libpython2.7.soの作成
(MacPortsのpython27はインストール済だとする。)

cd /opt/local/lib
sudo ln -s libpython2.7.dylib libpython2.7.so

2. Portfileの修正
cd /
sudo patch -p0 < ~/Downloads/Portfile-dia.diff

3. configureのパッチの用意
cd /opt/local/var/macports/sources/rsync.macports.org/release/ports/gnome/dia/
sudo mkdir files
sudo cp ~/Downloads/patch-configure.diff files/

4. port install dia
sudo port install dia

続きを読む "MacPortsでPython対応Diaをインストールするには" »

2012年04月07日

Dia 0.97.2の半円状の矢印が消える問題の修正パッチ

Dia 0.97.2-1には、ジグザグ線の先端の矢印の形状が開いた半円の場合、先端のY座標によっては表示されなくなってしまう不具合がある。例えば、UMLシートのReceptacle(要求I/F、Required I/F)の先端を上向きか下向きにして、Y座標が10〜50の間に先端を持って行くと、先端が消えてしまう。これは表示上だけの問題ではなく、その状態で図を画像ファイルとしてエクスポートすると、Diaがクラッシュしてしまうこともある。

筆者はMacPortsのDia 0.97.2-1とFreeBSDの0.97.1とでこの現象を確認した。

それを修正するパッチを作ったので、公開する。
patch-arrow.diff(MacPortsでのみテスト済み)

MacPortsでこれを使うには、
/opt/local/var/macports/sources/rsync.macports.org/release/ports/gnome/dia/Portfile

patchfiles patch-arrow.diff

という行を足して、Portfileがあるディレクトリにfilesというサブディレクトリを作って、そこにこのパッチを置く。

続きを読む "Dia 0.97.2の半円状の矢印が消える問題の修正パッチ" »

2012年04月14日

Macのマルチ画面環境の設定

11.6インチ液晶のMacBook Airを買って、13.3インチ液晶の白MacBookをお蔵入りさせた(CD/DVD-ROMドライブが必要な時は引き続き使うのであるが)ら、やっぱり11.6インチのLCDは小さく、使用頻度は激減したが一応まだ残しているWindowsデスクトップPCの17インチのモニターに繋ぎたくなった。
しかし、MacBook Airからケーブル1本でMini DisplayPortからTVにHDMI出力できるらしかったので、やっぱり26インチの液晶TVに接続することにした。

ケーブルを入手して実際に繋いでみると、一発でMacBook Airと同じ画面がTVに表示されて、「ディスプレイ設定」画面が表示された。出力画面サイズを変えたりTVの設定を変えたりしてもどうしても文字がぼやけるが、それでも画面が大きいのは快適であることがわかった。
出力解像度は1600x900(LCDとのミラーリング無し)、TV(Panasonic TH-26LX60)側の設定は、試行錯誤の末、
バックライト「+10」(TV番組受信用は+30)、
ピクチャー「+25」(TV用は+30)、
黒レベル「0」、
色の濃さ「0」、
色あい「0」、
シャープネス「±0」(TV用は+10) 、
色温度「中」(TV用は高) 、
エッジ補正「中」(以下TV用は無設定)、
細部補正「弱」、
ガンマ補正「中」、
黒伸長「+8」、
白文字補正「+8」
にした。これでLCDが狭いことは全く問題でなくなった。13.3インチモデルを買わなくて良かった。


さて、ケーブルを抜き差しして気付いたのが、外部ディスプレイの解像度やLCDをミラーリングするかどうかの設定はずっと残るのに、外部モニター使用時のウィンドウの配置は残らないことだ。何かウィンドウを外部モニターに移動し、ケーブルを抜くと外部モニターにあった全ウィンドウがLCDに移動するが、もう一度ケーブルを挿しても全てLCDに残ったままなのである。これは、特にHDMIの場合、テレビの電源を切ると、ケーブルが接続されたままでも、外部ディスプレイと切断されたことになってしまうので、その状態を保つようにPCをスリープして復帰させるには、

  1. スリープ
  2. TV OFF
  3. TV ON
  4. 復帰
という手順を守らなくてはならず、これを間違えると、せっかく外部モニターに移動した全ウィンドウがLCDに戻ってしまう。これでは外部ディスプレイを使いにくい、というか使えない。

それを解決するアプリケーションとして、Forget-Me-NotStayを見つけた。"Stay"の方が高機能で、外部ディスプレイの着脱に関係なく、ウィンドウの位置を保存することができるツールであるが、明示的にウィンドウ位置の記憶を指示する必要がある(ケーブル差しで自動的に復帰するがケーブル抜きで自動的に保存されない)のと、有料ソフトなのがいまいちである。Forget-Me-Notは、作者のページの見た目が誤解を招くが、フリーソフトであるし、ケーブルを抜いた時にウィンドウ位置が記憶されるので、Forget-Me-Notを導入した。


さてさて、LCD側にJava API Referenceを表示して、EclipseのウィンドウをドラッグしてTV側に移動して、何かJavaアプレットのコードを開いて、これぞ省スペースで理想的な開発環境、と満足してデバッグを開始すると、アプレットのウィンドウがLCD側に表示されてしまうのが非常に面倒であることがわかった。その時大体マウスカーソルがTV側にあるので、アプレット上のボタンを触るために、マウスカーソルを長距離移動しないといけないのである。
それを解決するために、色々調べた。こういう時は、
(a) アプリケーション毎に出力先ディスプレイを設定する
(b) 外部モニターを主にする
(c) キーボード操作でウィンドウを別モニターに移動する
といった方法が常道である。他には、(d)アクティブウィンドウが切り替わった時にマウスポインターがウィンドウ上に無ければ自動的にアクティブウィンドウに移動させるという解も考えられるが、自分が意図しないタイミングでダイアログとかが開いて、ポインターが勝手に移動するのは不便であろう。それに、フォーカスロストした時にワンクリックでフォーカスを復帰させることができなくなるのは不便に違いない。

(a)については、前のMacに最初から入っていたSpaces(仮想デスクトップ)なら、アプリケーション毎にどのデスクトップで開くかを設定できたのだが、新しいMacにはSpacesが無く、代わりにMission Controlというのはあったが、同じような設定をする方法がわからなかった。

(b)は、割と意外な方法で実現できる。「ディスプレイ環境設定」の「調整」ペインを開くと、外部ディスプレイとLCDとの位置関係と、メニューバーをどちらに置くかを決められるが、このメニューバーがある方がメインディスプレイになり、メニューバーを外部ディスプレイに置くと、新たに開くウィンドウは全て外部ディスプレイで開くようになるのである。上記の問題はこれであっさり解決した。

(c)は、ジェスチャー入力を拡張するフリーの超人気ソフト、BetterTouchToolで実現できるので、他の方法を探すまでもない。筆者は元々ジェスチャー入力でなくキー操作でのウィンドウのモニター間移動に病みつきなので、上記の問題とは別に、

Command+Opt+C
Move Window to Next Monitor(隣のモニターへ)
Command+Opt+E
Maximize Window to Next Monitor(隣のモニターで最大化)
という設定にした。

(d)については、意外と方法が見つからず、LazyMouseという、ダイアログが開く時にマウスカーソルを自動的にデフォルトのボタンに移動させる有料ソフトがあったのと、java.awt.Robotクラスを使ってマウスカーソルを移動させるJavaアプリを自分用に作って常駐させている人が世の中に存在するらしいのをどこかで目にしたくらいだった。

続きを読む "Macのマルチ画面環境の設定" »

2012年05月28日

VMWareでのFreeBSD 8.3のXの設定

たまにVMWare上で使っていたFreeBSD 7.3がFTPサイトでもサポートされなくなり、パッケージの追加が困難になったので、久々にFreeBSDをバージョンアップすることにした。

とりあえずFreeBSD-8.3-RELEASE-i386-bootonly.isoをダウンロードして、CD-ROMとしてマウントして起動してインストーラーからUpgradeを選んだら、あっという間に8.3にバージョンアップができたのだが、X Windowを起動すると、以前と同じように使えたが、CPU使用率が常に100%になるようになってしまった。
topで見ると、haldというのが常に走り続けていた。ここへの書き込みによると、haldがlibusbに依存するように作られていると、8.xのカーネルのUSBドライバーと不整合が起こって、このようになるらしい。haldをlibusb無しでmakeすれば直るらしいのだが、どうせこの状態だとsysinstall→Configuration→Packagesからのパッケージの追加が困難(関連パッケージのバージョン違いが多発する)なので、やっぱり仮想マシンをもう1つ作って1からインストールすることにした。

何も考えずにインストーラーからStandardを選んで、インストールが完了したらxorg-serverとxf86-video-vmwareととxf86-input-vmmouseと適当なXアプリやら何やらを追加して、いざ再起動してXorg -configureしてXorg -config xorg.conf.newしたら、 画面が真っ暗になってしまった。
最近のFreeBSDは、バージョンアップする度に、Xの設定方法が変わっていてトラブルが起こる。XサーバーがX.OrgじゃなくてXFree86だった頃は、長年使ってたこともあって、なんとなく勘があったのだが、Xorgになってから、さっぱりわからなくなってしまった。FreeBSDを7.2から7.3に移行した時も、haldやらdbusやらxfbdevやらの設定が必要になったりして、相当手間取った。
今回も、またか、と、げんなりしながらWebで色々調べて、

Xorg -config xorg.conf.new -retro
とすると、Xが起動成功した時に真っ黒にならず、マウスカーソルも表示されて、やれやれ、と思いながら、xorg.confを/etc/X11に置いてstartxとすると、Command not found.になってしまい、さらにげんなりした。何でこんなにちょくちょく色々あれこれそこもかしこも何やらかんやら変わるんだろうな、全く。

最終的には何とかその日の内にXが使えるようになったのだが、これも要るのか?これもか?とパッケージを追加しまくって、明らかに無駄だったので、もう一度整理してインストールし直した。結局、以下のようにして、VMWare 5.5及びVMWare Player 5でFreeBSD 8.3のXを動かすことができた。

■インストールしたパッケージ
xorg-minimal-7.5.1
xf86-video-vmware-*
xf86-input-vmmouse-*
xkbcomp-1.2.3
xorg-fonts-7.5.1

■xorg.confの作成と動作確認
X -configure
X -config xorg.conf.new -retro

■入力デバイスの設定
・/etc/rc.confに次の1行を追加

hald_enable="YES"

・再起動後、コンソールにメッセージが出続けるので、次のコマンドを実行
hal-disable-polling --device /dev/acd0

・日本語キーボードの設定
以下を/usr/local/etc/hal/fdi/policy/policy10-keyboard-jp106.fdiとして作成
<?xml version="1.0" encoding="UTF-8"?>
<deviceinfo version="0.2">
<device>
<match key="info.capabilities" contains="input.keyboard">
<merge key="input.x11_options.XkbRules" type="string">xorg</merge>
<merge key="input.x11_options.XkbModel" type="string">jp106</merge>
<merge key="input.x11_options.XkbLayout" type="string">jp</merge>
</match>
</device>
</deviceinfo>

参考:http://freebsd.xo-ox.net/x11/xorgconfnv160

続きを読む "VMWareでのFreeBSD 8.3のXの設定" »

2012年08月17日

(Mac)Win2000+JDK6のインストール@VirtualBox

今更ながら、VirtualBoxという無料の仮想マシンソフトがあることを知った。
昔からWindows PCではFreeBSDを動かす為にVMWareを愛用していたし、MacではUNIX系のツールが一通り使えてFreeBSDは必要なかったので、あまり興味が無かった。
今週、ちょっとMacでWindowsを動かしたいと思ったので、調べたら真っ先に見つかった。4年前にMacBookを買った時は、そういう時はBoot Camp(HDDの起動パーティション分割ツール)かParallels Desktop(仮想PCアプリ)を使うのが定番だったし、個人的にはCrossOver Mac(OS不要でWindowsアプリを動かすランタイムライブラリのようなもの)に注目していたし、今年買ったMacBook Airには最初からBoot Campが入っているようなので迷ったが、迷わずにVirtualBoxを使ってみることにした。

今回インストールしたのは、
Windows 2000 Professional
Microsoft Office 2000
柿木将棋VI
Java SE 6 JDK (JDK6)
である。この中で、特にJDK6のインストールにてこずったので、そのことを中心に記録する。

●仮想マシンへのWindows 2000のインストール

  1. インストールCDを、何らかの方法で、ディスクイメージファイルにしておく
    筆者はMacBook(CDドライブあり)の「ディスクユーティリティ」で、CDドライブを選択して、「ファイル」→「新規」→「"..."からのディスクイメージ」で作成した。
  2. VirtualBoxを起動して、仮想マシンを新規作成する
  3. インストールCDを仮想マシンのCDドライブに挿入する
    1つの方法としては、VirtualBoxマネージャーの画面で、新しい仮想マシンを選択して、「設定」→「ストレージ」の画面でCDドライブを選択して、「属性」欄のCDアイコンをクリックして、「仮想CD/DVDディスクファイルの選択」を選択して、インストールCDのディスクイメージを指定する。
  4. Windowsのインストール
    仮想マシンを起動するとWindows 2000のインストーラーが起動するので、それに従ってインストールする。
  5. Guest Additionsのインストール
    Windowsを起動して、VirtualBoxのメニューから「デバイス」→「Guest Additionsのインストール」を実行すると、それのインストールCDが挿入されたことになって、インストーラーが起動する。
  6. Service Pack 4のインストール
    SP4は今でもMicrosoftのページから入手できる
    これをインストールしないと、JDKのインストーラーが動かない。
    筆者の環境では「高速インストール」版はエラーになったので、「ネットワークインストール」版を推奨する。

●仮想マシンへのOffice 2000のインストール

  1. インストールCDを、何らかの方法で、ディスクイメージファイルにしておく
  2. インストールCDを仮想マシンのCDドライブに挿入する
    仮想マシンが起動していれば、ウィンドウ右下のCDアイコンをクリックすれば、「仮想CD/DVDディスクファイルの選択」という選択肢を含む、CDドライブのメニューが出てくる。
  3. Windows内でCDドライブからOffice 2000をインストールする
  4. Service Pack 3のインストール
    SP3は今でもMicrosoftのページから入手できる
    Office 2000 Service Release 1 (SR-1)がインストールされていなければ、先にSR-1をインストールする必要がある。入手元はわからなくなったが、SR-1のインストーラーのファイル名はo2ksr1.exeであり、128kのネットワークインストーラーでも、現在も使用できた。

●仮想マシンへの柿木将棋VIのインストール
押し入れからWindows 2000やOffice 2000のCDを発掘したついでに見つけたので、インストールした。
手順は同じで、CDのディスクイメージを作成し、マウントし、仮想マシン内でCDドライブからインストールした。

●JDK6のインストール
現在、SunじゃなくなってOracleのサイトからJava SE 6 Update 33 (jdk-6u33)がダウンロードできるが、これをWindows 2000にインストールするのは極めて難しい。このインストーラーを実行すると、

エラー 1721。この Windows インストーラパッケージに問題があります。

とか
このWindows インストーラ パッケージには問題があります。このインストールを完了するのに必要なDLLを実行できませんでした。

とか
インストールに失敗しました
このウィザードは、Java(TM) SE Development Kit 6 Update 33のインストールを完了する前に中断されました。改めてインストールする場合は、再度セットアップを実行してください。
「完了」をクリックして、ウィザードを終了してください。

とかいうエラーが出て、先に進めないのである。
共通するのは全てWindowsのInstallShieldの問題であり、webで検索すると、Win2000 SP4ではInstallShieldがゴミを残すことがあり、それを消すと解決したとか、ウィルス対策ソフトが邪魔していたとか、様々な情報及び報告があり、実際何かするとインストーラーの挙動が変わったりするので、これだけ情報があればきっと解決するだろう、あと一息だ、と思わせるのだが、その何らかのトラブルがあり失敗しました、原因はわかりません的なメッセージを読む度に絶望を感じるのである。
「Win2000 SP4の更新プログラム ロールアップ 1」Windows Installer 3.1は何らかの変化をもたらし、期待させたが、同じだけ絶望感ももたらした。
筆者は既にそこまでにのべ丸2日の時間と集中力を潰しており、これ以上取り組むと無駄になる予感と、諦めるとそこまでに費やした労力が完全に無駄になってしまうジレンマがあった。そのジレンマと向き合うことにより、ようやく、JDKのバージョンを下げて解決すればそれでいいという妥協の仕方があることに気付き、多数のJDKをダウンロードしながら情報を探した所、Win2000でjdk-6u24のインストールが静かに中断されるというスレッドを発見した。それは個人的に世紀の大発見であった。それに呼応してjdk-6u23のインストーラーを入手して実行すると、何のトラブルも無くインストールに成功した。

jdk-6u23は、オラクルのアカウントの作成が必要(Sun Java siteのアカウントではサインインできなかった)など面倒だが、Oracle Java ArchiveJava SE 6 Downloadsのページから入手することができる。

続きを読む "(Mac)Win2000+JDK6のインストール@VirtualBox" »

2012年09月19日

MVCパターンの適用限界を考える(1)

MVC(Model-View-Controller)パターンは、UIを持つアプリケーションのデザインパターンとして非常に有名であり、特にこれを参考にしたJ2EEやASP.NETを始めとするWebアプリのフレームワークが成功を収めてきたことから、その価値は誰もが認める所である。
しかし、これをローカルシステムのGUIアプリケーションの設計に適用しようとすると、妙に難しいと感じた人が少なくないのではないだろうか。

MVCパターンは、元々Smalltalk-80の開発において考案され実装されたものらしいが、MVCの考案者によると、


MVC was created as an obvious solution to the general problem of giving users control over their information as seen from multiple perspectives.
("The original MVC reports"より)すなわち、複数の視点で表現されたユーザー情報を制御することを可能にする、という一般的な問題に対する自明な解として考案したものとのことであり、Smalltalk専用に考案されたものではなく、ネットワークシステムの為に考案されたものでもない。実際、MVCの実績はSmalltalk以外の言語で上がっており、MVCのアイデアに通信ネットワークの存在を仮定するようなものは見当たらない。であればどんなGUIアプリケーションでも有用なはずだ、と推測するのは全く自然なことである。

筆者も、当然MVCはローカルシステムでも有効なはずだと10年前から信じており、MVCの適用がうまく行かないのはMVCの理解が間違っているからだ、と思ってきた。しかし、この10年間でたぶん4回くらい、仕事でMVCを取り入れようとしたソフトウェア開発のプロジェクトの様子を知る機会があったが、いずれもほとんど成果が上がっていないようだった。見た感じ、MVCの理解が間違っているような気がすることも多かったが、それにしてもそんなに効果が出ないものだろうか。
実は筆者も、プライベートで何回か、MVCパターンを意識してプログラムを作ってみたことがあるが、MVCの価値を感じたことは一度も無かった。そういえば、MVCパターンの利点というのは、根拠のあるもの、具体的で検証可能なものはほとんど聞いた覚えが無い。オブジェクト指向設計と同様、経験的にやりやすかったから、現実にうまく行ってるから、という理由で推奨されているような気がする。それなら、大規模システムに有効なオブジェクト指向設計がミッションクリティカルなシステムに適さないのと同様に、MVCパターンにも苦手な分野があるのではなかろうか。
…ということを最近になってやっと考え始めたので、調べたことや考えたことを少しずつここに書いてみることにした。

MVCを知っていれば、既存のMVCを応用したフレームワーク(アプリケーションの枠組み及びアプリケーションの開発を支援する環境)が使える、というのは現実的なMVCを知るメリットではあるが、そういったフレームワークを使わない場合、MVCパターンをアプリケーションの設計に取り入れるメリットは何だろうか。

まず、筆者がずっと、きっとあると信じてきた、MVCパターンの利点を挙げる。

  1. Modelが再利用可能なので、ViewやControllerが交換可能で、容易に追加できる
  2. システム分割方針が分かりやすくなり、担当者毎、モジュール毎の役割分担が明確になる
  3. ソフトウェア構造としてM,V,Cが分離しやすく、従って独立して設計しやすい
  4. M,V,Cそれぞれのプラットフォーム(実行環境)やコンポーネント(共通モジュール)を別々に設計することも容易である

しかし、実際にはそうはならないことも多い。

(1)については、UIが複雑に、またビジュアル(グラフィカル)で応答性を高めるほど、VはMに限らずCの状態も表示するためにCに依存するようになり、特にポインティングデバイスやタッチバネルの処理で顕著なように、ユーザー入力が表示状態に依存してCがVに依存するようになるため、VとCの結合が強くなり、VやCが単独で交換可能にならなくなる。V+Cのセットでなら交換可能な構造が保てても、そうなると既にV+Cの規模が巨大になっているので、もう1つ別のV+Cを作る労力が、もう1つアプリ全体を作るのと大して変わらないことになってしまう。

(2)については、上述のようにUIが高度になるほどV-Cの関係が密接になって境界がわかりにくくなる他に、元々M-Cの境界がはっきりしないという問題がある。
あるモデル(ユーザーデータとその操作手段)を制御(操作)するアプリケーションは多岐に渡り、極端に言うと無限のバリエーションがあり得るんだから、アプリケーション固有のロジックはCに置くべきだろうという考え方もあれば、有名な"Skinny Controller, Fat Model"のスローガンに代表されるように、ロジックはできるだけModelに置くべきだ、という考え方もあり、システムに機能追加する時にあるロジックをMに入れるべきかCに入れるべきかは、人によって判断が分かれるのである。

(3)は、例えばModelを含めてシステムに機能追加する場合、Modelに追加するI/FはViewを意識して設計せざるを得ないことは普通に起こる。何も考えずにModel内のあらゆるデータを参照可能にする為にI/Fを揃えようとすると開発量が多くなることもあるし、複雑な変換や抽出を必要とする巨大なデータをそのまま渡されるとViewの処理が大変になるので、ある程度ユーザーにとって意味のあるまとまりにして渡してくれ、と言われることもある。
VとCが独立に設計できなくなりがちであるのは上述の通りである。

(4)は、実際にJ2EEではModelのコンポーネントとしてJava Beans(EJB)、ViewのプラットフォームとしてJSP technology、ControllerのプラットフォームとしてServlet technologyがあると説明されることがあり(参考リンク:[@IT 1] [@IT 2] ["Multitiered Applications" in Java EE tutorial])、M/V/C別々にプラットフォームを構築しやすいように見えるが、よく見ると、EJB以外は汎用的すぎて、M,V,Cそれぞれに特化したプラットフォームとは言い難い。Java BeansはVでもCでも使われる仕組みであるし、JSPはMだけでなくCにもアクセスできるし、作ろうと思えばJava ServletだけでMもVもCも作れる。M,V,Cそれぞれのプラットフォームとしても向いている、用途が広いテクノロジーを使っているのである。
むしろ、M/V/Cそれぞれのプラットフォームを作ろうとすると、そこまで汎用的で高度に洗練されたものを作ることになるのだろうか、という疑念すら抱かせる。

大体、一般にソフトウェアのデザインパターンの価値は、設計のノウハウであることよりもむしろ、設計者間の共通言語として使われることにあるのである。大概のパターンは、それを知らなければそういう構造にならないような、思い付きにくいアイデアではない。また、シンプルで普遍的にまとめられているものが多いので、その構造をにすると直ちに多くのノウハウが取り込める訳でもない。デザインパターンのノウハウの本質はその構造にした理由にあるのである。ある設計の基本構造とその構造にする理由とを合わせて一言で伝えるためにデザインパターンはあるのである。理由はおろか、パターンの構造自体も設計者間に共通認識が無くて伝わらなければ、デザインパターンとしての価値が無い、印籠やブランドの類でしかないのである。

さらに考察を進める前に、MVCパターンをおさらいする。
上記MVCの考案者"MODELS-VIEWS-CONTROLLERS"によるModel, View, Controllerの定義は、次のようなものである。


Model
実世界における問題を、特定の視点で抜き出したものを表現するもの。言い換えると、実際の問題や事象をモデル(模式、模型)化するもの。("Models represent knowledge."の"knowledge"は問題や事象に関する知識のこと。)

View
Modelの視覚的な表現。1つのViewはModelに取り付けられ、Modelに対して問い合わせをすることにより必要なデータを取得する。Viewはそれが取り付けられるModelのことを良く知っており、Modelへの問い合わせは全てそのModelで使われる言葉を用いてされる。 ("It may also update the model by sending appropriate messages."についてはややこしくなるのでとりあえず保留。)

Controller
ユーザーとシステムとを繋ぐもの。画面上にそれとわかるように表示することにより、ユーザーに入力手段を提供する。また、ユーザーによって入力されたコマンドをメニュー等によって表示するためのメッセージをViewに送信する手段を提供する。
ControllerはViewを補完せず、Viewの表示に介入しない。同様に、Viewはマウス操作やキーなどのユーザー入力そのものには関知しない。


次に、MVCパターンの最もシンプルな構成を、UMLで表現してみる。
Class diagram of MVC without V-C connection
図1-1: 最もシンプルなMVCのクラス図(静的構造図)

Communication diagram of MVC without V-C connection
図1-2: 最もシンプルなMVCのコミュニケーション図(動的構造図)

上記の"MODELS-VIEWS-CONTROLLERS"の定義にはViewからModel、ControllerからViewへの関連が含まれているが、それらは必須の構成要件ではない(その関連におけるメッセージのやり取りが無くてもMVCパターンは動作する)ので、最もシンプルな構成ではそれらを省いている。

ポイントは、次のように集約される。

  • 1つのModelに複数のViewを動的に登録することができる
  • ModelはViewの存在を知っているが、Viewの具体的な実装を知る必要が無い
  • 1つのModelを複数のControllerから操作することができる
  • ViewとControllerは完全に独立している

なお、1つのViewが複数のModelに関連する、つまり複数のModelの表示を受け持つことも可能だとされることが多い(上記MVCの考案者による"THING-MODEL-VIEW-EDITOR"でも"each View being capable of showing one or more pictorial representations of the Model"という記述がある)ので、M-V, M-Cの多重度は1対多でなく多対多が正しい、と思われるかも知れないが、Modelを交換可能とすることはMVCの本質ではないことと、そのようなViewは役割をModelの数だけ分割し、その数だけMVCのセットがあるのと同じことと、一般にオブジェクト指向設計において多重度が多対多の関連は意味が曖昧であることを理由として、M-V, M-Cの多重度は1対多と書いている。
(続く)

続きを読む "MVCパターンの適用限界を考える(1)" »

2012年10月08日

MVCパターンの適用限界を考える(2)

前エントリーに引き続き、MVCパターンについて、その利点と、何が崩れるとそれが失われるかを考えてみる。以下、一部に図1-1図1-2には無いV-C間の関連が前提になっている記述があるが、それについては次のエントリーで説明する。

  1. Viewの追加/変更がViewクラスに閉じ、Modelに影響しない
  2. ModelはView/Controllerへの依存が無く、安定度を上げることができる
  3. ModelとViewとをそのままにして、Controllerを差し替えることができる
  4. ModelはViewの表示状態に関係なく動作することができる
  5. Model+ControllerとViewを並列実行することができる

(1)は、物理モデルやビジネスロジックと比較して、アプリケーションの表示系(Presentation)は変更が入りやすいことが前提となっており、変更されやすい部分、すなわちViewと変更されにくい部分、すなわちModelとを分離することによって、Viewの変更の影響が及ぶ範囲を限るのが、そもそもMVCパターンの発想の原点であり、第1の目的である。
元々、ControllerはViewとModelとを繋ぐ役割で小規模だと考えられており、Viewの変更がControllerに影響することについては論じられることが少ないが、Controllerが肥大し、変更が入りにくいロジックを含むようになって、しかもControllerがViewに依存するようになると、Viewの変更の影響が変更されにくいクラスに及ぶようになり、この第1の目的に合わなくなる。

(2)は、Modelの品質は特に重要であることが前提にあり、できるだけ他への依存を無くし、個別に重点的にテストして、変更されにくいコードとして確立することを目的とした結果である。もちろんViewやControllerもきっちりテストしてバグの少ないのものに仕上げるべきであるが、ソフトウェアのバグを完全に無くすのは確率的に不可能であることを理解し、特に重点的に品質確保するコードの範囲を選択して絞るのは、現実世界では絶対に必要なことである。
一般に、自パッケージからの依存数をCa、他のパッケージへの依存数をCeとした時にCe/(Ca+Ce)と計算される、クラスの不安定度(Instability)は、0か1に近い方が良いとされるが、M,V,Cそれぞれをパッケージとして、Model内のクラスの不安定度をほぼ0(ViewのObserver I/F以外には外部パッケージへの依存が無い)にできるのがMVCパターンのメリットである。ModelがViewに依存するようになって安定度が下がったり、Controllerに安定度が高いクラスが混ざってController全体の安定度が中途半端になると、このメリットが損なわれることになる。

(3)は、ビジネスロジックやドメインモデルをユーザーインターフェースから分離することによって、ビジネスロジックやドメインモデルに対する同じ操作を複数のUIから実行できる(例えば、キー入力でもマウス入力でも同じ操作が実行できる、異なるメニュー画面から同じ操作が実行できるなど)構成がスムーズに実装できることを意味する。当然、MVCパターンはUIが複数あるソフトウェアを対象にしていることになる。
この利点により、ControllerをModelのテスト用のUIまたはテストコードに置き換えることによって、Modelを全く変更せずにテストすることが可能になる。GUIを持つアプリケーションにテスト用のCUIを追加したり、Modelに対して連続してメッセージ送信する自動テストプログラムを追加することも可能である。
プログラマーの長年の経験則として、結局ソフトウェア開発はいかに効率よくテストすることを可能にするかに掛かっている、というのがあり、特にここ10年くらいの流れである。テストドリブン開発やxUnitフレームワークが邪道だと否定される様子はほとんど無い。テストさえすりゃ作りはどうでもいい、ということに繋がる考え方でうまく行く筈が無いだろう、と、まともな科学者や技術者ならそう考えるものだが、現場は感覚的にも実際にも、ある程度以上きちんと作るのは困難になるので、限界を追求すると、きちんと作るよりきちんとテストすることにコストを掛ける方が成功するのが、人間の知力の限界を物語る現実なのである。

(4)は、計算処理中に表示処理を混ぜることがいかにプログラムを保守しにくくしてきたかを多くの人が思い知った結果であり、MVCパターンに限らず、データとプレゼンテーションを分離することは、よくなされることである。
データ及びそれに対する操作(business logic)に比べ、データの表現手段(presentation logic)はよく変更されるので、business logic内に表示用の処理が混ざっているとプログラマーはよく嫌な思いをする、ということもあるが、その前に、business logicとpresentation logicは品質確保の要件や要求レベルが異なることが多いのである。

(5)は、やはりビジネスロジックと表示系に要求される品質が異なる前提が根底にあるために利点と言えるものであり、表示系がメインのシステムでない限りは、出力先が人間である表示系の動作速度は比較的クリティカルではないので、動作速度が問題になると、自然に表示系をビジネスロジックとは非同期に優先度を下げて動作させようということになる。
逆に言うと、MVCパターンは表示系に求められる性能が高い、表示系に対する要求がクリティカルなシステムには適さない可能性がある。極端に考えると、例えば映像効果のためのシステムでは、表示デバイスの制御が問題の中心であり、映像が最終的な出力なので、映像出力系がModel、目的の映像出力を得るためのUIの表示系がViewだと設計することが考えられる。なので、もしUIに表示効果バリバリのフレームレートの高い描画が求められると、その描画系はViewではなくModelとして扱う必要が出てくることになり、本来のModelとViewのためのModelと、2つのModelが現れることになる。その2つのModelの調停をどこが担うか(Controllerが担うか、それら2つのModelを統括するMagager Modelを作るか)という問題もあるし、そもそも変更が入り易いUIをModelから分離するという目的に反してしまう問題がある。

2012年10月21日

MVCパターンの適用限界を考える(3)

さて、2つ前のエントリーに書いた図1-1図1-2の最もシンプルなMVCパターンでは、すぐに行き詰まることを多くの人が知っている。というか、これではControllerがメニュー画面を出すことすらできない。Controllerの定義から、メニュー画面はControllerそのものであるが、画面表示するにはModelを変更するしかないからである。

まさか、ControllerがViewに依存しないように、メニュー画面のイメージをModelに送るようにすることはあり得ない。多少はViewの使い方を知っていてViewに対するメッセージをModelに渡すなら、それはControllerがViewに依存していることになるので無駄である。Viewへのメッセージの文法を知っているなら直接Viewにメッセージを送れば良い。

ViewとControllerとの間に関連も依存も無いMVCが最も美しい、と考えるあまり、メニュー画面の動作をもModelに含めるのが正しい、突き詰めると、アプリケーションの動作は全てModelにあるのが正しい、と考える人は少なくないが、それは本来のMVCパターンではない。
実は、originalなMVCに答えがある。

"MODELS-VIEWS-CONTROLLERS"のControllerの定義より

It provides means for user output by presenting the user with menus or other means of giving commands and data. The controller receives such user output, translates it into the appropriate messages and pass these messages on to one or more of the views.
(訳:Controllerは、ユーザーにメニューを見せること、または他のコマンドとデータを渡す手段により、ユーザーからの出力手段を提供する。Controllerはユーザーからの出力を適切なメッセージに変換し、そのメッセージを1つ以上のViewに渡す。)

つまり、元々、メニュー画面のようなControllerの表示は、Controllerが直接Viewにアクセスすることによって行うという考え方なのである。図にすると次のようになる。

Class diagram of MVC with V-C connection
図2-1: Controllerの表示が可能なMVCのクラス図(静的構造図)

Communication diagram of MVC with V-C connection
図2-2: Controllerの表示が可能なMVCのコミュニケーション図(動的構造図)

図1との違いのポイントを挙げる。

  • ConcreteViewにControllerによる表示のためのI/Fがある
  • ConcreteControllerからConcreteViewへの関連がある
  • ControllerのViewのみの変更は、C-Vに閉じて完結される

この具象Controllerと具象Viewとの関連は必須ではない(全てのViewがControllerの表示を行う必要はない、CUIやリモート制御だとModelのViewがControllerのUIを表示しないこともある)ので、抽象Controllerと抽象Viewとの間には現れない。

C-V間の関連が無いMVCは、抽象レベルだけを取り上げた、MVCの一部のエッセンスだけを示したものである。それで済む時もあるが、それが最も美しいということではない。
ソフトウェアのデザインパターンをかじった人ならわかると思うが、抽象クラスだけで構成されるパターンは存在しない。というより、抽象クラスを含むパターンは、オブジェクト指向の「依存関係逆転の原則」の存在箇所の明確化のためにも、最低どれか1つの具象クラスが無いと成り立たない。具象クラスまで含めてのデザインパターンである。

ディスプレイに表示するViewがあって、UIがそのディスプレイ上に無いシステムは、MVCパターンとして考える意味が無い(何の為にViewを分離しているのかわからない)。MVCパターンは元々GUIのあるシステムを対象に含めているので、その要素を省いて語るべきではないと、筆者は考える。


"MODELS-VIEWS-CONTROLLERS"のControllerの定義には、他にもV-C間の具象レベルでの関連が示されている。


Conversely, a view should never know about user input, such as mouse operations and keystrokes. It should always be possible to write a method in a controller that sends messages to views which exactly reproduce any sequence of user commands.
(訳:同様に、全てのviewはマウス操作やキー入力といったユーザー入力には一切関知しない。ユーザーコマンド列を再生成するだけのviewにメッセージを送るmethodをControllerに書くことは可能なだけである。)

この文書には他に"user output"という表現もあって、"user input"と"messages"と"user commands"の違いがわかりにくい(はっきり言って、筆者にはわからない)が、"THING-MODEL-VIEW-EDITOR"のVIEWのEXAMPLE 1の所に
It understands a message asking it for an item that is positioned at a given point within its frame, and a message asking it to select a given item.
(筆者注:この文脈ではframeはControllerによる表示のこと。"a given point within its frame"と書くことにより、"user input"そのものではなく、抽象的な意味合いの位置情報であることを強調しているものと思われる)
One possible sequence for selection is that the Editor reacts to redbug and asks the ListView for the item that is pointed to. The Editor then asks the ListView and any other Views to select that item.
(筆者注:この文脈ではEditorは具象Controller、ListViewは具象Viewのこと)
とあるので、Controllerは、現在Viewがある場所に何を表示しているかを問い合わせることができる、という意味だと解釈するのが妥当であろう。マウス操作による画面上のクリックなど、ユーザーがView上の表示に依存する操作を行った場合、Viewがその位置に何を表示しているかがわからない限り、Controllerはユーザーの指示を解釈できないが、その問題を解決する為の必要最小限のサポートをViewが(あくまでユーザー入力の生データには関知せずに)行うべきだということである。

また、その少し後ろのEXAMPLE 4の所に、


In addition, it will need some operations on the View itself, they have to do with the positioning of the symbols in the diagram.

つまり、Modelの要素の表示位置を変えるなど、Modelの変更とは無関係にModelのViewが変化する場合は、そのためのI/F(呼び出すのはController)がViewに必要になるということである。

また、"MODELS-VIEWS-CONTROLLERS"に戻って、VIEWの定義に、次のような具象M-V間の関係が書かれている。

A view ... gets the data necessary for the presentation from the model by asking questions. It may also update the model by sending appropriate messages. All these questions and messages ...

つまり、ViewがModelを変更することもあり得るということである。これだけでは何のことかさっぱりわからないが、"THING-MODEL-VIEW-EDITOR"のVIEWの所の冒頭に
A View is also able to perform such operations upon the Model that is reasonabely associated with that View.

とあり、その後ろのEXAMPLE 6の所に

It is also able to pass on operations on the network and its activities that are related to this particular View. Typical operations have to do with modifying the current schedule.
(筆者注:ここではnetworkはModel、activitiesとscheduleはnetworkの要素)

とあるので、(Controllerを介して)Modelの要素に十分に直結したView上の変更が発生すれば、それに対応する変更を、Controllerに関係なく(Controllerを介さずに)、Viewが直接Modelに対して行っても良いということなのである。

これらを含め、MVCパターンは次のように図示できる。

Class diagram of MVC without V-C connection
図3-1: MVCのクラス図(静的構造図)

Communication diagram of MVC without V-C connection
図3-2: MVCのコミュニケーション図(図2-2からの差分のみ)

ところで、以前のエントリーに、J2EEやASP.NETなどがMVCを参考にしていると書いたが、これらで使われているのは元々のMVCパターンとは異なる、"JSP Model 2 architecture"または単に"Model 2"と呼ばれるアーキテクチャーである。
一応、Webサーバー側のMVCパターン実装、ということになっているが、基本的にはサーバー側はconnectionlessかつstatelessであり、Viewの出力はHTTP requestに対するresponseとしてユーザーに出力するため、Modelの状態が変化した時にViewが表示を更新する、ということができず、M-V間にObserverパターンの関係が無いのが特徴である。

Class diagram of "Model 2"
図4-1: "Model 2"のクラス図

Communication diagram of "Model 2"
図4-2: "Model 2"のコミュニケーション図

元々、"Model 2"が最初に現れた時にはMVCの用語は使われていなかったが、次第にMVCの用語を用いて説明されるようになった(下記参考リンク参照)。それにより、MVCと"Model 2"がごっちゃにされ、人々のMVCの定義がバラバラになり、MVCをややこしくした。
筆者は、ViewがObserverでないMVCはあり得ないと思うので、"Model 2"もMVCではないと思う(し、結構多くの人に賛同頂けるものだと信じている)が、特にWeb系ではそんなことを気にしない人も少なくないようである。
"Model 2"を含めると本当に話がややこしくなるので、このサイトでは"Model 2"はMVCパターンに含めない。

●"Model 2"の参考リンク

  • Understanding JSP Model 2 architecture - JavaWorld --- 現時点での事実上の原典
  • MVC, Model 2, Java WebApps (Brian's Waste of Time) --- "Then the Web happens and Sun starts talking about Model2 in terms of MVC."("Model 2"がMVCの用語を使って語られるようになり、Sunもそれに乗っかった)ということが書かれている。
  • Java EE 5のTutorialの中の1ページ --- Webアプリの世界ではMVCはModel-2と同じものだとみなされることがしばしばある、と書かれている。
    Note - When employed in a web application, the MVC architecture is often referred to as a Model-2 architecture. The bookstore example discussed in Chapter 4, Java Servlet Technology, which intermixes presentation and business logic, follows what is known as a Model-1 architecture. The Model-2 architecture is the recommended approach to designing web applications.
  • How Struts Implements Model 2 --- "The Origins of Model 1/Model 2"という章に、MVCとModel 2との関係が書かれている。
  • ASP.NET Presentation Patterns --- The original MVCと"Model2"と"ASP.NET MVC Framework"との関係が解説されている。

2012年11月15日

MVCパターンの適用限界を考える(4)

1つ前のエントリーで、いわゆる"Model 2"がMVCパターンの用語で説明されるようになったことが、人々のMVCの理解をバラバラにする原因になった、と書いたが、そのことは、前エントリーの参考リンクの先のページでも見られるように、多くのWebページで触れられていることでもあるが、筆者はむしろ、いわゆるGoF本に書かれているMVCの説明が最大の混乱の元だと思っている。"Design Patterns: Elements of Reusable Object-Oriented Software"(邦訳タイトルは「オブジェクト指向における再利用のためのデザインパターン」)、いわゆるGoF本の§1.2にある以下の記述がそれである。

MVC consists of three kinds of objects. The Model is the application object, the View is its screen presentation, and the Controller defines the way the user interface reacts to user input.
MVC also lets you change the way a view responds to user input without changing its visual presentation. You might want to change the way it responds to the keyboard, for example, or have it use a pop-up menu instead of command keys. MVC encapsulates the response mechanism in a Controller object.
A view uses an instance of a Controller subclass to implement a particular response strategy; to implement a different strategy, simply replace the instance with a different kind of controller. It's even possible to change a view's controller at run-time to let the view change the way it responds to user input. For example, a view can be disabled so that it doesn't accept input simply by giving it a controller that ignores input events. (注:ここでは"disabled"はボタン等が操作できない状態のことを指す)

GoF本の1.2節を開いてまず目を引くのは、図にControllerが無く、1つのModelと複数のViewのみで構成されていることである。上記の引用文にある通り、これは、ControllerがViewの一部として扱われているからである。
MVCの最大の目的はPresentation logicとBusiness logicの分離による安定性の向上(変化する部分と変化しない部分の分離)である、というのはOriginal MVCから一貫した理念であるが、システム全体を(Controller抜きで)ViewとModelに分離すると書くことによって、それを最大限に強調しているのである。これはMVCの本質をわかりやすく表していると思う。

しかし、それから少し読むと違和感を覚えるのが、ユーザー入力を受けたViewがどう振る舞うかを、そのViewのControllerが決める、という記述である。このようなことは、Original MVCには書かれていない。また、Original MVCに書かれている、ControllerからViewへのメッセージ送信についても、GoF本には明確な記述が無い。
もしこれが、Original MVCのControllerの定義にある

Conversely, a view should never know about user input, such as mouse operations and keystrokes. It should always be possible to write a method in a controller that sends messages to views which exactly reproduce any sequence of user commands.

と矛盾しないとすれば、ユーザー入力がViewからControllerへ伝搬されるというのは、"views which exactly reproduce any sequence of user commands"の部分を拡大解釈したものであり、Controllerは入力デバイスのドライバーやウィンドウシステムの役割のみを担うものとしていると解釈するのが妥当であろう。
早い話が、JavaのSwingやQt, GTKなどのイベントドリブンなツールキットを用いて実装されるGUI全体をViewだとしているのである。
図にすると次のようになる。

Class diagram of MVC in the GoF design pattern book
図5-1: GoF本のMVCのクラス図(静的構造図)

Communication diagram of MVC in the GoF design pattern book
図5-2: GoF本のMVCのコミュニケーション図(動的構造図)

ポイントとしては、Viewがユーザーイベントの配信を制御することが可能になっており、Original MVCではV-C間の関連におけるControllerの役割だったものの大部分がViewに移動している。また、ViewはControllerより先に存在する前提であり、Viewが表示されていない状態からControllerがView上にメニュー画面の表示を要求するようなことは想定されていない(Swing等のGUIフレームワークを使えば、最初からGUIが何か表示されているのが普通だから、想像は難しくない)。

ViewとControllerの関連の向きがOriginal MVCと逆であることをわかりやすくするため、Controllerのデバイスドライバー/ウィンドウシステムとしての役割を省いたのが、次の図である。

Communication diagram of MVC in the GoF book
図5-3: GoF本の記述に沿ったMVCのコミュニケーション図(簡略化版)


このMVCのモデル(以下、GoF MVCとする)は、GoF本のテーマにはよく乗っており、同書の以下のデザインパターンが使われていると書かれるのは非常に納得できる。
・Observer
・Strategy
・Composite
・Factory Method
・Decorator

Observer patternが使われることは言うまでもないが、Modelが変化すると、observerとして登録されている全てのViewに何か変化があったことが通知されることを実現するのに使われている。
Observer pattern in GoF MVC
図5-4: GoF MVCにおけるObserverパターン

Strategy patternは、ユーザー入力に対するViewの振る舞いを決めるControllerが、Viewのインスタンス毎に切替可能であることを実現するのに使われている。多くのGUIフレームワークでは、イベントのコールバック関数(リスナー)が動的に登録可能であることに相当する。ここではControllerがViewの一部なのは、上述の通りである。
Strategy pattern in GoF MVC
図5-5: GoF MVCにおけるStrategyパターン

Composite patternは、View同士が包含関係になり、親Viewが受けたイベントを、内包するViewに伝搬することにより、複合ViewをViewと同じように扱うことを可能にするのに用いられている。GUIフレームワークでは、例えばコンテナの中にコンテナを含めることに相当する。Original MVCの"The Model-View-Controller (MVC) Its Past and Present"のP-9にある"Tool as a composite"も、大体同じような内容である。
Composite pattern in Gof MVC
図5-6: GoF MVCにおけるCompositeパターン

Factory Methodパターンは、各Viewのdefault controllerを得るのに用いられる、とされる。これによって具象Viewから具象Controllerへの依存を無くすことができ、例えば具象ViewがControllerのいずれかのサブクラスを指定してインスタンスをcreateする必要が無くなる。GUIフレームワークでは、起動時のパーツの配置や初期設定をアプリケーションのmainクラスがまとめて保持することが、強いて言えばこれに相当するだろうか。(世の中にはあるのかも知れないが、筆者はあまりControllerのFactoryというのを見たことがない)
Factory Method pattern in GoF MVC
図5-7: GoF MVCにおけるFactory Methodパターン

Decoratorパターンは、Viewのサブクラスに共通する機能の追加を、それぞれのサブクラスを新たに作ることなく、委譲を用いて実現するのに用いられる。GUIツールキットでは、例えばGoF本には、各コンポーネント(クラス)にスクロール機能を追加する時に用いられると書かれている。
オブジェクト指向の経験則として、既存クラスの拡張や既存クラスへの機能追加は、派生クラスを作成するより、AdapterやDecoratorなどを使った委譲によって行う方が良いことが多いと言われることがある。小規模な機能拡張で一々、それぞれの機能の有無毎に派生クラスを作成するのは面倒で非効率であることを考えば、納得できる。
Decorator pattern in GoF MVC
図5-8: GoF MVCにおけるDecoratorパターン


世の中にC→Vの関連が無くV→Cの関連があるMVCパターンの図が存在するのは、GoF本の記述が原因であろう。
また、MVCのデザインパターン的な側面を語られる時は大体、V-C間の関連はViewからControllerへの関連であることも興味深い。V-C間の関連がControllerからViewである、Original MVCに則してGoFのデザインパターンが語られることはほとんど無い。
これは、既存の多くのGUIフレームワークに当てはめてMVCを解釈するには悪くないし、MVCが将来そのような考え方に置き換わっていくのは必然なのかも知れないが、それは筆者にはどうしてもMVCの拡大解釈であり、MVCとは別の名前を付けるべきだったように思える。

GoF本に書かれているMVCは、極端に言えば、Controllerが無くても動作可能なMVCアーキテクチャーである。ウィンドウシステムがミドルウェアに、データモデルや表示系アプリがアプリケーション層にあれば、アプリケーション層だけを見るとControllerが存在しない。
実際、GoF本には、何もしないControllerをViewに与えることによって…という記述があるし、Original MVC的にはViewからのModelの更新はあり得る訳だから、システム全体としてController無しでMVCパターンを実装することも可能だということになる。

ControllerはViewとModelの橋渡しに過ぎず、MVCモデルはViewとModelが主役、というのはMVCの本質であり、それを強調することは悪いことではないと思うが、Controllerの役割やC-Vの関連についてほとんど触れられないのは如何なものであろうか。MVCの本質でなくても、Controllerの位置付けはMVCの必須構成要件である。
オブジェクト指向的に考えると、Controllerのオブジェクトは本質的には不要なので、どの方向から考えても結局はControllerの責務は最小にされるべきだという結論になるからだ、としても、ControllerがViewの一部であるように表現してしまうのは、MVCの説明としては問題があるのではないか。

筆者は、GoF本の記述は、テーマ的にMVCについて触れない訳にはいかないから、同書の内容、あるいは現存するGUIフレームワーク(筆頭著者のErich Gamma氏はJavaとの結び付きが強いからAWTだろうか)と整合する為に、このようにされたのだと考えるべきものだと思う。

続きを読む "MVCパターンの適用限界を考える(4)" »

2013年01月27日

MVCパターンの適用限界を考える(5)

MVCの適用限界について考えることをテーマにして書き続けてきたが、MVCとその変遷が複雑すぎて、それらを考察し終えるのにはまだまだ時間がかかりそうである。
なので、一旦、現時点での筆者のMVCの適用限界に関する見解をまとめることにした。

ここでは、ドメインモデルとビジネスモデルをまとめて「ビジネスロジック」と表現する。以下の「ビジネスロジック」は「ドメインモデル」に置き換えても同様である。


●1. MVCパターンは、現代のGUIフレームワークには適さない
ウィンドウやウィジェットを入れ子構造で配置し、ボタンやリストなどの表示部品を置き、それらが押された時の動作などを各表示部品に直接関連付け、マウス操作などのイベントがアクティブな子ウィジェットから親ウィンドウへと伝搬される、AWTやSwingや*/TkやGTKやVBやVCなど、現在開発に使用されている代表的なGUIフレームワークでは、MVCで言う所のControllerが階層構造のViewに分散しており、ControllerはViewの一部という形になる。それにより、ControllerがViewに依存する形となり、例えば、CをそのままにしてVを追加/変更できるという前提が崩れる。

いわゆるGoF本に書かれているMVCでは、originalの(Smalltalkの)MVCとは異なり、ControllerはViewの一部だと割り切り、presentation logicとdomain modelを分けて多対1の関係とすることに絞っている為、現代的なGUIフレームワークにも適した考え方となっているが、それはMとVだけで成り立つ概念であり、MVCと呼ぶ意味が無いものである。
なお、MVCはGoF本の発刊時点で既にこの理由でobsoletedであった為、デザインパターンの元祖として紹介するだけで、同書の23のデザインパターンとは並列にされなかったという説もある。

これに関して、参考リンクに挙げたWebページから、いくつか興味を引かれた文章を引用する。

[3]のコメント欄より:

if you need to send an onMouseOver event to a controller instead of just changing state on the button in the view, what do you gain?
...
Only things that change application states (be they persistent, session-specific, or application-wide) should be sent to a controller

(Viewしかわからない)MouseOverイベントをControllerが受けてViewに配送することに何の意味があるのか?(Viewが単独で処理すれば良いのではないか?)という疑問である。Modelの変更に繋がり得るイベントだけがControllerを通れば良い、というのも同感で、そこまできちっとViewからControllerを切り離す必要性は無いと思う。

[6]のコメント欄より:

MVC, and StateMachines are orthogonal concerns (depending on the app each tier may have it's own StateMachine). The differentiation typically is in MVC the model state is persistent, and survives a many to one relationship (e.g. a bar and line graph both using the same data.) where as state machines are useful for runtime states.

MVCと状態遷移モデルは直交する概念で、状態遷移モデルの状態は全てMVCのModelの状態であるべきだということにはならず、永続的でV-Cとは多対一の対応の関係にあるものがModelの状態であり、ViewやControllerがそれぞれ実行中の状態(及び遷移モデル)を持っていても良い。
GUIフレームワークでは、Viewの状態とControllerの状態が分離されない(全て同じ箇所に書けてしまう)ので、意識的に努めて行わない限りは、ViewとControllerが分離されない。というか、現実的には(単純な例題でもない限り)ViewとControllerとの状態の分離はやり切れないと筆者は思う。

同じく[6]のコメント欄より:

Some definitions of MVC call for a single controller, supporting multiple models, each with a view. Obviously, Flash apps almost never work like that - Flash MVC is really distributed MVC, often different views have their own controllers, and views are all managed by a master state management controller.

Adobe FlashのアプリをMVCに当てはめると、M:V:Cの関係が通常の多対多対1の関係ではなく、ViewはそれぞれControllerを持ち、それとは別の巨大なControllerによって制御される、"distributed MVC"である。

なお、Cocoa MVCでは、"Responder"(Controllerへの参照のようなもの)と"Responder chain"(イベントを処理できるハンドラーの検索順序を決めるリスト)という概念によってGUIフレームワークとMVCとを整合させているそうだが、本当にこれによってControllerがViewの構造とは独立の構造を持ち、例えばViewだけが交換可能になるのかどうかは、筆者にはよくわからなかった。(View毎にResponder chainを用意するというのは、View毎にControllerの構造を用意するというのと同じようなものなのでは…)


●2. MVCパターンは、リッチなクライアントを持つWebシステムには適さない
本来、MVCパターンでは、Modelが変化すればViewがその通知を受けて、ViewがModelの状態を取得して表示に反映するものであるが、通常使われるHTTPのような(PUSH型でなく)PULL型の通信手段を使用しているWebシステムでは、Modelの本体がサーバー側、Viewの本体がクライアント側にあるので、Modelの変化に伴ってViewを更新することができない。

Webシステムでよく使用される、J2EEやStrutsやFlexのような、いわゆる”Model 2”のフレームワークでは、サーバー側の構成にMVCパターンの考え方が採用されており、WebシステムでMVCと言えば大体これのことを指す。しかし、クライアント側も含めてMVCパターンにすることをサポートするフレームワークは存在しない。
"Model 2"でも、サーバー側、クライアント側それぞれ別々にMVCパターンを適用することは可能であるが、それはアプリケーションのGUIのみをMVCにするようなものなので、クライアント側をMVCにするメリットは少ない。

クライアント側がHTMLを表示するだけのViewの機能と、HTMLブラウザの機能だけで実現できるフォームやボタン程度のUIであれば、クライアントはViewのみとしてもまず問題にはならないが、AjaxやFlashを用いるなどして、クライアント側のUIをリッチにすることが可能になり、例えばユーザーの入力にリアルタイムに反応してボタンの有効状態が変わるようにしようとすると、それによってボタンが有効になるべきかどうかをキーストロークの度にいちいちサーバー側に問い合わせるか、Modelに相当するビジネスロジックをクライアント側にも含める必要が出てくる。

このような問題をまともにMVCパターンで解決するには、Webシステム全体としての機能をMVCに分割し、Model, Controllerそれぞれをサーバー側とクライアント側に配置し、Model同士、Controller同士がそれぞれ最小の通信量で済むように協調動作することが必要になる。それ自体が容易でないのに加えて、そういう構成をサポートするMVCフレームワークは現存せず、それに既存の"Model 2"のMVCフレームワークが使えないので、サーバー側とクライアント側を合わせてMVCパターンにすることは困難であろう。
ついでに言うと、"Model 2"のMVCと本来のMVCは異なるので、Webシステムで本来のMVCパターンを取り入れようとすると混乱が起こるのではないだろうか。

参考リンクからいくつか引用する。
[1] "MVC As Anti-Pattern"より:

But what if you are building a web application with a rich client?
[snip]
Do you still need an MVC framework?
I think the answer is most likely no.
このページ全体がこのテーマで、論旨がよくまとめられているので、興味のある方は是非読んで頂きたい。
Developers who use modern rich-client frameworks do not have to create controllers to manage events. What a runtime and its frameworks do internally is a different story.
この"rich-client frameworks"は、View内の構成要素それぞれがController(イベントハンドラー)を備える、通常のGUIフレームワークのことを指す。

[3]より:

I’ve come to the conclusion that… drumroll please… MVC is probably not needed for most RIAs.
RIAはRich Internet Applicationsの略で、クライアント側もリッチなアプリケーションであることを意味しており、この記事の所々にある"FrontController pattern"は、その対極に、クライアント側はシンプルで、何もかもサーバー側のFrontControllerを経由する構成のことを意味している。

You often see architectures in which a single controller handles all of the “actions” for the entire application. I see this as a necessary evil, not a “good thing”. The world of the giant switch statement is exactly what we wanted to avoid with OO programming, right?
ここでは、MVCのControllerは巨大なswitch文の塊になりがちであることが問題にされている。例のmost controversialなSkinny Controller, Fat Modelと同時期に呈された、MVCに対する不満である。

[3]のコメント欄より:

I agree, separate model and view is enough, controller sometime is abused
M-Vの分離には意味があるが、Cの分離はえてしてやり過ぎである。

[6]より:

Fact: No one codes Flash using pure MVC.
このページに書かれているのは主に一般論だが、なかなか深く鋭い話で、一読に値すると思う。

[3]のコメント欄より:

In addition, just consider the case (in the real Business world) when you need to deploy a program (which also holds the Controlling) on the client machine
クライアント側にView以外のロジック(ビジネスロジックそのものや、サーバー側のビジネスロジックを扱うController)を含めることについての問題提起であり、一般にクライアント側でのプログラムの実行は難しいので、故にV-Cの分離は重要だと述べられている。確かに考慮すべき事柄ではあり、V-Cの分離が現実的に可能であれば一理あるが、実際にはV-Cの分離に労力を掛けるより、クライアント側でプログラムを実行することに労力を費やす方が、コストが小さく、何よりも自由が利くので、労力に見合うのではないだろうか。

[3]のコメント欄より:

Anyone who justifies MVC as a way of making a web app “like” a desktop application’s MVC architecture is fooling themselves. The internet just doesn’t work that way. I’ve seen IT budgets balloon and schedules run late because of the blind, over-zealous switch to MVC, while chanting “industry standard” to anyone who asks “why?”


●3. MVCパターンは、リッチなGUIには向かない
MVCパターンでは、UIの制御は、必要であればViewに(カーソル位置、スクロール位置等の)表示状態を問い合わせながら、Controllerが主となって行うことになっている。それにより、ControllerはViewの画面構成に依存せず、Viewのみの交換や、表示の異なる複数のViewの追加が可能になるのである。

しかし、そのようなViewの抽象化は、全てのViewに「タイトルバー」と「メニューバー」があり、「メニューバー」は「項目」または「項目グループ」を含む、など、ある程度はUIの形状が決まっていないと困難である。

また、Viewの状態が複雑になるだけ、Controllerも複雑になる。いくら何でも、マルチウィンドウ状態を持つVを、1つのループとswitch文でなるControllerが制御するのは限界があるので、必然的にControllerは分解することになるが、その分解方法はある程度Viewの構造に依存しないと複雑になる。Viewを抽象化し切るのが困難になり、抽象化する労力が効果に釣り合わなくなるのである。ウィンドウマネージャーくらいの機能だとまだ実現可能かも知れないが(それでも相当大変だ)、クリックされた位置がどのような抽象状態の時にどうする、とか、メニュー画面がスクロール可能な場合はどうでページ送りの場合はどうで、とかいうのを、Viewを抽象化してViewがどんな形でも動けるようにするのは至難の業である。

ウィンドウシステムのあるマルチアプリ環境にMVCパターンを当てはめることを想像すると、アプリ毎にMVCがある構造を思い付くのが自然ではないだろうか。アプリの追加/削除まで考えると、仮にMはアプリケーション間で共通のものを使用できるとしても、VやCをも共通にできると思う人は居ないだろう。それができるとすれば、メニュー画面しかないアプリくらいである。つまり、アプリケーションマネージャーを除き、Viewの数だけControllerが存在すると思うのが自然である。1つのアプリケーションでも、巨大になれば構造を分解することになるが、マルチアプリ環境と同じ発想で、ViewとControllerを複数のV-Cのセットに分解する構造にすることを考えることが可能なので、それに行き着くのが自然であろう。GUIが大規模になれば、Controllerの構造がViewの構造に一致する、というより、ControllerがViewの構造に縛られるのは宿命なのである。

このことは、実はoriginal MVCでも少し触れられている。前の記事にも挙げた、"The Model-View-Controller (MVC) Its Past and Present"のP-8 "Tools for Tasks"とP-9 "Tool as a composite"がそれであり、タスク依存のV-CはToolとして独立させれば良く、ToolはToolを組み合わせて構成すれば良いということなので、これは、現在主流のGUIフレームワークを使うと自然とその形に誘導される、GUIをウィジェットの入れ子構造で実現することとほぼ同じことである。これでアプリケーション全体がToolの組み合わせだけで実現することがMVCパターンの範囲に入るなら、現在主流のGUIフレームワークはMVCパターンから逸脱していないことになるが、さすがにアプリケーションのトップレベルがMVCの構成でないのはMVCパターンとは呼べないであろう。また、通常MVCという用語が使われる時に、P-8やP-9の意味を含むことは稀である。

また、GUIが複雑になれば、GUIそのものをMVCに分ければいいという意見が出てくることもあるが、一般にV-Mが強結合だとMVCはうまくいかないことが知られているし、それこそMVCパターンを使うことが目的になり、何の為にMVCにするのかがわからなくなってしまう。

[3]のコメント欄より:

One of the reasons that people build controllers is to isolate the interaction logic from the view, with the goal of being able to swap out the view. The problem with this approach is that with the richer UI vocabulary that RIAs allow, I believe it is not possible to do this.
UIがリッチになると、UIのロジックをViewから切り離すのは不可能だと思うと書かれている。
筆者も、M,CをそのままにVだけ交換可能にするというのは、スキン(画像ファイル)の交換くらいが限界だろうと思う。GUIの見た目が変わって操作が全く同じということはほとんど無いだろう。


●4. MVCパターンは、エディターには向かない
テキストエディターや文書作成アプリや画像編集アプリなどは、MVCパターンへの当てはめ方が難しいアプリケーションの例としてよく挙げられるものである。
何をModelにするべきかという問題ではあるが、デザインパターン、つまり共通の知識としてのMVCの一般的なコンセンサスとして、Modelの状態は永続的なものであることと、ModelはViewに依存しないことがあるが、それからすると、テキストエディターのModelの責務は、文書を保存して読み出すことくらいになってしまう。

人間にわかり易い形、つまりビジュアルなデータ編集ツールの中心部を、ビジュアル抜きに形式化するのは困難である。かと言って、ビジュアルがメインのアプリケーションなら表示状態もModelに含めれば良いかというと、そもそもPresentationとModelが分離される背景には表示系はよく変更されるという前提があり、表示系が変更するとMにもVにもCにも変更が入ってしまうのでは、何の為にMとVとCとに分離するのかがわからない。

また、通常、ControllerとModelとの間のセッション状態(ひとまとまりの協調動作における状態)をModel側に持たせることは好ましくない。C-Mの関係においてはControllerが能動的、Modelが受動的に動作するので、セッションの開始と終了がわかるのはControllerだからである。例えば、テキストエディターの編集中の状態をModelに含めると、同時に編集するデータの数に比例してModelの状態が複雑になってしまう。(セッションの数だけModelのオブジェクトを作成するならさほど複雑にならないが、それはControllerがセッション管理しているのと同じことである)

Original MVCでも"THING-MODEL-VIEW-EDITOR"という分け方があるように、オブジェクトの中身を操作するUIはMVCパターンの中でも特殊な位置付けであり、"EDITOR"はMVCに当てはまらないものの代名詞とも言えるのである。

エディターの他に、MVCの適用が困難な例としては、ゲームがよく挙げられる。エディターと同様に、画面表示がメインのアプリケーションは、ModelとViewの結合が強いので、MVCの分割に向かない。また、HMI(Human-Machine Interface)や感性モデルやバーチャルリアリティーのように、変化しないModelを確立するのが難しい分野にも向かないと考えられる。現実世界のモデル、現実のビジネスモデルとHMIは追究の仕方が異なるので、開発スタイルも異なるはずである。

アプリケーション全体としては直感的にMVCに分けることができても、部分的にMVCに分けるのが困難になる例としては、Modelの実行中の確認ダイアログが有名である。
そのビジネスロジックにおいてその手順中にユーザー確認が必要であったら、その情報はModelにあるべきであるが、ダイアログ画面の表示制御はControllerの仕事である。確認ダイアログ1枚くらいなら、Modelの処理をその前後で分けて、Controllerが前半の処理とダイアログ制御と後半の処理を順に行えば簡単であるが、一連のビジネスロジックの処理中に何枚かのユーザー入力画面が必要だったり、それらが画面遷移モデルを伴ったりすると、問題は一気に複雑になる。あくまでControllerがビジネスロジックを含めない方針にすると、ModelはControllerへの応答だけで次に出す入力画面を伝える必要があり、ユーザーからの入力が不正かどうかをControllerは判断できないので、Modelは不正入力を判断して、場合によってはエラー表示を伴う異常処理を行わなければならなくなる。
ビジネスロジック固有のUIの画面遷移と、タスクやView固有のUIの画面遷移とがあり、View固有としてV-C側に置かれていた画面遷移が、Viewが増えた時にView間で共通のビジネスロジックだと決まると、Modelに移動させる必要が生じたりする。
そもそも、ビジネスロジックがUIを伴っても、その実行時の画面遷移状態はセッション状態であり、Modelの状態としてはふさわしくない。
かくして、UIを伴うビジネスロジックがControllerに置かれるようになり、Modelの存在意義が希薄になり、Controllerがビジネスロジックの塊となるのである。

これも突き詰めると、MVCでは、UIを伴う処理をModelに取り込むのは厄介だということであり、エディターがMVCに向かないのと共通点の多い問題である。

各々のプロジェクトにおいて、何をModelにするかという方針を明確にしさえすれば、そんな問題は起こらない、という意見もあるが、対象が物理モデルやH/Wでも無い限りは、どこまでをビジネスロジックと見做すのかは、それほど一意に決まらない。始めはアプリケーション固有ロジックであっても、それが再利用されるようになって初めてビジネスロジックになることも少なくない。どう考えてもpresentation logic、そのViewでしか使わない固有の処理だろうと思ったものが、実はそのビジネスモデルで共通だったというのは、よくあることである。
UI intensiveなエディターをどうMVCに分解するかというのは、SmalltalkのMVCでも同様の問題がある気がするのだが、筆者はこれについて何らかの解が示されているwebページを見た記憶が無い。筆者は、エディターからModelを迷い無く切り出せるようなModelの定義は存在しないと思っている。

参考リンクからいくつか引用する。

[2]より:

By letting the service layer refer to sessions, I am also creating a Model that is hard to test without having an entire application in place.

ここでは"service layer"はModelの上層部、ControllerやViewから直接呼び出されるI/Fを含む層を指す。Modelがセッションを参照すると、Modelを単体でテストするのが困難になる、と書かれている。

同じく[2]のコメント欄より:

This makes me think of what Misko Hevery was saying in a previous blog post - that the more testable something is, the better it is probably architected. And, the more easily tested it is, the more it likely decoupled from other objects.

One of the huge benefits of separation of concerns is testability. If you've designed your components properly, you should be able to write test cases that can give you some peace of mind.

[4]より

If you are designing a graphics intensive program, like a game, you would probably couple the View and Model classes much more tightly than what MVC suggests. As we can see from the Basic sample application, when programming a very simple application it is common to combine the controller with the view classes.

[3]のコメント欄より:

Things, of course, get tricky when user interaction comes into play. For example, if you need to pop up an alert that asks the user a question before completing the transaction.

[3]のコメント欄より:

If you’re doing a word processing application, is the keypress sent to the controller, added to the document via the model, then the view is refreshed via the controller? Or is the model used to save changed files and the view handles all the live updates to the text?

[6]より:

MVC says that the entire application state is supposed to reside in the model. However, many applications maintain application view state in the controller, and the data state in the model.

MVCではアプリケーションの状態は全てModelに置かれることになっている(Original MVCではある程度のGUIの状態はViewが保持してもいいことは省略されている)が、実際にはControllerがViewの状態を保持していることが多い、ということ。

ControllerにGUIの状態を置かないことが実際には難しいことを暗に言っていると思う。
Original MVCでControllerがViewに表示状態を問い合わせても良いと書かれているのは、当時はGUIの状態は少なく、リスト上の表示範囲やカーソル位置など、View固有の状態だけで表現できるという前提があったから、もっと言うとSmalltalkではアプリケーションの形をある程度決めることができたからそういう設計思想が通用したのであり、ボタンの状態や文字色やパネルの入れ子や不定形なGUIやフォーカスやアニメーションなど、昨今のバリエーション豊富なGUIでは、むしろModelよりも状態数が多いことも少なくない。


●5. MVCパターンは、リアルタイムなUIに向かない
これは、Viewの画面更新はModelの変更完了後になされ、ユーザー入力を受けるControllerがそのままViewの画面更新を同時に行わないことによる。
よくあるMVCの実装だと、Model-Vew間のObserver patternがきっちり作られているため、ModelとViewが非同期的に動作し、Modelに何か変化があったという通知をViewが受けて、ViewがModelに最新の状態を問い合わせるという、Model内のデータの更新と画面の更新とを同時に行うのに比べて無駄の多いシーケンスとなる。Controllerから見ると、もう画面をどう更新すればいいかがわかっていても、イベント配信のスケジューラーに任せる形で、表示更新を後回しにするしか無いことがある。

もちろん、Viewは必ず非同期に動作しないといけないということは無いので、Modelの更新中にViewに更新要求しつつ、即座にViewに実行権を明け渡すような同期処理にすることも可能だが、Modelの更新に同期して都度表示更新するのがユーザーレスポンスが再短時間になるとは限らない。すなわち、Modelの更新がある程度済んでから描画する方が速いケースも少なくない。
一続きのModelの更新が完了したタイミングがわかるのは基本的にControllerなので、必要な時はModelの更新前にViewに表示更新禁止指示を出し、Modelの更新後にViewに表示更新要求を出せば良いのだが、そのような必要が多いシステムであれば、VewをModelのObserverにする意味が無いであろう。

また、MVCパターンでは表示に関するビジネスロジックもModelにあるのが前提(Viewが交換可能なのが前提だから)なので、必然的にViewからModelへの問い合わせが増えることも考えておく必要がある。Modelへの問い合わせが、メソッドの同期呼び出し等、同じ実行コンテキスト内の同期処理であればオーバーヘッドは小さいだろうが、ViewとModelが並列動作していて排他制御が必要だったり、Modelがネットワーク越しにあったりすると、一部のビジネスロジックはViewにも含めるような、MVCパターンから外れる対策が必要になるだろう。

参考文献[5]より:

The first problem area is to deal with setting the color of the variance. This shouldn't really fit into a domain object, as the color by which we display a value isn't part of the domain.
表示する値(ここではvariance)に定性的な意味合いに基づいて色付けする場合にその色の決定処理をどこに置くかという例題で、この文章に続いて、MVCパターンにおける、表示に関するビジネスロジックの扱い方について、深く考察されている。長くなるので詳細は割愛するが、オブジェクト指向言語だとtext fieldのsubclassを作るのが好みだと書きながら、結論としてはMVCの他に"Presentation Model"(View側のビジネスロジックを含める部分)を設けることによって大抵解決できると書かれているのが興味深い。


●6. MVCは絶対的な指針にはならない
人々のMVCパターンの理解がこれほどまでにばらつく理由は、Web系のフレームワークにおいて絶対的な地位を築いた"Model 2"や、かのGoF本に書かれた技術的なロマン溢れるMVCなどの、原典であるSmalltalkのMVCからかけ離れた亜種がSmalltalkのMVCより圧倒的に有名になったことが直接の原因だと思って間違い無さそうである。そのように多種多様なMVCが生まれる原因には、それらのいずれもがMVCを誤解したものだと言われないことがあると思うし、それは、MVCに正解が存在しないからであろう。

Smalltalkの開発中に考案されたMVC理論は、複雑すぎて人々に理解されず、そして、SmalltalkのMVCは現代の進化したGUIを持つアプリケーションにそのままでは使えなくなったので、MVCの原典を繙く人が増えなかったのだと思う。
さらに、ソフトウェアの規模が大きくなったのに合わせてMVCに色々と改良が加えられても、人々が納得する決定版が現れないので、人々のMVCの理解がばらつくのを止められないのだと思う。

人々は、ある理論を理解することを放棄する時、それが全てではないでしょ、という言い訳をする傾向がある。ソフトウェア設計者は、既存の設計が理解できない時、もっとシンプルにできるはずだと言い訳して放棄する傾向がある。ことこれほど広まっているMVCパターンに限って、理解できないという言い訳は敗北と考えて、せめて割り切ってMVCと接することができるように理解に努めてきたが、筆者は、MVCの理論にも適用可能範囲にも絶対的なものは無い、つまりMVCとして絶対不可侵な範囲も無く、こういうアプリケーションに有効と言える対象も無く、ましてやどんなアプリケーションでもMVCパターンに従うべきだということは無いと確信するに至った。

MVCパターンは、ただ闇雲に何が何でもMVCにすれば良いものではない。理論を知らなくても、忠実に守っていれば何かご利益があるような便利な知識ではなく、オブジェクト指向と同様、単なる設計理論の1つであり、設計思想の追求の仕方、設計の突き詰め方のヒントを与えてくれる程度のものである。

[6]より:

Patterns are architectural heuristics. Understanding them should widen your solution spectrum, not narrow it. They provide you with proven ways of solving a specific problem in programming. The reality is that you will rarely encounter that specific problem in the wild, but you will encounter variants of that problem

デザインパターンを理解すると問題解決能力が上がるべきであり、逆にそれに従うことによって問題解決手段の選択肢を狭めるものであるべきでない、という意味だと思う。
筆者は、デザインパターンは将棋の定跡のようなものだと思う。実戦において、定跡と全く同じ局面になることは滅多に無いので、ただ手順を覚えるだけではあまり役に立たないが、似たような局面になることはよくあり、定跡手順の意味を理解していると、大きなヒントになる。
同じ手でも、定跡を知らずに指すのと、定跡手順を知った上で外すのとでは、試行錯誤の質が大きく異なり、その後の勝率に差が出る。定跡を知らなくても、終盤で逆転するとか、力づくで勝ち続けることは可能だが、限界がある。終盤は法則化、体系化できないので、勝率を上げるには終盤力、つまり計算速度を上げることしか無くなる。
同様に、ソフトウェア設計も、コーディング能力に頼るのでは限界がある。それは個人の資質にも依存する職人芸であり、何よりも、習得方法、指導方法の確立が難しいので、伝達及び維持が困難である。この業界は、技術は盗むもの、とか言っていて間に合う世界ではない。だからこそ、アルゴリズムや設計理論やデザインパターンを知ることに価値があるのである。

[6]のコメント欄より:

I only like to talk about MVC now when there is something resembling Smalltalk MVC triads at work and controllers are required to move messages between triads. Otherwise I think the GOF book got it right to not include MVC as a pattern.

When people say they are using MVC I take that to mean they see their application as being organized somehow into three parts that can be worked on separately:

1. data and business logic,
2. presentations of that data, and
3. some sort of event management and propagation system.

I don't really think MVC means much more than that anymore. Consequently its value in discussing real-world designs has become suspect.

[3]のコメント欄より:

One of my main issues with the whole framework/mvc debate is that it has been my experience that one size can not fit all.

どんな規模の開発にも有効なフレームワークやデザインパターンは存在しない、と言い換えても良いと思う。

[3]のコメント欄より:

The most important part of an abstraction is not what patterns it strictly adheres to, but what it provides the programmer in terms of power, maintainability and ease of use.

… and back to the discussion on MVC: my ‘tool’ abstraction encapsulates user input processing, drawing transparent scratch information (for example to display a selection rectangle) and model object manipulation. Is the tool a controller or a view? Could it even be a model (at the application level as opposed to the document level)? The answer: who cares. These labels we use are for architectural communication and education, not as rules that must be adhered to regardless of the problem domain.

A more important question than “do your developers implement design patterns?” is “do your developers *understand* design patterns and spend the time creating, analysing and discussing abstractions that may enhance the design of your application?”

問題の抽象化の目的は、作り易さ、メンテナンス性、扱い易さなどの実際の成果であり、デザインパターンに厳密に従うことではない、と書かれている。
また、デザインパターンを使っているかどうかでなく、デザインパターンを理解して使っているかどうかが大事な問題だ、とも書かれている。


参考文献
[1] MVC As Anti-Pattern
 MVCパターンは、リッチなクライアントを持つWebシステムには適さない、という主旨で、よくまとめられている。MVCパターンの限界を考える出発点として最適だと思う。
[2] A Better Understanding Of MVC (Model-View-Controller) Thanks To Steven Neiland
 MVCにきちんと従うためのわかりやすい指針というか、格言のようなものが考え出されている。
[3] MVC considered harmful
 本文には大したことが書かれていないが、コメント欄の議論は非常に深く、参考になる。
[4] Best Practice Software Engineering - Model View Controller
 簡単なMVCの紹介。1ヶ所、上記の引用部分の記述が非常に鋭いと感じたので、挙げておく。
[5] GUI Architectures
 その筋では有名な、Martin Fowler氏による、Webアプリケーションのフレームワークの進化の歴史が書かれている。必読。
[6] On Design Patterns and Reality
 一般論が多いが、なかなか深く鋭い話で、一読に値すると思う。

2013年02月03日

Safariで複数のURLを一度に開く方法

MacのSafariで、テキスト表示されている50個くらいの写真のURLを、全て一発で開きたいと思ったが、適当な方法がすぐにわからなかった。

筆者はWindows XPではLunascapeを愛用しており、Lunascapeでは、メニューバーにある「クリップボードで開く」を実行すると、クリップボードに複数のURLが含まれていれば全て抽出して開いてくれるので、造作もないことである。

URLの先が全てJPEGファイル等の画像ファイルであれば、それら全てを<img>タグで表示するHTMLを作るという手があるが、今回はURLの先が全てHTMLファイルであった。

そういえば、Internet Explorerならどうするんだろう、と探してみたら、わからなかった。通常のWebブラウザーでは無理なのか…と思ったが、しつこく探してみたら、いい方法が見つかった。

1. メニューバーから「ブックマーク」→「全てのブックマークを表示」
2. 「ブックマークフォルダを追加」
3. URLを含むテキストをコピー
4. 追加したブックマークフォルダを開いて(フォルダーアイコンの横の三角が下向きの状態にして)ペースト
→ テキストに含まれる全てのURLがフォルダーに追加される
5. フォルダーのサブメニュー(右クリックやトラックパッドに指2本タッチ+クリック) から「すべてをタブで開く」

続きを読む "Safariで複数のURLを一度に開く方法" »

2013年02月21日

GLUTでOpenGLしてみた

Java3Dには何の不満も無いのだが、Mac OS Xには初めからGLUTが入っていることを知ったので、ちょっと、OpenGLも試しておくことにした。

・ソースコード
polygon_sphere_test1.c

・コンパイル方法の例
Mac OS X 10.7.5で成功したコマンド

gcc -framework OpenGL -framework GLUT polygon_sphere_test1.c

FreeBSD 7.3 + libGL7.4.4 + libglut-7.4.4 で成功したコマンド

gcc -g polygon_sphere_test1.c `pkg-config --cflags --libs glut`

・実行画面

球面の1/8を均等な三角形に分ける方法で球のポリゴンを作成したものである。
マウスでドラッグすると球がその方向に回転し、ボタンを離すと逆方向に少し回転して止まる。
Sキーで、ワイヤーフレーム表示と塗り潰し表示が切り替わる。

回転軸の合成と、ポリゴンの頂点座標の算出に用いる球面線形補間(Slerp: Spherical linear interpolation)には、クォータニオンを使っている。クォータニオンの演算やSlerp等については、参考になる多数のwebページがあるので、説明を省く。GLUTやGLUにはクォータニオンに関する関数が見つからなかったので、適当に作った。特殊なことはしていないと思う。

ドラッグによる回転は、クリックした位置に半径1のトラックボールが接しているような感じに、トラックボールの中心からクリック位置へのベクトル(x,y,-1)とドラッグ先のベクトル(x',y',-1)との外積を回転軸として行っている。カーソル位置に表示されている球面上の座標を求めるのと比べるとかなり手抜きな方法であるが、まあまあそれらしく動いているので良しとする。

描画の度に全ポリゴンの頂点座標を再計算してVertex3dコマンドで転送しており、とても効率が悪い。VAOやVBOを使って頂点データを保存しておくか、せめてGenLists, NewList(GL_COMPILE)を使ってコンパイルしておきたかったが、1つのシンプルな実装例として、一旦公開することにした。

続きを読む "GLUTでOpenGLしてみた" »

2013年02月25日

OpenGLのVAOを使ってみた

前回のコードは、一連のコマンドのコンパイルもせず、vertex arrayやbuffer objectも使わずに、描画の度に毎回大量の命令を行っており、大変非効率なので、vertex arrayを使って少しマシにする。

・ソースコード
polygon_sphere_test2.c

・コンパイル方法
前のエントリー参照

・実行画面

※今回は処理負荷軽減が主旨であり、ポリゴンの各頂点を使い回すため、各頂点の法線ベクトルは頂点座標と同じ値にしているので、表面が前回よりツルツルになっている。

Vertex array初体験の筆者にとっては、vertex arrayとbuffer objectとvertex array object(VAO)の関係がややこしい。今週理解した限りでは、おそらく、次のような感じだと思う。


Vertex array
頂点座標データを含む配列。"Vertex"と言いながら、頂点の法線ベクトルや頂点の色情報やテクスチャーの座標データなど、いくつかの決められた種類のデータをvertex arrayにすることができる。データの種類と、種類によっては1要素当たりのバイト数を指定して使う。

Buffer object
H/Wが直接アクセス可能なメモリ領域に置くことができるオブジェクト。任意のデータを含めることができる。Vertex arrayを入れることもできる。
Vertex buffer object(VBO)はvertex arrayを入れたbuffer objectのこと。

Vertex array object(VAO)
各vertex arrayの実データの位置(buffer object内でも良い)と状態を保存するオブジェクト。これにより、複数のvertex arrayとbuffer objectを一発で切り替えることが可能になる。OpenGL 3.0以降で標準サポートされているが、それ以前のバージョンの環境やOpenGL ES 1.1の環境でも拡張I/Fとしてサポートされていることが多い。

今回、一旦VAOやVBOを使わずにglVertexPointer()等のvertex arrayのI/Fだけを使ってglVertex3d()やglNormal3d()の嵐を解消したが、いかにも中途半端な感じになった。負荷軽減の目的ならVBOを使わないと意味が無く、VAOが使えるならその方がすっきりするので、結局、頂点座標と法線ベクトルをVBOにしてVAOにまとめた。

glDrawElements()を使うには頂点のインデックスの配列(element array)(vertex arrayの文脈ではindex arrayは色番号の配列のことなので別物) が必要で、どうも頂点のインデックスの配列はVAOに登録できないようなので、glDrawElements()を使うにはglBindVertexArray()するのとは別に、 glBindBuffer()で頂点のインデックスの配列を有効にすることが必要になるようだ(上記ソースコードのdrawSphere()参照)。
なので、座標も法線も全く同じ頂点を使い回さないなら、glDrawElements()でなくglDrawArrays()を使う方がすっきりすると思ったが、連続平面ならポリゴンの各頂点を3回使うのが普通だろうと思い直して、やっぱりVAO+element arrayのbuffer objectという構成にした。

続きを読む "OpenGLのVAOを使ってみた" »

2013年03月06日

VAOでテキスチャーも貼ってみた

続いて、vertex arrayにテキスチャーの頂点座標も含めてみる。

・ソースコード
polygon_sphere_test3.c

・コンパイル方法
前のエントリー参照

・実行画面

例によって、ドラッグで回転する。

前回は頂点座標の配列と法線ベクトルの配列を別にして、それぞれのVBOを作るような形にした(法線ベクトルは不要になったのでコメントアウトした)が、今回は頂点座標と法線ベクトルとテキスチャー座標を1つの配列にまとめて、VBOも1つにした。つまり、

struct {
 GLfloat vertex[3];
 GLfloat normal[3];
 GLfloat texCoord[2];
};
こういう型の配列である。こうすると、それぞれを別の配列にするのと比べ、少なくとも1つの頂点の処理中は頂点座標も法線ベクトルもテキスチャー座標も同じキャッシュラインに乗る確率が上がるので、CPUやGPUのメモリキャッシュの効率が上がると言われている。

続きを読む "VAOでテキスチャーも貼ってみた" »

2013年04月03日

Mesa+GLUTでもシェーダーを使ってみた

GLUTを使うことによって移植性を確保しながらOpenGLのプログラム作りの練習を進める中、VMWare+FreeBSD+Mesa 7.4.4の環境でもなんとかOpenGLのシェーダーが動くことがわかったので、続いて、シェーダーを使って鏡面反射っぽいことをしてみる。

・ソースコード
polygon_sphere_test4.c

・コンパイル方法
前のエントリー参照

・実行画面

ちょっとわかりづらいが、画面中央にある球に、周囲の景色が映り込んでいるつもりである。今回の環境マッピングはcube mappingで行っているので、景色の背景画像は巨大な立方体の面の内側に貼られている感じになっている。

背景画像を変えたりもしてみたが、やっぱりわかりづらい。

床が格子模様、天井が同心円模様、側壁が波模様のつもりである。
きっと、意味不明な絵だからであろう。

絵心が無い癖に、プログラムで背景を生成しようと思ったのが間違いだった。

・操作方法
画面上をドラッグすると、背景が回転し、球への映り込みも同時に変化する。
'c'を押すと、背景画像が切り替わる。


GLSLによるshaderのコードの作成には、「Win32APIによるOpenGL 3Dプログラミング 」(工学社)という本を多分に参考にした。
Vertex shaderのコード(initShader()内のvertexShaderSource[])にmatrixが増えたので、後で読む為にそれぞれの意味をメモしておく。

modelViewMatrix
オブジェクト座標系の各頂点の法線ベクトルを回転させて視点座標系にするためのModel-View変換行列
参考:OpenGL 2.1 Spec.のFigure2.6
viewTransposeMatrix
視点が原点から離れている分、平行移動させる行列
視点から物体の頂点までのベクトルを得るのにこれを使用し、そのベクトルを法線ベクトルで反射させて、環境マッピングのベクトルを計算している
modelViewProjectionMatrix
Z軸方向の必要な範囲を[-1,1]に収めるための、いわゆるProjection Matrix
glFrustrum()で得られる行列をそのままshaderに引き渡すのに使用している
environmentRotateMatrix
環境を回転させる行列
環境マッピングの参照ベクトルを回転させるのに使用している

Shaderの記述に用いるプログラミング言語であるGLSL(OpenGL Shading Language)のバージョンは、1.5以降と1.2とではかなり異なり、1.2は時代遅れのようである(参考書籍では4.0)が、筆者の手持ちのMacOSX 10.7+GLUTの環境では1.2しか使えないようであるため、今回はGLSL 1.2で記述している。それでも、今回のようにtextureCube()でcube mappingはできるし、反射ベクトルを求めるreflect()や屈折ベクトルを求めるrefract()も使えるので、十分に使い勝手があると思った。

前回のVAOに続き、今回も、glUseProgram()等のOpenGL 2.x標準のshader APIをOpenGL 1.3相当のMesaで動かす為に、glutGetProcAddress()による拡張機能のリンクを使いまくって、何とかOpenGL 2.1用のコードをそのまま動かすことに成功した。
しかし、それぞれの拡張APIとOpenGL 2.x以降の標準APIとの違いや、拡張APIの中でも、名前の後ろに"ARB"が付いているAPIと"EXT"が付いているAPIとの違いとかは全く調べていない。ただ、試行錯誤したら動いたというだけである。
1つ分かったのは、glUseProgramをglUseProgramObjectARBで代用しているが、

glUseProgram(0);
(shader program不使用への切り替え)のつもりで
glUseProgramObjectARB(0);
とすると異常動作するようである。幸い、今回はglUseProgram(0)しなくて済んだので、MesaではglUseProgram(0)を無処理に置き換えて回避した。

一応、Mesaのソフトウェアレンダリング環境でも動作したが、グラフィックハードウェアでユーザープログラムを処理する為の仕組みであるshaderをソフトウェアで処理するだけあって、極端に遅い上に、表示が汚い。上の画像はMac OS X上の実行結果であるが、同じMacでVirtualBox+FreeBSD上で実行すると、次のようになる。

ソフトウェアレンダリング環境でも動くようにシェーダーを作るというのはあまり意味が無いと思った。

続きを読む "Mesa+GLUTでもシェーダーを使ってみた" »

2013年05月06日

AndroidでOpenGLしてみた

GW直前に運良くAndroid端末を借りることができたので、このGWは以前作ったOpenGLのテストアプリをAndroidに移植するのに費やした。

・実行画面(シミュレーター)

ドラッグして離すと、反対方向に回る。

・Android用実行ファイル
AndroidPolygonSphereTest.apk

・ソースコード
AndroidPolygonSphereTest.tgz
Android SDKの"ApiDemos"(Eclipseで"Android Sample Project"を作成すると選択できる)の中のGLSurfaceViewを使用したサンプルコードに毛を生やした程度のもの。
Activityがどういう状態になったらどうすべきか等のAndroid特有の制御は全く理解していない。
見よう見まねで書いてみたら動いた、というだけのものである。

・コンパイル方法
Eclipse+Android SDKで"Android Application Project"を"Blank activity"付きで作成して*.javaを上書きして"Run As"->"Android Application"でbin/に.apkファイルができる。

続きを読む "AndroidでOpenGLしてみた" »

2013年09月02日

[OpenGL] 平面への投影変換で影を付けてみる

OpenGLでオブジェクトに影を付けようとして、最初に思い付いた方法は、地面に投影する変換行列を作って、同じオブジェクトを黒で描画することだったが、それが意外と難しかった。

そこで調べてみたが、オブジェクトへの影の付け方には色々な方法があるようで、影オブジェクトを別に作成する方法を除くと、大体次の3通りが多いようだ。
・変換行列による平面への投影
・シャドウボリューム法
・シャドウマッピング法
正確に説明できる自信が無いので、説明は省く。とりあえずわかったのは、簡単に実装できる方法は無いということだった。
平面上にできる影であれば、やはり行列による投影変換で描画するのが最も簡単なので、引き続き、納得できるまで取り組むことにした。

・結果
object drawn with shadowobject drawn with shadowobject drawn with shadow
ソースコード

光源の位置をL:(Lx,Ly,Lz,1)とすると、任意の3次元座標を、法線(a,b,c)が光源側を向く平面ax+by+cz+d=0に投影する行列は、次のような行列であることは、色々な所に書かれている。
shadow matrix
一応、これを筆者なりに求めてみる。

L=(Lx Ly Lz Lw)T : 光源
P=(x y z 1)T : 物体の位置
P'=(x' y' z' 1)T : 平面 ax+by+cz+d=0 に投影されたPの位置
N=(a b c d)T : 平面のパラメーター、但しNT L > 0(法線(a,b,c)が光源側を向くことより)

とし、P' = M P を満たす行列 M を求めることを考える。
P'はLとPを含む直線上にあるので、スカラー媒介変数 t を用いて
P' = L + t(P-L) --- (1)
と表せる。P'は平面上にあるので、
NTP' = 0
成り立つ。これより、
NT P' = NT L + t NT (P-L) = 0
なので、t = - NTL / (NT(P-L)) とわかる。NTL > 0なので、
t = NT L / (NT(L-P))
としておく。このtを(1)に代入し、両辺をNT(L-P)倍すると、
NT(L-P) P' = NT(L-P)L + NTLP - NTLL
= NTL P - NTP L
= NTL P - L NTP ※(NTPはスカラーなのでこのように入替可能)
= (NTL - LNT) P
となる。ここで、P'の4つ目の要素をscale factorだとすると、P'とそれをスカラーk倍したk P'は同じ位置を表すので、P'とNT(L-P) P'も同じ位置を表している。従って、Mは NT(L-P) P' = M P を満たすMでも良いので、Iを単位行列として
M = (NTL I - LNT)、つまり
shadow matrix
と求まる。


この行列をmodel view matrixに掛け合わせて、描画した地面に重ねて影を描画すると、影にノイズが出たり、全く影が表示されなかったりすることがある。
object drawn with shadow
少しでも影を浮かせると正常に表示されるが、全く同じ位置に重ね書きしようとすると、depth値の丸め誤差の都合で、所々に地面より向こうだと判断されてしまうフラグメントが発生するのが原因である。(Zテストで落とされるので、ブレンディング描画にしても解決しない。)
地面と全てのオブジェクトの影を先に描くことが可能なら、depth testを無効化して影を描画することも考えられるが、こういう時はpolygon offsetを設定するのがOpenGLの定跡であるので、今回もそのようにした。

glPolygonOffset(-1, -1);
glEnable(GL_POLYGON_OFFSET_FILL);
PolygonOffset()の引数の意味は難解なのだが、大抵の場合、少し向こう側ということにするなら(+1,+1)、少し手前ということにするなら(-1,-1)とすれば良いとされている。
このソースコードでは、手前に見える(地面に隠されない)はずの影を描画するための設定なので、(-1,-1)としているが、逆に地面に対してPolygonOffset(+1,+1)としても良い。


影を真っ黒で描画してしまうと、環境光で多少は照らされるはずの地面が全く見えなくなって不自然なので、影は半透明の黒で、つまり多少は地面が透けて見えるように描画したい。
その為には、αブレンディング描画をすれば良い。

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
これだけでオーソドックスなαブレンディングが有効になる。


影をαブレンディング描画をすると、平面に投影したポリゴンが重なると、影が2重になってしまう。
object drawn with shadow
このオブジェクトはいくつかのパーツを重ねて描画しているので、パーツ毎の影が重なるとこのようになる。
物体が半透明でない限りは、これは不自然である。これを防ぐには、ステンシルバッファを使うのが最善だと思う。
OpenGLのステンシルはマスクのような役割をし、ステンシルテストをパスしたフラグメント(viewport上のピクセル)を描画しながら、描画した位置のステンシル値を更新することができる。
例えば、このようにする。

glEnable(GL_STENCIL_TEST);
glClear(GL_STENCIL_BUFFER_BIT);
glStencilFunc(GL_NOTEQUAL, 1, 1);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
描画をステンシル値が0のピクセルに限りながら、描画したピクセルのステンシル値を1に置き換えることにより、どのピクセルも2回描かれないようにするものである。


無限に広がる平面でなく、有限の地面を描画する場合、影が地面をはみ出すと不自然である。影が地面をはみ出さないようにするには、地面が四角形であればOpenGLのclip planeを無理矢理使う方法も考えられるが、今回はこれもステンシルテストで解決した。

glEnable(GL_STENCIL_TEST);
glClear(GL_STENCIL_BUFFER_BIT);
glStencilFunc(GL_ALWAYS, 1, 1);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
/* 地面の描画 */

glStencilFunc(GL_EQUAL, 1, ~0);
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
/* 影の描画 */
glDisable(GL_STENCIL_TEST);

地面を描く時にステンシル値を1に置換し、影の描画をステンシル値が1のピクセルに限りながら、影を描画したピクセルのステンシル値を+1することにより、地面がある場所のみ描画するのと影が2重に描かれないのを実現している。


なお、影を描画する際にはdepth maskによってdepth bufferへの書き込みを停止するのが、OpenGLにおける影描画の定跡である。

glDepthMask(GL_FALSE);
これによって、depth buffer上で影の位置が地面より少し盛り上がるのを避けることができる。
そのためにも、上記のpolygon offsetの件は、地面をPolygonOffset(+1,+1)して描画するより、影をPolygonOffset(-1,-1)して描画する方が適切だと思う。

続きを読む "[OpenGL] 平面への投影変換で影を付けてみる" »

2013年09月22日

AndroidでOpenGLしてみた(2)

またしても運良くAndroidタブレットを借りることができたので、この間のOpenGLのテストアプリをAndroidに移植してみた。

・実行画面(シミュレーター)

・Android用実行ファイル
AndroidTestP1.apk

・ソースコード
AndroidTestP1src.tar.gz

筆者はAndroidに疎く、見よう見まねで何とか動かしたというだけのものである。
以下、今回苦労した点を記録する。

・ADT付属のemulatorではOpenGLの動作が完全には再現されない
最も時間を取られたのは、ポリゴンを重ねて描画した時のシミュレーターの動作だった。前回のエントリーに書いた影行列を設定してオブジェクトを描画すると、上の画像でもそうなのだが、影が所々にしか塗られなかったり、汚いまだら模様になったりする。
色々試した結果、DEPTH_TESTが有効で、2つのポリゴンの面が重なるように近くにあると正しく動かないようであることがわかった。基本的にはdepth値の精度に起因する誤差による不具合だと思うが、表面と裏面を重ねてCULL_FACEを有効にしても表面が裏面に隠されて何も描画されない部分が出てくるし、色もおかしくなるなることがある((R,G,B)=(1,0.5,0)のオレンジのポリゴン表裏2枚をゆっくり重ねると、だんだん黄色になることを確認した)。
ふと、実機ではどうなるのだろう、と思って実機で動かしてみると、何の問題もなく影が表示されて、力が抜けた。
Android SDKで動作がおかしかったら、まずは実機の動作を確認することだと思い知った。

・glMaterialfv()の引数はGL_FRONT_AND_BACKでないと反映されない
GL10#glMaterialfv()の引数のfaceにGL10.GL_FRONTを指定すると、ambient colorやdiffuse colorの設定が反映されなかった(シミュレーター、実機共)。まさかそれが誤りだとは思わなかったので、そのことが原因だと特定するのに時間がかかった。何なんだろうこれは。
って、OpenGL ESの仕様なんだな。glGetError()すればGL_INVALID_ENUMが返されるのですぐにわかるのだが、glGetError()の値をこまめに見る習慣が無かったのが致命的な敗因だった。

・GLSurfaceViewのデフォルト設定ではステンシルバッファが存在しない
そのこと自体は簡単に試してみればすぐわかることであり、android.opengl.GLSurfaceViewのreferenceにも

By default GLSurfaceView chooses a EGLConfig that has an RGB_888 pixel format, with at least a 16-bit depth buffer and no stencil.
とあるので、setEGLConfigChooser()すればいいこともすぐにわかる。
そこで、
setEGLConfigChooser(8, 8, 8, 8, 16, 8);
とすると、実機ではステンシルテストが動くようになるが、シミュレーターではステンシルバッファがサポートされていないので、
Unfortunately, AndroidTestP1 has stopped.
と出てアプリがクラッシュする。シミュレーターでは、ステンシルバッファ付きのEGLConfigが得られないことが原因である。その場合はステンシルバッファ無しのEGLConfigで継続実行するようにしようと思ったが、setEGLConfigChooserの呼び出しをtry/catchで括ってみても、ここですぐにeglChooseConfig()によるEGLConfigの検索が行われる訳ではないので、ここでは例外が発生しない。setEGLConfigChooser()はsetRenderer()より前に行わないといけないとなっているので、実際に例外が発生してから引数を変えてsetEGLConfigChooser()をやり直す訳にもいかない。
実機で動作すればそれで良い、という人も居るかも知れないが、シミュレーターで全く動作確認できなくなるのは不便である。何より、筆者は普段Androidの実機を所有しておらず、基本的にシミュレーターしか使わないのである。
すると、EGL10に依存するGLSurfaceView.EGLConfigChooserインターフェースを、ステンシルバッファを有効にするためだけに実装しないといけないことになる。
…という結論に達するまでに、結構時間が掛かった。
GLUTなら前回のエントリーに書いたようにglutInitDisplayMode()の引数にGLUT_STENCILを加えれば済む所、GLSurfaceViewだとEGLを呼び出すコードを自分で用意しないといけないとは、面倒である。自由度が高いことに、実用的なメリットはあると思うが、すっきりしない。

・GL10クラスにglGetMaterialfvメソッドが無かった
COLOR_MATERIALを有効にするとambientやdiffuseが変化してしまうので、後で戻す為に現在のambientやdiffuseの値を取得しようとしたら、glGetMaterialfvメソッドが無かった。GL11クラスにはあったが、GL10のインスタンスからGL11のインスタンスを取得する方法が見つからず、ネットで検索するまで、単に(GL11)でキャストすれば良いとはわからなかった。

Javaでは、AWTのGraphicsクラスのインスタンスを(Graphics2D)でキャストしてGraphics2Dのインスタンスを得るようなことがよくあるが、クラスの継承関係からするとGraphics2DがGraphicsのサブクラス、GL10とGL11についてはGL11 implements GL10であり、いずれもサブクラスへのダウンキャストなのだが、そうするのが正しいことは何を見ればわかるのだろうか?(一般にスーパークラスへのキャストは可能だがサブクラスへのキャストは可能とは限らない)

・例外発生時の解析手段がわからない
試行錯誤中に誤ってnull参照するコードを埋め込んでしまった時、デバッガで例外をキャッチして停止させても、どこでnullアクセスしたのかさっぱりわからず、原因コードを特定するのにすごく時間を取られた。EclipseのDebugモードでは、
(Suspended (exception NullPointerException))

(Suspended (exception RuntimeException))
と出るだけである。画面上で"Unfortunately, AndroidTestP1 has stopped."と出るだけより1つは情報が加わるが、それでピンと来ることは困難であり、結局、Log.dやSystem.out.printlnやtry/catchを使いまくって原因箇所を見つけるのでは、何の為にデバッガを使ってるのかわからない。
まさかAndroid SDKはそういうものなのだとは思わないが、printfデバッグでもすぐ見つかるだろうと思っていたので、Android SDKでのまともな解析手段を探す気力は起きなかった。しかし、結局数時間調べる羽目になって、後悔すると共に、二度とAndroidでOpenGLしたくないと、二度くらいは思ったと思う。

・視点を動かしたので、トラックボール風の回転動作を変更する必要があった
今回、ついカメラ位置を自動的にオブジェクト中心に回転するようにしてしまったが、そのせいで、前回Android用に作成した、タッチドラッグ(スライド?)による回転動作がそのままでは使えなくなり、それを視点の回転に対応させる方法にかなり悩んでしまった。

画面上のドラッグ操作をトラックボールの回転操作のように扱うのは、下の図の手前の矩形のように、画面が球面上の(x,y,z)=(0,0,1)の位置(手前)に、画面の上方向がY軸が上になるように接しており、画面上のドラッグはその球面上をなぞっているものとして、ドラッグ開始位置と終了位置からトラックボールの回転軸と回転角を計算することによって実現していた。
SVG image
これは視点がZ軸上にある前提によって成り立っているものであり、視点が変わると、上の図の左上の矩形のように、画面がそちらに張り付いているものとして計算しないといけない。

最初は、一方の画面の法線をもう一方の画面の法線に移す回転軸と回転角度(クォータニオン)を求めて、ドラッグの開始終了位置から求めた回転軸と回転角度にそれを掛け合わせるか何かして回転を合成すれば良いのではないかと思ったが、どうしても2つの画面の縦軸同士、横軸同士を対応付ける方法がわからなかった。
ドラッグの回転軸を移すだけでは、画面右方向へのドラッグが画面右方向へのドラッグとして保たれるとは限らないので、少なくとも、画面間の法線を移す回転軸と回転角度の他に、法線を移した後に、画面の上が画面の上に対応するように画面上で回転させる角度が必要なのである。
この文章を書いている途中に、またちょっと考えればできそうな気がしてきたが、ドラッグ回転軸を求めてからの変換はクォータニオンベースでできないと意味が無いこともあるので、またドツボにはまる気がする。もしもう一度頭の中がクォータニオンまみれになることあったりしたらついでに考えてみることにする。

今回は結局、ドラッグの開始位置と終了位置をもう一方の画面に移動してから、ドラッグの回転軸を求めるようにした。上を+Y方向とする画面上の点(x,y,1)を、上をup=(upx,upyupz)、手前をeye=(eyex,eyey,eyez)とするもう一方の画面上の(x',y',z')に移動させる変換行列Mは、(1,0,0)がupとeyeの外積、(0,1,0)がup、(0,0,1)がeyeに変換されることから、
[up×eye up eye]
だとわかる。行列の積やsin()/cos()も無いので計算量が少ないし、シンプルでわかり易いので、実用的にはこの方法が最適なのかも知れない。

・意外と遅くなった
前回パソコンで描画したのと同じポリゴン数では、極端に遅くなった。PCでは15fpsで動かしてもCPU負荷は3%程度なのだが、Androidタブレットでは3fpsくらいしか出なかった。今回使用したAndroidタブレットのCPUやGPUは不明だが、ベンチマークテストではNexus OneよりCPU性能も3D性能も遥かに上回る機種である。Androidタブレットは、画面の見た目がパソコンのようでも、やっぱりパソコンとは処理速度が2桁くらい違うのだろうか。

影を付けるのをやめると速くなるので、ライティングによって影に着色してるのが重いのかと思ったが、ライティングを無効にして影描画してもあまり変わらなかった。
カリングを有効にしたり、depth testを有効にしたり無効にしたりしても体感的には変わらなかった。つまり、GPUによるポリゴンの着色処理に時間が掛かっているのではなく、depth testや表裏判定に至る前の処理に時間が掛かっているのだと思う。

ポリゴン数を増減すると見た目にはっきりと速くなったり遅くなったりするので、ポリゴン数に依存する処理に時間が掛かっていることはわかった。従って、例えばシーングラフを作っている為に、1回の描画で何度もDrawElementsしていることは問題ではなさそうである。

OpenGL ES 1.1ではvertex arrayが使えないので、glVertexPointerやglNormalPointerを使って頻繁にvertex bufferを切り替えているが、このやり方がまずくて不必要に遅くなっている可能性が高い。現在のコードはGL10クラスを使ったサンプルコードを流用しているが、GL10クラスにはBindBufferもBufferDataも無い(GL11やGLES11にはある)ので、vertex bufferのデータがGPUに転送されておらず、描画する度に全頂点のデータがGPUに転送されている気がするのである。

続きを読む "AndroidでOpenGLしてみた(2)" »

2014年01月02日

長方形を任意の四角形に変換する行列

この前、長方形の画像を、任意の4頂点からなる凸四角形に変形して表示したくなった。

長方形を、2x2の行列による一次変換や、それに平行移動を加えた3x3のアフィン変換で、任意の凸四角形に移すことは不可能である。これらの一次変換では、長方形を平行四辺形に変換することはできるが、台形には変換できないからである。

しかし、変換行列を任意の3x3行列とし、変換後のベクトルの3番目の要素を拡大係数として斉次座標系を構成する、アフィン変換を拡張したものとも言える、射影変換(perspective transform)なら、それは可能である。

可能なはずなのだが、Webを検索しても、具体的な変換行列の数式が意外と見つからない。筆者の検索方法が悪いからだとは思うが、どうにも見つけられなかったので、自分で計算してみた。

0≦x≦W, 0≦y≦Hの領域にある長方形を0≦x≦1, 0≦y≦1の正方形に移す射影行列は自明(diag(1/W,1/H,1)である)なので、(0,0),(1,0),(0,1),(1,1)の4点を(Px,Py),(Qx,Qy),(Rx,Ry),(Sx,Sy)に移す射影変換を考える。

(≡は列ベクトルが斉次座標(homogeneous coordinate)として等価であることを表す)を満たすsx〜w2があるかどうかを考える。

上の合同式を展開する。

射影変換の斉次座標の定義より、(x y w)T≡(x/w y/w 1)Tなので、上の合同式は

という等式にできる。つまり、

という連立方程式であり、これを解くと、

と求まる。(力づくで解くなら、tx,tyの次にw0,w1をまとめて求めると良い。)(一見複雑だが、分母は全て同じ)
従って、w2=SxQy-SyQx+QxRy-QyRx+RxSy-RySxとすると、(0,0),(W,0),(0,H),(H,H)の4点を(Px,Py),(Qx,Qy),(Rx,Ry),(Sx,Sy)に移す射影変換は、

である。

■テストプログラム
Javaアプレットの起動用のページ
ソースコード
・使い方
 四角形の頂点の赤い丸をドラッグすると、イメージがそれに合わせて変形します。
 四角形に凹みができる(鈍角の内角ができる)と、正しく動作しません。

続きを読む "長方形を任意の四角形に変換する行列" »

2014年04月25日

今更iPhone3Gで自作アプリを動かしてみる

昨年9月に、とある方から、とあるお祝いの品として、使わなくなったiPhone3Gを譲り受けた。
最初から電話として使うつもりは全く無く、iPhoneという物を触ってみたいと思ったのと、丁度、Androidの実機で自作アプリを動かしていた頃で、iPhoneでも自作アプリを動かしてみたいと思ってお願いしたものだ。

入手後すぐ、意気揚々と「すべてのコンテンツと設定を消去」してiOSを最新の4.2.1にバージョンアップすると、ロック画面で「アクティベーションが必要です」か何かのメッセージが出て、何もできなくなってしまった。
数日後にアクティベーション専用SIMというものを手に入れて、起動はできたが、Safariを使ってても全体的に動作が遅いし、App Storeで色々なアプリをインストールしようとしても、「このアプリケーションはこのiPhoneとは互換性がありません」とか「このアプリケーションにはiOS X.Xが必要です」とかと表示されてほとんど失敗し、1週間もするとiPhoneに触るのが億劫になり、同時にiPhone3Gは古すぎて自作アプリを開発する上で条件が悪いことも知って億劫になり、その後はiPodの代わりとして使うだけになっていた。

iPhone3Gとその次機種であるiPhone3GS以降とでは、アプリ開発をする上で大きく異なる点がいくつかある。
・iPhone3Gは、インストールできるiOSのバージョンが4.2.1まで
 それに対してiPhone3GSならiOS 6もインストールできる
・3GはOpenGLES 1.1のみ、3GSなら2.0も使用可能
・3GはARMv6アーキテクチャー、3GSはARMv7アーキテクチャー
従って、折角ならiPhone3GS以降でアプリ開発を始めてみたかったのである。

その後、新しいiPhoneを買うかどうかを迷っていたのだが、年が明けて孔子の論語に記載される所の不惑となる年齢に達したことにより、やっぱり今使っているガラケーに何の不満も無い自分にはこれが使える限り金のかかるスマートフォンは不要、と惑わなくなり、当分iPhoneを買うことは無いと確定したので、もらい物のiPhone3Gで自作アプリを動かすことにした。

●開発環境入手
iPhoneの開発環境はXcodeであるが、3GはARMv6アーキテクチャーなので、Xcodeのバージョンに制約がある。Xcode 4.5でARMv6がサポートされなくなったので、Xcode 4.4.1がARMv6用の最新である(*1)

古いバージョンのXcodeは、https://developer.apple.com/xcode/downloads/の"Additional Tools"の所の"View downloads"というリンクの先のリストからダウンロードできる(Apple IDが必要)。

●iPhone3G用の"Hello, world!"アプリの作成
Xcode 4.4.1にはiPhoneシミュレーターも同梱されているので、シミュレーターで動作するiPhoneアプリならすぐに着手できる。

  1. XcodeのメニューバーからFile→New→Projectとする。
  2. "Choose a template"の画面で、iOS Applicationの"Single View Application"を選ぶ。
    ※Xcode 4.4.1のiOS Applicationのtemplateの内、iOS 4.2.1でも動くのは、"Single View Application"と"Tabbed Application"の2つだけのようである。"OpenGL Game"はiOS 4.2.1にGLKitが無いので動作せず、それ以外はiOS 4.2でStoryboardがサポートされていないのでbuildできない。
  3. "Choose options"の画面では、"Use Storyboards"のチェックは外す。
  4. プロジェクトが作成されたら、左上のソースコードツリーの"ViewController.xib"を開く。
  5. InterfaceBuilder(GUI編集画面)が開くので、"Label"とか"Round Rect Button"とかがある所から"Label"をドラッグし、iPhoneの画面の適当な所にドロップする。
  6. ドロップした"Label"をダブルクリックし、"Hello, world!"に変える。
  7. "Run"を押す
→ シミュレーターが開き、"Hello world!"が表示されるはず。

※4.〜6.について、InterfaceBuilderを使うのでなく、真面目に何らかのソースコードを書く場合は、以下の2つの参考記事のそれぞれの4ページ目辺りを参照されたい。
参考記事:
iOS SDKで始めるObjective-C入門(2):iOSアプリ開発初心者に捧ぐ開発環境Xcodeの概要とインストール - @IT
SDKで始めるiPad/iPhoneアプリ開発の勘所(1):いまさら聞けないiPhone/iPadアプリの作り方の基礎 - @IT

●実機へのインストールについて
さて、これが問題である。

正しくは、"Apple Developer"に登録(無料)した上、"iPhone Developer Program"に登録(有料:$99/year)する必要があり、その上で長大な手順が続く。
参考:ここが大変だよiPhone開発(4):ここが大変だよiPhone実機テスト+iPhone OS 3.0の新機能 - @IT

無料で実機に自作アプリをインストールするには、いわゆるJailBreakを行う以外に、手段が全く無いようである。

今回、特にアプリを配布するつもりが無く、試しに簡単なプログラムを動かしてみたい程度で、しかも時代遅れの機種に、1万円を払うのは厳しい。
このiPhoneは、特に使っておらず、筆者にとっては自作アプリを動かせなければ無用である。JailBreakすることによって再起不能になっても問題無い。
ただ、iPhoneのJailbreakingは法に触れるかどうかがグレーであり、それ故に実施するかどうか悩んだのだが、日本における判例が無いことと、著作権法第20条の2に、プログラムを動かす為のプログラムの改変は同一性保持権の侵害に当たらないとある(*2)ことと、Appleが異議を申し立てていた、アメリカの著作権局が発行したDMCAのJailbreakingに関する見直し条項が、その後の2012年10月22日の改正でも、合法的に入手したプログラムを動作させる為の「携帯無線電話」のプログラムの改変は禁止されない、と維持された(*3)ことから、自作アプリをiPhoneで動作させる目的に限ってのJailbreakingは、2015年10月28日までは違法性を問われる確率が極めて低いと判断し、実施に踏み切った。

以下は、JailBreakingが済んでいることと、iPhoneにsshがインストールされている前提の手順である。

  1. Xcodeの左側のツリー表示の一番上のプロジェクトのアイコンを選び、プロジェクト設定の画面にする。
  2. "PROJECT"と"TARGETS"がある列の、"TARGETS"の所のアイコンを選ぶ。
  3. "Build Settings"の"Architectures"を、"Standard (armv7)"から"armv6"に書き換える(*5)
    編集可能な列が2列あるが、右側の列を操作すると左側の列も連動するので、右側の列を操作すると良い。
  4. 同じく"Build Settings"の"Code Signing"の所を全て"Don't Code Sign"にする。
  5. 下の方にある"Validate Settings"を押して、修正させる。
  6. ツールバーにある"iPhone 5.1 Simulator"を"iOS Device"に変える。
  7. メニューバーから"Product"→"Build"する。(*7)
  8. ツリー表示のProductsの下のXXX.appを右クリックし、"Show in Finder"を選ぶ
  9. ターミナルを開き、
    codesign -fs "[適当な証明書の名前]" [パス]
    とする。[パス]の所は、8.で開いたFinderからdrag&dropする。
  10. iPhoneに転送する。
    1つの方法としては、ターミナルから
    scp -r [パス] root@[iPhoneのIPアドレス]:/Applications/
    とする(*4)
  11. iPhoneを"Respring"する。
    1つの方法としては、sshでiPhoneにログインし、
    killall -HUP SpringBoard
    とする。

●トラブルシューティング
正規の手順だと、iPhoneをPCにUSB接続していると、実機のアプリをデバッガで実行できるのだが、上記の手順だとそれができないので、クラッシュログとデバッグコンソールに頼ることになる(*6)

シミュレーター上では動作しても、実機ではハングアップすることは頻繁にある(*4)
アプリ起動するとハングアップして勝手に閉じてしまう場合、クラッシュログが残っているかどうかを確認する。

  1. iPhone3GをPCにUSB接続する。
  2. "Window"→"Organizer"を開く。
  3. "iPhone3G"の"Device Logs"を開く。

または、MacのiTunesでiPhoneを同期すると、~/Library/Logs/CrashReporter/の中にコピーされるようだが、筆者の経験上、同期してもなかなかコピーされないことがあったので、お勧めしない。

コードが思い通りに動かない場合は、printfデバッグを行う。

  1. コードに、
    NSLog(@"[フォーマット文字列]"[, 引数1, 引数2, ...]);
    を加える。[フォーマット文字列]には、"%d"など、Cのprintfと同じ書式が使えるようである。
  2. "Window"→"Organizer"を開く。
  3. "iPhone3G"の"Console"を開く。
  4. iPhoneでアプリを実行する。

●免責事項
本記事を参考にしたことにより生じたいかなる損害に関しても、筆者は一切の責任を負わないものとする。

続きを読む "今更iPhone3Gで自作アプリを動かしてみる" »

2014年05月04日

今更iPhoneでOpenGLES1.1を動かしてみる

iPhoneでもOpenGLを動かしてみたいと思って、知人から入手したiPhone3Gであるが、3Gというのが曲者だった。

iPhone3Gは、OpenGLアプリを作成するのにいくつかの制約がある。
・OpenGLES2.0が使えない
 OpenGLES1.1しか使えない。
・GLKitが使えない
 GLKitはiOS5以降に含まれているが、iPhone3GはiOS4.2.1までしかバージョンアップできない為。
・ステンシルバッファが使えない

それに対し、iPhone3Gの1つ次の3GSは、OpenGLES2.0が動く。これは、3GS以降のGPUは全てPowerVRのSGXシリーズであるのに対し、3GはPowerVRの"MBX Lite 3D"であることが関係しているようだ。
また、3GSにはiOS5がインストールできるので、そうすればGLKitが使える。
そして、3GSはステンシルバッファが使える。

iPhone3GはARMv6アーキテクチャーである為、Xcode 4.4.1を使用しているが、これに含まれる、iOS Applicationの"OpenGL Game"というテンプレートは、付属のiPhoneシミュレーターでは動いたので、簡単にiPhone3GでOpenGLを始められそうと思ったが、このテンプレートはGLKitを使用しているため、iPhone3Gでは動かなかった。
しかも、このテンプレートはOpenGLES2.0にも対応しているため、冗長である。

その為、GLKitに依存していないOpenGLES1.1用のテンプレートをインターネット上で探した。その結果、以下を発見した。
(1) Xcode 3.xのOpenGL ES Application テンプレート
 OpenGLES2.0も使うようになっているので、シミュレーターでは表示されるがiPhone3Gの実機では表示されないオブジェクト(物体)がある。
 但し、depth bufferは用意されていないらしい。
 また、Xcode 4では開けない。
(2) OpenGL ES 道場(1) - こじ研(携帯メディア) のGLBaseプロジェクト
 Xcode 3.xのテンプレートをベースに、depth bufferが追加されており、多少OpenGLES1.1向けに整理されている。但し、多少GLES2.0用の残骸が残っている。
(3) jlamarche/iOS-OpenGLES-Stuff · GitHub紹介記事)のSimple OpenGL ES 1.1 example
 OpenGLES1.1のサンプルプロジェクト。下記(4)のテンプレートを使用したプロジェクトと類似している。但し、Xcode 4.4.1で確認する限り、そのままではシミュレーターでも動かない。
(4) jlamarche/iOS-OpenGLES-Stuff · GitHub紹介記事)のOpenGL ES 1.1 Project Template
 OpenGLES1.1に特化したテンプレート。Quaternionを扱うためのマクロや、gluLookAt()や、アセンブラで書かれた行列積のマクロが用意されており、専門的な香りがする。
 但し、Xcode 3用のテンプレートであり、Xcode 4以降では開けない。

これらの内、(1)は使用するメリットがほとんど無いので、(2)-(4)について、実際にiPhone3Gで動作するまでに行ったことを記録する。

■(2)-(4)共通の、iPhone3Gのための手順
・"TARGETS"の"Build Settings"の"Architectures"を、"Standard (armv7)"から"armv6"に書き換える
・(推奨)同じく"Build Settings"の"LLVM GCC 4.2 - Code Generation"の所の"Optimization Level"を、"None [-O0]"以外にする
("None [-O0]"にしていると、整数型の割り算や剰余が、libgccの___divsi3や___modsi3を使うコードになることがあり、実機で"Symbol not found: ___divsi3"等のエラーになってハングアップする)

■(2) GLBaseプロジェクトの使用手順
特に何もいじらなくても、そのまま動く。
GLBaseViewController.mのdrawFrameを書き換えれば、OpenGLのコードで好きな描画をさせることができる。

なお、ディレクトリーツリーのルートの"GLBase"をrenameすれば、ディレクトリー内の"GLBase"を含むファイル名を一斉にrenameできる。

■(3) Simple OpenGL ES 1.1 exampleの使用手順
・"git clone https://github.com/jlamarche/iOS-OpenGLES-Stuff.git"等として取得
・(推奨)Build Settingsの画面で"Validate Settings"を実行し、修正を許可
・TARGETSのBuild SettingsのBuild OptionsのCompiler for C/C++/Objective-Cを"LLVM GCC 4.2"に変更(Default Compilerだと実機用のコンパイルがエラーになる)
・そのままではPart6ProjectAppDelegate.applicationDidFinishLaunchingが呼ばれないため、以下のパッチを適用、または同様に変更

--- Original/iOS-OpenGLES-Stuff/Simple OpenGL ES 1.1 example/Classes/Part6ProjectAppDelegate.m
+++ tmp/Simple OpenGL ES 1.1 example/Classes/Part6ProjectAppDelegate.m
@@ -23,6 +23,9 @@
GLViewController *theController = [[GLViewController alloc] init];
self.controller = theController;
[theController release];
+
+ self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
+ self.window.rootViewController = self.controller;

GLView *glView = [[GLView alloc] initWithFrame:rect];
[window addSubview:glView];
--- Original/iOS-OpenGLES-Stuff/Simple OpenGL ES 1.1 example/main.m
+++ tmp/Simple OpenGL ES 1.1 example/main.m
@@ -8,11 +8,12 @@


#import
+#import "Part6ProjectAppDelegate.h"

int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
- UIApplicationMain(argc, argv, nil, nil);
+ UIApplicationMain(argc, argv, nil, NSStringFromClass([Part6ProjectAppDelegate class]));
[pool release];
return 0;
}

これで、カラフルな12面体が回転する。
GLViewController.mののdrawFrameを書き換えれば、OpenGLのコードで好きな描画をさせることができる。

■(4) OpenGL ES 1.1 Project Templateの使用手順
これは残念ながらXcode3用のテンプレートであり、Xcode4では使えない。Xcode3とXcode4のテンプレートは互換性が無い上、Xcode4用に書き換えるのも困難なのである。
参考URL:XCode4のプロジェクトテンプレートが作れない! - とっくりばー
Xcode3でこのテンプレートを使ってプロジェクトを作成するのが1つの方法だが、ファイル名や一部文字列を置換するだけなので、そういうスクリプトを作っても良いし、上記URLに載せられているRubyスクリプトでもプロジェクトファイルのコピー&置換ができる。

1. 上記URLのReplacer.rbを使う等により、テンプレートからプロジェクトを作成
2. なぜかGLView.mの場所が間違っているので、Classes/に移動する
3. (推奨)Build Settingsの画面で"Validate Settings"を実行し、修正を許可
4. (推奨)TARGETSのBuild SettingsのBuild OptionsのCompiler for C/C++/Objective-Cを"LLVM GCC 4.2"に変更(Default CompilerだとOpenGLCommon.hのアセンブラを用いたマクロがコンパイルエラーになる)

これで、GLViewController.mののdrawFrameに何かを書けば、OpenGLのコードで好きな描画をさせることができる。
何も書かないと画面が真っ暗になるので、試しに何か表示するなら、drawFrameをiOS-OpenGLES-Stuff/Simple OpenGL ES 1.1 example/Classes/GLViewController.mのものに置き換えてみても良いし、画面が暗い青になるだけで良ければ、1行目の"glColor4f"を"glClearColor"に書き換えても良い。

続きを読む "今更iPhoneでOpenGLES1.1を動かしてみる" »

2014年05月18日

iOSのInterfaceBuilderを使ってみる

先日作成したiPhone3GのOpenGLアプリに、簡単なUIを付け加えたので、行った手順を記録する。

●Setupボタンを追加してみる

  1. MainWindow.xibを開く(*1)
    → InterfaceBuilderの画面が開く。
  2. Toolbarを貼り付ける

    → Bar Button Itemが1つ勝手に付けられる
  3. "Item"を何かに変える
    "Item"を選択し、Attributeの画面で、IdentifierをCustomから何かに変えると、いくつかの既定のアイコンや文字列が選べる。
    残念ながら「設定」っぽいアイコンが無いので、今回は"Edit"にしてみた。

    Titleを"Setup"に変えることもできたが、省略した。
  4. コールバックメソッドを宣言する
    GLViewController.hに
    - (IBAction)setup:(UIBarButtonItem *)sender;
    という宣言を追加する。(*2)(*3)
    UIBarButtonItemというクラス名は、Identity Inspectorで調べることができる。
  5. UIとメソッドを関連付ける
    MainWindow.xibを開き、"View Controller"(GLViewControllerクラスに対応している)を選択し、Connections inspectorのsetupの右の○印(outletと呼ばれる)をUIエディター上の"Edit"までドラッグ

    または、UIエディター上で"Edit"を選択し、Ctrlを押しながら"View Controller"までドラッグ
    ドラッグしたら、○印の中に黒丸が付くことを確認する。
  6. コールバックメソッドを定義(実装)する
    とりあえず、ボタンが押されたらログ出力するよう、GLViewController.mに
    - (IBAction)setup:(UIBarButtonItem *)sender
    {
        NSLog(@"GLViewController.setup called.");
    }
    を追加する。

以上でシミュレーター上で実行し、ボタンが押されたらログが出ることを確認した。

(*1)今回使用したテンプレートに、ViewControllerに対応するXIBファイル(GLViewController.xib)が無いので、今回はMainWindowにボタンを追加した。

(*2)GLViewControllerクラスに置くのが最善かは不明。 MainWindow.xibのOwnerはUIApplicationクラスなので、XxxAppDelegateクラスに置くことも考えられるが、IBのコールバックメソッドはUIViewControllerクラスに置かれることが多いので、GLViewControllerクラスを選んだ。

(*3)NIB File(XIB File)のAction Methodは全て

- (IBAction)setup:(id)sender;
この形らしい。(詳しくはHelpで"IB action methods"を検索、idはObjective-Cの基底クラス)

●スピード調整バーを追加してみる

上記の手順で作ったEditボタンの右側が淋しいので、Toolbarの余白に

こういうのを入れてみる。

  1. MainWindow.xibを開き、LabelとSliderとText Fieldを追加

    LabelはToolbarのItemにはならず、Labelを無視して左詰めされてしまうので、Flexible Space Bar Button Itemを置いてスペースを確保する。
    Labelの文字列は"Interval"にし、Sliderの値の範囲は1〜100(フレーム間隔が1ms〜100msの意味)に変更する。
  2. GLViewController.hにメンバー変数とコールバックメソッドの宣言追加
    メンバー変数は、プログラムで変化させる部品について作成する。
    @interface GLViewController : UIViewController <GLViewDelegate>
    {
        IBOutlet UITextField* textField;
        IBOutlet UISlider *slider;
    }
    - (IBAction)slided:(UISlider *)sender;
    - (IBAction)setup:(UIBarButtonItem *)sender;
    
  3. UIとメンバー変数、メソッドを関連付ける
    MainWindow.xibを開き、"View Controller"を選択し、textField, sliderのoutletから部品へドラッグして接続
    slidedのoutletをSliderにドラッグし、Value Changedを選択して接続
  4. GLViewController.mにメソッド定義追加
    - (IBAction)slided:(UISlider *)sender
    {
        /* スライダーに連動してテキストボックスの文字列更新する */
        NSString *text = [[NSString alloc]initWithFormat:@"%d ms", (int)sender.value];
        [textField setText:text];
        [text release];
        /* アニメーション速度を変える */
       [(GLView *)self.view setAnimationInterval:sender.value/1000];
    }
    
    /* 起動時にスライダーの位置を初期値に設定する */
    -(void)viewDidLoad
    {
        [super viewDidLoad];
    
        float animationIntervalInMsec = [(GLView *)self.view animationInterval] * 1000;
        [slider setValue:animationIntervalInMsec];
        [self slided:slider]; //テキストボックス初期化のため、slidedメソッドを呼び出し
    }
    

以上で、スライドバーでアニメーションのスピード調整ができることを、シミュレーターで確認した。

●別画面でSetup画面を追加してみる

3Dオブジェクトの複数のバラメーターを操作するための画面を作成してみる。

  1. SetupViewControllerクラスを追加
    XcodeのメニューバーからFile → New → File
    テンプレート選択画面では、iOSの"Objective-C class"を選択
    Option選択画面では、"With XIB for user interface"にチェック
  2. SetupViewController.xibを開いて、UI部品一式を追加
  3. SetupViewController.hにメンバー変数とメソッド宣言追加
    @interface SetupViewController : UIViewController
    {
        IBOutlet UISlider* bodyGranularitySlider;
        IBOutlet UILabel* bodyGranularityLabel;
        IBOutlet UISwitch* textureSwitch;
        IBOutlet UISlider* flipperGranularitySlider;
        IBOutlet UILabel* flipperGranularityLabel;
    }
    - (IBAction)bodyGranularitySlide:(UISlider *)sender;
    - (IBAction)textureSwitched:(UISwitch *)sender;
    - (IBAction)flipperGranularitySlide:(UISlider *)sender;
    
    - (IBAction)backToMain:(UIBarButtonItem *)sender;
    @end
    
  4. UIとメンバー変数、メソッドを関連付ける
  5. 3Dオブジェクトのバラメーター変更の為のメソッド追加
    P2Object.hに以下を追加
    + (void)reinitialize;
    
    /* クラス変数へのアクセサー */
    + (bool) useTexture;
    + (void) useTexture :(bool)value;
    + (int) bodyGranularity;
    + (void) bodyGranularity :(int)value;
    + (int) flipperGranularity;
    + (void) flipperGranularity :(int)value;
    
    P2Object.mに以下を追加
    /* クラス変数 */
    static bool useTexture = true;
    static bool drawingShadow = false;
    static int bodyGranularity = 18;
    static int flipperGranularity = 10;
    
    +(void)reinitialize
    {
        /* パラメーターの変化をオブジェクトに反映させる */
        (コードは省略)
    }
    
    + (bool) useTexture
    {
        return useTexture;
    }
    + (void) useTexture :(bool)value
    {
        useTexture = value;
    }
    + (int) bodyGranularity
    {
        return bodyGranularity;
    }
    + (void) bodyGranularity :(int)value
    {
        bodyGranularity = value;
    }
    + (int) flipperGranularity
    {
        return flipperGranularity;
    }
    + (void) flipperGranularity :(int)value
    {
        flipperGranularity = value;
    }
    
    Objective-Cには、クラスメソッドは存在するが、クラス変数という概念が存在しないので、Cのコードをそのまま使う等の理由でインスタンスを生成せずにクラスメソッドで全てを実装している場合は、このように、状態を保存する変数を.mファイル内で静的変数にして、クラスメソッドとしてアクセサーを用意するしか無さそうである。
  6. SetupViewのコールバックメソッドの定義追加
    SetupViewController.mに以下を追加する。
    #import "P2Object.h"
    
    - (IBAction)bodyGranularitySlide:(UISlider *)sender
    {
        /* スライドバーの数値を右側のラベルに反映させる */
        NSString *text = [[NSString alloc]initWithFormat:@"%d", (int)sender.value];
        [bodyGranularityLabel setText:text];
        [text release];
        /* スライドバーの数値をオブジェクトのパラメーターに反映させる */
        [P2Object bodyGranularity:sender.value];
    }
    - (IBAction)textureSwitched:(UISwitch *)sender;
    {
        /* ユーザー操作結果をオブジェクトのパラメーターに反映させる */
        [P2Object useTexture:sender.on];
    }
    - (IBAction)flipperGranularitySlide:(UISlider *)sender
    {
        /* スライドバーの数値を右側のラベルに反映させる */
        NSString *text = [[NSString alloc]initWithFormat:@"%d", (int)sender.value];
        [flipperGranularityLabel setText:text];
        [text release];
        /* スライドバーの数値をオブジェクトのパラメーターに反映させる */
        [P2Object flipperGranularity:sender.value];
    }
    
    SetupViewController.mのviewDidLoadメソッドに、以下の、スライドバーやスイッチの値をオブジェクトのパラメーターに初期化するコードを追加する。
        [bodyGranularitySlider setValue:[P2Object bodyGranularity]];
        [flipperGranularitySlider setValue:[P2Object flipperGranularity]];
        [textureSwitch setOn:[P2Object useTexture]];
        /* スライドバーの右のラベルを更新する */
        [self bodyGranularitySlide:bodyGranularitySlider];
        [self flipperGranularitySlide:flipperGranularitySlider];
    
  7. 画面遷移処理追加
    MainWindowの"Edit"ボタンが押されたらSetupViewが開くよう、GLViewController.mのsetupメソッドを以下のように変える。
    #import "SetupViewController.h"
    
    - (IBAction)setup:(UIBarButtonItem *)sender
    {
        SetupViewController *setupView = [[SetupViewController alloc] initWithNibName:@"SetupViewController" bundle:nil];
        [(GLView *)self.view stopAnimation];
        [self presentModalViewController:setupView animated:YES];
        [setupView release];
    }
    
    presentViewControllerでなくpresentModalViewControllerを使っているのは、筆者の端末がiPhone3Gである都合で、iOS5以降でないからである。
    SetupViewの"Done"ボタンが押されたらMainWindowに戻るよう、"Done"ボタンのコールバックメソッドを次のようにする。
    - (IBAction)backToMain:(id)sender;
    {
        [self dismissModalViewControllerAnimated:YES];
        [P2Object reinitialize];
    }
    
    "Edit"ボタン押下時にGLViewController.setupでアニメーションを停止しているので、SetupViewが閉じたらアニメーションを再開するコードを追加する。幸い、MainWindowに戻ったらGLViewController.viewWillAppearが呼ばれるので、そこに追加するのが良さそうである。
    -(void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
        [(GLView *)self.view startAnimation];
    }
    

●結果

・プロジェクトファイル一式
UsingTheTemplateAndIB.tar.gz

・追加したSetup画面の操作でオブジェクトが変化する様子
Use textureをONにすると→ 腹の白の輪郭がきれい

Use textureをOFFにすると→ Granularity(ポリゴンの細かさ)が50でも汚い

●参考文献

iPhone SDK: Interface Builder Basic Training
筆者は、このページの通りにやってみるだけで、InterfaceBuilderの使い方がほとんどわからない状態から、色々なUI部品を使うことができるようになった。とても良いチュートリアルであった。
XcodeのHelp
リファレンスだけあって、ある程度の知識が無いと理解できない情報が出てくる場合が多いが、何を検索しても何か見つかるし、何よりもサンプルコードが充実しているので、すごく助かる。

続きを読む "iOSのInterfaceBuilderを使ってみる" »

2014年11月29日

VMWareでのFreeBSD 9.3のX Window環境セットアップ

6月くらいに、JIRAをFreeBSDで動かしてみようと思って、以前と同じ要領で、当時、バージョン番号の末尾が0のものを除いた最新のFreeBSD 9.2をVMWare 5.5にインストールしたが、rxvtで日本語が表示されなくて、苦労することがあった。
結局、それは解決したのだが、その頃にはFreeBSD 9.3が出ており、折角なのでそれをセットアップしようと思い、sysinstallコマンドからPackagesをインストールしようとすると、"Unable to get packages/INDEX file from selected media."というエラーになり、右往左往してしまった。FreeBSD 9.3/10.0からpackagesの管理システムが"pkgng"に変わったことに関係しているらしい。
筆者は近年、FreeBSDの新たなバージョンをインストールする度に、必ず何かにつまずいている。最近の動向にキャッチアップする努力をせずに過去の環境に固執しているのが原因であり、色々情報を漁ればもっと便利な環境が手に入るとは思うのだが、今回も、とりあえず必要だった最低限の情報をまとめる。

●VMWareにてFreeBSD 9.3のX Window立ち上げまでにやったこと

  1. FreeBSD 9.3をインストール後、コマンドラインからパッケージをインストール
    pkg install open-vm-tools xf86-video-vmware xf86-input-vmmouse xorg-minimal
  2. /etc/rc.confに以下を追加して再起動
    hald_enable="YES"
    dbus_enable="YES"
    vmware_guest_vmblock_enable="YES"
    vmware_guest_vmhgfs_enable="YES"
    vmware_guest_vmmemctl_enable="YES"
    vmware_guest_vmxnet_enable="YES"
    vmware_guestd_enable="YES"
    ※再起動しなくても
    foreach s (hald dbus vmware-kmod vmware-guestd)
    service $s start
    end
    とすれば良いのだが、起動時にそれらが自動的に起動されることを確認するために再起動した。
  3. /usr/local/etc/hal/fdi/policy/x11-input.fdi を以下の内容で作成し、service hald restart
    <?xml version="1.0" encoding="ISO-8859-1"?>
    <deviceinfo version="0.2">
     <device>
      <match key="info.capabilities" contains="input.keyboard">
       <merge key="input.x11_options.XkbRules" type="string">xorg</merge>
       <merge key="input.x11_options.XkbModel" type="string">jp106</merge>
       <merge key="input.x11_options.XkbLayout" type="string">jp</merge>
       <merge key="input.x11_options.XkbOptions" type="strlist">terminate:ctrl_alt_bksp</merge>
       <append key="input.x11_options.XkbOptions" type="strlist">ctrl:nocaps</append>
      </match>
     </device>
    </deviceinfo>
    
    参考:/usr/local/share/hal/fdi/policy/10osvendor/10-x11-input.fdi
    反映を確認するには、lshalを実行する
  4. /etc/X11/xorg.confの作成
    Xorg -configure
    /root/xorg.conf.newを開き、Depth 24の所に
    Modes "1024x768"
    を追加してデフォルトの解像度を変更し、
    mv /root/xorg.conf.new /etc/X11/xorg.conf
  5. /etc/hostsにhostnameを127.0.0.1として追加
    (無いと、xauthがbad display name "...:1"というエラーになる)
  6. テスト実行
    上の組み合わせではxtermやtwmがインストールされないので、いずれ必要になりそうな、代わりのものをインストールする
    pkg install rxvt-unicode
    pkg install open-motif # mwmを含む
    ~/.xinitrcを次の内容で作成し、startx
    nohup vmware-user-suid-wrapper 2>&1 > /dev/null &
    mwm &
    exec urxvt
  • 日本語キーボード対応になってること
  • マウスカーソルがシームレスに動くこと
  • Windowsとクリップボードを共有できること
  • Ctrl+Alt+Backspaceが効くこと
  • Caps Lockは効かないこと
を確認

なお、FreeBSD 9.2のPackagesでopen-vm-toolsをインストールするとvmware-toolboxコマンドがインストールされたのだが、9.3でpkgngでインストールすると、vmware-toolboxコマンドが見当たらない。
しかし、仮想ディスクのshrinkくらいにしか使ってないし、それも

vmware-toolbox-cmd disk shrink /
とすればできるので、それほど困らない。

なお、open-vm-toolsのファイル共有機能は、kernel panicになることが知られているが、筆者の環境でも確かにkernel panicになったので、諦めてSambaをインストールした。

【12/30追記】
この記事を書いた当時は、このようにfdiファイルを用意すれば日本語キーボード(jp106)の設定になっていたのだが、今日、pkg upgradeすると、これでは日本語キーボードにならなくなった。
原因はよくわからないが、とりあえず

setxkbmap -layout jp -model jp106
とすると日本語キーボードになるようなので、これを~/.xinitrcに書けば良さそうである。

●Xorgの日本語環境のセットアップ

まず、日本語フォントであるが、

pkg install ja-font-std
とすると、大体足りる。しかし、筆者はxlsfontsとやってcourierとかlucidaとかtimesとかが出て来ないと不安だったり、rk24の大ファンだったりするので、
pkg install xorg-fonts
ともする。

urxvt(rxvt-unicode)で日本語を表示するには、

setenv LANG ja_JP.UTF-8
又は、bashの場合や.xinitrcに書く場合は
export LANG=ja_JP.UTF-8
とした後に、urxvtを起動すれば良い。urxvt起動後に行うのは無効である。

urxvtのフォントを変更するには、引数-fnで指定するか、Xリソースにて指定する。
例えば、このようにする。

urxvt -fn rk24 &
XFTのフォント名も使用可能である。XFTのフォント名一覧はfc-listコマンドで取得できる。
urxvt -fn 'xft:terminal, xft:M+2VM+IPAG' &
urxvt -fn 'xft:IPAGothic:size=12' &
良いのが決まったら、Xリソースに登録する為、~/.Xdefaultsか、xrdbを使っている場合は~/.Xresourcesに書いておく。
urxvt.font: xft:IPAGothic:size=12
なお、こういう書式とか(並べされたフォントが順に検索される)、
urxvt.font: xft:terminal, xft:IPAGothic:size=14
こういう書式もある。
urxvt*font:\
  [codeset=ISO8859]x:-*-*-*,\
  [codeset=JISX0201]x:-*-*-*,\
  [codeset=JISX0208]x:-*-*-*
この辺りの書式は、man urxvt に書かれている。

本記事の冒頭で、rxvtで日本語表示ができないと悩んだのは、以前のFreeBSDにはja-rxvtというpackageがあり、それをインストールするとrxvtで日本語が表示できていたのだが、9.xではja-rxvtが無くなっており、それに気付かずに普通のrxvtを入れていた(元々rxvtは日本語表示ができない)のが原因だった。

なお、ktermは、-kmオプションやXリソースのkanjiModeでeucやsjisやutf-8を指定しないと、ロケールに関係なく、JISコードの日本語テキストしか表示されない。これまであまり意識してなかったのだが、今回はこれを忘れて、ど壷にはまってしまった。
rxvtでもktermでも日本語が表示されないので、フォントかロケールに問題があるのだと思い、あらぬ方向に莫大な時間を使い、無駄にしてしまった。
rxvt/urxvtで日本語が表示されて以来、ktermは必要なくなったが、nkfとかが無くてもShift-JISのテキストファイルが表示されたりするので、使い道はあるのかも知れない。

筆者は過去のある時点からkinput2の設定がうまくできなくなり、それ以降、over-the-topな日本語入力FEPを使わず、X Windowでの漢字変換は全てEmacs+anthy.elで行っていたのだが、今回、ibus-anthyというのを使ってみた。

pkg install ibus-anthy
Xを起動し、ibus-setupすると、次の環境変数を$HOME/.bashrcへ追記しろと出てくるが、bashは使ってないので、.xinitrcに追記する。
export GTK_IM_MODULE=ibus
export XMODIFIERS=@im=ibus
export QT_IM_MODULE=xim
ついでに、次の、ibus-daemonを起動するコマンド等も追記しておく。
export XIM=ibus
export XIM_PROGRAM="ibus-daemon"
export XIM_ARGS="-r --daemonize --xim"
${XIM_PROGRAM} ${XIM_ARGS}
Xを再起動し、再度ibus-setupを起動し、
  • キーボードショートカットにShift+spaceを追加
    (kinput2時代からの定番キーだから。デフォルトの<Super>spaceとはWindowsキー+spaceのことらしい)
  • 「インプットメソッド」として「日本語 - Anthy」と「英語 - English(international AltGr dead keys)」を追加
とすると、Shift+spaceで日本語入力モードと英語入力モードが切り替わるようになる。

さて、FreeBSD 9.2でインストールするとibus-1.4.1だったのだが、9.3ではibus-1.5.5になった。このibus-1.5というのが、すこぶる評判が悪いのだが、筆者も、非常に使いづらいと思った。
まず、上記の、日本語モードと英語モードが切り替え可能な状態にするのに、かなり手間取った。ibus-1.4だと「インプットメソッド」に「日本語 - Anthy」を足すだけで良かったのに、ibus-1.5だと、それだとショートカットキーを押しても英語入力モードに戻らなくなった。
さらに、日本語入力モードにした時に現れる入力バーがバグっている。ibus-1.4だと

こんな感じのバーだが、ibus-1.5だと

こうなる。しかも勝手に消えるので、今が日本語入力モードなのかどうかがわからなくなる。
設定方法を調べようという意欲が削がれるのに十分な悪印象である。
FreeBSD 9.2で使い始めて間もないが、もうibusを触るのが嫌になった。

本格的なデスクトップ環境が必要になるまでは、Emacs+anthy.elで頑張ることにする。

参考リンク 404 Blog Not Found:備忘録 - FreeBSD 10 あれこれ

続きを読む "VMWareでのFreeBSD 9.3のX Window環境セットアップ" »

2015年03月12日

[JIRA] 作成できる課題タイプをグループによって制限したい

JIRA 6.2で、同じプロジェクトで2種類のバグ票を運用したいと思った。
通常、JIRAのバグ票は、そのプロジェクトの関係者(のRole)をAdministrators, Developers, Usersと分ける時、Usersが起票し、Developersが処理する。それに加えて、今回は、Developers+α("group1"に所属するメンバーとする)だけが起票可能、閲覧可能な、開発者側の内部バグ票("IT Bug"とする)を、通常のバグ票とは別の課題タイプで運用したかった。
※結合テスト=Integration Testで発見された不具合、またはInternal Bugの略

特定の課題タイプを、特定のグループのメンバーだけが閲覧可能にするのも、直接的な設定方法が無く、容易ではないが、例えば、
  • Issue Security Schemeのセキュリティレベル定義を、default=Private(group1のみ閲覧可能)にする
  • Set Issue Security権限を課題作成可能者全員に与える
  • 画面にSecurity Levelフィールドを設ける
とすると、group1が開いた課題作成画面ではセキュリティレベルがPrivateになり、それ以外のメンバーが開いた課題作成画面ではセキュリティレベルがNoneになる(Noneは制限無し、設定権限があるのにNoneしか選べないからそうなる)ので、IT Bug以外は手操作でセキュリティレベルをNoneにすれば、何とか近いことが実現できる。
かなり強引で多少面倒だが、通常のバグ票を開発者が起票することがあまり無く、あっても非公開にしたいことがあり得るとすれば、現実的には妥当な解と言えなくはない。
(ちなみに、画面にSecurity Levelフィールドを設けなければ、全ての課題のセキュリティレベルがPrivateになる。これがNoneになるなら、IT Bugの作成画面だけにSecurity Levelフィールドを設ければ、IT Bug以外は自動的に全員閲覧可能にできるのだが、そうはならない。また、IT Bugのワークフローだけ、Create IssueトランジションのPost FunctionsにてSecurity Level=Noneにできれば良いが、JIRA 6.2にはその関数が無く、有料のプラグインをインストールしないとできない。)

しかし、特定の課題タイプは、特定のグループのメンバーだけが課題を作成可能にすることは、次の理由で、さらに難しい。

  • Create Issue権限を課題タイプ毎に別にできれば良いだけなのだが、Permission Schemeは、課題タイプ毎には設定できない。
  • 課題タイプ毎にworkflowは別にできるので、Create IssueトランジションのConditionsで制限すれば良いだけなのだが、なぜかCreate IssueトランジションにはConditionsが無い。
  • Create IssueトランジションにValidatorはあるが、グループではなく、何らかのPermissionしか指定できない(そのPermissionが無いというエラーになるので、Create Issue権限以外を指定すると意味不明なエラーになってしまう)し、作成ボタンを押した後にしかエラーにならない。
  • Workflow propertiesで制限できれば良いが、"(you can use in) Step"と書いてあり、実際に試してみたが、jira.permission.*はtransitionに対しては無効だった。Create Issueをする前には状態(Step)が無いので、jira.permission.*を設定する術が無い。
  • 同じプロジェクトで、課題タイプ毎に変えられる設定というのは、ワークフロー、画面、フィールド設定と、カスタムフィールドの初期値くらいしか見当たらない。

JIRA関連のドキュメントでもGoogleでも、"Permission per Issue Type"で検索して辿って行くと、大体、次の課題票に行き着く。
[JRA-5865] Allow permission schemes to be configured per issue type - Atlassian JIRA
約10年前に出され、多くの人に必要性を語られ、継続的に議論されてきた要望だが、JIRAを開発するAtlassian社によって、"Won't Fix"として昨年7月にcloseされている。
1つのプロジェクトで、課題タイプ毎に権限を変えるようなことはするな、という意味にしか取れない。
しかし、同じプロジェクトで、どの帳票も起票するメンバー、処理するメンバーが同じ、という制限では不便である。
例えば、発注者がバグ報告をする場合、開発者でない発注者が担当者になることは無いので、バグ票のAssigneeの権限は開発者に限定したいが、開発者が発注者に質問する場合、発注者が担当者になるので、Q&A票のAssigneeの権限は発注者に限定したい。
しかし、それだけのことすら、JIRAでは可能にす対応する予定が無いという。
プロジェクトを別にすれば良い、ということかも知れないが、課題タイプ毎にプロジェクトを別にするのでは課題タイプの意味が無いし、JIRAでプロジェクトを別に立ち上げるのは、バージョンやロールなど、共有できないので二重管理になるものもあるし、カスタムフィールドなど、プロジェクトに依存する設定もあり、結構面倒である。

帳票毎に権限を制約できないが為に、例えばAssignee権限を広くすると、その帳票の担当者になり得ないメンバーに間違ってassignしてしまう可能性があるし、一部の帳票は閲覧を制限するように課題にセキュリティレベルを設けていると、間違ってassignされた人がその課題を参照できず、課題が行方不明になってしまう。
今回やりたいことは、課題タイプが"IT Bug"の課題は、開発受注者のグループしか作成できないようにすることである。これも、帳票毎に権限を制約できないが為に、発注者も"IT Bug"を作成可能にすると、発注者にとっては、起票時に知らない課題タイプが選択肢に出て来るのがいまいちだし、間違って"IT Bug"で起票されてしまうと、いちいち開発者側で課題タイプを修正するのが面倒である。
それだけの為に、"IT Bug"を別プロジェクトにするのも、管理が面倒だし、見苦しい。

どうしても諦められなくて、上記の課題票JRA-5865のコメント欄を読むと、初期の頃から、JavaScriptでCreateボタンを消すことによる暫定対策が検討されており、貼られているJavaScriptを試してみると、JIRA 6.2でも部分的には動いたので、JavaScriptで何とかできないかを追究してみることにした。
なお、筆者にはJavaScriptの知識はほとんど無い。documentクラスでHTMLを操作する方法も、windowクラスでWebブラウザを制御する方法も知らない。AJS(Atlassian JavaScriptライブラリ) 頼みであり、AJSのドキュメントが無いのでGoogle頼みである。

■JIRAでのJavaScript使用の基本

フィールドのDescriptionにJavaScriptを書くと、そのDescriptionが表示される画面で実行される。
例えば、次のテキストをSummaryフィールド等のDescriptionに貼り付けると、課題作成画面を開く時に"JavaScript ran!"という警告ダイアログが出るようになる。

This description has some JavaScript.
<script language="JavaScript">
<!--
alert("JavaScript ran!");
//-->
</script>
Announcement Bannerに含めても実行されるが、scriptタグ以外に何も無くても、細いAnnoucement Bannerが表示されるようになってしまう。
参考:Fields Allowing Custom HTML or JavaScript - Atlassian Documentation
なお、 JIRA 6.2.x以降、これらのJavaScriptを埋め込む方法の一部は使えなくなる、と書いてあるが、JIRA 6.2.7では上の方法でJavaScriptが実行されることを確認した。

AJSを使用する場合は、実行されるタイミングが問題になるので、次の例の(function($) { ... })(AJS.$);の部分のように書くのが定跡のようである。

<script language="JavaScript">
(function($) {
  AJS.toInit(function(){
    //(A)
    alert("init on load: current user = " + AJS.params.loggedInUser);
  })

  JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function (e, context) {
    //(B)
    alert("init on refresh: current user = " + AJS.params.loggedInUser);
  });
})(AJS.$);

//(C)
alert("outside function: current user = " + AJS.params.loggedInUser);
</script>
これをSummaryフィールドのDescriptionに埋め込むと、次のことがわかる。
  • 通常の(ポップアップでない)課題作成画面が開く時に(A)が実行される。
  • 通常の課題作成画面が開く時は(C)の位置ではAJSが働かない。
  • ポッブアッブ形式の課題作成画面が開く時に(A)が実行される。
    その前に(B)が呼ばれることもある。(元のページによっては、例えばClosedでない課題画面は、開いた途端に(A)が呼ばれ、(B)のコールバックも登録されるので、Create Issueボタンを押してポップアップ課題画面が開いた途端に(B)が呼ばれる。)
  • ポッブアッブ形式の課題作成画面でプロジェクトや課題タイプを変えると(B)が呼ばれる。
なお、ポップアップ課題作成画面を開く度にコールバックが追加されるので、元の画面をそのままに、何度もポップアップ課題作成画面を開くと、開く度に(B)が何度も呼ばれるようになってしまうが、これは仕方が無さそうだ。

ポップアップ課題作成画面におけるJavaScript実行について

ポップアップ形式の課題作成画面は、開いた状態でプロジェクトや課題タイプが切り替えることが可能で、それらの切り替えに連動してフィールドが出現したりDescriptionが変化することがあるが、切り替えによって表示される新たなDescriptionのJavaScriptは実行されない。従って、ポップアップ課題作成画面で実行されるべきJavaScriptは、ポップアップ課題作成画面が開く時にどのプロジェクトのどの課題タイプの画面から始まっても、必ず実行されるようにする必要がある。
例えば、全てのフィールド設定において、いずれかのDescriptionにJavaScriptを含める方法や、Announcement Bannerに含める方法が考えられる。
今回は、あらゆるプロジェクトのあらゆる課題タイプでSummaryフィールドが表示されるものとして、全てのフィールド設定のSummaryフィールドのDescriptionにJavaScriptを埋め込んでテストした。

■作戦1

該当プロジェクトの、作成が制限された課題タイプなら、Createボタンをdisabledにする。

<script language="JavaScript">
<!--
function isUserInGroup(group){
  var groups;
    AJS.$.ajax({
    url: AJS.params.baseURL + "/rest/api/2/myself?expand=groups",
    type: 'GET',
    dataType: 'json',
    async: false,
    success: function(data) { groups = data.groups.items; }
  });
  for (i = 0; i < groups.length; i++){
    if (groups[i].name == group){ 
      return true;
    }
  }
  return false;
}

function disableCreate(value) {
  var inps = document.getElementsByTagName('INPUT');
  for (i = 0; i < inps.length; i++) {
    if (inps[i].type == 'submit') {
      inps[i].disabled = value;
    }
  }
}

function initCreateIssueScreen() {
  if (isUserInGroup('group1') == false) {
    var project = AJS.$("#issue-create-project-name").text();
    var issueType = AJS.$("#issue-create-issue-type").text();
    if (project == 'Test Project 1' && issueType == 'IT Bug') {
      disableCreate(true);
    }
  }
}

function initPopUpCreateIssueScreen() {
  if (isUserInGroup('group1') == false){
    var project = AJS.$("#project-field").val();
    var issueType = AJS.$("#issuetype-field").val();
    if (project == 'Test Project 1' && issueType == 'IT Bug') {
      disableCreate(true);
    } else {
      disableCreate(false);
    }
  }
}

(function($) {
  AJS.toInit(function(){
    if (location.pathname.indexOf("CreateIssue") != -1) {
      initCreateIssueScreen();
    }
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  })

  JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function (e, context) {
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  });
})(AJS.$);
//-->
</script>

スクリプトの説明

isUserInGroup()は、Webのどこかから拾ったものである。
disableCreate()は、JRA-5865のコメント欄にあるものを、display='none'(非表示)でなくdisabled=true(グレーアウトして無効化)にするよう変えたものである。Createボタン(type="submit"のINPUTタグ)を非表示にしても、いくつかのブラウザではSubmitのショートカットキー(ChromeやIEではAlt+S、FirefoxではAlt+Shift+S、SafariではCtrl+Shift+Sなど)は効いてしまうため、disabledにした。(ChromeとSafariは効く、FirefoxとIE8は効かない)
initCreateIssueScreen()は、通常の(ポップアップでない)課題作成画面のCreateボタンの処理である。ユーザーが'group1'に属さず、プロジェクトが'Test Project 1'で、課題タイプが'IT Bug'なら、Createボタンをdisabledにしている。
initPopUpCreateIssueScreen()は、ポップアップ形式の課題作成画面のCreateボタンの処理である。ユーザーが'group1'に属さず、プロジェクトが'Test Project 1'で、課題タイプが'IT Bug'なら、Createボタンをdisabledに、そうでなければ、enabledにしている。
AJS.toInit()の部分は、このスクリプトの中で最初に実行される処理である。課題作成画面が開く時にも実行される。通常の課題作成画面ならinitCreateIssueScreen()を、ポップアップ課題作成画面ならinitPopUpCreateIssueScreen()を呼び出している。
JIRA.bind()の部分は、同じページで画面が変化すると実行される処理である。現在のページがポップアップ課題作成画面なら、initPopUpCreateIssueScreen()を呼び出している。
なお、プロジェクトIDや課題タイプIDを取り出す方法がわからなかったので、プロジェクトや課題タイプは、IDではなく実際に表示される文字列を比較に使っている。これらの文字列は変更される可能性があるので、このやり方はあまり好ましくない。この方法だと、課題タイプは多言語訳の登録が可能なので、登録した全言語の文字列と比較するようなことも必要になるが、ここでは省略している。

結果

通常の課題作成画面では、作成が制限される条件であれば、うまくCreateボタンが無効化される。
ポップアップ形式の課題作成画面でも、開いた時はCreateボタンが無効化されるが、開いたままプロジェクトや課題タイプを切り替えると、disableCreate(true)が呼び出されても、Createボタンが有効になってしまう。(Safari, Firefox, Chrome, IE全て同様)
元々、ポップアップの課題作成画面では、プロジェクトや課題タイプを切り替えると、Createボタンが一時的に無効になって有効に戻るので、この有効にする処理が後から走ってしまうのだと推測される。
色々試したが(後述)、最終的にCreateボタンをdisabledにする適当な方法は見つからなかった。
但し、Createボタンを非表示にすると、その状態は維持されることがわかった。

■作戦2

通常の課題作成画面では、該当プロジェクトの、作成が制限された課題タイプなら、Createボタンをdisabledにする。
該当プロジェクトの、作成が制限された課題タイプでは、Createボタンをdisabledかつ非表示にする。

<script language="JavaScript">
<!--
function isUserInGroup(group){
  var groups;
    AJS.$.ajax({
    url: AJS.params.baseURL + "/rest/api/2/myself?expand=groups",
    type: 'GET',
    dataType: 'json',
    async: false,
    success: function(data) { groups = data.groups.items; }
  });
  for (i = 0; i < groups.length; i++){
    if (groups[i].name == group){ 
      return true;
    }
  }
  return false;
}

function disableCreate(value) {
  var inps = document.getElementsByTagName('INPUT');
  for (i = 0; i < inps.length; i++) {
    if (inps[i].type == 'submit') {
      inps[i].disabled = value;
    }
  }
}

function hideCreate(value) {
  var inps = document.getElementsByTagName('INPUT');
  for (i = 0; i < inps.length; i++) {
    if (inps[i].type == 'submit') {
      if (value == true) {
        inps[i].style.display = 'none';
      } else {
        inps[i].style.display = '';
      }
    }
  }
}

function initCreateIssueScreen() {
  if (isUserInGroup('group1') == false) {
    var project = AJS.$("#issue-create-project-name").text();
    var issueType = AJS.$("#issue-create-issue-type").text();
    if (project == 'Test Project 1' && issueType == 'IT Bug') {
      disableCreate(true);
    }
  }
}

function initPopUpCreateIssueScreen() {
  if (AJS.$("#create-issue-dialog").length) {
    if (isUserInGroup('group1') == false){
      var project = AJS.$("#project-field").val();
      var issueType = AJS.$("#issuetype-field").val();
      if (project == 'Test Project 1' && issueType == 'IT Bug') {
        disableCreate(true);
        hideCreate(true);
      } else {
        disableCreate(false);
        hideCreate(false);
      }
    }
  }
}

(function($) {
  AJS.toInit(function(){
    if (location.pathname.indexOf("CreateIssue") != -1) {
      initCreateIssueScreen();
    }
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  })

  JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function (e, context) {
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  });
})(AJS.$);
//-->
</script>

スクリプトの説明

作戦1のスクリプトのinitPopUpCreateIssueScreen()に、hideCreate()の呼び出しを加えた。。
hideCreate()は、Createボタンをdisplay='none'にする処理である。

結果

作戦1同様、通常の課題作成画面では、作成が制限される条件であれば、Createボタンが無効化される。
ポップアップ形式の課題作成画面では、意図通りに、作成が制限される条件ならCreateボタンが消え、そうでなければCreateボタンが出現する。画面を開いたままプロジェクトや課題タイプを切り替えると、条件に従ってCreateボタンが出たり消えたりする。
但し、開いた直後にCreateボタンが消えていればSubmitのショートカットキーも効かないが、開いた後にプロジェクトや課題タイプを切り替えると、Createボタンが消えても、作戦1と同様、ショートカットキーは効いてしまう。

■作戦3

通常の課題作成画面では、作成が制限された課題タイプなら、Createボタンをdisabledにする。
ポップアップ課題作成画面では、課題タイプの選択肢から、制限された課題タイプを削除する。
※プロジェクトを問わず、その課題タイプの使用を制限する。その理由は後述。

<script language="JavaScript">
<!--
function isUserInGroup(group){
  var groups;
  AJS.$.ajax({
    url: AJS.params.baseURL + "/rest/api/2/myself?expand=groups",
    type: 'GET',
    dataType: 'json',
    async: false,
    success: function(data) { groups = data.groups.items; }
  });
  for (i = 0; i < groups.length; i++){
    if (groups[i].name == group){ 
      return true;
    }
  }
  return false;
}

function disableCreate() {
  var inps = document.getElementsByTagName('INPUT');
  for (i = 0; i < inps.length; i++) {
    if (inps[i].type == 'submit') {
      inps[i].disabled = true;
    }
  }
}

function hideITBug(){
  var ops = AJS.$("#issuetype option");
  for (i = 0; i < ops.length; i++) {
    if(ops[i].text == 'IT Bug'){
      AJS.$("#" + ops[i].id).remove();
      return;
    }
  }
}

function initCreateIssueScreen() {
  if (isUserInGroup('group1') == false) {
    var issueType = AJS.$("#issue-create-issue-type").text();
    if (issueType == 'IT Bug') {
      disableCreate();
    }
  }
}

function initPopUpCreateIssueScreen() {
  if (AJS.$("#create-issue-dialog").length) {
    if (isUserInGroup('group1') == false){
      hideITBug();
    }
  }
}

(function($) {
  AJS.toInit(function(){
    if (location.pathname.indexOf("CreateIssue") != -1) {
      initCreateIssueScreen();
    }
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  })

  JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function (e, context) {
    if (AJS.$("#create-issue-dialog").length) {
      initPopUpCreateIssueScreen();
    }
  });
})(AJS.$);
//-->
</script>

スクリプトの説明

作戦1のスクリプトのinitPopUpCreateIssueScreen()は、hideITBug()を呼び出すように変えた。
hideITBug()は、課題タイプのプルダウンメニューから'IT Bug'のオプションを削除する処理である。
また、全体的にプロジェクト名の比較は無くした。
その結果、disableCreate()の引数は不要になったので、無くした。

補足説明

このようにして課題タイプの選択肢から'IT Bug'を削除しても、ポップアップ画面が開く時の初期値が'IT Bug'だったり、ポップアップ画面で別プロジェクトにして'IT Bug'を選択してからプロジェクトを切り替えると、課題タイプが'IT Bug'になってしまう。その為、課題タイプの選択肢から削除する方針なら、全プロジェクトにおいてその課題タイプを制限する必要がある。
通常の課題作成画面に入る前の、プロジェクトと課題タイプを選択する画面では、フィールドのDescriptionが表示されないので、JavaScriptによる課題タイプの制限が難しい。その為、通常の課題作成画面では、Createボタンをdisabledにしている。

結果

作戦1同様、通常の課題作成画面では、作成が制限される条件であれば、Createボタンが無効化される。
ポップアップ形式の課題作成画面では、意図通りに、課題タイプの選択肢から'IT Bug'が無くなる。
しかし、デフォルトの課題タイプを'IT Bug'にしていると、ポップアップ画面で'Test Project 1'に存在しない課題タイプを選択した状態から'Test Project 1'に切り替えると、課題タイプが'IT Bug'になってしまう。従って、例えば結合テストのフェーズではデフォルトの課題タイプを'IT Bug'にしたくても、それができないことになる。デフォルトの課題タイプを'IT Bug'にし得るなら、作戦2のように、その時にCreateボタンを非表示かつdisabledにする必要がありそうだ(それでも、作戦2と同じく、ショートカットキーによるSubmitまでは止められない)。
また、この方針だと、通常の課題作成画面の手前の課題タイプ選択画面では相変わらず'IT Bug'が選択できてしまうのが、統一感が無くて不満である。

結論

いずれの作戦もデメリットがあるし、全プロジェクトに影響してしまうので、特定の課題タイプの課題の作成をJavaScriptで制限するのは諦める。

面倒でもプロジェクトを分けるか、どうしてもプロジェクトを分けたくなければ、エラーメッセージがわかりにくくなるが、Create IssueトランジションのValidatorsで作成者を制限するのが最善だと思われる。
なお、Create IssueトランジションのValidatorsは、ワークフロー編集画面のDiagram版から開くことができる。Validatorとしてはいずれかの権限しか選択できないので、いずれの課題タイプについても内部バグ票の作成者に制限しても良さそうな権限、例えばSchedule Issues権限をを選んでそれを内部バグ票の作成可能者に制限し、内部バグ票のCreate IssueトランジションのValidatorにも使用する。

続きを読む "[JIRA] 作成できる課題タイプをグループによって制限したい" »

2015年06月14日

[OpenGL] COLOR MATERIALの利点

OpenGLのcolor materialとは、material parameterの一部をcurrent colorに同期させるモードである。例えば、

glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); /* (1) */
glEnable(GL_COLOR_MATERIAL);
glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
(some drawing)
glDisable(GL_COLOR_MATERIAL);
とすると、
GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, red);
(some drawing)
とするのと同じ効果が得られる。((1)のColorMaterialの設定は、この引数だとOpenGLのデフォルトと同じ設定なので、省略できる。OpenGL ESだと、そもそもこの関数は存在しない=ColorMaterialは常にFRONT_AND_BACK, AMBIENT_AND_DIFFUSEに働く)

これが何の役に立つのだろうか?と、ふと思った時にわからず、調べても見つからなかったので、自分なりにCOLOR_MATERIALのメリットを整理してみた。

1. 軽量("less expensive")である

Webを検索すると、いくつかのページに、color materialは"less expensive"と書かれてある。例えば、Avoiding 16 Common OpenGL Pitfallsに、

OpenGL's color material feature provides a less expensive way to change material parameters.
と書かれている。実は、筆者には"less expensive"の意味がよくわからないが、まさか
GLfloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, red);
より
glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
の方がコードが短くて済む、という意味ではないと思う(前者が1行で書けないのはC言語だからである)ので、ハードウェアの処理負荷が少ないという意味だと思いたい。

2. LightingがOFFの場合と同じコードになる

LightingがOFFだとcurrent colorが描画に反映され、ONだとmaterial colorが描画に反映される。Material colorをglMaterialfv()で設定すると、例えば

glBegin(GL_TRIANGLES);
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, red);
glVertex3f(0, 1, 0);
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, green);
glVertex3f(-0.866f, -0.5f, 0);
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, blue);
glVertex3f( 0.866f, -0.5f, 0);
glEnd();
このようになるのに対し、color materialを使うと
glBegin(GL_TRIANGLES);
glColor4f(1,0,0,1);
glVertex3f(0, 1, 0);
glColor4f(0,1,0,1);
glVertex3f(-0.866f, -0.5f, 0);
glColor4f(0,0,1,1);
glVertex3f( 0.866f, -0.5f, 0);
glEnd();
のように、lightingがOFFでもそのまま使えるコードにすることができる。

3. Color arrayの色をmaterialに反映できる

glBegin()〜glEnd()とするのでなく、頂点配列を用意してglDrawArrays()やElements()を使って描画する場合、頂点毎にmaterial colorを設定するには、color material+color arrayを用いるしかない。

一応、GLUTのサンプルコードを作ってみた。
color_material_test.c
display()のcase 1〜7(左クリックで出るメニューのTest1〜7)は、それぞれ、以下の内容である。

  1. Material colorをglMaterialfv()で設定する例(glBegin()〜glEnd()使用、case 3まで同様)
  2. Material colorをcolor materialで設定する例
  3. 同じコードがlighting OFFでも使えることを示す例
  4. Material colorをglMaterialfv()で設定する例(glDrawArrays()使用、以下同様)
    全ての頂点が同じ色になる。
  5. Material colorをcolor materialで設定する例(color array不使用)
    case 4と同じ表示になる。
  6. Material colorをcolor materialで設定する例(color array使用)
    Color materialを使用すれば、頂点毎にmaterialを変えられる。
  7. 同じコードがlighting OFFでも使えることを示す例(color array使用)
case 0(初期状態)は、Color materialを使用しないと、current colorもcolor arrayも反映されないことを示す例である。(灰色の四面体が表示される。)

実行例
case 1-3 case 4-5 case 6
左から、case 1-3、case 4-5、case 6の画面

続きを読む "[OpenGL] COLOR MATERIALの利点" »

2015年07月12日

OpenGLは右手座標系?

OpenGLは右手座標系、DirectXは左手座標系である。
…色々な所にそう書いてあり、それを信じていたが、この前、自分が書いたコードが、それを否定する動作をして、混乱してしまった。
次のプログラムを実行すると、Z=0の赤い三角よりZ=-1の白い三角の方が手前に表示されてしまう。

#include <GL/glut.h>

void draw_triangle()
{
	glBegin(GL_TRIANGLES);
	glVertex3f(0, 1, 0);
	glVertex3f(-0.866f, -0.5f, 0);
	glVertex3f( 0.866f, -0.5f, 0);
	glEnd();
}

void display(void)
{
	glEnable(GL_DEPTH_TEST);
	glClearColor(0.0, 0.0, 0.2, 1.0);  /* dark blue */
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();

	/* red triangle at Z=0 */
	glColor4f(1.0f, 0.0f, 0.0f, 1.0f);	/* red */
	draw_triangle();

	/* white triangle at Z=-1 */
	glTranslatef(0.0f, 0.0f, -1.0f);
	glScalef(0.2f, 0.2f, 1.0f);
	glColor4f(1.0f, 1.0f, 1.0f, 1.0f);	/* white */
	draw_triangle();

	glutSwapBuffers();
}

int main(int argc, char *argv[])
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
	glutCreateWindow(argv[0]);
	glutDisplayFunc(display);
	glutMainLoop();
	return 0;
}
図1
GLUTのせいか?とも思ったが、これまで自分が作ったGLUTのプログラムは間違いなく右手座標系だった。

調べた所、OpenGLの初期状態は左手座標系であることがわかった。
OpenGL Spec 2.1のAppendix B "Corollaries"の所に、

15. OpenGL does not force left- or right-handedness on any of its coordinates systems. Consider, however, the following conditions: (1) the object coordinate system is right-handed; (2) the only commands used to manipulate the model-view matrix are Scale (with positive scaling values only), Rotate, and Translate; (3) exactly one of either Frustum or Ortho is used to set the projection matrix; (4) the near value is less than the far value for DepthRange. If these conditions are all satisfied, then the eye coordinate system is right-handed and the clip, normalized device, and window coordinate systems are left-handed.
とある。ここに書かれている通り、DepthRangeがnear < farであれば、Model-View MatrixやProjection Matrixが適用された後のClip CoordinatesやNormalized Device Coordinates(NDC、X,Y,Z座標が全て-1〜+1の範囲)は左手座標系であるが、Model-View MatrixやProjection Matrixをそのように設定すれば、Object CoordinatesやEye Coordinatesを右手座標系にすることが可能なのである。
DepthRangeの初期値は、near=0、far=1であり、NDCがViewport TransformationでWindow Coordinatesになる時に、Z=0が手前、Z=1が奥と扱われるので、NDCは左手座標系である。
Projection Matrixを設定する時によく用いられる、glFrustum()やgluPerspective()は、よく見るとZ座標に掛かる係数が負の値であり、これによって、Z座標の前後関係が反転され、Eye CoordinatesではZ軸の+方向が手前、つまり右手座標系になるのである。glOrtho()を用いる場合でも、引数がleft, right, bottom, top, near, farの順なので、何も考えずにglOrtho(-1, 1, -1, 1, -1, 1);とすると、Z座標に掛かる係数が負の値になり、Eye Coordinatesは右手座標系になる。
しかし、Projection Matrixの初期値は単位行列(glOrtho(-1, 1, -1, 1, 1, -1);するのと同じ)であり、Z座標の前後関係を反転させないので、OpenGLの初期状態では、Eye Coordinatesは左手座標系である。

さて、上記の引用文にある通り、OpenGLは左手座標系(left-handed)でも右手座標系(right-handed)でも良い、とのことであるが、それでは困ることがある。特に、右手座標系だったり左手座標系だったりすると、3D物体の表面が裏返しになってしまうのが困るのである。
ポリゴンの裏表は、Window Coordinatesでポリゴンの頂点が時計回りか反時計回りかだけで決まるので、例えば、右手系であることを前提にして手前(Z軸の+方向)に表、奥(Z軸の−方向)に裏のポリゴンを配置した物体は、左手系だと手前(Z軸の−方向)に裏のポリゴン、奥(Z軸の+方向)に表向きのポリゴンがある状態になる。つまり、物体の表面が物体の内側を向いてしまう。
ポリゴンが右手系で配置されているとわかっているなら、左手系で表示するならglFrontFace()を用いて時計回りが表か反時計回りが表かを逆転させれば良いのであるが、ポリゴンが右手系で配置されているか左手系で配置されているかを考慮する必要があることが面倒である。

例えば、GLUTのオブジェクトも右手座標系を前提にしている(Teapotのポリゴンの頂点が時計回りである不具合を除く。glutSolidTeapotのman page参照)ので、CULL_FACEを有効にして左手座標系で描画すると、おかしなことが起こる。

/* showing that GLUT objects are right-handed */
void display2(void)
{
	const GLfloat lightpos_for_LH[4] = {0.0f, 0.0f, -1.0f, 0.0f};	/* directional light */
	const GLfloat default_lightpos[4] = {0.0f, 0.0f, 1.0f, 0.0f};
	
	glEnable(GL_CULL_FACE);
	glEnable(GL_DEPTH_TEST);
	glClearColor(0.0, 0.0, 0.2, 1.0);  /* dark blue */
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glEnable(GL_COLOR_MATERIAL);

	/* LH drawing */
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-1, 1, -1, 1, 1, -1);	/* left-handed */
	glMatrixMode(GL_MODELVIEW);

	glLoadIdentity();
	glLightfv(GL_LIGHT0, GL_POSITION, lightpos_for_LH);

	glTranslatef(-0.5f, 0.2f, 0.0f);
	glColor4f(1, 0, 0, 1);

	glRotatef(-90-20, 1, 0, 0);
	glutSolidCone(0.3, 0.7, 20, 10);

	glLoadIdentity();
	glLightfv(GL_LIGHT0, GL_POSITION, default_lightpos);	/* to see the strangeness more clearly */
	glTranslatef(-0.5f, -0.5f, 0.0f);
	glColor4f(0, 1, 0, 1);

	glRotatef(20, 1, 0, 0);
	glFrontFace(GL_CW);	/* for glutSolidTeapot bug (see man page) */
	glutSolidTeapot(0.3);
	glFrontFace(GL_CCW);

	/* RH drawing */
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-1, 1, -1, 1, -1, 1);	/* right-handed */
	glMatrixMode(GL_MODELVIEW);

	glLoadIdentity();
	glLightfv(GL_LIGHT0, GL_POSITION, default_lightpos);
	glTranslatef(0.5f, 0.2f, 0.0f);
	glColor4f(1, 0, 0, 1);

	glRotatef(-90+20, 1, 0, 0);
	glutSolidCone(0.3, 0.7, 20, 10);

	glLoadIdentity();
	glTranslatef(0.5f, -0.5f, 0.0f);
	glColor4f(0, 1, 0, 1);

	glRotatef(20, 1, 0, 0);
	glFrontFace(GL_CW);
	glutSolidTeapot(0.3);
	glFrontFace(GL_CCW);

	glFlush();
	glutSwapBuffers();
}
図2
左半分が左手座標系で、右半分が右手座標系で同じものを描画したものである。左上の円錐は、手前の表向きのポリゴンが消えて、奥の裏向きのポリゴンが見えている。左下のティーポットは、手前から光を当てるとシルエットだけになるので、後ろから光を当てており、丁度ポリゴンの法線と光源の方向が一致して明るくなっているが、見えているのはやはり奥のポリゴンであり、所々が変である。

また、ライティングのパラメーターも、右手座標系か左手座標系かに関係する。
OpenGL Spec 2.1の記述を引用すると、lightingに関して、

All computations are carried out in eye coordinates.
とあり、例えば
The current model-view matrix is applied to the position parameter indicated with Light for a particular light source when that position is specified.
なので、Projection Matrixで右手座標系にするなら、光源の位置を含め、ライティングに関するパラメーターは右手座標系の前提で決めないといけない。
例えば、手前から光を照射する場合、右手座標系だとZ軸が+の方に光源を置くことになるが、左手座標系だとZ軸が-の方に光源を置くことになる。

ここで注目すべきは、Lighting parametersのPOSITIONの初期値は(0.0, 0.0, 1.0, 0.0)、SPOT_DIRECTIONの初期値は(0.0, 0.0, -1.0)であることだ。つまり、OpenGLの初期状態では、Z軸が+の方向から−の方向へ平行光が照射しており、スポットライトの反射方向はZ軸が−の方向であり、右手座標系を前提とした設定になっているのである。

そういう意味では、やはりOpenGLは右手座標系で使うのが基本と言えるのではないだろうか。

右手座標系の方が数学で見慣れて利便性が高いし、glFrustum()やgluPerspective()を使って右手座標系にして使うことの方が多いので、3D物体も右手座標系で作るべきだと思った。

参考URL
http://stackoverflow.com/questions/4124041/is-opengl-coordinate-system-left-handed-or-right-handed

続きを読む "OpenGLは右手座標系?" »

2015年12月25日

特定の文字列を含まないという正規表現

正規表現で、特定の文字列を含まないパターンを記述するのは難しい。

その文字列の文字数が少なければ、例えば"ABC"を含まないとするなら

([^A]|A[^B]|AB[^C])*
という方法がありそうで、筆者は時々やってしまうのだが、これは正しくない。"AABC"があると"AA"がA[^B]にマッチ、"ABABC"があると"ABA"がAB[^C]にマッチしてスルーしてしまうし、"A"や"AB"で終わるとマッチしないからである。
この方向で進めると、一例としては、
([^A]|A(B?A)*([^AB]|B[^AC]))*(A(B?A)*B?)?
という式になるそうだが(大崎 博基さんの「Perl正規表現雑技」より)、これは少なくとも筆者には読めない。

特定の文字を含まない、という式は、例えば"A"を含まないなら[^A]*と簡単に書けるのに、特定の文字列を含まない、となると途端に難しくなるのは不思議である。

特定の文字列を含まない行にマッチする、よく知られたパターンとして、

(?!.*ABC).*
というやつがある。(?!...)はnegative lookahead assertion(否定先読み)というやつで、その先に...が続かない、という条件を表す。(?=...)はその先に...が続くというpositive lookahead assertionであり、ABC(?=DEF)は"ABCDEF"の"ABC"にマッチし、ABC(?!DEF)は"ABCDEF"の一部でない"ABC"にマッチする。
(?=...)(?!...)は、POSIX仕様などの正規表現の古い仕様には含まれず、grepなど一部使えない処理系があるが(Emacsもサポートしていない。外部コマンドを活用しろということらしい)、PerlやJavaScriptでもサポートされており、現在はほとんどの処理系で使えると言える。

(?!.*ABC).*の弱点は、文字列全体のマッチにしか使えないことである。例えば、文字列中の"STA"〜"END"の部分を検索したいが、途中に"ABC"を含む場合は除外したいと思って、STA(?!.*ABC).*ENDと書くと、"STAxxENDxABC"のように、"END"の後に"ABC"がある場合もマッチしなくなってしまう。

今回、そういうのが必要になって、悩みながら、何かいい方法は無いかと思ってWeb上を探してみたら、

((?!ABC).)*
というのを見つけた。この文字から先が"ABC"でない任意の文字が0個以上、である。すごい発想だと思った。

Web上には(.(?!ABC))*と書いている例もあり、同じだと勘違いして、早速(.(?!ABC))*を使い始めたのだが、よく考えると(.(?!ABC))*には問題があることに気付いた。これだと、"ABC"を2文字目から検索することになり、先頭がABCの場合に除外されず、マッチしてしまうのである。さらに、例えば、途中に"EN"を含まない"STA"〜"END"の部分を検索するつもりでSTA(.(?!EN))*ENDと書くと、途中に"EN"が無くても、"END"の直前の1文字が必ず.(?!EN)に当てはまらない("END"の"EN"で引っ掛かる)ので、何にもマッチしなくなってしまう。
((?!ABC).)*にはこれらの問題は起こらない。
それと対称にして、(?<!)のnegative lookbehind assertionを使って(.(?<ABC))*としても上記の問題は起こらないが、より複雑だし、手元のPerlで試すと10%ほどの速度低下が見られたので、これを使うメリットは無いだろう。

結局、外側の()によって後で参照可能なグループが増えないように、(?:(?!ABC).)*という形にすることによって、筆者の問題は解決した。

続きを読む "特定の文字列を含まないという正規表現" »

2016年05月29日

CppUTestでテスト駆動開発(1)

図書館で、「テスト駆動開発による組み込みプログラミング」という本に目を引かれ、流し読みしたら結構面白かったので、借りて帰ったのだが、筆者はこの所公私共に多忙で、ほとんど読めないまま返却期限を迎えてしまった。
とりあえず、その本で推薦されていた、CppUTestというユニットテストフレームワークを使ってみた。

筆者は、凄まじく高コストで生産性の低い日本企業のソフトウェア開発を数年間目の当たりにした後、21世紀に入った頃にテストファーストを含むExtreme Programmingの考え方に全面的に共感し、その時にCUnitというユニットテストフレームワークを使ったことがあるのだが、その価値がわからなかった。わざわざそんなのを使わなくても、ユニットテストを駆動する短いテストドライバを自分で書けば事足りると思ったし、実際、これまでそのようにしてきて困ったことが無い。

しかし、今回、この本でUnityやCppUTestの話を読むと、ユニットテストフレームワークには最先端の考え方が反映されており、この枠に嵌められてユニットテストを書いたりTDDをすれば、手軽に世界のトップクラスのインテリジェンスに触れられるような気がして、面白そうだと思った。

そこで、CppUTestを使ってみることにした。

書籍の第3章のLEDドライバの例を題材に、TDD(テスト駆動開発)方式で、まずは未実装のエラーが出る実行可能なテストを作ってみる。

LedDriver/LedDriverTest.cpp
#include "CppUTest/TestHarness.h"

TEST_GROUP(LedDriver)
{
  void setup()
  {
  }
  void teardown()
  {
  }
};

TEST(LedDriver, LedsOffAfterCreate)
{
  FAIL("Start here");
}
test_main.cpp
//from README.md
#include <CppUTest/CommandLineTestRunner.h>

int main(int ac, char** av)
{
   return CommandLineTestRunner::RunAllTests(ac, av);
}
Makefile
CXX = g++
CPPUTEST_HOME := $(HOME)/tmp/cpputest

#from README.md
CPPFLAGS += -I$(CPPUTEST_HOME)/include
CXXFLAGS += -include $(CPPUTEST_HOME)/include/CppUTest/MemoryLeakDetectorNewMacros.h
CFLAGS += -include $(CPPUTEST_HOME)/include/CppUTest/MemoryLeakDetectorMallocMacros.h
LD_LIBRARIES = -L$(CPPUTEST_HOME)/lib -lCppUTest -lCppUTestExt

TARGET = test_main
SRCS = test_main.cpp LedDriver/LedDriverTest.cpp
OBJS = $(SRCS:.cpp=.o)

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CXX) -o $@ $^ $(CXXFLAGS) $(LD_LIBRARIES)

%.o: %.cpp
#	GNU make implicit rule + "-o $@"
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

.PHONY: check
check: $(TARGET)
	./$(TARGET) -v

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS) *.gcno *.gcov *~ */*~
	find . -name "*.gcda" | xargs rm -f

これでmake checkとすると、1つのテストが失敗し、"Start here"と表示される。
筆者は普段、テストの実行はmake testであるが、make checkが多数派らしいので、今後はそれに倣うことにした。(make testはPerlの世界に多い?)

MakefileにLedDriver.cのコンパイルが無いのは、TDDの、エラーにならない限りは作らない原則に従っているからである。

次に、LedDriver/LedDriverTest.cppにテストを追加し、それがpassするようにLedDriver.cを開発するのを繰り返す。
途中と詳細説明を省略するが、例えば途中段階では次のようになった。

LedDriver/LedDriverTest.cpp
#include "CppUTest/TestHarness.h"
extern "C" {
#include "LedDriver.h"
}

static uint16_t virtualLeds;

TEST_GROUP(LedDriver)
{
  void setup()
  {
    LedDriver_Create(&virtualLeds);
  }
  void teardown()
  {
  }
};

TEST(LedDriver, LedsOffAfterCreate)
{
	uint16_t virtualLeds = 0xffff;
	LedDriver_Create(&virtualLeds);
	LONGS_EQUAL(0, virtualLeds);
}

TEST(LedDriver, TurnOnLedOne)
{
	LedDriver_TurnOn(1);
	LONGS_EQUAL(1, virtualLeds);
}

TEST(LedDriver, TurnOffLedOne)
{
	LedDriver_TurnOn(1);
	LedDriver_TurnOff(1);
	LONGS_EQUAL(0, virtualLeds);
}

TEST(LedDriver, TurnOnMultipleLeds)
{
	LedDriver_TurnOn(9);
	LedDriver_TurnOn(8);
	LONGS_EQUAL(0x0180, virtualLeds);
}

TEST(LedDriver, AllOn)
{
	LedDriver_TurnAllOn();
	LONGS_EQUAL(0xffff, virtualLeds);
}
	
TEST(LedDriver, TurnOffAnyLed)
{
	LedDriver_TurnAllOn();
	LedDriver_TurnOff(8);
	LONGS_EQUAL(0xff7f, virtualLeds);
}

IGNORE_TEST(LedDriver, LedMemoryIsNotReadable)
{
	// TODO: let LED state read-only
}
LedDriver/LedDriver.h
#pragma once
#include <stdint.h>

extern void LedDriver_Create(uint16_t *address);
extern void LedDriver_Destroy(void);
extern void LedDriver_TurnOn(int ledNumber);
extern void LedDriver_TurnOff(int ledNumber);
extern void LedDriver_TurnAllOn(void);
LedDriver/LedDriver.c
#include "LedDriver.h"

enum {ALL_LEDS_ON = ~0, ALL_LEDS_OFF = ~ALL_LEDS_ON};

static uint16_t *ledsAddress;

static uint16_t convertLedNumberToBit(int ledNumber)
{
	return 1 << (ledNumber - 1);
}

void LedDriver_Create(uint16_t *address)
{
	ledsAddress = address;
	*ledsAddress = ALL_LEDS_OFF;
}

void LedDriver_Destroy(void)
{
}

void LedDriver_TurnOn(int ledNumber)
{
	*ledsAddress |= convertLedNumberToBit(ledNumber);
}

void LedDriver_TurnOff(int ledNumber)
{
	*ledsAddress &= ~convertLedNumberToBit(ledNumber);
}

void LedDriver_TurnAllOn(void)
{
	*ledsAddress = ALL_LEDS_ON;
}
Makefile
CXX = g++
CC = gcc
CPPUTEST_HOME := $(HOME)/tmp/cpputest

#from README.md
CPPFLAGS += -I$(CPPUTEST_HOME)/include
CXXFLAGS += -include $(CPPUTEST_HOME)/include/CppUTest/MemoryLeakDetectorNewMacros.h
CFLAGS += -include $(CPPUTEST_HOME)/include/CppUTest/MemoryLeakDetectorMallocMacros.h
LD_LIBRARIES = -L$(CPPUTEST_HOME)/lib -lCppUTest -lCppUTestExt

TARGET = test_main
SRCS = test_main.cpp LedDriver/LedDriverTest.cpp
CSRCS = LedDriver/LedDriver.c
OBJS = $(SRCS:.cpp=.o) $(CSRCS:.c=.o)

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CXX) -o $@ $^ $(CXXFLAGS) $(LD_LIBRARIES)

%.o: %.cpp
#	GNU make implicit rule + "-o $@"
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

%.o: %.c
#	GNU make implicit rule + "-o $@"
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

.PHONY: check
check: $(TARGET)
	./$(TARGET) -v

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS) *.gcno *.gcov *~ */*~
	find . -name "*.gcda" | xargs rm -f

make checkすると、"OK (7 tests, 6 ran, 6 checks, 1 ignored, ...)"となる。
ignoredなのは「実行可能なリマインダ」である。

ところで、CppUTestには簡単なメモリリーク検出機能がある。各TEST()の実行前にTEST_GROUPのsetup()が、実行後にteardown()が呼び出されるが、そのsetup()前とteardown()後とでメモリ確保状態を比較するようである。
試しに、LedDriver/LedDriver.cのLedDriver_TurnAllOn()を

#include <stdlib.h>
void LedDriver_TurnAllOn(void)
{
	void *p = malloc(1);
	*ledsAddress = ALL_LEDS_ON;
}
に変えてmake checkとすると、次のエラーメッセージが出た。
TEST(LedDriver, AllOn)
LedDriver/LedDriverTest.cpp:46: error: Failure in TEST(LedDriver, AllOn)
	Memory leak(s) found.
Alloc num (6) Leak size: 1 Allocated at: LedDriver/LedDriver.c and line: 35. Type: "malloc"

うまく使えば便利そうである。
CppUTestに触れてみて、TDDをきちんと勉強しようと思った。

続きを読む "CppUTestでテスト駆動開発(1)" »

2017年03月01日

Carbon EmacsにPython 3環境導入

筆者はMacで未だにEmacs 22ベースのCarbon Emacsを多用している。Emacs 24ベースのCocoa Emacsもインストールはしているのだが、色々な環境をCarbon Emacsに作ってしまっているので、移行するのが億劫なのである。

最近、Python 3を使い始めたのだが、Carbon Emacsのpython.elはPython 2にしか対応していないので、何とかPython 3に対応させる方法は無いかと探した結果、
http://www.loveshack.ukfsn.org/emacs/
から
python.el
emacs.py
sym-comp.el
の3つをダウンロードして、Emacs.app/内に置けば良いことがわかった。(python.elとemacs.pyは既にあるものを置換、sym-comp.elはpython.elと同じディレクトリに追加)
そして、Emacsを立ち上げて、M-x customize-groupとし、
"Python Default Version" = 3
"Python Python Command" = python3
にすれば完成である。

これで、C-c TABとM-TABを駆使すればシンボルの補完ができて、まあまあコーディングが楽になるのだが、やっぱり補完機能は便利にしたいので、
Pythonの補完をEmacsでシンプルに最小労力で手早く使えるようにする - 牌語備忘録 -pygo
を参考にして、auto-complete.el + ac-python.elをインストールした。
auto-complete.elは、Emacs 22で動作実績のある、auto-complete-1.3.1.tar.bz2をどこかから入手した。

そして、.emacsに次の4行を書けば完成である。

(require 'auto-complete)
(require 'auto-complete-config)
(ac-config-default)
(require 'ac-python)

これで、python-modeにして、import mathと書いてC-c TABして、math.と打つと、math.sqrtやらmath.piやらが補完候補として自動的に現れるようになった。

続きを読む "Carbon EmacsにPython 3環境導入" »

2017年03月05日

pandas.read_csv+numpy.bincountでエラー

ちょっと前に、はやりのAIのプログラミングでよく使われるPythonとNumPyとPandasを使い始めたのだが、PandasでCSVファイルを読み込んで、各ラベルの出現回数を得る為にNumPyのbincountメソッドを使うと、次のようなエラーが出て、困った。

TypeError: Cannot cast array data from dtype('int64') to dtype('int32') according to the rule 'safe'
その時使った環境は、Windows 7(32bit版)+Python 3.5.2(Anaconda 4.1.1 (32-bit))である。Mac OS Xでは出なかった。

これは、32bitのPythonを使っていると起こることらしい。例えば、32bit Pythonで次のプログラムを実行すると、同じエラーが出る。

import numpy as np
import pandas as pd
import io

data = np.random.randint(10, size=30)
buf = io.StringIO("\n".join(str(x) for x in data))
df = pd.read_csv(buf, header=None)
x = df[0].values
print(x)
print(np.bincount(x))	#Error on 32bit Python
これは、Pandasが32bit Pythonでもint64を使うのが原因のようである。

このエラーを回避するには、bincountに渡すデータの型をint32にすれば良い。

print(np.bincount(x.astype('int32')))	#also OK on 32bit Python
出力例
[1 0 6 7 1 7 7 6 9 5 6 2 8 0 7 6 7 8 9 7 8 9 8 0 2 1 2 6 0 3]
[4 3 3 1 0 1 5 6 4 3]

または、np.unique(return_counts=True)を使う方法があり、こちらの方が、大抵の場合はnp.bincountを使うよりも好ましいとされるようである。

print(np.unique(x, return_counts=True))
出力例
(array([0, 1, 2, 3, 5, 6, 7, 8, 9]), array([4, 3, 3, 1, 1, 5, 6, 4, 3]))
確かに、np.unique(return_counts=True)の方が、データに負の値があっても使えるし、大きな値が混ざってても配列が巨大にならないので、安全そうである。

なお、今動作しているPythonが32bitか64bitかを判定する方法は、いくつかあるようである。 例1

import platform
platform.architecture()
出力(上がMacOSX、下がWin32)
('64bit', '')
('32bit', 'WindowsPE')
例2
import sys
"%x" % sys.maxsize
出力(上がMacOSX、下がWin32)
'7fffffffffffffff'
'7fffffff'
例3(platform.architecture()の実装にも使われている方法)
import struct
struct.calcsize("P") * 8
出力(上がMacOSX、下がWin32)
64
32

続きを読む "pandas.read_csv+numpy.bincountでエラー" »

2017年03月31日

pandasでmergeせずにgroupbyで間接参照したい

この前、pandasを使っていて、次のような感じの、関連する2つのテーブル、access_logとchoice_logがある時に、結合したテーブルを作らずにchoice毎のtimestampの最小値を求めたかったのだが、どう書けば良いのかわからなかった。

import pandas as pd
import numpy as np
access_log = pd.DataFrame({'session': [100, 101, 102, 103, 104, 105, 106, 107, 108, 109],
                           'timestamp': [314, 159, 265, 358, 979, 323, 846, 264, 338, 327]})
choice_log = pd.DataFrame({'session': [100, 100, 101, 102, 102, 103, 104, 104, 105, 106, 106, 107, 108, 108, 109],
                           'choice':  ['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E']})
>>> access_log
   session  timestamp
0      100        314
1      101        159
2      102        265
3      103        358
4      104        979
5      105        323
6      106        846
7      107        264
8      108        338
9      109        327
>>> choice_log
   choice  session
0       A      100
1       B      100
2       C      101
3       D      102
4       E      102
5       A      103
6       B      104
7       C      104
8       D      105
9       E      106
10      A      106
11      B      107
12      C      108
13      D      108
14      E      109
>>> 

結合テーブルを作るなら、次のように書ける。

merged = choice_log.merge(access_log, on='session', how='left')
result = merged.groupby('choice')['timestamp'].min()
>>> result
choice
A    314
B    264
C    159
D    265
E    265
Name: timestamp, dtype: int64
>>> 

実際にあったテーブルは巨大で、他にも列がたくさんあり、単純に結合テーブルを作るとRAMが足りなくなってメモリスワップが多発したので、結合テーブルを作らずにこれと同じことがしたかったのだが、その書き方がわからなかった。
結局access_log.set_index('session').to_dict()のようにして一時的なdictを作って、スワップを多発させながら処理してしまった。
それが心残りだったので、改めてpandasのドキュメントを拾い読みしながら方法を探してみた。

  1. 単純に、別のテーブルを参照する関数をSeries.mapに渡す
    def session_to_timestamp(session_series):
        return session_series.map(lambda x: access_log[access_log.session==x]['timestamp'].iat[0])
    
    result = choice_log.groupby('choice').agg(lambda x: session_to_timestamp(x).min())
    

    Seriesの先頭の要素を取り出す方法には、.iat[0]の他に.iloc[0]や.values[0]などがあり、筆者が試した所values[0]の方が速かったりしたが、pandasのドキュメントに書かれているのはilocとiatなので、ここでは添字が整数なら高速なiatを用いた。

  2. リスト内包表現(list comprehension)で別のテーブルを参照する
    def session_to_timestamp(session_series):
        return [access_log[access_log.session==x]['timestamp'].iat[0] for x in session_series]
    
    result = choice_log.groupby('choice').agg(lambda x: min(session_to_timestamp(x)))
    

    リストにはminメソッドが無いので、session_to_timestamp(x).min()とはできない。

  3. isinを使ったBoolean Indexingで別の表のサブセットを得る
    def session_to_timestamp(session_series):
        return access_log[access_log.session.isin(session_series.values)]['timestamp']
    
    result = choice_log.groupby('choice').agg(lambda x: session_to_timestamp(x).min())
    
  4. 別の表からSeriesを作ってSeries.mapに渡す
    def session_to_timestamp(session_series):
        return session_series.map(access_log.set_index('session').timestamp)
    
    result = choice_log.groupby('choice').agg(lambda x: session_to_timestamp(x).min())
    

    一見シンプルで美しそうだが、set_index()はコピーを返すので、timestampの一時的なdictを作るのと変わらない。しかも、グループ数だけ新たなテーブルを作るので、無駄である。

これらの処理時間を色々測ってみたが、2つのテーブルのサイズやグループの数によって変わり、どう比較すれば良いかわからなかったので、省略する。
大まかな傾向としては、1.と2.の処理時間はchoice_logのサイズに依存し、3.と4.の処理時間はsession_logのサイズに依存するようだった。4.はset_indexした中間テーブルを事前に作っておくと高速化するが、それでも、大抵の場合3.が一番速かった。
いずれの方法も最速になる場合があるようなので、場合毎に色々試してみるしかなさそうである。

肝心のメモリ使用量は、適当な測り方がわからなかった。
そもそも、スワップしながらの処理時間が問題だったので、単純にメモリ使用量では測れないと思う。

他にも、以下のような方向で書き方を考えてみたが、うまくできなかった。

  • groupbyでaggregateでなくtransformしてmin()
    transformする時に別のテーブルを参照することを考えたが、transformするとgroup解除されてしまうので、使えなかった。transformする時にmin()するのなら、min()した値を増殖させるだけ無駄なので、確実にaggregateの方が効率が良い。
  • pandas.DataFrame.lookupを使う
    引数としてindexしか受けられないので、使えなかった。
  • pandas.DataFrame.joinを使う
    pandas.DataFrame.mergeを使うのと変わらなかった。

そもそも、lambda式を使わずに書く方法は無いのだろうか。
teratailとかStack Overflowとかで聞いた方が早いか。

続きを読む "pandasでmergeせずにgroupbyで間接参照したい" »

2017年04月09日

ROC曲線を理解する

2値の予測(判別、識別、…)に用いる特徴量の良し悪しを評価する1つの方法として、ROC曲線というものがある。
実際は正で予測も正であるデータの数をTP(True Positive)、
実際は負で予測も負であるデータの数をTN(True Negative)、
実際は負で予測は正であるデータの数をFP(False Positive)、
実際は正で予測は負であるデータの数をFN(False Negative)、
と呼ぶ時、
TPR(True Positive Ratio)=TP/(TP+FN)を縦軸、
FPR(False Positive Ratio)=FP/(FP+TN)を横軸、
としたグラフである。

ROC曲線の例
ROC curve sample

ROC曲線は、必ず(0,0)から始まって(1,1)で終わる。
特徴量が全く予測の役に立たない、ランダムな値であれば、TPR=FPRの線になる。
ROC曲線より下の面積、AUR(Area Under ROC curve)(または単にAUC(Area Under the Curve))が大きいほど、特徴量の値の全域に渡って良い特徴量だとされる。 理想的な特徴量だと、ROC曲線はFPR=0とTPR=1の線になる。

ROC曲線は機械学習で識別器の評価によく用いられるらしいので、とりあえず覚えておこうと思ったのだが、筆者はこれの理解にえらく苦労したので、調べたことや考えたことをメモする。
統計学の検定と同様、こういう確率と論理を組み合わせたものは、人によって向き不向きがあるのだと思いたい。

TP,TN,FP,FNの関係を再度整理すると、次のようになる。

予測

TPFN (Type II error)
FP (Type I error)TN
PやNは予測がPositiveかNegativeかであり、TやFはそれが正解かどうかである。
FPは誤検出のことであり、統計学の検定でも使われる「第一種の過誤」(検定では帰無仮説を棄却できないのに棄却する条件に誤ってヒットしてしまうこと)である。
FNは検出不能であり、「第二種の過誤」(検定では帰無仮説が誤りなのに棄却する条件にヒットしないこと)である。

予測の精度に関する尺度としては、Accuracy, Presicision, Recall, F値があり、それぞれ次のように定義される。
Accuracy = (TP+TN) / (TP+TN+FP+FN)
予測の正解率。
Precision = TP / (TP+FP)
Positiveと予測される中の正解率。
Recall = TP / (TP+FN)
実際にPositiveの内、Positiveと予測される割合。検出力、Sensitivity。
F値(F-measure, F1 score) = ((Precision-1 + Recall-1)/2)-1
PrecisionとRecallの調和平均。統計学のF分布に従うF値とは関係ない。
PrecisionとRecallはトレードオフの関係にあるので、それらをバランス良く合成した尺度。

予測の精度はAccuracyで評価するのが簡単だが、実際の正のデータ数と負のデータ数に偏りがあると、データ数が少ない方の正解率が低くても、データ数が多い方の正解率が高ければAccuracyが高くなってしまうので、Accuracyだけでは適切に評価できない。
そのような場合にPrecisionやRecallが用いられるが、これらは一般に特徴量の閾値によってトレードオフの関係があり、セットで評価しないといけないので、単純比較には向かない。そこで用いられるスカラー値が、F値や、ROC曲線のAUCである。
Precision-Recall曲線のAUCも使われることがあるが、Presicionは実際の正のデータの割合に依存するので、正のデータの割合が同じでないと比較には使えない。実際の正のデータの割合が極端に小さい場合など、Precisionが大きな意味を持つ場合にはPrecision-Recall曲線が用いられる。

ROC曲線は、TPR=TP/(TP+FN)とFPR=FP/(FP+TN)のグラフである。TPRを陽性率、FPRを偽陽性率と呼ぶこともある。TPRはRecallと同じである。FPRはfall-out(副産物)と呼ばれることもある。

次の図の3つのROC曲線が、正のデータと負のデータがどのように分布する特徴量に対応するかを考えてみる。
ROC curve sample 2

例えば、次のような分布になる特徴量だと、青いROC曲線になる。
distribution of totally independent feature
横軸は特徴量、縦軸は赤い部分が正のデータの分布、青い部分が負のデータの分布を表している。このグラフでは、正のデータも負のデータも一様分布している。閾値tより右ならPositive、左ならNegativeと予測する時、tを右端から左に動かすと、TPRもFPRも0から1に向かって増大するが、常にTPR=FPRである。正のデータと負のデータの割合はグラフの形状には関係しない。

次のような、正のデータと負のデータが完全に分かれる理想的な特徴量だと、緑のROC曲線になる。
distribution of ideal feature
tを右端から左に動かすと、FPR=0のままTPRが0から1に変化し、青のゾーンに入ると、FPRが0から1に変化する。

次のような分布だと、赤いROC曲線になる。
distribution of which makes ROC curve perfect arc
tを右端から左へ動かすと、FPRよりもTPRの方が早く上昇する。なるべく正のデータと負のデータの分布が分離している良い特徴量ほどFPRが上昇する前にTPRが上昇するので、AUCが大きくなることがわかる。

AUCはどれくらいだと良いか、という基準は一般的なものも色々あるようだが、大体、最低0.7は無いと有効ではないとされるようである。

なお、特徴量の最良の閾値(cut-off)はROC曲線の(0,1)に最も近い点とする、という方法を複数の箇所で目にしたが、明確な理論的根拠がある訳ではなく、必ずしもそれに限定されないようである。そもそも、(0,1)に最も近いというのがユークリッド距離で良いのかどうかがわからない。

続きを読む "ROC曲線を理解する" »

2017年05月06日

JediにAutoCompleよりCompanyModeを優先させる

2ヶ月前にCarbon EmacsのPython環境を整備したばかりなのだが、MacのOSを10.7.5から10.12.4にバージョンアップすると、Carbon Emacsがまともに動かなくなってしまった。起動はするのだが、すぐに固まってしまう。しかも、その後distnotedというプロセスが全てのCPUを奪い続けて、Macが激重になる。
この現象はOS X 10.9+Emacs 24.3で起こるらしく、Carbon Emacs(Emacs 22)でも起こったという情報は見つけられなかったが、おそらく同じ問題である。どちらかと言うとOSのバグなのだが、現時点ではEmacsを24.4以降にバージョンアップする以外に解決方法が見当たらない。

その為、長年お世話になったCarbon Emacsを手放し、Emacsの最新版である25.2を使うことにした。

EmacsWikiのPython Programming In EmacsのページにはPythonの開発環境を便利にする方法が色々書かれているが、筆者は高機能なPythonの開発環境を使いたい時はSpyderを使っており、EmacsのPython環境はPython.elでPython3が使えて、もう少し便利な補完が効けば十分なので、Jediだけを追加インストールすることにした。

Emacs 25.2にbuilt-inのPython.elはPython3に対応しているのだが、MacPortsでpython35をインストールして、M-x customize-group -> pythonして"Python Shell Interpreter"をpython3にすると、Warningが出まくり、妙に不安定だった。M-x list-packagesしてpython packageを0.25.2にバージョンアップすると少し改善したが、完全には直らなかった。
さらに、Jediをインストールすると、これまた不安定で、時々補完候補が出る前にEmacsが固まった。(C-gで抜けることはできる)

Emacs 24の最新版である24.5.1ではこの問題は起こらなかったので、当面、EmacsでPythonのコードを書く時は24.5.1を使うことにした。

Jediのインストールは、

(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
(package-initialize)
としてpackage.elにMELPAのリポジトリを登録し、M-x list-packagesしてjediをインストールし、Jedi.elのドキュメントに従ってセットアップした。

なお、Emacs25でjediをインストールすると、Emacs24で"Symbol's function is void: cl-struct-define"というエラーになった。これはjediに限らず、よくあることだそうで、Emacsの24と25を併用する場合、package.elで何かをインストールするならEmacs24でする方が良さそうだ。

さて、JediはAutoCompleteとCompanyModeの両方に対応しているが、package.elで"jedi"をインストールするとAutoCompleteが使われる。"company-jedi"をインストールするとCompanyModeが使われるのだが、"jedi"と"company-jedi"の両方をインストールすると、AutoCompleteが優先される。特にAutoCompleteに不満は無く、和製なので贔屓したいが、色々読んでいると、世界的には、日本でも現在は、CompanyModeの方が人気があるようなので、何が良いのかを知るために、これからしばらくはCompanyModeを積極的に使うべく、両方をインストールしてCompanyModeをデフォルトにすることに決めた。

しかし、.emacsや.emacs.d/init.elでCompanyModeを優先する適当な方法がわからなかった。Python modeにしてM-x auto-complete-modeとすればAutoCompleteのON/OFFが切り替わり、OFFだとCompanyModeが使われるのだが、Emacs Lispで(auto-complete-mode)としてもON/OFFが切り替わらず、auto-complete-mode関数にはOFFにする引数も無いのである。auto-complete-mode関数のソースコードを見ると、"(if auto-complete-mode ..."とあるので、

(setq auto-complete-mode nil)
(auto-complete-mode)
とすれば良さそうに思ったが、これでもOFFにならない。
但し、(setq auto-complete-mode nil)すると、即座にAutoCompleteがOFFになる。このことを使って、試行錯誤の末、.emacs等に
(add-hook 'python-mode-hook 'jedi:setup)
よりも前のどこかに
(add-hook 'python-mode-hook
(lambda () (setq auto-complete-mode nil)))
と書けば、Python mode開始時にAutoCompleteがOFFになり、CompanyModeが使われることがわかった。

続きを読む "JediにAutoCompleよりCompanyModeを優先させる" »

2017年05月21日

MacPortsでmaximaをインストールするとエラーになるのを回避

現在、MacPortsを使って

sudo port install maxima
すると、下のようなエラーになって失敗してしまう。(macOS Sierra 10.12.4で確認)
../../doc/info//errormessages.texi:296: `Warning messages' has no Up field (perhaps incorrect sectioning?).
../../doc/info//errormessages.texi:162: Prev field of node `Operators of arguments must all be the same' not pointed to.
../../doc/info//errormessages.texi:152: This node (Only symbols can be bound) has the bad Next.
../../doc/info//errormessages.texi:152: Next field of node `Only symbols can be bound' not pointed to (perhaps incorrect sectioning?).
../../doc/info//errormessages.texi:204: This node (out of memory) has the bad Prev.
../../doc/info//errormessages.texi:8: `Error messages' has no Up field (perhaps incorrect sectioning?).
makeinfo: Removing output file `/opt/local/var/macports/build/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_math_maxima/maxima/work/maxima-5.39.0/doc/info/maxima.info' due to errors; use --force to preserve.
make[3]: *** [maxima.info] Error 1

原因は、makeinfoのバージョンが古いからのようだ。
http://maxima-discuss.narkive.com/KeLR9qhb/can-t-install-5-39
に、makeinfoのバージョンが4.xだとこうなることが書かれている。 実際、macOS Sierraで makeinfo --version すると、(GNU texinfo) 4.8と表示される。

そこで、

sudo port install texinfo
(GNU texinfo 6.3のmakeinfoがインストールされる)してから再度
sudo port install maxima
すると、上記のエラーは出なくなった。Activationの段階で
--->  Installing maxima @5.39.0_3+xmaxima
--->  Activating maxima @5.39.0_3+xmaxima
Error: Failed to activate maxima: Image error: /opt/local/bin/maxima already exists and does not belong to a registered port.  Unable to activate port maxima. Use 'port -f activate maxima' to force the activation.
というエラーになるが、メッセージの通り、
sudo port -f activate maxima
とすると、一応wxMaximaと併用しても問題なく動いた。

続きを読む "MacPortsでmaximaをインストールするとエラーになるのを回避" »

About PC一般

ブログ「Weblog on mebius.tokaichiba.jp」のカテゴリ「PC一般」に投稿されたすべてのエントリーのアーカイブのページです。過去のものから新しいものへ順番に並んでいます。

前のカテゴリはJavaです。

次のカテゴリはUNIXです。

他にも多くのエントリーがあります。メインページアーカイブページも見てください。

Powered by
Movable Type 3.35