Hatena::ブログ(Diary)

葡萄酒の雑記集 2.0(笑)

2012-08-05

Perl6のGrammarでサーバログをパースする

| 13:37

何がしたいか

某編集部で運用中のマインクラフトサーバのログを解析して、プレイ時間の一番長い廃人を算出したい。折角なのでPerl6でやりましょうという企画です。lldecadeでPerl6の話が少し出たようなので、それに乗っかろうという感じですね。


はじめに

Grammarとは何ぞや?という話があると思いますが、これについては去年のPerl6 AdCで書かれた@Yoshimura/_yuuくんの記事を参考にして下さい。おそらく日本語でGrammarを解説した記事の中では、彼のが一番まとまっていると思います。



今回のテーマについて

GrammarとActionsはとても強力です。強力なんですが、あまりに複雑なのと制約が多いので使い道があまりまりません。対象となる文字列パースする際にメモリ上に構文木が展開されてしまうため、あまり大きなファイル(例えばプログラムソースコード)などをパースするのに向いているかと言われると微妙な気がします。また、普通の正規表現に比べて格段に処理速度が遅いのも残念なところです。


なので今回は、メモリと速度という制約を受けない、サーバログのパースをしてみようと思います。ログの解析なら行指向なので一回のパース量は少なくて住みますし、リアルタイム性も必要ありませんからね。


ログのフォーマット

今回は、サーバログの中から「ログイン」と「ログアウト」を探し出し、プレイ時間の集計を撮りたいと思います。それぞれのログはこんなふうになっています。

2011-09-09 21:29:42 [INFO] VienosNotes [/192.168.xxx.xxx:xxxxx] logged in with entity id xxx at (X, Y, Z)
  • ログアウト
2011-09-09 21:56:37 [INFO] VienosNotes lost connection: disconnect.quitting

当然細かい部分は場合に応じて変わるのですが、だいたいこんな感じです。


ルールを書く

ではこの2つについて、どういう場合にマッチするのかのルール(rules)を実際に書いて行きましょう。個人的な好みですが、Grammarを書いていく時は、トップダウンでrulesを書いていくと楽な気がします。細かい部分は名前をつけて切り分け、後回しにすると見通しの良いGrammarが書けるようになるからです。ただ、rulesの複雑さは処理速度に直結しますので、そのへんの兼ね合いを考える必要があるかもしれません。


まずは"TOP"です。これはあらゆるGrammarに必要な、特殊なルールです。Grammarを用いたマッチングは、先頭から部分正規表現について深さ優先探索でマッチングを試みていくのですが、その根の部分に当たるのが"TOP"になります。今回の場合はこんな感じでしょうか。

        rule TOP {
            <log_login> | <log_logout>
        }

要は、このGrammarでは"log_login"もしくは"log_logout"という名前の正規表現にマッチさせるよ、というのがTOPにあたります。


次は"log_login"を書きましょう。タイムスタンプIPアドレスなどを記述すると煩雑になるため、これも名前付きで下に切り分けます。また、名前をつけることでフックするActionを書けるようになるので、パースした結果として使いたい部分には名前をつけるのが良いでしょう。

        rule log_login {
            <timestamp> <.ws> '[INFO]' <.ws> <username> <.ws> '[/' <ipaddress> ']' <.ws> 'logged in with' .*
        }

        rule log_logout {
            <timestamp> <.ws> '[INFO]' <.ws> [<username>|'/' <ipaddress>] <.ws> ['[/' <ipaddress> ']']? <.ws> 'lost connection:' .*
        }
    }

ログアウトは、たまにユーザ名でなくIPアドレスになることがあるので(原因はよくわからん)、少し手を入れてます。


タイムスタンプは、あとで年や月、日を使うので名前で切り分けましょう。

        rule timestamp {
            $<year>=(\d ** 4) '-' $<month>=(\d ** 2) '-' $<day>=(\d ** 2) <.ws> $<hour>=(\d ** 2) ':' $<minute>=(\d ** 2) ':' $<second>=(\d ** 2)
        }

ユーザ名はマインクラフトの細かい仕様を知らないので適当です。

        rule username {
            <[0..9a..zA..Z_]>+
        }

IPアドレスのバリデーションとかは必要ないでしょう。手抜きですがこんな感じ。

        rule ipaddress {
            \d ** 1..3 \. \d ** 1..3 \. \d ** 1..3 \. \d ** 1..3 [':' \d ** 1..5]?
        }

