yunomuのブログ

趣味のこと

パッケージ管理システムのこと

 自分で作ったプログラムを実行することは、ほとんどの場合で簡単にできます。だいたい動作確認しながら作るので当たり前です。
 でも自分で作ったプログラムを人に渡して使ってもらおうとすると、これが結構面倒くさい。
 ここでプログラムというのは、実行ファイルというかOSのロードモジュールの事を指すとします。

 まず、渡した相手のコンピュータのCPUが違うと動かない。PowerPCやARM用のプログラムはIntelアーキテクチャのプロセッサでは動かない。
 動的リンクライブラリを使っている場合は、動的リンクライブラリのパスが違うと動かない。
 そもそも依存しているライブラリがインストールされていないと動かない。
 設定ファイルを使う場合はパスやファイル名が違うと動かない。
 プログラムファイルの形式が、OSのローダが対応しているものでないと動かない。ELFなのかMach-OなのかPortable Executableなのか。
 OSの機能を呼び出すなら、呼び出し方が合っていないといけないし、呼び出す機能が存在しないといけない。
 デバイスを使っている場合は、デバイスの指定方法やアクセス方法が違うと動かない。まあこの辺はだいたいOSに任せるけども。
 などなど、結構面倒くさい。

 実のところこれらの面倒くさいあれこれは、プログラムをソースコードのまま渡すことである程度解決します。
 通常、コンパイラは自分の環境で動くプログラムを出力するので、CPUが違って動かないとか、プログラムの形式が違って動かないとかいう事は起きません。
 動的リンクライブラリにしても、パスはコンパイル時(正確にはリンク時)にプログラムに埋め込んでくれるので、インストールしてさえあればおおむね大丈夫です。
 自分の環境で動かないという事があれば、なんだったらソースコードを直してもいい。ヘッダファイルに環境依存情報(ファイルパスとかメモリ量とか)が書いてあって、そこをちょっと直すと動くという事は、よくあるというかあったというかあったらしいというか想像に難くない。

 そういう環境依存でのコンパイル段階でのトラブルを避けるために、ヘッダファイルやマクロを使って色んな環境でコンパイルできるように工夫します。例えば、

#if VERSION <= 5
#include "env5.h"
#endif

などとやると、VERSIONマクロが5以下の時だけenv5.hというヘッダファイルが読み込まれます。
 こういう風に、OSが違ったら、バージョンが違ったら、ライブラリがインストールされてなかったら、int型のバイト数が違ったら……と、様々な状況を想定してコードを書きます。
 場合によっては、環境に依存したヘッダファイルを生成するためのプログラムを作って、コンパイル前に実行したりします。OSやコンパイラコンパイルではたぶん必ずやっています。

 そうなってくると、コンパイル時に色んな事を考えないといけなくなります。どういう手順でコンパイルすればいいのか、マクロ変数の値をなんにしたらいいか、ヘッダファイルはどれを使えばいいのか、そもそもどのファイルをコンパイルしてリンクすればいいのか。
 いろいろあって面倒なので、たいていそういう情報はMakefileに書いてあります。というかこれくらい色々あると開発中でも面倒なので、そういうコンパイル手順のアレコレはだいたいMakefileに書いてあります。なので、

% gcc -o program src1.c src2.c src3.c -Iinclude -L/usr/local/lib -DARCH=I386

みたいにやっていたのが、

% make

とかで一発で実行ファイルができあがる。便利!

 ところが、まだまだそううまくいかない。
 Makefileには、プログラムのコンパイルの手順が書いてあるんですが、中身は単にコマンドの羅列なので、コマンドが無かったり名前が違ったりすると動きません。ファイルを移動するmvコマンドがmoveだったり、whichが無かったり、ライブラリが変な場所にインストールされていたりするとコンパイルできなかったりします。
 さらに、仮にプログラムがコンパイルできたとしても、動的リンクライブラリが無かったりパスを間違えていたりすると実行できません。

 これを解決するのがautoconfだったりconfigureスクリプトです。
 よくプログラムをソースからインストールする時に

% ./configure
% make
% make install

