ラベル ruby の投稿を表示しています。 すべての投稿を表示
ラベル ruby の投稿を表示しています。 すべての投稿を表示

2011年1月9日日曜日

mikutterをRuby1.9.1で動作させる

mikutterは、いままでRuby 1.8.7専用で開発してきた。
理由は、GUIツールキットとして採用しているRuby-Gnome2が、Ubuntuでは1.8用にビルドされたものしか用意されていないからだ。もし1.9を採用するなら、mikutterを使うためにRubyGtkをわざわざソースコードからビルドしなければならない。一方、1.8を採用すれば、apt-getで全て手に入る。

Ruby1.8と1.9では大きな変更があったが、両方で動くコードは充分に実現可能だ。そこで、今回はmikutterを両方で動くようにしてみた。

なぜ1.9を使うのか

では、1.9の追加機能を使わずにサポートするメリットとはなんなのか。主な理由は
  1. 速くなると思われる
  2. 1.8は古いので、いずれ1.9にシフトしていくだろうから
  3. Twitterでmikutterの開発協力者にそそのかされた
だ。とくに最後の要素が非常に大きい。
1.9系列の最新版は今のところ1.9.2だが、今回はubuntu 10.04で手に入る最新の1.9.1を使用する。

やりかた

1.9で動作させるには、trunkを使う必要がある。
参考サイトには10.10用と書いてあったが、10.04でも同じ手順で問題なかった。ただし、現在Ruby-Gtkはより新しいバージョンがリリースされている。
また、Ruby-GtkのフルセットとGtkのみが入ったパッケージがあるが、mikutterを動かすならGtkだけで良い。
加えて、READMEに書いてあるライブラリも1.9用のものを入れる必要がある。また、libhmac-sha1は1.9用のものが用意されていないので、gemでインストールする必要があった。
# apt-get install rubygems1.9.1
# gem1.9.1 install ruby-hmac
mikutterはrubyコマンドで起動するようになっているが、この方法ではruby1.9.1というコマンド名でインストールされる。そのため、
$ ruby1.9.1 mikutter.rb
のように起動しなければいけない。通常通り起動すれば、1.9.1をインストールした後でも1.8で動作させることができる。

どう変わったか

私の開発環境では、起動時間に圧倒的な差があった。1.8が23秒であったのに対し、1.9は7秒で起動できた。実に三倍である。
mikutterはリプライ元など取得に時間がかかる可能性のあるものは遅延して表示するが、起動時間にはこれは含まれていない。あくまでつぶやきが表示されるまでの時間だ。
しかし、その遅延して表示されるウィジェットもいずれも高速に表示され(見た感じ倍速程度)、非常に快適になった。
高速化のための遅延レンダリングが露骨に見えることも多かったが、1.9を使えばそれも殆ど見えなくなった。

今後

mikutterは非常に高度で複雑なウィジェットの配置をしている。UIを妥協なしに実装したいがためであったが、マシンパワーを異常に浪費してしまっていた。そこでCairoによるタイムラインのレンダリングを検討していたが、1.9を採用すれば充分に高速化されることが期待できるので、このまま様子を見ることにした。
更に改善を続ければ、1.9でも問題なく使えるようになるだろう。

2010年8月27日金曜日

Rubyでメソッド定義のときに引数の型をチェック

例えば、以下は二つの文字列引数xをとり、x self xを連結するメソッドsandのRubyによる定義。

class String
  def sand(x)
    x + self + x end end

これは一見うまく動くようだが、

"hello".sand("'") # => 'hello'

実は文字列を指定しなければエラーとなる。

'hello'.sand(1) # =>
# ~> -:2:in `+': String can't be coerced into Fixnum (TypeError)
# ~>    from -:2:in `sand'
# ~>    from -:5

原因は、xに渡された1(Integer)の+メソッドは、String型を渡すとTypeErrorを投げるから。文字と数字が足せないのは当たり前なので、妥当だといえる。
このようなエラーを実行前にトラップするために、静的型付けが有用だとされてきたが、事実Rubyではこの初歩的なバグは実行時ならないとわからない。
もちろん言語仕様上コンパイル時にエラーを上げることなど不可能なので、エラー処理をわざわざ入れるというのはよくやることだ。ここでは、sandを「エラー時にはnilを返す」ようにする(ここは、例外を投げるようにしてもいいかもしれないが、とりあえず)。