これで、ログイン/ログアウトの行だけを抜き出して、マッチオブジェクトを生成することができるようになりました。まとめると、次のようなGrammarになります。

  grammar Minecraft::ServerLog::Format {
        rule timestamp {
            $<year>=(\d ** 4) '-' $<month>=(\d ** 2) '-' $<day>=(\d ** 2) <.ws> $<hour>=(\d ** 2) ':' $<minute>=(\d ** 2) ':' $<second>=(\d ** 2)
        }

        rule ipaddress {
            \d ** 1..3 \. \d ** 1..3 \. \d ** 1..3 \. \d ** 1..3 [':' \d ** 1..5]?
        }

        rule username {
            <[0..9a..zA..Z_]>+
        }

        rule TOP {
            <log_login> | <log_logout>
        }

        rule log_login {
            <timestamp> <.ws> '[INFO]' <.ws> <username> <.ws> '[/' <ipaddress> ']' <.ws> 'logged in with' .*
        }

        rule log_logout {
            <timestamp> <.ws> '[INFO]' <.ws> [<username>|'/' <ipaddress>] <.ws> ['[/' <ipaddress> ']']? <.ws> 'lost connection:' .*
        }
    }

Actionsを書く

parseメソッドのactionsにクラスまたはインスタンスを指定することで、各部分マッチ成功時にフックするメソッドを設定することができます。ステートレスな処理ではクラスを指定すればいいですが、今回はログを解析しながらプレイ時間の集計を取るので、インスタンスを指定しましょう。

(正直なところ、私はActionsを使った場合の綺麗なクラス設計がよくわかりません。ステートフルな処理を行いたいとい、私が設計するとselfを渡すかグローバルな変数を使うかのどちらかになってしまいます。なんか上手いアイデアがあったら教えて下さい。)

10/25追記

とりあえずMinecraft::ServerLogクラスを作ります。また、各ユーザごとの情報を保持するためにUserクラスも作りましょう。

class Minecraft::ServerLog {
    has $.logfile;
    has %.users is rw;

    method new (Str $filename where { $_.IO.f }) {
        return self.bless(*, logfile => $filename);
    }
}

class User {
    has Int $.count is rw;
    has Int $.time is rw;
    has DateTime $.lastlogin is rw;
    method new () {
        return self.bless(*, count => 1, time => 0);
    }
}

また、ActionsとしてMinecraft::ServerLog自身を使うのですが、ここでは見通しを良くするためにroleに分離します。今回フックしたいのはlog_loginとlog_logoutだけなので、これらのメソッドを定義しましょう。ぶっちゃけこれくらいだとActionsを使うメリットは薄いのですが、ログから引っ張りたい行の種類(エラー、警告、etc...)が増えた場合に、どの種類がマッチしたのかを判定する部分を記述する必要がなくなります。

role Minecraft::ServerLog::Actions {
    method log_login ($/) {
        my $name = $<username>.Str.chomp;
        if self.users{$name} -> $user {
            $user.count++;
        } else {
            self.users{$name} = User.new;
        }
        my $time = $<timestamp>;

        my $dt = DateTime.new(year => $time<year>.Int, month => $time<month>.Int, day => $time<day>.Int,
                              hour => $time<hour>.Int, minute => $time<minute>.Int, second => $time<second>.Int);
        self.users{$name}.lastlogin = $dt;
    }

    method log_logout ($/) {
        my $name = $<username>.Str.chomp;
        if self.users.exists($name) && self.users{$name}.lastlogin.defined {
            my $time = $<timestamp>;
            my $dt = DateTime.new(year => $time<year>.Int, month => $time<month>.Int, day => $time<day>.Int,
                                  hour => $time<hour>.Int, minute => $time<minute>.Int, second => $time<second>.Int);
            self.users{$name}.time += ($dt.Instant.Int - self.users{$name}.lastlogin.Instant.Int);
        }
        self.users{$name}.lastlogin = DateTime;
    }
}

内容としては、ログイン時にはタイムスタンプから時刻を取得してlastloginに記録する、ログアウト時には時刻をlastloginと比較してtimeに記録するという単純なものです。

これを先ほどのMinecraft::ServerLogクラスにmixinしましょう。後は一行ずつログファイルを読み込んでGrammarに食わせるだけです。

class Minecraft::ServerLog {
    also does Minecraft::ServerLog::Actions;

    ...