みたいな事をすると思いますが、この"./configure"というやつ。これを実行すると、コンパイルに必要なライブラリやコマンドやヘッダファイルがインストールされているかをチェックして、環境に適したMakefileを生成してくれます。
 configureが無い場合はまたそれを生成するツールなんかもあったりします。ここらへんは、だいたいREADMEとかMakefileの頭のコメントとか適当に実行した時のエラーメッセージに書いてあるし、慣れればファイル構成を見ただけでどうすればいいかわかるようになります。

 これでもうソースコードで公開されているソフトウェアの大半はコンパイルして使う事ができるようになる気がします。が、そもそもそのソースコードはどこで配布されてるのか。まあ今だったらググれば大抵公式サイトが出てくるのでそこから取ってくればよろしい。でも公式サイトがわからなかったり、検索エンジンが無い時代だったりすると、探すのが大変だったり、人に教えるのも大変だったり。
 バージョンアップされた時も、更新した情報をどこからか入手して、ソースコードを入手して、コンパイルする必要があって、結構面倒です。

 そこで、公開されている便利なプログラムの一覧を管理するシステムができます。とりあえず私が知ってるのはFreeBSDportsなのでportsの話をします。
 portsは、プログラムのリストを持っていて、例えばemacsなら/usr/ports/editor/emacs/とか、curlなら/usr/ports/net/curl/という風に、カテゴリごとに分けたディレクトリに情報が保存されています。いや正確なパスなどは忘れましたが。これで、例えば

% cd /usr/ports/editor/emacs
% make && make install

みたいにやるとめでたくemacsがインストールされます。

 例えばこの場合の/usr/ports/editor/emacsにあるMakefileが何をやっているかというと、emacsソースコードを配布しているサイトからソースコードと、場合によっては修正パッチが入ったファイルをダウンロードしてきて、解凍してパッチを当ててコンパイルしている。ソースコードがどこで配布されているかとか、パッチはどれかとか、そういうのは全部このMakefileやそれが呼び出すシェルスクリプトなんかに書いてあって、自動的に処理してくれる。といってもまあそういうのもパッケージの管理者が書いていてくれているんですが。

 で、バージョンアップの時はどうやっているのかというと、まあバージョンアップがあった事を知るのはやっぱりこまめな情報収集が必要だったりするんですけども、このportsのディレクトリ構成自体がCVSとかを使ってどこかのマスターデータと同期するようになっていて、誰かがソフトウェアを更新するとパッケージ管理者がportsのマスターデータを修正して、利用者はデータ同期してその情報を持ってきて、あとは最初と同じように"make && make install"で更新するというか上書きする。
 非常にシンプルで想像しやすい。

 ただ、portsは基本的にはソースコードからインストールする構造になっているので、インストールに結構時間がかかる。
 もちろん最初の方で書いたように、ソースコードからコンパイルした方が安全ではあるものの、例えば同じOSを使っていてハードウェアもOSで吸収できる程度の違いしか無いような環境だとか、実行ファイルやOSに互換性がある場合なんかは、あるコンピュータ上でコンパイルした実行ファイルがそのまま他のコンピュータにコピーしても動いたりします。
 というかWindowsなんかはそうだし、最近のOSはだいたいそうですね。
 そうなると、わざわざソースコードをダウンロードしてコンパイルするよりも実行ファイルをダウンロードした方が圧倒的に速いのでそうしようとなる。コンパイルは、どこかの似たような環境の誰かがやってくれていればいい。
 ただし、依存する他のプログラムやライブラリなんかが存在しないとやっぱり動かないので、その辺はきちんと把握しておく必要がある。
 ということで、バイナリパッケージというのが生まれた、のだと思います。

 例えばRed Hat LinuxRPMなんかは、パッケージをrpm形式のファイルで管理していて、rpmファイルの中には実行ファイルやライブラリや設定ファイルが入っている。それと、それらのファイルをどこに置けばいいのかとか、あとは依存する外部ライブラリの名前なんかの情報も書かれています。
 rpmファイルをインストールすると、中の情報にしたがって適切にプログラムやファイルを配置してくれて、RPMが持っているデータベースに情報が登録されます。これで現在どんなパッケージがインストールされているかを管理することができるようになっていて、例えば依存するパッケージがインストールされていなかった場合にはエラーを出してインストールが失敗します。
 RPMの場合はこんな感じで、自分でRPMファイルを探してきて、依存するパッケージも自分で探してインストールする必要がありますが、パッケージ管理の基本としてはそんな感じです。

 もちろんそれだと面倒くさいので、Yumみたいなのが登場します。
 Yumは、パッケージ名を指定するとRPMファイルをダウンロードするだけでなく、依存するパッケージがある場合はそれらもインストールしてくれるとても便利なものです。もちろんyumも中央で管理されているリポジトリがあって、それが更新されることでパッケージのバージョンアップにも追随できます。これまでの問題を概ね全部解決できるような素敵なシステムです。
 Debian使ってたからdpkg/APTで説明すればよかった……。まあAPTも同じような感じです。
 YumやAPTのみならず、RubyのgemとかJavaMavenとかHaskellのcabalとかもおおむね同じ仕組になっています。いやcabalはソースからビルドだからportsに近いし、gemもコンパイル無いし、MavenはバイナリだけどJavaだし、仕組みは似てるけど違うか。

 そしてこれより先の事は知らん。

 というのが、私が認識しているパッケージ管理システムの話です。実際のところは、APTあたりからこの世界に入ってきたのでよくわかりません。
 いや、そういえば数ヶ月前にこの辺の事を教えてと言われていたので。
 だからどうってことは無いんですけども、歴史というか、流れをある程度知っていると次の新しいものを理解しやすかったりするし、なんだったら作れる可能性もできるし、面白いんじゃないですかね。

 で、そもそも質問ってなんでしたっけ。