class String
  def sand(x)
    if(x.is_a?(String))
      x + self + x end end end

これで、Integerを引数に指定するとnilを返すようになる。しかし、このような煩雑な処理を毎回書いていると冗長になるし、オブジェクト志向言語では、型ではなくて「特定のメソッドが定義されていれば」など込み入った定義もしたくなる。そこで、これらをできるだけ簡単に判断できるようなユーティリティを書いてみた。

def type_check(args, &proc)
  check_function = lambda{ |val, check|
    if check.nil?
      nil
    elsif check.respond_to?(:call)
      check.call(val)
    elsif check.is_a? Array
      val.__send__(*check)
    elsif check.is_a? Module
      val.is_a?(check) end }
  error = args.find{ |a| not(check_function.call(*a)) }
  if(error)
    warn "argument error: #{error[0].inspect} is not passed #{error[1].inspect}"
    nil
  else
    proc.call if proc end end

関数type_checkは、すべてのチェックにパスしたら渡されたブロックを実行してその結果を返す関数。いずれかのチェックに失敗したら、エラーを出力してnilを返す。
チェックは、値 => 判定式 のような連想配列もしくは[[値,判定式],...]のような配列。
使える判定式は、

  • nil ... 常にパス
  • [symbol, *args] ... 値.send(symbol, *args)がtrueを返せばパス
  • lambda{|x|} ... 値を引数に取る無名関数(厳密にはcallメソッドが定義されているもの)。trueを返せばパス。
  • Module ... その型とis_a?関係ならパス

例えば以下のように使う。

type_check([[1 , Integer], [2 ,[:is_a?, Integer]]]){ :passed } # => :passed

これを利用すれば、sandは以下のように定義できる。

class String
  def sand(x)
    type_check(x => String) do
      x + self + x end end end

利点は、 x => Stringは、「xはString」のように自然言語に読み替えやすいことか。このままでは微妙なので、型チェックを伴う無名関数を定義するメソッドを定義してみる。

def tclambda(*args, &proc)
  lambda{ |*a|
    if proc.arity >= 0
      if proc.arity != a.size
        raise ArgumentError.new("wrong number of arguments (#{a.size} for #{proc.arity})") end
    else
      if -(proc.arity+1) > a.size
        raise ArgumentError.new("wrong number of arguments (#{a.size} for #{proc.arity})") end end
    type_check(a.slice(0, args.size).zip(args)){
      proc.call(*a) } } end

type_checked_lambdaは、lambdaとほぼ同じように使うが、ブロック以外に引数を取る。それは、各引数のチェック式になる。
例えば、配列aryの中の数を2倍にして返したい場合、以下のようにできる。

ary.map(&tclambda(Integer){ |x| x * 2 })

こうすれば、aryに数値以外がはいっていれば、そこはnilとなる。かなりすっきり書けた。
これがあれば、型をチェックするdefを定義することもでき、それを使えばsandの定義はかなりすっきりする。

class Object
  def self.defun(method_name, *args, &proc)
    define_method(method_name, &tclambda(*args, &proc)) end end

class String
  defun(:sand, Integer) do |x|
    x + self + x end end

このように、やや強引ではあるが、型を定義したメソッドを定義できた。できるだけこれでメソッドを定義すれば、エラー箇所がより明確になるかもしれない。

動機

関数、とくに自分の書いたものは信用してしまいがちで、この類のエラーは後を立たない。今回は、静的型付け言語の引数チェックを取り入れられないかという実験の一環でコードを書いてみた。
こういったメタなコードを書くたび、Lispへの憧れが深まっていく(笑)。

2010年7月24日土曜日

Rubyで遅延評価

いろんなところで既にやられているけど、自分でやってみたくなったので。
遅延評価とは、計算結果が実際に必要になるまで処理を遅延すること。

a = huge_proc()
b = large_proc()
print a