    method get_user_list {
        my Int $now;
        for $!logfile.IO.lines -> $line {
            print "\rproccessing line " ~ $now++;
            Minecraft::ServerLog::Format.parse($line, actions => self);
        }
        say "\nAnalyze completed...\n";
    }
}

あとは、集計が終わったユーザごとのログイン時間を表示するだけですね。今までのコードをまとめると、以下のようになります。

2012-07-10

TeXでマクロを書いたときにハマった落とし穴と這い出し方

| 14:32

序文

テーブルトークRPGのシナリオを記述するにあたって、ツールとしてTeXを使うことにした。これに伴って初めてTeXマクロを記述することになったのだが、予想以上に散々引っかかって痛い目にあった。この記事は更なるTeX犠牲者を出してはならないという強い決意を示すために記すものであり、また近い将来に自分がTeXの毒牙に掛かりそうになった時のための覚え書きでもある。


目的/方針

テーブルトークにおいて、ある行動の成否を判定する際にサイコロを振り、出目とキャラクターの能力値を比較して結果を決定するという操作を多く行う。これについての記述を簡略化するマクロ - judge環境を作りたい。


具体的には、

\begin{judge}{能力名}
 \item 結果A
 \item 結果B
\end{judge}

と記述すると

■判定 - 能力名

 成功 
      結果A
 失敗 
      結果B

のように表示させるマクロを実装することを目標とする。


judge環境の作成

begin-endの対応で記述する命令(これを「環境」と呼ぶ)を新たに作成するには、\newenvironment命令を使用する。judge環境を例とした使い方は以下のとおり。

\newenvoironment{judge}[1]{\beginを置き換える文字列}{\endを置き換える文字列}

カギ括弧で括られた1は引数の数であり、マクロ中ではn番目の引数を#nのように参照する。


まずは\begin句を置き換える部分の記述である。上から数えた\itemの番号で表記を変えたいので、ここではenumerate環境をベースに実装を行うことにする。

とりあえずシンプルに以下のように書いてみる。パーセント記号は改行文字をコメントアウトするために必要らしいので、適宜読み飛ばして欲しい。

\newenvironment{judge}[1]%
{\begin{enumerate}}%
{\end{enumerate}}

これでenumerate環境と同等の挙動をするjudge環境が作成された。これに、「■判定 - 能力名」を表示させる機能を付加する。ここでは\paragraphを用いて表示を行うことにする。