死んでもTwitterで遊ぼう

 先生が「死ぬまでTwitterで遊ぼう」みたいな事を言ったら、学生に「いや別に死んでもTwitterできますよプログラマなんだから」みたいな事を言われた、という話を去年京都で飲んだ時に聞いた。
 そういえば私は昔、人間を作りたくて色々勉強してた事を思い出した。いや今も作りたいんですが。

 で、とりあえずそれっぽく見えるものだけでも作ろうと思って、作ったのがこれ。botです。

 仕組みは単に、元ネタの文章を形態素解析して、前の語に続く語を出現頻度に応じてランダムに選んでいるだけ。文はmecabで解析して、作った辞書はredisに突っ込んでる。

 ついでにサーバ引っ越しとかなんとかもいっしょにやってたので20時間くらいかかったというか、久しぶりに気がついたら朝になっているというやつをやりました。
 なぜかテンションが上がりすぎて、1週間くらいかけて遊ぼうと思ってたのに1日で終わってしまった。まあまだ改良はいくらでもできるんですけど。
 なんかこういう、気がついたら朝になってたみたいなのは5〜6年ぶりな気がしますけども、楽しいは楽しいんですが食事も睡眠も忘れるというのはあまり健康には良くないんじゃないかという気もしますね。楽しかったけど。

 このbotの元データとしては私のTwitterのpostを使っていて、1万ツイート1MBくらいで、辞書の構築には1分半くらいかかる。
 さすがに元データが少ないので、元の発言とほとんど変わらないものが出てくる事がそれなりにあって微妙なんですが、そこら辺はなんか突然変異みたいな仕組みでも入れてやってみようかなぁと思っているところです。
 でも試しに私が書いた10MBくらいのテキストを食わせてみたところ、かなり意味わかんない言葉を生成できるようになったので、辞書のデータ量を増やした上で言葉を覚えさせる方向に改良していった方がいいような気がします。
 ただ10MBのテキストからの辞書構築には3時間かかったのでちょっと面倒ですね。いや構築途中でもしゃべれるのでいいんですが。アルゴリズム見直すのも面倒くさいし。

 そのうち記憶とか知覚とかも実装したいというか、2歳児くらいにはならないもんかなと思っているんですが、どうしたものだろうか。外界からの刺激を受け取るなり与えるなりさせないといけないんだけど、どう処理しようかなぁ。
 などと考えているうちにGWの残り2日が終わりそうです。

 @yunomu11がbotに延々話しかけるだけのアカウントになっていてもそっとしておいてください。

Android SDKのSQLiteDatabase#execSQLのbindArgsの型

AndroidSDKのSQLiteDatabase#execSQLのマニュアル
[http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#execSQL(java.lang.String, java.lang.Object[])]
よくあるsqlとbindArgs(Object配列)を渡す構造なんですが、このbindArgsのマニュアルを見ると

Parameters

sql the SQL statement to be executed. Multiple statements separated by semicolons are not supported.
bindArgs only byte[], String, Long and Double are supported in bindArgs.

byte[], String, Long and Doubleのどれかしかサポートしてないよと書いてあります。

すると「え、でもInteger入れたいんだけどどうなのよ」ってなるわけじゃないですか。
で、中身を見てみると

SQLiteDatabase.java

    public void execSQL(String sql, Object[] bindArgs) throws SQLException {
        if (bindArgs == null) {
            throw new IllegalArgumentException("Empty bindArgs");
        }
        executeSql(sql, bindArgs);
    }

ここはnullを弾いてるだけ。

SQLiteDatabase.java

    private int executeSql(String sql, Object[] bindArgs) throws SQLException {
        acquireReference();
        try {
            if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) {
                boolean disableWal = false;
                synchronized (mLock) {
                    if (!mHasAttachedDbsLocked) {
                        mHasAttachedDbsLocked = true;
                        disableWal = true;
                    }
                }
                if (disableWal) {
                    disableWriteAheadLogging();
                }
            }

            SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs);
            try {
                return statement.executeUpdateDelete();
            } finally {
                statement.close();
            }
        } finally {
            releaseReference();
        }
    }