この仮想コードのhuge_procの呼び出しは、3行目のprintで出力されるまで遅延される。また、bは使われていないのでlarge_procは呼び出されない。
こういった仕組みがあれば、処理に時間がかかるが戻り値を使うかどうか分からない関数をムダに実行することがなくなるので、ある種の処理は非常に早くなる。これをRubyでやりたい。

実装

今回作ったユーティリティは、lazy{ 遅延させたい処理 }のように使い、戻り値として遅延オブジェクトを返す。
使い方は例えば、a = lazy{ x }と書けば、lazy自体はすぐに遅延オブジェクトを返し、aのメソッドが呼ばれたときに初めてxが実行される。

class Lazy
  def initialize
    @proc = Proc.new
    @obj = nil end

  def method_missing(method, *args, &block)
    if @proc
      @obj = @proc.call
      @proc = nil end
    @obj.__send__(method, *args, &block) end end

def lazy(&proc)
  Lazy.new(&proc) end

method_missingは、メソッドが無かった時に呼び出されるメソッドで、このメソッドはブロックの戻り値のオブジェクトのメソッドを呼び出す。これによって、遅延オブジェクトから実行結果のオブジェクトのメソッドを透過的に呼べる。

竹内関数によるベンチマーク

試しに、竹内関数(たらい回し関数)の遅延版と通常版と最適化版を作って、どちらも同じ引数(12,6,0)を指定して、速度の差を計測してみた。

def tak(x, y, z)
  if x <= y.to_i
    y
  else
    tak(tak(x-1, y, z), tak(y-1,z,x), tak(z-1,x,y)) end end

def ltak(x, y, z)
  lazy{
    if x <= y.to_i
      y
    else
      ltak(ltak(x-1, y, z), ltak(y-1,z,x), ltak(z-1,x,y)) end } end

def btak(x, y, z)
  if x <= y.to_i
    y
  else
    btak(btak(x-1, y, z), btak(y-1,z,x), lazy{ btak(z-1,x,y) }) end end

a = Benchmark.realtime{ tak(12,6,0).to_i } # => 9.08182191848755
b = Benchmark.realtime{ ltak(12,6,0).to_i } # => 0.00162792205810547
c = Benchmark.realtime{ btak(12,6,0).to_i } # => 0.000227928161621094
a / b # => 5578.78178090217
a / c # => 39845.1066945607
b / c # => 7.14225941422594

関数ltakは、takの内容をlazyで囲っただけで、takは素直な竹内関数の実装。btakは、takの第一引数と第二引数は必ず必要になる=遅延する必要がないので第三引数だけを遅延したバージョンだ。

通常9秒かかるこの計算が、ltakだと10ミリ秒に、btakでは2ミリ秒になり、btakはtakに比べて39845倍の速度になった。

まとめ
当然だが、遅延オブジェクトの生成にもオーバヘッドがあるので、必要以上に使うことは却って速度の低下につながるので避けたい。
また、この類のコードは汎用性が高いので、もうすこし実装を詰めてもいいかもしれない。
例えば、is_a?等、Objectクラスのメソッドが適切に動くようにすれば、関数の内部の処理を考えずに遅延オブジェクトを渡したり返したり出来るだろう。

2009年10月17日土曜日

重複ファイルを表示「difflatten」

ファイル名などには意味がなく、内容だけに意味があるファイルの集合に、同じルールで適当に集められたファイルをマージする話。(製作動機は後述)

概要