\newenvironment{judge}[1]%
{\paragraph{判定 - #1}%
 \begin{enumerate}%
}%
{\end{enumerate}}

これでenumerate環境の上にキャプションのような形で判定についての説明が表示されるようになった。


enumerate環境の改造

このままでは、各itemの前に表示されるのは通常のenumerate環境同様にインデックス番号である。これを任意の文字列に変更するためには以下の様な命令を用いる。\defは\newcommandと同様に新しい命令を定義する命令である。この二つの違いはいくつかあるが、ここでは割愛する。

\def\labelenumi{任意の文字列}

\labelenumiは、enumerate環境の1段目のラベルに対応する命令である。入れ子にして段数を深くしていく際は\labelenumii、\labelenmuiiiのように最後のiを重ねていく。また、この「任意の文字列中」では\theenumiという命令でインデックス番号が参照できる。こちらについても同様に、最後のiの数が入れ子の段数に対応している。

たとえばenumerate環境の一段目のラベルを丸括弧付きのインデックス番号にしたい場合は以下のように記述すれば良い。

\def\labelenumi{(\theenumi )}

今回は\theenumiが1の時に「成功」、2の時に「失敗」と表示させたいので、条件分岐を行う必要がある。


条件分岐

TeXでは\ifなどの命令を用いて条件分岐を行うのだが、これは他のプログラミング言語と比較してあまりに貧弱であるため、パズルのような思考を求められる。幸い今回は単純な目標であるので、ある数値が奇数かどうかを判定する\ifodd命令を使うことにする。使い方は以下のとおり。

\ifodd 数値 奇数の場合の処理 \else 偶数の場合の処理 \fi

これをjudge環境のbeginを置き換える部分に記述すれば良い。視認性を上げるため、「成功」「失敗」の文字はタイプライタ体にした。

\newenvironment{judge}[1]%
{\paragraph{判定 - #1}%
 \def\labelenumi{\ifodd \theenumi \tt{成功} \else \tt{失敗} \fi}%
 \begin{enumerate}%
}%
{\end{enumerate}}

このままでは、このマクロが呼ばれたあとのenumerate環境がすべて壊れてしまうため、endを置き換える部分で元の定義に戻してやる必要がある。通常のスタイルのままなら、以下のようにすれば良いだろう。

\newenvironment{judge}[1]%
{%
 \paragraph{判定 - #1}%
 \def\labelenumi{\ifodd \theenumi \tt{成功} \else \tt{失敗} \fi}%
 \begin{enumerate}%
}%
{%
 \end{enumerate}%
 \def\labelenumi{\theenumi}%
}

itemの拡張

大体の機能は実装し終えたが、このままでは「成功」「失敗」のあとに直接文字列が続いてしまい、(個人的な好みを言えば)改行があったほうが見やすくなると思う。インデックス番号のあとに改行を入れる方法については少しググると出てくるので説明は割愛するが、概ね以下の様なやり方が主流であるらしい。

\item \mbox{}\\ 文字列

つまり、\item命令を「\item \mbox{}\\」という命令列で置き換えるようにすれば良い。既存の命令を置き換えるには\renewcommand命令を使用する。使い方は以下のとおり。気をつける点としては、\newenvironmentでは定義する環境にバックスラッシュなり円記号は必要なかったが、\renewcommandでは必要なので注意されたい。

\renewcommand{\item}{置き換え後の命令列}

この\renewcommandを使って\itemを次のように定義したとする。

\renewcommand{\item}{\item \mbox{}\\}

素直に考えればこれで動いて欲しいところだが、残念ながらこれは動かない。TeXマクロが展開されるのはそのマクロが出現した時点なので、その時にはマクロ中の\itemの定義もすでに置き変わっている。TeXはこれを再帰的に展開しようとして、(処理系にもよるが)5000程度でスタックオーバーフローを引き起こすだろう。


これを避け、既存の命令それ自身を使ったマクロを同名で定義する際には、\letを用いる。これはある命令に別の名前をつける命令である。これを用いて元々も\itemの定義を保持しておき、マクロ本体の中ではこちらを使うようにすることで無限再帰を回避することができる。具体的には以下のように記述する。

\let\origitem\item
\renewcommand{\item}{\origitem \mbox{}\\}

これで\itemの定義を置き換えることができた。また、これを元に戻すには以下のようにする。

\renewcommand{\item}{\origitem}

まとめ

ここまでの内容をまとめると、judge環境の定義は以下のようになる。

\newenvironment{judge}[1]%
{%
 \paragraph{判定 - #1}%
 \let\origitem\item%
 \renewcommand{\item}{\origitem \mbox{}\\}%
 \def\labelenumi%
 {%
  \ifodd \theenumi \tt{成功} \else \tt{失敗} \fi %
 }%
 \begin{enumerate}%
}%
{%
 \end{enumerate}%
 \def\labelenumi{\theenumi}%
 \let\item\origitem%
}

また、この記事で扱った内容を以下にまとめる。

  • begin-endで記述する環境は\newenvironmentで定義する
  • 通常の命令は\defもしくは\newcommandで定義する
  • enumerate環境のインデックス番号のスタイルを変更するには\labelenumiを使う
    • iの数で入れ子の深さに対応させる
    • \theenumiで現在のインデックス番号を参照できる
  • 条件分岐命令を用いて条件分岐を行う
    • \ifoddを用いて、ある数が奇数かどうかで分岐できる
    • 条件が真の場合は直後の命令列が、偽の場合は\else以降の命令列が呼ばれる
    • \fiで終端する
  • 既存の命令を拡張するには\renewcommandを用いる
    • 元々の命令をマクロに組み込むには\letを使って別名で定義を保存しておく必要がある

長々と綴ってきたが、もし私と同じようにマクロを書こうとして行き詰まった人にとって、この記事が何かしらの役に立てるのならば幸いである。

名無し名無し 2012/07/12 22:24 突然すみません。
\begin{enumerate}%
を一番初めに持って来れば、\end{enumerate}のあとに\labelenumiや\itemを元に戻す必要は無くなるのではないでしょうか?

通りすがりA通りすがりA 2012/07/12 22:48 いや、そもそも環境の定義なので、今の位置でも元に戻す必要はないですよ!

2012-06-07

HTML5 Canvas with JSX

| 04:21

JSXを触ってみる

Canvasのリッチな機能を使って描画するのはid:xaicronさんが既にやっていたので、私はピクセルをゴリゴリ描画する低レイヤーな部分を触ってみる。

あんまり面白いテーマが思いつかなかったので、とりあえずマンデルブロ集合を描画することに。どの程度最適化されるのかが気になったので、あまり手でコードは崩さずに安直で素直な実装を心がけた。


ちなみに、同じくJSXを使ってWebGLを触るのをid:santarhくんがやっていたので、こっちも見ると良いかもしれない。


JSXでWebGL - Santarh.mm


デモ

ソースはデモページに併記してある。Canvasの高さを基準に虚数軸をiから-iまで計算しているので、幅を高さの二倍程度に設定すると綺麗に全体が描画されるはず。

初期値だと1秒程度で描画されるが、あまりに大きな数字を設定するとブラウザが死ぬので注意*1

f:id:Vieno:20120608044810p:image


demo:

Mandelbrot Set on HTML5 Canvas with JSX


思ったこと

私はJavaScript弱者だけど、JSXは寧ろJavaに近い感じでサクサク書けたので、習得コストは低めで手軽という印象。極力JavaScriptは書きたくないので今後お世話になるかも。


Optimization

面白かった部分は複素数演算周り。今回はオーバーヘッドを気にせず、素直に複素数型を実装して演算メソッドとして定義したのだけれども、JavaScriptコンパイルした際に全てインライン展開されてメソッド呼び出しがゴッソリ消えていた。この辺のオプティマイズは良くできてるなーという印象。


JSX:

for (var i = 0; i < this.count; ++i) {
    k = k.mul(k).plus(new Complex(x, y));
    if (k.abs() > 2) {
	return i;
    }
}

Compiled JavaScript:

for (i = 0; i < this.count; ++ i) {
    this$0 = new Complex$NN(k.real * k.real - k.im * k.im, k.im * k.real + k.real * k.im);
    c$0 = new Complex$NN(x, y);
    k = new Complex$NN(this$0.real + c$0.real, this$0.im + c$0.im);
    if (Math.sqrt(Math.pow(k.real, 2) + Math.pow(k.im, 2)) > 2) {
    	 return i;
    }
}

JS側から呼ぶ

逆に面倒だった部分は、JSXで記述された関数JavaScript側から呼ぼうとするときに名前が分からないこと。JSXでは関数オーバーロードができるので、コンパイルされたJavaScript関数は名前にポストフィクスを付けて型ごとの実装を区別しているようだ*2。途中で方針を変えて_Main.main()の引数を増やしたら唐突に動かなくなって焦った。これはコンパイルされたJavaScriptを読まないと分からないような気がするけど、実は私が知らないだけでスマートなやり方が用意されているのかもしれない。

JSXレベルの関数名を引数に渡したら、適合する型の実装をディスパッチしてくれるような機能があれば便利な気がする。


型宣言

これは仕方ないことかもしれないけど、高階関数を定義するときの型宣言があまりに煩雑に成ってる感じが。具体的にどう書けるようになれば嬉しいかは分からないけど、現行のはちょっとなぁ、という風に感じた。


/* 引数にMandelbrot型を受け取り、返り値として
  「引数にint型を受け取り、返り値としてint型の配列を返す関数」
  を返す関数 */
static function schema (m: Mandelbrot) : function(:int) : Array.<int> {
       ...
}

最後に

これはJSXが優秀なのかブラウザが優秀なのかわからないけど、思ったよりCanvasが高速で描画してくれるのが面白い。スマートフォンでもちゃんと動くし。

むしろデモページ作るためのJavaScript書くのが一番大変だった気がする。別ファイルのソースコードを引っ張ってくる方法がよくわからなくて、結局ググってXHRで書いた。JavaScript難しい…

とりあえずCanvasに絵が書けたので、次はアニメーションに挑戦してみよう。


宣伝

7/7に筑波大学でTsukuba.pmやります。今回もゲストにid:gfxさんをお呼びしているので、JSXのお話が盛り上がると思います(私も準備できればJSXネタで一本やるかも)。別に話題はPerlやJSXに限定しないのでガンガン発表しましょう、現在ぜんぜん発表者が足りないので絶賛募集中です。


Tsukuba.pm #2

*1:私の環境だと、20000x10000で実行するとChromeが死に、15000x7500くらいまでなら数分で描画される

*2:この場合だと、mainはintを2つ取るので_Main.main$II()という名前で呼び出す必要がある。

2011-12-27

With iPhone, No Music.

| 21:34

先に言っておこう。私はiPhoneが大嫌いだ


私はiPhone4を所有している。電話や出先でのメールはもちろん、音楽を聴くのも大体はこのiPhoneを使っている。(iOS5で多少マシになったとは言え)フルブラウザを名乗るのが烏滸がましいとさえ思える「Safari」の役立たずっぷりや、煩雑でレスポンスの遅い「設定」、本当にアプリを買わせる気があるのか疑わしい「App Store」のゴミ同然の使い勝手にはこの際目をつぶろう。


だが「ミュージック」、お前はダメだ。


全体的にプリインストールアプリは出来が悪いとは言え、本当に「ミュージック」だけは許せない。使えば使うほどに嫌いになっていく。私が何故こんなに苛ついているか、これから愚痴を吐き出すような気持ちで書き綴っていこうと思う。


プレイリスト

iTunesのプレイリストの並び順はスマートプレイリストが上に来るけど、iPhoneでは関係無しに名前順なので「最近追加した項目」が遠くて困る。どうしてUIのこういう所を母艦と統一しないのか。しかも、iTunesの名前順は[記号>数字>ABC>ひら/カタ>漢字]なのに、iPhoneは[ひら/カタ>ABC>記号>数字>漢字]という不可解な順番で尚更理解に苦しむ。


スマートプレイリストを一番下までスクロールした時、最後の曲の下にプレイリストの曲数が表示されるのに、どうして普通のプレイリストだと何も表示されないんだろう?プレイリストに何曲入ってて何分間再生されるかをiPhone上で知る方法は用意されていない。どうしてプレイリストの一番下に表示しないんだ?


「プレイリストを追加」のUIも多分に漏れず馬鹿げている。曲を追加するボタン(しかも別にそのボタンでなく曲名をタップしても普通に追加できる、何のためにあるのか判らない)がリストの右側を占拠している所為で、頭文字のシークバーが使えなくなり、下の方の曲を入れるには凄まじい回数スクロールする必要がある。また、アーティスト>「アーティスト名」と遷移した際、今まで「全ての曲」だった部分が「全ての曲を追加」に変わっているのが本当に腐っている。クソ使い辛い検索機能には触りたくないからアーティスト別のリストを使おうとしてるのに、どうしてアーティストの曲を全表示出来ないのか。何十枚もアルバム出してるアーティストの、ある曲がどのアルバムに収録されていたかを全て憶えろというのだろうか。それならスマートフォンなんか使う必要無いんじゃないか?


カバーフロウ

「ミュージック」で横向きにすると強制的にカバーフロウ表示になるのも謎。プレイリストを表示中にカバーフロウ画面にしてもiPhoneに入ってる曲全てが表示されるし、これもiTunesと挙動が違う。カバーフロウが嫌でiPhoneの向きをロックすると、今度はSafari使ってる時に文字が小さくて困る。どうしてカバーフロウがペインとして独立してないのか意味不明。


曲リスト

曲リストのUIも酷い出来で、どうして曲名の下に出る曲情報が[アルバム名 - アーティスト名]なのか理解できない。長い名前のオムニバスとかだとアーティストの名前が入り切らなくて、誰が歌ってるのか知りたい時に困る。常識的に考えてアーティストの名前の方がアルバム名より短いのだから、どちらも表示できる様にアーティスト名を前に表示すべきではないか?

アルバム

「アルバム」ペインで、「コンピレーションの一部」オプションを付けてない複数のアーティストの曲が収録されているアルバムがアーティスト毎に分割されてしまうのも馬鹿馬鹿しい。何のために「アーティスト」ペインがあると思っているのか。オプションを付けわすれたアルバム、どうやって全曲通して聞けば良いんだ?そのためだけに新しいプレイリストを作るのか?

まとめ

他のApple製品と比べても、iPhoneの圧倒的な出来の悪さは目を覆いたくなるものがある。知人には「Androidは音楽プレイヤー機能に力を入れてないから、まだiPhoneはマシ」と言われた事もあるが、そういう問題じゃないだろう。残念ながら私はそんな寛容な心は持ち合わせていないので、毎日iPhoneを使うたびにイライラが溜まっていく。iPhoneの開発陣が本当にiPhoneを毎日楽しんで使っているのか、疑問が残る所である。

2011-12-25

Merry Christmas!

| 23:59

この記事はPerl6 Advent Calender 2011、25日目の記事です。


ようやくここまで続いてきたAdvent Calendarも今日で終わりです。最終日は、今まで解説してきた文法を使って適当なプログラムを書いてみます。


(某学類誌に掲載した記事とあんまり変わらない内容です、そちらを先に読まれた方にはごめんなさい)

目標

次の仕様を満たすGraphクラスの実装を目指します。

  • 関数を渡して、そのグラフをビットマップで出力する
    • 実数を1つ渡して、実数が返ってくる関数(例: y = sin(x))
    • 実数を2つ渡して、真理値が返ってくる関数(例: x**2 + y**2 < 1)
  • 関数に定義域を指定できるようにする

Bitmapへの出力は拙作のBitmapクラス継承します。用意されている機能は以下の通り。

method new (Int $width where { $_ > 0 } , Int $height where { $_ != 0 })
# コンストラクタ。引数には出力する画像のピクセル数を指定

method write (Str $file)
# 引数のファイル名への書き出し

method getpixel (Int $x where { 0 <= $_ < $!header.width },
  	 	   Int $y where { 0 <= $_ < $!header.height.abs })
# 指定した座標の色情報を取得

method setpixel (Int $x where { 0 <= $_ < $!header.width },
  	 	   Int $y where { 0 <= $_ < $!header.height.abs },
                   Int $b where { $_ ~~ 0..255 } , Int $g where { $_ ~~ 0..255 }, Int $r where { $_ ~~ 0..255 })
# 指定した座標の色情報を設定

method fill (Int $b where { $_ ~~ 0..255 }, Int $g where { $_ ~~ 0..255 }, Int $r where { $_ ~~ 0..255 }) 
# 画像全体を指定した色で塗りつぶす

実装

属性

まず描画する関数を保持する配列を用意する必要があります。また、描画する範囲を設定するための変数も用意しましょう。

has Code @.funcs
# 設定された関数を保持

has $.x_limit is rw = 1
has $.y_limit is rw = 1
# 画像端の座標を保持
# デフォルト値は1
関数の設定

描画する関数を受け取って格納するためのメソッドです。


このプログラム二次元のグラフしか対象としないので、設定する関数が受け取る引数の数を1つ、または2つに制約します。関数オブジェクト引数の個数を取得するにはarityメソッドを使います。

method set_func (Code $f where { $_.arity == 1|2 }) {
    @!funcs.push($f);
}

関数の描画

グラフの前に、座標軸を描画したい気がするので、オプションで動作を変えられる様にしましょう。この関数では画像を白で塗りつぶすだけで、実際の処理は別の関数でやります。

method draw (:$axis?) {
    self.fill(255,255,255);

    if $axis {
        self.draw_axis;
    }

    for @!funcs {
        self.calc($_);
    }
}

とりあえず軸の描画の部分はこんな感じで。高さと幅の中間を取って、ピクセルのデータを上書きしていきます。

method draw_axis {
    my $center_x = ($!header.width / 2).Int;
    my $center_y = ($!header.height / 2).Int;

    for ^$!header.height {
        self.setpixel($center_x, $_, 128, 128, 128);
        # グラフと被ると見辛いので灰色
    }

    for ^$!header.width {
        self.setpixel($_, $center_y, 128, 128, 128);
    }
}

次は実際の描画部分です。引数が1つの関数と2つの関数で処理を分けたいので、multi methodを使います。まずは1つの引数を取る方から。


コード中のコメントにある通り、座標を計算して関数に渡すという素直な実装になっています。この関数のキモはCATCHを使った例外処理で、描画した関数にwhereで定義域が設定してある場合、その外での呼び出しは例外が発生しますが、それを無視して次の座標に進むようになっています。

multi method calc (Code $f where { $_.arity == 1}) {
    my @val;
    my $width = $!header.width;
    my $height = $!header.height;

    for ^$width {
        my $x = -$!x_limit + (1 / ($width) * 2 * $!x_limit) * $_;
	# 画像上の座標をグラフ上の座標に変換

        @val = ($f($x.Num)).list;
	# x座標を関数に渡してy座標を計算

	CATCH { next; }
	# 定義域外での呼び出しエラーをキャッチ

        for @val -> $y {
            my $h = ($height / 2 + ($y * $height / (2 * $!y_limit))).Int;
            self.setpixel($_, $h, 0, 0, 0) if 0 <= $h < $height;
	    # y座標が画像の中に入っていればsetpixelに渡す
        }
    }
}

2引数関数は、x座標とy座標を渡した時に条件を満たすかどうかを返すものとします。画像上の座標とグラフ上の座標がきちんと対応しないため、線のグラフは殆ど描けませんが、「範囲に入るかどうか」という関数だと大体綺麗に描けます。

multi method calc (Code $f where { $_.arity == 2 }) {
    my $width = $!header.width;
    my $height = $!header.height;

    for ^$width -> $w {
        my $x = -$!x_limit + (1 / ($width) * 2 * $!x_limit) * $w;

        for ^$height -> $h {
            my $y = -$!y_limit + (1 / ($height) * 2 * $!y_limit) * $h;
             self.setpixel($h, $w, 0, 0, 0) if $f($x, $y);

            CATCH { next; }
        }
    }
}
ソースコードまとめ

今まで書いてきたメソッドを纏めると、こんな感じになります。

use v6;
use Bitmap;

class Graph is Bitmap {

    has Code @.funcs;
    has Header $.header;
    has $.x_limit is rw = 1;
    has $.y_limit is rw = 1;

    method set_func (Code $f where { $_.arity == 1|2 }) {
        @!funcs.push($f);
    }

    method draw (:$axis?) {

        self.fill(255,255,255);

        if $axis {
            self.draw_axis;
        }

        for @!funcs {
            self.calc($_);
        }
    }

    method draw_axis {
        my $center_x = ($!header.width / 2).Int;
        my $center_y = ($!header.height / 2).Int;

        for ^$!header.height {
            self.setpixel($center_x, $_, 128, 128, 128);
        }

        for ^$!header.width {
            self.setpixel($_, $center_y, 128, 128, 128);
        }
    }

    multi method calc (Code $f where { $_.arity == 1}) {
        my @val;
        my $width = $!header.width;
        my $height = $!header.height;

        for ^$width {
            my $x = -$!x_limit + (1 / ($width) * 2 * $!x_limit) * $_;
            # 画像上の座標をグラフ上の座標に変換

            @val = ($f($x.Num)).list;
            # x座標を関数に渡してy座標を計算

            CATCH { next; }
            # 定義域外での呼び出しエラーをキャッチ

            for @val -> $y {
                my $h = ($height / 2 + ($y * $height / (2 * $!y_limit))).Int;
                self.setpixel($_, $h, 0, 0, 0) if 0 <= $h < $height;
                # y座標が画像の中に入っていればsetpixelに渡す
            }
        }
    }

    multi method calc (Code $f where { $_.arity == 2 }) {
        my $width = $!header.width;
        my $height = $!header.height;

        for ^$width -> $w {
            my $x = -$!x_limit + (1 / ($width) * 2 * $!x_limit) * $w;

            for ^$height -> $h {
                my $y = -$!y_limit + (1 / ($height) * 2 * $!y_limit) * $h;
                self.setpixel($h, $w, 0, 0, 0) if $f($x, $y);

                CATCH { next; }
            }
        }
    }

}

newメソッドなんかはBitmapクラスのをそのまま流用してますので、そちらをご覧ください。

使ってみる

newに画像のサイズを渡した後は、set_funcで関数を設定します。drawで描画を実行して、writeにファイル名を指定して書き出しですね。

my $g = Graph.new(200,200);
$g.set_func: *.sin;
$g.x_limit = 2;
$g.y_limit = 2;
$g.draw(:axis).Str;
$g.write("graph.bmp");

f:id:Vieno:20111225234041j:image

my $g = Graph.new(200,200);
$g.set_func: do -> $x, $y { $x ** 2 + $y ** 2 < 0.3 };
$g.x_limit = 2;
$g.y_limit = 2;
$g.draw(:axis).Str;
$g.write("graph.bmp");

f:id:Vieno:20111225234104j:image

どちらも上手く動いているようです。

まとめ

さて、ようやく12月のカレンダーに全て穴があきました。人数の少ない中、この無茶な企画に付き合って頂いたyoshimuraくん、uasiさん、ぜっぱちさん、risouさん、そして(居るか判らないけど)読んで頂いた読者の皆さん、本当にありがとうございました。特にuasiさんには私の記事の中で間違っている箇所を幾つも指摘して頂き、とても助かりました。重ねて感謝を。


このAdvent CalendarでPerl6に興味を持ってくれた方が1人でも居れば、無理言って立ち上げた甲斐があったかと思います。

Perl6の世界はまだ始まったばかりです。これから開発が進めば、もっと素敵で刺激的な大地が待っている事でしょう。まだ見ぬPerl6の魅力、これからも一緒に見届けて行きませんか?


Merry Christmas for all Perl6 Mongers!!


おまけ

今年もPerl6のコーディングコンテストが行われるようです。優勝者には100ユーロ相当の書籍が贈られるようですので、皆さん奮って参加しましょう!