ここはSQLiteStatementのコンストラクタに渡して、その後にSQLiteStatement#executeUpdateDeleteを呼んでる。

SQLiteStatement.java

    SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) {
        super(db, sql, bindArgs, null);
    }

コンストラクタでは親クラスのコンストラクタに渡しているだけ。おそらく単に親のフィールドに格納していて、getBindArgs()で取ってきたりしているんだろう。というかしてた。ちょっと気を使った書き方になってたのでライブラリ作る人は見てみてもいいかも。

SQLiteStatement.java

    public int executeUpdateDelete() {
        acquireReference();
        try {
            return getSession().executeForChangedRowCount(
                    getSql(), getBindArgs(), getConnectionFlags(), null);
        } catch (SQLiteDatabaseCorruptException ex) {
            onCorruption();
            throw ex;
        } finally {
            releaseReference();
        }
    }

executeUpdateDeleteメソッドでは、getBindArgsで親クラスから取ってきた値をSQLiteSession#executeForChangedRowCountに渡している。

SQLiteSession.java

    public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
            return 0;
        }

        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
        try {
            return mConnection.executeForChangedRowCount(sql, bindArgs,
                    cancellationSignal); // might throw
        } finally {
            releaseConnection(); // might throw
        }
    }

ここでは、2箇所でbindArgsが使われているけれど、1つ目のexecuteSpecialの中では使われていなかったので、2つ目のSQLiteConnection#executeForChangedRowCountに渡されている方が本命。

SQLiteConnection.java

    public int executeForChangedRowCount(String sql, Object[] bindArgs,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        int changedRows = 0;
        final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
                sql, bindArgs);
        try {
            final PreparedStatement statement = acquirePreparedStatement(sql);
            try {
                throwIfStatementForbidden(statement);
                bindArguments(statement, bindArgs);
                applyBlockGuardPolicy(statement);
                attachCancellationSignal(cancellationSignal);
                try {
                    changedRows = nativeExecuteForChangedRowCount(
                            mConnectionPtr, statement.mStatementPtr);
                    return changedRows;
                } finally {
                    detachCancellationSignal(cancellationSignal);
                }
            } finally {
                releasePreparedStatement(statement);
            }
        } catch (RuntimeException ex) {
            mRecentOperations.failOperation(cookie, ex);
            throw ex;
        } finally {
            if (mRecentOperations.endOperationDeferLog(cookie)) {
                mRecentOperations.logOperation(cookie, "changedRows=" + changedRows);
            }
        }
    }

真ん中あたりのbindArgumentsで使われている。これ以降には出てこないので、たぶんここでstatementの中に入っちゃってるんだろう。nativeExecuteForChangedRowCountまで見れば処理の本体が見られるだろうけど、今回の興味はここではない。
ところでこのtry節のネストっぷりは、なるほどなぁと思った。次からこうしよう。

SQLiteConnection.java

    private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
        final int count = bindArgs != null ? bindArgs.length : 0;
        if (count != statement.mNumParameters) {
            throw new SQLiteBindOrColumnIndexOutOfRangeException(
                    "Expected " + statement.mNumParameters + " bind arguments but "
                    + bindArgs.length + " were provided.");
        }
        if (count == 0) {
            return;
        }

        final int statementPtr = statement.mStatementPtr;
        for (int i = 0; i < count; i++) {
            final Object arg = bindArgs[i];
            switch (DatabaseUtils.getTypeOfObject(arg)) {
                case Cursor.FIELD_TYPE_NULL:
                    nativeBindNull(mConnectionPtr, statementPtr, i + 1);
                    break;
                case Cursor.FIELD_TYPE_INTEGER:
                    nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).longValue());
                    break;
                case Cursor.FIELD_TYPE_FLOAT:
                    nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).doubleValue());
                    break;
                case Cursor.FIELD_TYPE_BLOB:
                    nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
                    break;
                case Cursor.FIELD_TYPE_STRING:
                default:
                    if (arg instanceof Boolean) {
                        // Provide compatibility with legacy applications which may pass
                        // Boolean values in bind args.
                        nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                                ((Boolean)arg).booleanValue() ? 1 : 0);
                    } else {
                        nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
                    }
                    break;
            }
        }
    }