普通、ファイルが同じかどうかはdiffコマンドで調べることができる。
$ diff a.jpg b.jpg
しかし、最初に述べたような状況の場合、diff a.jpg storage/などとはできない。私は「echo storage/* | xargs diff a.jpg」と考えたがこれはstorage以下にディレクトリがあるとうまくいかない。また、一気に多数のファイルの重複を確認しようとした場合、ちょっと頭をひねらなければならない。

そこで、「ディレクトリAとディレクトリBの中のファイルで同じものがあれば、そのファイル名を出力する」スクリプトを書いた。外出っぽいけどちょっと見つからなかったので。

うちのWebページ
で配布してます。

以下のように実行すると、「A = B」のような行が出てくる。これは、二つのファイルが同じであることを示している。

$ difflatten.rb src/ dst/
src/1880.jpg = dst/images/sky/sunset14.jpeg
src/994.jpg = dst/images/material/takuan.jpg


これを応用すれば、以下のようにするとdst/以下のファイルでsrc/以下にもあるファイルは(dst側から)すべて削除できる。

$ difflatten.rb src/ dst/ | awk -F' = ' '{print $2}' | xargs rm


仕組み

今回はRubyで実装し、ライブラリは標準のものしか使っていない。
まず、src以下を再帰的に走査して、すべてのファイルのサイズを取得し、dst以下を走査して、srcと同じサイズのものがあれば、ダイジェスト(SHA512)で二つを比較している。当初はダイジェストのみで比較していたが、srcに500個、dstに2000個程度のファイルを入れて試したところ37秒もかかってしまった。そこで、まずサイズを比較することで、ダイジェストの計算回数を最小限に抑え、実行時間を0.2秒にまで縮めることに成功した。実際には、dstに同じファイルが2つ無い限り、ダイジェストより直接比較した方が速いのだが、そこまで速度的に不利にはならないと思われる。

動機とか

最近、というかずっと画像収集をしているのだが、あまり時間も裂けないのでとりあえずzipで落としてきて、「うーん」と思った奴は適宜削除、というスタンスをとっていた。しかし結構同じ画像が入っていることが多かったので、自分のコレクションに入れる前に重複を確認するスクリプトを作って既に持っている画像を弾き出そうと考えた。
もちろん、加工されるとハッシュ値が変わるので、本当は画像の類似度などを考慮する必要があるが、補助的には使えると思う。
今回は「削除する」ではなく「重複を表示する」だけに止めたので、いろんな使い方ができるかもしれない。

ちなみに、動作にはruby(作者環境1.8)がインストールされていること、diffなのに同じ物を表示してるじゃん!とか言わないこと、引数を必ず2個与えることが要求されます。

2009年8月8日土曜日

サーバがまた死んだ

この間の豪雨の際、雷で三回ほど瞬間停電したのだが、どうやらこの間にサーバのデータが壊れていたようで、起動不能になった(起動中にエラーを吐いてランレベル6に移行するのだが、そっちでもエラーを吐いて止まる)。
ちょっと調べても原因がわからなかったので、(この間やったばかりなので覚えているということもあり)Debian Lennyを再設定することにした。

ApacheとSubversionのインストール( + ApacheのWebDAVを使ってSVNを動かす)
必要なパッケージのインストール

# aptitude install subversion libapache2-svn

libapache2-svnをインストールした時に、apacheのdavというモジュールが有効になる(apache2をパッケージで入れた場合、無効になっているだけで既に入っている)。

# mkdir /var/svn
# svnadmin create /var/svn
# chown -R www-data:www-data /var/svn

リポジトリを作成する。ただし、ここでリポジトリの所有者をapacheにしておかなければならない。

以下の内容を/etc/apache2/conf.d/以下に適当な名前(svnなど)で作る。

<Location /svn/miku>
DAV svn
SVNPath /var/svn/miku

Order Deny,Allow
Allow from 192.168.1.0/24
Deny from all

</Location>


Order ~ Deny from allまでは、192.168.1.*しか許可しないという意味になるので、外からのリポジトリへのアクセスを一切拒否している。外からアクセスしなければならない時はSSHでトンネルを張れば良いだけのことなのでこれで十分。なお、ローカルからもトンネル経由でないと許可しない、という設定にする場合は、Allowのところにlocalhostと書いてもいいかもしれない。いずれにしろ、SVNはソースコードなどの重要なデータがインターネット上を流れるので、SSH、VPN、SSL等で暗号化するのが望ましい。

ここまでの設定を確認。ローカル(別のPC。別に同じマシンでも可)にてチェックアウト。
本当はインポートとか使うんだろうけれど、普段やり慣れている方法でやってしまうよね(その方がミスが少ない・・・成長を妨げる要因だ)。

$ svn checkout http://server/svn
$ cp -ra /path/to/original svn/
$ svn add svn/*
$ svn commit

コミットできたら一応成功。

コミットしたら公開

こういうのを何て呼ぶのかは忘れたが、リポジトリのhook scriptを触れば実現できた。
/var/svn/hooks/post-commitに以下のように書く。

#! /bin/sh
/usr/bin/svn export --force file:///var/svn/ /var/www/

post-commitはコミットが成功した後に実行されるコマンドなので、この中にエクスポートするコマンドを書いておけばいいのだ。
これで、コミットの度にファイルが/var/wwwにエクスポートされるようになった。

eRubyを有効にする
RubyをPHPみたいに組み込みっぽく使いたいので(というか我がWebサイトで使っているので)eRubyを導入する。普通のHTMLのように書けるが、の間に書いた文章はRubyスクリプトとして実行される。
このインストール作業が意外と面倒。

# aptitude install euby eruby libapahe2-mod-ruby

まずは普通にインストール。
そして、/etc/apache2/mod-enable/ruby.loadに以下を追記(参考サイトからコピー)。

<IfModule mod_ruby.c>

RubySafeLevel 1

RubyRequire apache/ruby-run

RubyRequire apache/eruby-run

<Files *.rhtml>
SetHandler ruby-object
RubyHandler Apache::ERubyRun.instance
</Files>

</IfModule>

なお、私の場合はRuby Script内でファイルの最終更新日を調べているが、これはsafe levelが0でなければ動かないらしい。なんだか気味が悪いが、とりあえず0にした。

これだけでは、*.rhtmlをクリックしてもファイルダウンロード画面になってしまう。原因は、Live HTTP Header等で調べれば一目瞭然だが、MIME Typeが「application/x-httpd-eruby」というよくわからないものになっている(通常、text/htmlになっている)。これを正すために、/etc/mime.typesにちょっと手を加える。
以下のような行があるので、その行頭に # (コメント)をつける
application/x-httpd-eruby rhtml
これで完了。なんでも、eRubyがこういう風にmime typeを決め打ちされることを想定していないらしい。

これで私の環境復元は終了したのだが、6月、8月と二度も立て続けにクラッシュしたという事実をみすみす見逃してはいられない。あまりにもばかばかしいことだが、一応まとめると:
  • 停電対策(UPS)がまったくなされていない
  • バックアップサーバ自体のバックアップは取られていない
という問題があった(というか、ある)。
まず、UPSは雷の多い地域なので見当の余地はあるが、お金のかかることなのですぐには手は出ない。
問題は二つ目である。これは絶対にやっておくべき対策で、今すぐにでも手を打つべきだ。いままでやってなかった理由としては、「HDDの空き容量がないよ!」だったのだが、今やサーバにHDDを4台ほど積めるスペースがあり、バックアップ用HDDも搭載しているので、やろうとおもえばすぐにできる。

異常気象とはいえ、組んだばかりのサーバをあまり痛めつけてほしくないものだ。

2009年2月25日水曜日

横道に逸れるのが常

最近、Webページをリニューアルしようとしているのだが、フレームワーク癖がついてしまったのか、知らないうちに、ViewとかControllerなどというクラスを作ってしまっている。どうせだから、Modelも作って本当にフレームワークもどきにしてもいいかもしれない。

頭の中を整理するために、今のところの仕様を書く。

Model
  • クラス名: Model::NameOfModel(単語の先頭大文字)
  • スーパクラス: Model::Base
  • ソースディレクトリ: /model/name-of-model.rb
  • データベースには、KirbyBaseを使おうかと考えている。

View
  • クラス: View
  • ソースディレクトリ: /view/name-of-controller/name-of-view.rhtml
  • ERubyを使って実装。
  • 結果はStringで変数content_for_layoutに代入され、レイアウトに渡される。

Controller
  • クラス名: Controller::NameOfController(単語の先頭大文字)
  • スーパクラス: Controller::Base
  • ソースディレクトリ: /controller/name-of-controller.rb

あんまり本格的にRubyのクラスとか叩いたことがなかったので、「Stringに代入されているクラス名からクラスのインスタンスを作る」とか「ハッシュに含まれるキーを変数名、値を変数の値としてもったBindingオブジェクトを生成する」といった、普段やらないような、知ってるとちょっと役に立ちそうなことがいろいろわかってよかった。
もうやめたけど、一瞬ArrayをスーパクラスとしてController::Baseを定義していたのは内緒。

※それにしても、ひどいサイト名だな