Perl6のGrammarでサーバログをパースする
何がしたいか
某編集部で運用中のマインクラフトサーバのログを解析して、プレイ時間の一番長い廃人を算出したい。折角なので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"; } }
あとは、集計が終わったユーザごとのログイン時間を表示するだけですね。今までのコードをまとめると、以下のようになります。
TeXでマクロを書いたときにハマった落とし穴と這い出し方
序文
テーブルトーク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を使って別名で定義を保存しておく必要がある
長々と綴ってきたが、もし私と同じようにマクロを書こうとして行き詰まった人にとって、この記事が何かしらの役に立てるのならば幸いである。
HTML5 Canvas with JSX
JSXを触ってみる
Canvasのリッチな機能を使って描画するのはid:xaicronさんが既にやっていたので、私はピクセルをゴリゴリ描画する低レイヤーな部分を触ってみる。
あんまり面白いテーマが思いつかなかったので、とりあえずマンデルブロ集合を描画することに。どの程度最適化されるのかが気になったので、あまり手でコードは崩さずに安直で素直な実装を心がけた。
ちなみに、同じくJSXを使ってWebGLを触るのをid:santarhくんがやっていたので、こっちも見ると良いかもしれない。
デモ
ソースはデモページに併記してある。Canvasの高さを基準に虚数軸をiから-iまで計算しているので、幅を高さの二倍程度に設定すると綺麗に全体が描画されるはず。
初期値だと1秒程度で描画されるが、あまりに大きな数字を設定するとブラウザが死ぬので注意*1。
思ったこと
私は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に絵が書けたので、次はアニメーションに挑戦してみよう。
With iPhone, No Music.
先に言っておこう。私はiPhoneが大嫌いだ。
私はiPhone4を所有している。電話や出先でのメールはもちろん、音楽を聴くのも大体はこのiPhoneを使っている。(iOS5で多少マシになったとは言え)フルブラウザを名乗るのが烏滸がましいとさえ思える「Safari」の役立たずっぷりや、煩雑でレスポンスの遅い「設定」、本当にアプリを買わせる気があるのか疑わしい「App Store」のゴミ同然の使い勝手にはこの際目をつぶろう。
だが「ミュージック」、お前はダメだ。
全体的にプリインストールのアプリは出来が悪いとは言え、本当に「ミュージック」だけは許せない。使えば使うほどに嫌いになっていく。私が何故こんなに苛ついているか、これから愚痴を吐き出すような気持ちで書き綴っていこうと思う。
プレイリスト
iTunesのプレイリストの並び順はスマートプレイリストが上に来るけど、iPhoneでは関係無しに名前順なので「最近追加した項目」が遠くて困る。どうしてUIのこういう所を母艦と統一しないのか。しかも、iTunesの名前順は[記号>数字>ABC>ひら/カタ>漢字]なのに、iPhoneは[ひら/カタ>ABC>記号>数字>漢字]という不可解な順番で尚更理解に苦しむ。
スマートプレイリストを一番下までスクロールした時、最後の曲の下にプレイリストの曲数が表示されるのに、どうして普通のプレイリストだと何も表示されないんだろう?プレイリストに何曲入ってて何分間再生されるかをiPhone上で知る方法は用意されていない。どうしてプレイリストの一番下に表示しないんだ?
「プレイリストを追加」のUIも多分に漏れず馬鹿げている。曲を追加するボタン(しかも別にそのボタンでなく曲名をタップしても普通に追加できる、何のためにあるのか判らない)がリストの右側を占拠している所為で、頭文字のシークバーが使えなくなり、下の方の曲を入れるには凄まじい回数スクロールする必要がある。また、アーティスト>「アーティスト名」と遷移した際、今まで「全ての曲」だった部分が「全ての曲を追加」に変わっているのが本当に腐っている。クソ使い辛い検索機能には触りたくないからアーティスト別のリストを使おうとしてるのに、どうしてアーティストの曲を全表示出来ないのか。何十枚もアルバム出してるアーティストの、ある曲がどのアルバムに収録されていたかを全て憶えろというのだろうか。それならスマートフォンなんか使う必要無いんじゃないか?
カバーフロウ
「ミュージック」で横向きにすると強制的にカバーフロウ表示になるのも謎。プレイリストを表示中にカバーフロウ画面にしてもiPhoneに入ってる曲全てが表示されるし、これもiTunesと挙動が違う。カバーフロウが嫌でiPhoneの向きをロックすると、今度はSafari使ってる時に文字が小さくて困る。どうしてカバーフロウがペインとして独立してないのか意味不明。
曲リスト
曲リストのUIも酷い出来で、どうして曲名の下に出る曲情報が[アルバム名 - アーティスト名]なのか理解できない。長い名前のオムニバスとかだとアーティストの名前が入り切らなくて、誰が歌ってるのか知りたい時に困る。常識的に考えてアーティストの名前の方がアルバム名より短いのだから、どちらも表示できる様にアーティスト名を前に表示すべきではないか?
アルバム
「アルバム」ペインで、「コンピレーションの一部」オプションを付けてない複数のアーティストの曲が収録されているアルバムがアーティスト毎に分割されてしまうのも馬鹿馬鹿しい。何のために「アーティスト」ペインがあると思っているのか。オプションを付けわすれたアルバム、どうやって全曲通して聞けば良いんだ?そのためだけに新しいプレイリストを作るのか?
Merry Christmas!
この記事は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にファイル名を指定して書き出しですね。
- 1引数の場合
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");
- 2引数の場合
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");
どちらも上手く動いているようです。
まとめ
さて、ようやく12月のカレンダーに全て穴があきました。人数の少ない中、この無茶な企画に付き合って頂いたyoshimuraくん、uasiさん、ぜっぱちさん、risouさん、そして(居るか判らないけど)読んで頂いた読者の皆さん、本当にありがとうございました。特にuasiさんには私の記事の中で間違っている箇所を幾つも指摘して頂き、とても助かりました。重ねて感謝を。
このAdvent CalendarでPerl6に興味を持ってくれた方が1人でも居れば、無理言って立ち上げた甲斐があったかと思います。
Perl6の世界はまだ始まったばかりです。これから開発が進めば、もっと素敵で刺激的な大地が待っている事でしょう。まだ見ぬPerl6の魅力、これからも一緒に見届けて行きませんか?
Merry Christmas for all Perl6 Mongers!!
おまけ
今年もPerl6のコーディングコンテストが行われるようです。優勝者には100ユーロ相当の書籍が贈られるようですので、皆さん奮って参加しましょう!
左辺値代入
この記事はPerl6 Advent Calendar 2011、22日目の記事です。
さて、記事が落ちたのも二回目ですが、まぁ何とかなるでしょう。明日はクリスマスなので奇跡が起きるに違いない。
今日紹介するのは左辺値代入です。
左辺値代入とは
読者の方が全員ご存知かどうかは解りませんが、Perl5のsubstr関数は次のような書き方が可能でした。
my $s = "abcdefg"; substr($s, 2, 4) = "123"; say $s; #> ab123fg
初めて見る方には奇妙に見えるかもしれませんが、substr関数は左辺値特性を持つため、代入式の左辺に置く事が出来ます。substrの場合、代入式の右辺を第4引数に与えた時と同じ挙動を示します(「インデックスの2..4番目に対して代入を行う」と読めば解りやすいかもしれません)。
このような左辺値代入の仕組みは複雑で、Perl5でこのような関数を定義するのは少々面倒でした。定義する方法は憶えてないので適当にググってもらうとして、これがどういう場面で役立つかを考えてみましょう。
と言っても答えは簡単で、オブジェクトの属性への代入に使います。Perl5のMooseの場合、標準的には属性への値のセットは次のような書き方で実現していました。
- アクセサメソッドに引数を与えない場合は値を返す
- アクセサメソッドに引数を与えた場合は値を格納する
しかし、Perl6では属性への値のセットは直接代入するという文法になっていますよね。これに左辺値代入を使っているのです。つまり、属性への書き込みに自動で作られるアクセサではなく、自分で処理を挟みたい場合は左辺値代入を使ったアクセサを書く必要がある訳です。
左辺値代入できる関数を書いてみる
では実際にどうやって定義すれば良いのかを見てみましょう。例を単純にするため、左辺値代入された値を保持する関数を考えます。つまり、以下のような挙動になります。
- 右辺値として呼び出された場合は格納した値を返す(FETCH)
- 左辺値代入された場合は値を格納する(STORE)
では、実際にコードの例を示します。
sub lvalue_sub () is rw { state $val; return Proxy.new( FETCH => method { return $val; }, STORE => method ($rvalue) { $val = $rvalue; }); }
まず、この関数が左辺値代入可能な関数である事を示すis rwトレイトを指定します。
次に、この例では値を格納するための変数としてstate変数を用意しています。stateはmyと同じ様に変数宣言に用いるキーワードで、そのブロックの中で状態(state)が保持される静的な変数を宣言できます。
最後に、Proxyオブジェクトを返すようにすれば完成です。コンストラクタにFETCHとして渡している無名メソッドが格納する場合の処理、STOREとして渡している無名メソッドが呼ばれます。STOREメソッドには引数を1つ指定し、代入される右辺値はその引数に束縛されます。
実際に実行してみましょう。
lvalue_sub = 1; say lvalue_sub; #> 1 lvalue_sub = 2; say lvalue_sub; #> 2
正しく動いていますね。
まとめ
- is rwで左辺値特性を指定できる
- Proxyを返す
- FETCHで読み出し
- STOREで代入
ワンライナーのすすめ
この記事はPerl6 Advent Calendar 2011の20日目です。
残念ながら昨日の記事は落ちてしまいましたが、気にせず進めましょう。明日の記事を2本書けば問題無く25日まで進めますね!
ここまで私の担当では基本的な文法を紹介してきましたが、今回は少し趣向を変えて(ネタ切れとも言う)、標準ライブラリのStrクラスなどに付いて紹介してみようと思います。
Strクラスのメソッド
Perl6ではPerl5とは違い、文字列と数値は別のクラスで表現されています。標準のStrクラスだけでも簡単な文字列処理は問題無い程度にはメソッドが充実していますので、正規表現に頼らずとも文字列処理が行えます。これから紹介して行くメソッドを繋げていく事で、ワンライナーがラクラク書ける様になると思います。
comb
@Zeppachiさんの記事でも紹介されていましたが、文字列を先頭から舐めていって、引数に与えたパターンにマッチした部分をリストにして返します。引数無しで呼んだ場合は、文字列を1文字ずつに分割したリストが返ります。ワンライナーで文字列処理する場合は、combで分割した文字列をmapに食わせるというやり方がとても有効です。
join
これは殆どPerl5のそれと同じで、文字列リストを順に結合していきます。引数に文字列を与えることで要素間にデリミタを挟む事も出来るので、CSVなどの単純な構造のデータを書き出す際には重宝するでしょう。
fmt
呼び出し元の文字列を、引数に与えたテンプレートに変換して返します。sprintfみたいなものですね。10進<->16進の基数変換を手抜きしたり、combで切り出した数字の桁をゼロ埋めで揃える時に便利です。
flip
文字列を逆順にして返します。
Listクラスなどのメソッド
combで刻んだ文字列のリストを上手く扱うためのメソッドを紹介します。
map
Perl5でもお馴染みのmapです。引数に与えたブロックを要素に対して実行して行き、その結果のリストを返します。
pick
リストの中から引数に与えた数の要素をランダムに抜き出します。Whatever(*)を引数に与えた場合は全ての要素をランダムに抜き出すので、結果としてリストをシャッフルした結果を返します。
reverse
リストを逆順にして返します。
まとめ
ここに挙げた以外にも、痒い所に手が届くようなメソッドがたくさんありますので、「こういうメソッド無いかなー」という時にはmethodsメタメソッドを参照してみるのも良いでしょう。それでは、快適なワンライナー生活を!