ここではfor文の中でなんぞ判定して、数値ならNumberにキャストしてからlongValueなりdoubleValueを呼び出してる。これならIntegerでもいけるっぽい。
ところでこのDatabaseUtils.getTypeOfObjectって何だ。

DatabaseUtils.java

    public static int getTypeOfObject(Object obj) {
        if (obj == null) {
            return Cursor.FIELD_TYPE_NULL;
        } else if (obj instanceof byte[]) {
            return Cursor.FIELD_TYPE_BLOB;
        } else if (obj instanceof Float || obj instanceof Double) {
            return Cursor.FIELD_TYPE_FLOAT;
        } else if (obj instanceof Long || obj instanceof Integer
                || obj instanceof Short || obj instanceof Byte) {
            return Cursor.FIELD_TYPE_INTEGER;
        } else {
            return Cursor.FIELD_TYPE_STRING;
        }
    }

このように、実際にはここでinstanceofを使って型を判別しております。見たところIntegerでもShortでもFloatでも良さそうですね。まあFloatをintValueで変換とかはしてくれなさそうですが、おおむね問題は起きないようにしてあるっぽいです。

で、マッチしなかったらStringとみなされて、さっきのbindArgumentsに戻ってみると、そこでさらにBooleanかどうかの判定が行われて、trueなら1、falseなら0、BooleanじゃなければObject#toStringが呼ばれる、と。
これ、このswitch文は必要なのかな。DatabaseUtils.getTypeOfObjectは消して、このswitch文と同化してしまえばいい気がするけどどうなんだろう。getTypeOfObjectがどこかの使い回しだったりするのかな。まあするんだろうなUtilsだし。

最後にtoStringしてるってことは、enumにtoStringを実装しておいてぶっ込むとかもできるし、細かく楽ができそうですね。

SSHがつながらない

前回と同じネットワーク構成で。
f:id:yunomu:20140311120402p:plain
AからVPN経由でCへのSSH接続を試みる。

% ssh 192.168.2.11
ssh_exchange_identification: read: Connection reset by peer

つながらない。

Aからインターネット経由でC(eth0)へのSSH接続は普通にできる。

% ssh server_public_ip
Last login: Fri Mar 14 14:36:19 2014 from 192.168.1.2
[yunomu@host ~] $

だいたいこういう感じ。

/var/log/secureで関係ありそうなのは以下の1行だけ。

Mar 14 13:41:41 host sshd[30405]: Did not receive identification string from 192.168.1.2

他の/var/log/messagesとかは何も出てなかった。

TCPWrapperやxinetdは使っていなくて、生sshdで動いている。
なのでhosts.allowやhosts.denyは空で問題ないはず。

死んだsshdのプロセスがウヨウヨいたりして新規接続の確立ができてないのかなと思ったけど特にそういう気配も無し。

というかそもそも/var/log/secureの通り一応sshdが動くところまでは行ってるわけで。sshdが動いた上で、クライアントからのデータが何も来ないからしばらくしてコネクションが切られているっぽい。

さらに面倒くさいことに、2日前まではインターネット経由でもVPN経由でもつながっていたし、この記事を書き始めてから試したらつながった。
つながったじゃないか! なんなんだこれ!

というわけで、またよくわからないうちに事件が解決してしまった。いや解決してないんだけど。

ネットワークがつながらない

基本がわかっていないとなにもかもわからなくなる。

出来事

外で借りてるサーバ(C)を使ってこんな感じのネットワークを作った。
f:id:yunomu:20140311120402p:plain

  • AはWindows 7
  • Aの隣に何台かノート(D)やら携帯(E)やらがある
  • BはUT-VPNクライアントが動く程度のLinux(CentOS 5系だったと思う)
  • CはVPSLinux(これもCentOS 5系のはず)
  • BとCはUT-VPNで接続していて、仮想HUBはCにある
  • ルータはなんか数千円の安いやつ
  • 白い四角はルーティングテーブル

この状態で、CにはDBやらWebサーバやらいろいろ置いてあって、AからVPN経由で触って遊べるようにしたつもり。

で、しばらくなんの問題もなく遊んでいたんですが、なんかAのネットワークとCの通信がうまくいったりいかなかったりする。
症状はこんな感じ。

  • AとEからはCのサービス(HTTP)が見られる
  • DからはCのサービス(HTTP)が見られない
    • Bのeth0ではHTTPのrequest/responseパケットをキャプチャできる
  • A, D, EからC(192.168.2.11)にpingを飛ばすと全て応答する
  • CからAやDにpingを飛ばすとどちらも応答しない
    • Bのeth0ではicmpパケットのecho/replyをキャプチャできる

何がうまくいかないのかさっぱりわからん。なんでDのTCPだけ通信できないんだ。

よく見てみると、DからCにTCPで通信する時、行きはルータ->B->Cと経由しているけど、帰りはBからルータを経由せずにDに帰るようになっている。
ということでBのルーティングテーブルを変更して192.168.1.0/24を192.168.1.1に向けてみた。
するとDはCとTCPでも通信できるようになったが、今度はAとEがCと通信できなくなった。

まあそういういい加減な対処は無かったことにして。

なんとなく、ルータを経由したりしなかったりするのが良くないんじゃないかという気がするので、さしあたりDに192.168.2.0/24->192.168.1.10のルーティングを追加して、まあなんとか使える感じにしてみた。
一晩寝ると、CからAへのpingは通るようになっていたけども、CからDへは通らず。

なんか、何年も前に何か問題が起きたわけじゃないけども、こういう感じの問題に対処するために何かした記憶がおぼろげにあるような気がしないでもないけども、全然思い出せない。

よくわからない

問題は、何が悪いかよくわかっていないこと。割とつながってるやつもいるし、ネットワーク的には間違っていないようにも見える。
つながったりつながらなかったりするってことは、ARPテーブルがどうのこうのいう感じかなぁとも思ったものの、いじってみても特に変化はなさそう。

こういう時に、これがIPとして正しい動きなのか、TCPとして正しい動きなのか、Ethernetとして正しい動きなのか、それともそれぞれの機器の実装の問題なのか、そのあたりの切り分けができない。
自信を持って「これはIP的にはちゃんとできてるハズなんだけどねぇ~」みたいな事が言えない。
本当に教科書的な意味で、基本がよくわかっていない。

私のネットワークの知識はなんかルータいじってる過程でいつの間にか身についたものなので、プロトコルに関しては実のところよく知らなかったりするのです。誰だよ私がネットワークに詳しいなんて言ったボンクラは。(私です)

そろそろTCP/IPの本を一冊くらい読んでまともに勉強してみようかなぁと思いました。

それはそれとして件の問題は片付いていないんですが。

haskell-relational-recordとMySQLの識別子の大文字小文字

haskell-relational-record(hrr)とMySQLを使ってプログラムを書いてみようと思った。

書き方自体はこのあたり。
https://github.com/khibino/haskell-relational-record
https://github.com/krdlab/haskell-relational-record-driver-mysql

元々haskell-relational-recordはPostgreSQLのために作られているので(訂正:IBM-DB2 + ODBCで動かしていたのが最初だそうです)、MySQLから使おうとするとちょっと詰まるのかもしれない。
まずdefineTableFromDBが動かなかったんですけど、それはちょっと原因がよくわからないので保留。

そもそもの問題が適当に作られたテーブルの形式だったりするわけなんですが、おおむねこういう感じになっている。

mysql> connect myapp
Connection id:    126780
Current database: myapp
mysql> desc USER;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| NAME         | varchar(32)  | NO   | PRI | NULL    |       |
| MAIL_ADDRESS | varchar(128) | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

MySQLではスキーマとデータベースの区別が無いというか、同じ物を指すっぽいので、データベース名もスキーマ名も同じ"myapp"になっている。そして、テーブル名とフィールド名を大文字のスネークケース。どこの常識だこれはという感じ。まあこれはそういうものなので仕方ないものとする。

これを元にテーブル定義を作るとこうなる。defineTableFromDBが動かないので自分で定義する。

{-# LANGUAGE TemplateHaskell, MultiParamTypeClasses, FlexibleInstances #-}

module User where

import Database.HDBC.Query.TH (defineTableDefault')
import Database.Record.TH (derivingShow)
import Language.Haskell.TH
import DataSource

defineTableDefault' "myapp" "USER"
    [ ("NAME", conT ''String)
    , ("MAIL_ADDRESS", conT ''String)
    ] [derivingShow]

でき上がるテーブルとクエリの定義はこうなる。

*Main> :i USER
data USER
  = USER {nAME :: !String,
          mAILADDRESS :: !String}
     -- Defined at User.hs:10:1
instance Show USER -- Defined at User.hs:10:1
instance TableDerivable USER -- Defined at User.hs:10:1
instance ProductConstructor (String -> String -> USER)
  -- Defined at User.hs:10:1
*Main> uSER
SELECT NAME, MAIL_ADDRESS FROM MYAPP.user

とてもきもちわるい。その上この時点で既に破綻している。

  • データ型名が全部大文字で気持悪い
  • フィールド名の'_'が消えて先頭だけが小文字になっていて気持悪い
  • クエリの関数名が同様に"uSER"て何よ
  • クエリの中身のデータベース名が大文字に、テーブル名が小文字になっている

この、最後のやつが困る。他はまあ気持悪いだけなのでいいのだけど。
どう困るかというと、このクエリを発行するとそんなテーブル無いよと言われる。MySQLはテーブル名の大文字と小文字を区別することがある。

MySQLのドキュメントにはこういう事が書いてある。

5.2.2. 識別子の大文字小文字の区別
MySQL において、データベースはデータディレクトリ内のディレクトリに対応しています。データベース内の各テーブルも、データベースディレクトリ内の少なくとも 1 つ(記憶エンジンによってはそれ以上)のファイルに対応しています。トリガーもファイルに対応しています。そのため、ベースとなるオペレーティングシステムで大文字と小文字が区別される場合、データベース名とテーブル名でも大文字と小文字が区別されます。
MySQL :: MySQL 5.1 リファレンスマニュアル (オンラインヘルプ) :: 5.2.2 識別子の大文字小文字の区別

つまりMySQLのデータがWindowsファイルシステム上に置かれている場合はデータベース名やテーブル名の大文字小文字は区別しないけれど、UnixLinuxの多くのファイルシステム上に置かれていた場合は区別しますという事。なんじゃそりゃという感じだけども、これも仕方ない。

これをなんとかしてhrrを使うためには、ソースをいじる必要がある。
問題はテーブル定義と同時に作成されるクエリ関数なわけなので、それを定義している場所を探すと、tableSQLという関数が見つかる。場所はここ。
haskell-relational-record/relational-query/src/Database/Relational/Query/TH.hs at master · khibino/haskell-relational-record · GitHub

tableSQL :: String -> String -> String
tableSQL schema table = map toUpper schema ++ '.' : map toLower table

とてもわかりやすいですね。
これの"map toUpper"と"map toLower"を消してあげればいいのです。PostgreSQLでは基本的にテーブル名は小文字なのでこうなってるんだと思います。スキーマ名は忘れたけど大文字なんだっけ?

ということで、hrrはまだhackageに登録されていないので皆様各々githubからcloneして使っておられると思いますので、この辺りをいじる心理的障壁は比較的少ないんじゃないかと思います。やっちゃいましょう。branch作ってcommitしておくと本家が更新した時に追随が楽だと思います。いや検証してPull Request送れという話はもっともなんですが。

ともあれこれで一応クエリは利用可能になります。めでたしめでたし。

*Main> uSER
SELECT NAME, MAIL_ADDRESS FROM myapp.USER

ところで実はMySQLはデータベース名とテーブル名についてはなんか面倒な仕様があるんですが、フィールド名の大文字小文字は区別しないので、テーブル定義のところはフィールド名を小文字のスネークケースにすると少しだけ幸せになります。

defineTableDefault' "myapp" "USER"
    [ ("name", conT ''String)
    , ("mail_address", conT ''String)
    ] [derivingShow]

ごらんよ、焼け石に水って感じだけども。これでも普通に使えます。

*Main> :i USER
data USER
  = USER {name :: !String,
          mailAddress :: !String}
     -- Defined at User.hs:10:1
instance Show USER -- Defined at User.hs:10:1
instance TableDerivable USER -- Defined at User.hs:10:1
instance ProductConstructor (String -> String -> USER)
  -- Defined at User.hs:10:1
*Main> uSER
SELECT name, mail_address FROM myapp.USER

Frappuccinoの使い方

前回、Frappuccinoの件をあまりにももののついでみたいに書いてしまったんですが、使おうとすると例によってドキュメントが無くてソースを読む事になって、なったので、その時の足跡です。
https://github.com/steveklabnik/frappuccino

Source

前準備として、streamとイベント発生装置(Source)を作る。公式のReadmeではButtonとなってるやつ。

class Source
  def send(event)
    emit(event)
  end
end

source = Source.new
stream = Frappuccino::Stream.new(source)

このemitは、ソースを見てみると、まずStream.newで渡されたインスタンスにFrappuccino::Sourceを継承させてる。
frappuccino/stream.rb

module Frappuccino
  class Stream
    include Observable

    def initialize(*sources)
      sources.each do |source|
        source.extend(Frappuccino::Source).add_observer(self)
      end 
    end
(略)

それでemitはSourceに定義されている。中では変更フラグを立てて、イベントをObserverに通知している。
frappuccino/source.rb

require 'observer'

module Frappuccino
  module Source
    def self.extended(object)
      object.extend(Observable)
    end 

    def emit(value)
      changed
      notify_observers(value)
    end 
  end 
end

というわけでSource.sendの中でemitを呼び出すことができるようになっている。Observableは標準添付のobserverに入ってるやつです。そんなの標準であったのかという感じですが。

Streamにはmap, drop, take, select, zip, merge, scan, count, inject, merge, update, on_valueというメソッドがあって、分類すると以下のような感じになります。

  • Streamを返す
    • map
    • drop
    • take
    • select
    • zip
    • merge
    • scan
  • Proptertyを返す
    • inject
    • count
  • その他
    • update
    • on_value

Stream系

Streamを返すやつらは、分類したり変換したり統合したり、まあメソッド名を見ればでわかるような事をやります。Java8のStreamと同じような感じです。
mapは変換、dropは指定した個数のイベントを無視、takeは指定した個数のイベント以降を無視、selectは条件に合うイベントのみを拾うようなStreamを返します。だいたい想像通りに動くと思います。

mapはブロックじゃなくてHashを取ることもできて、

  def map(hash = nil, &blk)
    blk = lambda { |event| hash.fetch(event) { hash[:default] } } if hash
    Map.new(self, &blk)
  end

みたいになってて、

stream.map(:e1 => 1, :e2 => 2, :default => 0)

みたいなHashを渡すと、キーと一致するイベントがきたら特定の値に変換するというのが簡単に書けるんですが、まああんまし使わないと思います。

zipとmergeはStreamの統合で
zipは

stream3 = stream1.zip(stream2)

とやると、stream1とstream2の両方のイベントが1つずつそろった時にstream3が発火するような感じ。ソース見ると本当に単にバッファを2つ作って、両方そろったら発火させているだけで、あーこんなに簡単に作れるのかーという気がする。

mergeは

stream3 = Stream.merge(stream1, stream2)

zipと似てるんですけど、これはstream1とstream2のどちらかが発火したら発火する。

scanは畳み込みみたいな感じで、前回のイベントを参照しながらmapみたいな処理ができる。
例えば、イベントの数を数えるのはこう。

stream2 = stream1.scan(0) {|a| a + 1 }

イベントとして飛んでくる整数の合計値を取る場合はこう。

stream2 = stream1.scan(0) {|a, v| a + v }

ブロックの第一引数が蓄積変数というか前回の値で、第二引数が今回のイベントになってる。集計以外では、値のストリームを前回との差分のストリームに変換したりとか、そんなふうに使うんじゃないでしょうか。

Property系

Property系は、StreamではなくPropertyオブジェクトを返します。

module Frappuccino
  class Property
    def initialize(zero, stream)
      @value = zero
      stream.add_observer(self)
    end 
        
    def now
      @value
    end
    
    def update(value)
      @value = value
    end
  end
end

ご覧の通り、nowとupdateがあって、nowは現在の値を返します。updateはどうでもいいというか使わないと思います。
「現在の」と言ったのは、イベントが発生するとnowの値が変わるからです。公式のReadmeにあるとおりなんですが。

injectは、scanの結果をPropertyにしたやつで、scanの合計値を数えるのと同じ事をやると、

counter = stream1.inject(0) {|a| a + 1 }

これでcounter.nowで現在のイベントの個数が取得できます。以下と全く同じです。

stream2 = stream1.scan(0) {|a| a + 1 }
counter = Frappuccino::Property.new(0, stream2)

countはinjectのラッパーで、個数を数えるのに特化しています。引数を渡すと内部でselectされます。

counter = stream1.count #=> イベントの個数を数える
counter1 = stream1.count(:event1) #=> :event1の個数を数える
counter2 = stream1.count {|e| e[:value] >= 2 } #=> 条件に合うイベントの個数を数える

その他

updateはイベントを発生させます。
Source以外の場所からemit相当の事をするために使うっぽい。というかこれがあれば実はemitを使わなくてよくて、実は前回の例はこう書ける。(Emitterクラスを定義してない)

ws = WebSocket::Client::Simple.connect url
stream = Frappuccino::Stream.new
ws.on :message do |msg|
  stream.update msg
end

公式のReadmeの例に引っ張られすぎだった。

on_valueは、イベントが発生したら引数で渡したブロックを評価する。Streamをイベントに変換するメソッドです。便利!

おわり

実装が思いのほか単純で、読むのが楽なのが素晴らしいですね。まあドキュメントあるにこしたことはないんですけど。
ところでこのライブラリって何に使うんでしょうね。