近&況

Recent Posts
Edit

Rubyで競技プログラミング

2016-12-25
Tech

この記事はRuby Advent Calendar 2016の最終日の記事です。

発端

秋に行われたRubyKaigi 2016のあとのAfter partyで、以下のような発表をしました。

内容は、会社の同僚と一緒にRubyでAtCoderをやっているという話でした。AtCoderはオンラインで競技プログラミングができるサイトで、Rubyを含めたさまざまな言語で参加することができ、過去問についても解答を受け付けているため好きなときにチャレンジすることができます。

競技プログラミングではC, C++, C#, Javaといった言語が主流なので、Rubyでは速度が足らないのではないか?と思われるかもしれません。実際、AtCoderでは2秒の制限時間に対し、入力データが10万行を超えることもあります。それでも、適切なアルゴリズムを使うことで、9月時点でチャレンジしていた範囲(ABC035〜041)はRubyでもちゃんとクリア(AC)することができました。

…ただ一問を除いては。

ABC037 D 経路

問題の一問がこれです。数字が敷き詰められた盤面を「数字が大きくなっていくように歩く」とき、何通りの歩き方があるかという問題です。(今回は実行速度の話なので具体的な解法については触れませんが、ネタバレが気になる場合は以下を読む前に一度チャレンジしてみて下さい)

この問題だけがどうしても2秒以内に解けませんでした。TLE(Time Limit Error)だけでなくRE(Runtime Error)が出ていることから、stack level too deepが出ていることが予想されます。アルゴリズムは再帰呼び出しを使ったもので、盤面は最大で1000×1000マスなので、デフォルトのスタックサイズをオーバーしてしまうことは十分に考えられます。

Rubyとスタックサイズ

RubyKaigiでこの件についてささださんに尋ねてみたところ、「スタックサイズは環境変数経由で設定することができる」という情報を頂きました。

ただし残念ながら、環境変数を使わずにスクリプト内からこれを設定する方法はまだないとのことでした(理由は「特に要望がなかったから」)。AtCoderには投稿したプログラムの環境変数をするようなインターフェイスはないので、この方法は使えなさそうです。

と思いきや…

冒頭のスライドを見た@_simanmanさんから、クリアできたという報告が!

どうやら、以下のようにKernel#execでRubyインタプリタをもう一度起動させることで、環境変数を指定しているようです。そんなことをしたら2秒制限にひっかかりそうですが、1991msというぎりぎりのタイムでクリアできています。

if !ENV['RUBY_THREAD_VM_STACK_SIZE']
  exec({'RUBY_THREAD_VM_STACK_SIZE'=>'1000000000'}, '/usr/bin/ruby', $0)
...

どうすれば良かったのか?

クリアできることが分かったので、自分が投稿したプログラムと比較して、どこが悪かったのかを調べてみました。

1. Bignumを経由する箇所があった

まず単純なミスとして、問題文の「10^9+7で割った余りを求めてください」という一節が実装できていませんでした。このため盤面が大きい場合に間違った答えが出る上に、答えが出るまでにとても時間がかかるようになっていました。

Rubyでは四則演算の結果が大きい数になると、自動的にFixnumからBignumへ結果が拡張されるようになっています。これはとても便利な機能ですが、Bignumの計算はFixnumよりも低速です。今回は結果の総数ではなく10^9+7で割った余りだけ分かれば良いので、計算の途中でこまめに10^9+7で割るようにすれば、Bignumを経由しなくて済みます。

2. スタックを浅くする必要があった

AtCoderでは時間制限の他に、メモリ量の制限もあります。ABC037 D問題では使えるメモリは256MBまでで、これを超えるとMLE(Memory Limit Error)というエラーになります。

いろいろ試してみた結果、筆者のもとのプログラムだとスタック制限を回避できたとしてもMLEで止まってしまうことが分かりました。問題の箇所はここです。

def calc(pos)
  ret = 1
  DIRS.each do |d|
    if MAP[pos] < MAP[pos+d]
      plus = ANS[pos+d] || calc(pos+d)   # 再帰呼出し
      ...

このDIRS.eachをwhileに置き換えると、無事Acceptedになります

def calc(pos)
  ret = 1
  i = 0
  v = MAP[pos]
  while i < 4
    d = DIRS[i]
    if v < MAP[pos+d]
      plus = ANS[pos+d] || calc(pos+d)
      ...

@_simanmanさんのプログラムではwhileの代わりにループアンローリングを使っています。ループアンローリングは主に高速化のためにループをべた書きに展開するという手法で、例えば上のwhileを展開すると以下のようになります。

def calc(pos)
  ret = 1

  if MAP[pos] < MAP[pos+1]
    plus = ANS[pos+1] || calc(pos+1); ret += plus
  end
  if MAP[pos] < MAP[pos-1]
    plus = ANS[pos-1] || calc(pos-1); ret += plus
  end
  if MAP[pos] < MAP[pos+W]
    plus = ANS[pos+W] || calc(pos+W); ret += plus
  end
  if MAP[pos] < MAP[pos-W]
    plus = ANS[pos-W] || calc(pos-W); ret += plus
  end

まとめ

今回は以上です。一見どうしようもなさそうな状況でも、意外な方法で回避できるというのが面白かったです。

Edit

Opalでゲームを作るには(opal-phaser)

2016-12-21
Tech

この記事はOpal Advent Calendar 2016Ruby Game Developing Advent Calendar 2016の21日目の記事です。

OpalはRubyスクリプトをJavaScriptに変換してくれる処理系です。ということは、Opalを使えばRubyを使ってブラウザで動くゲームが作れるはずです。

opal-phaser

opal-phaserはPhaserというブラウザゲーム用ライブラリをOpalから使えるようにしたものです。今回はこれを触ってみたいと思います。

サンプルを動かしてみる

PhaserのサンプルをOpalに移植したものがあるみたいなので、とりあえず動かしてみましょう。READMEに書いてある通り、git cloneしてbundle installします。

$ git clone https://github.com/orbitalimpact/opal-phaser-examples
$ cd opal-phaser-examples
$ bundle install

そのあと、適当なサンプルのディレクトリに移動してbundle exec rackupし、http://localhost:9292/ を開きます。

$ cd examples/basics/click_on_image/
$ bundle exec rackup

実行結果です。自分が何をしようとしていたのか一瞬分からなくなりましたが、opal-phaserのサンプルを動かしていたのでした。画像をクリックするとクリックした回数が画面に表示されます。とりあえず動いているようで良かったです。

実行結果

他のサンプルも俄然、気になってきましたが、ここから先は自分の目で確かめてほしい。サブディレクトリがたくさんありますが、config.ruで検索すればサンプルだけ列挙できます。

% find . -name config.ru
./examples/animation/animation_events/config.ru
./examples/animation/change_frame/config.ru
./examples/animation/change_texture_on_click/config.ru
./examples/animation/destroy_animation/config.ru
./examples/animation/dynamic_animation/config.ru
./examples/animation/group_creation/config.ru
./examples/animation/load_texture/config.ru
./examples/animation/looped_animation/config.ru
./examples/animation/multiple_anims/config.ru
./examples/basics/click_on_an_image/config.ru
./examples/basics/image_follow_input/config.ru
./examples/basics/load_an_animation/config.ru
./examples/basics/load_an_image/config.ru
./examples/basics/move_an_image/config.ru
./examples/basics/render_text/config.ru
./examples/basics/tween_an_image/config.ru
./examples/bitmap_data/alpha_mask/config.ru
./examples/sprites/dynamic-crop/config.ru

また、phaser.js本体のExampleはもっとたくさんあるようです。

サンプルを読む

先ほどのbasics/click_on_image/のソースを見てみましょう。まず、index.htmlです。

<head>
    <title>Click On An Image</title>
    <script src="//cdn.jsdelivr.net/phaser/2.4.4/phaser.js"></script>
    <script src="/assets/main.js"></script>
</head>

phaser.jsをCDNから読み込んでいます。/assets/main.jsは、同ディレクトリのmain.rbがOpalによってコンパイルされるので、それを読み込んでいます。bodyタグの方も見てみましょう。

    <span id="title">click on an image</span>
    <script>
      Opal.load("main");
    </script>
    <div id="example"></div>

このscriptタグはOpalでコンパイルしたJSを実行開始する部分なので、消してはいけません。それ以外はclick on an imageというメッセージと、divタグが一つあるだけです。このdivタグは画像を表示するために使いそうですね。

main.rb

雰囲気が掴めたところでmain.rbを見てみましょう。これがOpalによってJSに変換されるRubyスクリプトで、このデモの中心部分です。

最初にImageというクラスがあります。こいつがクリックできるアインシュタイン(やっぱりアインシュタインだったのか…)のオブジェクトです。画像ファイルはこのディレクトリではなくて、少し上の階層のassetsというディレクトリにあります。そのあたりの設定はconfig.ruでやっているようです。

class Image
  def initialize(game)
    @sprite_key = "einstein"
    @sprite_url = "assets/pics/ra_einstein.png"
    @game       = game
  end

createメソッドに、クリックイベントを拾う処理が書いてあります。events.onでイベントハンドラを登録できるようですね。

  def create
    @counter = 0

    listener = proc do
      @counter   += 1
      @text.text = "You clicked #{@counter} times!"
    end 
...

    @text = @game.add.text(250, 16, '', { fill: '#ffffff' })

    @image.events.on(:down, self, &listener)
  end

@gameという変数は、以下のGameクラスのオブジェクトです。ファイルの最後がGame.newなので、ここがゲームプログラムのエントリポイントになるようですね。parent: "example"という指定は、index.htmlの<div id="example">を描画領域として使う、という指定でしょう。

class Game
  def initialize
    game  = Phaser::Game.new(width: 800, height: 600, renderer: Phaser::AUTO, parent: "example")
    state = MainState.new(game)
    game.state.add(:main, state, true)
  end
end

MainStateクラスはその下で定義されています。これとかを見ると、「タイトル画面」「プレイ中画面」みたいにシーンごとにStateを定義するみたいです。

class MainState < Phaser::State
  def initialize(game)
    @game = game
  end

  def preload
    @image = Image.new(@game)
    @image.preload
  end

  def create
    @image.create
  end
end

おわりに

今回はopal-phaserを紹介しました。プロジェクトとしては2年前からあるようで、今も開発が続けられています。

https://github.com/orbitalimpact/opal-phaser のGamesの項に、もう少しゲームらしいサンプルがあります。(tankgameはどうやって操作するのかよくわからない感じでしたが)

触ってみて良く分からないとかうまく動かないことがあれば、Twitter等で聞いてもらえればお手伝いできるかもしれません。

Edit

Opalのハッシュはどのようにして実装されているのか

2016-12-19
Tech

本記事はOpal Advent Calendar 2016の19日目のエントリです。

今回はOpalのHashクラスの実装について見ていきます。

corelib/runtime.js

opal/corelibは、array.rbやstring.rbなど、組み込みクラスの実装が置かれているディレクトリです。この中に一つだけ、runtime.jsという、拡張子が.jsのファイルがあります。

runtime.jsはコンパイル後のJavaScriptの一部としてそのまま埋め込まれます。クラスの生成定数の探索といった処理系の基礎となる機能が実装されており、Opalの心臓部といえるでしょう。

Opal.hash_xx

runtime.jsでは、ハッシュを扱う以下のような関数が定義されています

hash_getやhash_putを眺めてみると、ハッシュアルゴリズムを自前で実装していることが分かります。

単純な、文字列だけをキーとするハッシュであればJavaScriptのオブジェクトにそのままマップできるかもしれませんが、RubyのHashクラスは任意のRubyオブジェクトをキーにできたり、ハッシュ値を明示できたりするため、このようになっています。

Hashクラスとの関係

Hashクラスはopal/corelib/hash.rbで定義されています。これを見ると、Hashクラスのメソッドは先ほどのhash_getやhash_putを使って実装されていることが分かります。

以下はHash#[]=メソッドの定義です。単純にhash_putを呼ぶだけですね。

  def []=(key, value)
    %x{
      Opal.hash_put(self, key, value);
      return value;
    }
  end

Hash#[]の方も見てみましょう。こちらもほぼhash_getを呼ぶだけですが、値が存在しなかったときはdefaultメソッドを呼ぶようになっています(mrubyのアドカレ記事でもこの話しましたね)。

  def [](key)
    %x{
      var value = Opal.hash_get(self, key);
      if (value !== undefined) {
        return value;
      }
      return self.$default(key);
    }
  end

各関数とメソッドの対応をまとめておきます。

ハッシュオブジェクトの実装

Opal.hash_xxの実装も少し見てみましょう。まず、ハッシュオブジェクトは以下のプロパティを持つことが分かります。

  Opal.hash_init = function(hash) {
    hash.$$smap = {};
    hash.$$map  = {};
    hash.$$keys = [];
  };

$$smapと$$mapが、キーと値の組を保持します。RubyのハッシュのキーはたいていStringかSymbolなので、キーが文字列のときは$$smapを使い、そうでないときだけ$$mapを使うようです。(OpalではSymbolはStringと同じものです)

$$keysはキーの一覧を高速化のためキャッシュしたもので、Hash#keysメソッド等で使われます。またこのプロパティを見ればハッシュのサイズが分かるため、Hash#lengthの実装にも使われています。

  def length
    `self.$$keys.length`
  end

ハッシュテーブルの実装

最後に、ハッシュテーブルの実装を確認するため、Opal.hash_putを見てみましょう。

キーが文字列のときは、$$smapをハッシュテーブル代わりにして値を格納するだけです。

    if (key.$$is_string) {
      if (!hash.$$smap.hasOwnProperty(key)) {
        hash.$$keys.push(key);
      }
      hash.$$smap[key] = value;
      return;
    }

キーが文字列でない場合は、Object#hashメソッドを実行してハッシュ値(key_hash)を得ます。これを使ってキーと値のペアを格納するのですが、ハッシュ値は衝突する可能性があるため注意が必要です。

    var key_hash = key.$hash(), bucket, last_bucket;

まず、衝突が起こらなかった場合です。$$mapにハッシュ値がkey_hashであるオブジェクトが格納されていない場合は、新しいbucketを作って保存します。bucketは生のキーと値を保持するJSオブジェクトで、ここには出てきていないですがもう一つnextというプロパティがあり、要するにリンクドリストになっています。

    if (!hash.$$map.hasOwnProperty(key_hash)) {
      bucket = {key: key, key_hash: key_hash, value: value};
      hash.$$keys.push(bucket);
      hash.$$map[key_hash] = bucket;
      return;
    }

衝突が起こった場合、つまり当該ハッシュ値のbucketが既に存在する場合は、各bucketの中身を調べます。キーが一致する(Object#eql?)ものがあれば、このキーに対する値を上書きしようとしているわけなので、bucket.valueを更新します。

    bucket = hash.$$map[key_hash];

    while (bucket) {
      if (key === bucket.key || key['$eql?'](bucket.key)) {
        last_bucket = undefined;
        bucket.value = value;
        break;
      }
      last_bucket = bucket;
      bucket = bucket.next;
    }

上書きでなかった場合は、最終的にbucket.nextがundefinedになるのでループを抜けます。このとき、一番最後のbucketを覚えておきます(last_bucket)。新しいbucketを作り、last_bucketの後ろに連結します。

    if (last_bucket) {
      bucket = {key: key, key_hash: key_hash, value: value};
      hash.$$keys.push(bucket);
      last_bucket.next = bucket;
    }

以上でOpal.hash_putの実装は終わりです。

まとめ

Edit

Opalはどうやってmethod_missingを実装しているのか

2016-12-17
Tech

本記事はOpal Advent Calendar 2016の17日目の記事です。

OpalはRubyからJavaScriptへのコンパイラです。今回はRubyの黒魔術の一つであるmethod_missingの実装について見ていきます。

method_missingとは

Module#method_missingは、あるオブジェクトに対して定義されていないメソッドを呼び出したときに走るフックを定義する機能です。

class A
  def method_missing(name, *args)
    puts "hooked (name: #{name}, args: #{args.inspect})"
  end
end

A.new.foo(1, 2, 3)

上記を実行すると以下のようなメッセージが出力されます。

hooked (name: foo, args: [1, 2, 3])

クラスAにはfooというメソッドがないので、fooの代わりにmethod_missingメソッドが呼ばれるということですね。用途としては、DSLを作る際に使われたりします。

どうやって実装するか?

Opalはこの機能をどうやって実装しているのでしょうか。Opalでは、RubyのメソッドはJavaScriptのメソッド呼び出しに変換されます。

a.foo()
a.$foo();

残念ながら、JavaScriptには存在しないメソッド呼び出しをフックする機能はなく、呼び出そうとすると例外が発生します。

これを回避する単純な方法としては、「メソッド呼び出しの前に毎回、メソッドが存在するか確認する」というやり方があります。実際、昔のOpalはこのようにしていたのですが、これだとmethod_missingを使っていない場合でも動作速度が落ちるという問題があります。

メソッド呼び出しを列挙する

今のOpalはもっと良い方法を使っています。まず、実行するソースコードを解析して、メソッド呼び出しをすべて列挙します。(解析というと大変そうですが、どのみちコンパイルするためにはパースしないといけないので、そこからメソッド呼び出しを列挙するのは簡単です。)

その後、継承関係の上の方(BasicObject)にこれらの名前のメソッドを実装し、method_missingメソッドを呼ぶように設定しておきます。コンパイル後のソースコードのadd_stubsを呼んでいる箇所がそれです。

Opal.add_stubs(["$foo"]);

これにより、fooメソッドをもたないオブジェクトに対してfooを呼ぼうとするとBasicObject#fooが呼ばれ、めでたくmethod_missingが呼ばれるようになります。

補足:sendを使った場合は?

Rubyに詳しい人なら、上の方法では「メソッド呼び出しの一覧」を網羅できないことに気づいたかもしれません。Kernel#sendを使うと、以下のように任意の式がメソッド名になる可能性があるので、実行するまでメソッド名が確定しません。

a.send(["foo", "bar"].sample)  #=> fooかbarのどちらかを呼び出す

しかしsendの場合は実行時にメソッド名が取れるわけなので、sendの側でメソッドがなければmethod_missingを呼ぶよう実装されています。抜かりありませんね。

まとめ

Edit

Opalはどうやってmethod_missingを実装しているのか

2016-12-17
Tech

本記事はOpal Advent Calendar 2016の17日目の記事です。

OpalはRubyからJavaScriptへのコンパイラです。今回はRubyの黒魔術の一つであるmethod_missingの実装について見ていきます。

method_missingとは

Module#method_missingは、あるオブジェクトに対して定義されていないメソッドを呼び出したときに走るフックを定義する機能です。

class A
  def method_missing(name, *args)
    puts "hooked (name: #{name}, args: #{args.inspect})"
  end
end

A.new.foo(1, 2, 3)

上記を実行すると以下のようなメッセージが出力されます。

hooked (name: foo, args: [1, 2, 3])

クラスAにはfooというメソッドがないので、fooの代わりにmethod_missingメソッドが呼ばれるということですね。用途としては、DSLを作る際に使われたりします。

どうやって実装するか?

Opalはこの機能をどうやって実装しているのでしょうか。Opalでは、RubyのメソッドはJavaScriptのメソッド呼び出しに変換されます。

a.foo()
a.$foo();

残念ながら、JavaScriptには存在しないメソッド呼び出しをフックする機能はなく、呼び出そうとすると例外が発生します。

これを回避する単純な方法としては、「メソッド呼び出しの前に毎回、メソッドが存在するか確認する」というやり方があります。実際、昔のOpalはこのようにしていたのですが、これだとmethod_missingを使っていない場合でも動作速度が落ちるという問題があります。

メソッド呼び出しを列挙する

今のOpalはもっと良い方法を使っています。まず、実行するソースコードを解析して、メソッド呼び出しをすべて列挙します。(解析というと大変そうですが、どのみちコンパイルするためにはパースしないといけないので、そこからメソッド呼び出しを列挙するのは簡単です。)

その後、継承関係の上の方(BasicObject)にこれらの名前のメソッドを実装し、method_missingメソッドを呼ぶように設定しておきます。コンパイル後のソースコードのadd_stubsを呼んでいる箇所がそれです。

Opal.add_stubs(["$foo"]);

これにより、fooメソッドをもたないオブジェクトに対してfooを呼ぼうとするとBasicObject#fooが呼ばれ、めでたくmethod_missingが呼ばれるようになります。

補足:sendを使った場合は?

Rubyに詳しい人なら、上の方法では「メソッド呼び出しの一覧」を網羅できないことに気づいたかもしれません。Kernel#sendを使うと、以下のように任意の式がメソッド名になる可能性があるので、実行するまでメソッド名が確定しません。

a.send(["foo", "bar"].sample)  #=> fooかbarのどちらかを呼び出す

しかしsendの場合は実行時にメソッド名が取れるわけなので、sendの側でメソッドがなければmethod_missingを呼ぶよう実装されています。抜かりありませんね。

まとめ

Edit

OpalはどうやってRubyコードをパースするか

2016-12-13
Tech

本記事はOpal Advent Calendar 2016の13日目の記事です。

11日目のエントリではOpalのString以外クラスを読むと書いたのですが、実際読んでみるとあんまり解説することがない(Stringの回と似たような内容になってしまう)ことに気づいたので、今回はパーサを読むことにしました。

パーサはどこにある?

OpalはRubyからJavaScriptへのコンパイラです。コンパイラなのでパーサはホスト側言語(=Ruby)にあれば良い…と思いきや、Opalにはevalがあります。Kernel#evalが呼ばれた場合、Opalプログラムのパースを「実行時に」行う必要があります。このためパーサはJavaScriptかOpal自身で実装されていると予想できます。

ということを念頭に置きつつ、githubでparserで検索してみます。どうもlib/opal/parser.rbがそれのようです。あれ、lib以下ということはRuby用なのか?と一瞬思いましたが、if RUBY_ENGINE == 'opal'という行があるので、RubyでもOpalでも動かせるようにしてあるようです。

lib/opal/parser.rb

中身を見ると、Parser::Ruby23というクラスを参照しています。ところがリポジトリにはruby23.rbのようなファイルがありません。調べてみると、masterではparserというgemを使うようになっているようです。

parser gemはRubyのコードをパースするためのRubyGemsです。リポジトリを見ると、Rubyのバージョンごとにruby24.y, ruby23.y, ...といったファイルが見つかります。これをもとにraccでパーサを生成しているわけですね。

生成されたコードはOpalでちゃんと動くのだろうか?と思いましたが、パーサジェネレータが生成するコードは決まった処理の繰り返しなので、むしろ普通のプログラムより動かすのは簡単だったかもしれません。

まとめ

おまけ: 0.10.x

上記のPull Requestは0-10-stableブランチには適用されていないため、次のminor update(0.11ですかね?)からparser gemが使われるようになるようです。

それ以前は、Opalが独自に.yファイルを持っていました。最新のopal gem 0.10.3ではこれが使われています。

おまけ: evalについて

このへんがソースのようです。

  1. Kernel#eval
  2. Opal.compile
  3. Opal::Compiler.compile
Edit

OpalのStringクラスはどのような実装になっているのか

2016-12-11
Tech

これはOpal Advent Calendar 2016の11日目の記事です。

OpalのAdvent Calendarは今年が初めてということで、Opalのソースを少し読んでみたいと思います。今回はStringクラスです。

string.rb

Opalの組み込みクラスのソースはopal/corelib以下にあります。

https://github.com/opal/opal/blob/7310b27a1c6bec135610e5df5f20753eb349965e/opal/corelib/string.rb

拡張子的にはRubyで書かれていそうですが、中身を見ると、バッククオート記法を使ってJavaScriptがたくさん埋め込まれていることが分かります。バッククオートはRubyでは外部コマンド実行ですが、Opalでは外部コマンドを実行することは無いので、代わりに「生のJavaScriptを記述する」という用途に置き換えられています。

Stringクラスの場合、selfで実体となるJavaScript文字列を取得できます。ということで、例えばString#lengthメソッドは以下のような定義になっています。JavaScriptのlengthを呼び出すだけです。簡単ですね。

  def length
    `self.length`
  end

String#downcaseとかも同じように簡単に実装できています。

  def downcase
    `self.toLowerCase()`
  end

String#empty?はこう。

  def empty?
    `self.length === 0`
  end

String#reverseは、JSレベルで文字に分解して逆順にしてつなぎ直しているようです。

  def reverse
    `self.split('').reverse().join('')`
  end

String#start_with?

もう少し長いのも見てみましょう。以下はstart_with?メソッドの定義です。selfが指定した文字列から始まるかどうかをチェックするメソッドですね。

"foobar".start_with?("foo") #=> true
"foobar".start_with?("bar") #=> false

指定文字列は複数渡すことができ、その場合はどれか1つでもマッチすれば真を返します。

"Pen-Pineapple-Apple-Pen".start_with?("Apple", "Pinapple", "Pen")  #=> true

OpalのString#start_with?の実装は以下です。JavaScriptのfor文を使って指定文字列を順に見ていき、先頭が一致する(=indexOfが0)ものがあればtrueを返しています。

  def start_with?(*prefixes)
    %x{
      for (var i = 0, length = prefixes.length; i < length; i++) {
        var prefix = #{Opal.coerce_to(`prefixes[i]`, String, :to_str).to_s};
        if (self.indexOf(prefix) === 0) {
          return true;
        }
      }
      return false;
    }
  end

ところで%xについては解説が必要かもしれません。%x{}というのは%記法の一種で、バッククオートと同じ意味です(あまり知られていないですがRubyにもあります)。そのため%x{}で囲まれた部分はJavaScriptのコードと見なされ、コンパイル後のJavaScriptにそのまま展開されます。ただしその中に#{}があった場合は、そこだけはOpalのコードと見なしてJavaScriptへのコンパイルが行われます。上記のコードでも、真ん中あたりで#{}を使っている箇所があります。ここでは引数に文字列以外が渡された場合を考慮して、Opal.coerce_toでto_strを呼んで文字列への変換を試みています。

ここでOpal.coerce_toの引数をよく見ると、ここでもバッククオートが使われています。ということは、Opalのソースに書かれたJavaScriptの中に埋め込まれたOpalの中にJavaScriptが埋め込まれているということになります!

JSの部分を線で囲ったもの

今回はここまでです。時間があったらまた別のクラスも見てみたいと思います。

Edit

Opalはいかにしてrubyspecをいい感じにアレしているのか

2016-12-07
Tech

OpalはJavaScriptで書かれたRuby処理系です。OpalではArray#findといった組み込みライブラリもJavaScriptで再実装しているので、テストが膨大な数になりそうですが、そのあたりはどうしているのでしょうか。

ruby/spec

Rubyにはruby/specというテストスイートがあります。これは、CRubyやJRubyなど複数の処理系間で挙動を揃えるために作られたものです。READMEにあるようにもとはRubySpecという名前でしたが、現在はThe Ruby Spec Suiteというのが正式名称になっています(が、カジュアルには今でもrubyspecといえばこのruby/specを指すことが多いようです)。

Opalはこのrubyspecをテストとして使うことで、テストを0から書く手間を省きつつ、他の処理系との互換性を担保しています。

Opalとrubyspec

とはいえrubyspecはCRubyやJRubyをターゲットに作られたものなので、Opalでは動かせないものも含まれています。例えばOpalにないものの一つとして、String#gsub!が挙げられます。Opalでは文字列を使ったプログラムはJavaScriptの文字列をそのまま使うように変換されます(多分速度のため)。しかしJavaScriptの文字列はimmutableなので、gsub!のような破壊的なメソッドはサポートできません。

Opalではspec/filters/unsupported以下にサポートしない機能の一覧があり、ここに書かれたテスト項目については実行がスキップされるようになっています。

bugs

spec/filters以下にはもう一つ、bugsというディレクトリがあります。バグ、というよりは既知の未実装機能がここに含まれており、このコミットのように実装が終わったものは消していくという形になっています。

これにより、未実装の機能でテストが落ちまくるのが回避されており、かつこれから作業が必要なものの一覧を明確にするのにも役立っています。Opalに何かコントリビュートしたいという人は、bugsの中から実装できそうなものを見つけるというのも手です。

まとめ

Edit

なるべく移行コストが少ないErgoDoxのキー配置

2016-12-06
Tech

このエントリはErgoDox Advent Calendar 2016の6日目のエントリです。

今年の5月に仕事用キーボードとしてErgoDox EZ (組み立て済みErgoDox) を購入し、半年が経ちました。ErgoDoxのメリットの一つとしてキーマップのカスタマイズ性の高さがありますが、一方で「Pキーの右にたった一列しか場所がない」という特殊な形状のため、キー配置をどうカスタマイズしても慣れるまでに多少の練習が必要になるという問題もあります。

本稿ではなるべく練習が要らないキー配置について考えます。

ポイント

キーボード上にはたくさんのキーが存在しますが、それぞれの使用頻度には大きな隔たりがあります。「%」や「&」など使用頻度の少ないキーは、多少変なところに配置してもそれほどストレスにはなりませんが、EnterやBackSpaceなどの基本的なキーを変わったところに置くと慣れるまで大変です。

ということで、使用頻度が高いキーから順に、「これはどうしてもここに置きたい」という場所に配置していくことになります。

その1 : ハイフン、BackSpace、Enter、Shift

最初に、筆者が現在使用している配置を貼っておきます。(画像はconfigure.ergodox-ez.comをベースにしています)

キー配置図

一番右の列にはハイフン、BackSpace、Enter、Shiftを配置しました。このあたりは非常によく使うキーで、脳で考えるより前に指が勝手に押してしまうので、ここに置くのが一番楽でした。ハイフンは他の3つに比べるとそんなに押さないのではと思われるかもしれませんが、実は日本語入力のときに伸ばし棒として使うので地味に利用頻度が高いです。

EnterおよびBackSpaceはErgoDox ezの出荷時設定だと親指で押すようになっていて格好良いのですが、やってみるとかなり難しかったです(指が勝手にいつもの位置を押してしまう)。

その2 : コロンとセミコロン

次に考えたのはLキーの右に何を置くかです。ここで、ベースを日本語配列・英語配列のどちらをにするかという選択肢が生じます。日本語配列だとShift + ; で「+」が入力されますが、英語配列だとShift + ; で「:」が入力されます。ErgoDoxはカスタマイズ性が高いとは言っても、既存のキー配列の枠組みを超えることはできず、;が出るキーをShift付きで押すと「*」になるようなことはできません(と思ってるんですが合ってますかね?)

個人的な事情としてコロンはVim、セミコロンはAZIKで頻繁に使うので、これらはできればLの右側に配置したいと思いました。幸い英語配列だとこれらが1キーにまとまっているので、現在は英語配列をベースにしています。また英語配列だとShift + 0が有効活用できるというメリットもあります(日本語配列だとShift + 0では何も入力されず、そこそこ押しやすい配置なのに勿体ない)。

その3 : ESC、Tab、Ctrl、Shift

左端の列にはESC、Tab、Ctrl、Shiftを配置しています。ただしESCは左上ではなく、Ctrlを単押ししたらESCが入力されるよう設定しています。モディファイヤを単押しした場合の挙動をカスタマイズできるのはErgoDoxの便利なところです。ESCはVimで頻繁に押すので、このように押しやすい位置にあると快適です。

普通のキーボードに近づけるならESCは左上に置きたくなりますが、購入初期に1キーを押そうとしてここを叩いてしまうミスが頻発したので、ここはあえて空きにしてあります。

おまけ

本題は以上ですが、以下ではおまけとして、残りの部分のキー配置について解説しておきます。

レイヤー0の配置図

レイヤー1

左下の赤丸のキーを押すと、配置がレイヤー1になります。例えば赤丸 + 1 でF1が入力されます。

レイヤー1にはファンクションキーとカーソル移動関係のキーを配置しています。ファンクションキーは数字キーと対応させておくと覚えやすいですが、F11とF12が余るので、適当に右側に置いてあります(たぶん半年間一度も押してない)。

レイヤー1の配置図

カーソルキーはWASDやnpfbなども試したのですが、最終的にHJKL(roguelike風)に落ち着きました。MacだとCtrl-f/Ctrl-bでカーソル移動ができたりしますが、一部のシチュエーション(例:VMWare上のUbuntuでGitHub Issuesにコメントするとき)ではCtrl-f/Ctrl-bが変な挙動に上書きされている可能性があります。一方、レイヤー1でのカーソル移動は上書きされない(=必ず「カーソルキーを押した」ことになる)ので、変な挙動にならないか心配するストレスが無くて良いです。

レイヤー2

右下の赤丸キーを押している間、配置がレイヤー2になります。

レイヤー2にはメディア関係の機能を置いています。レイヤー1のHJKLにカーソルキーを置いたので、矢印の向きに合うようにPrev/Next、音量Up/Downを配置しています。EnterをPlay/Stopにするのもなんとなく感覚に合っていて覚えやすかったです(あとStopしたいのって席を立つ場合とかなので、片手で操作できて良い)。

レイヤー2の配置図

ちなみにレイヤー2左側にはマウスキー機能を置いてみたんですが、あまりしっくり来なかったのでいまのところ使っていません。

まとめ

実際の設定(keymap.c)はgistに上げて置きました。参考になれば幸いです。

Edit

OpalでThree.jsを使う

2016-12-06
Tech

この記事はOpal Advent Calendar 2016Three.js Advent Calendar 2016の6日目の記事です。両方のアドベントカレンダーが空いていたので、こう、くっつけたらどうなるかなっていう…。安易な発想ですいません。

Opalとは

OpalはJavaScriptで書かれたRuby処理系です。ブラウザ上でRubyのコードを動かすことができます。

とりあえず検索してみる

opalとthree.jsで検索してみるとthree.rbというリポジトリが引っかかりました。1年ほど更新がないですが、とりあえず先人はいるみたいです。

とりあえず動かしてみる

リポジトリをgit cloneします。demoというディレクトリがあるので、これを試してみましょうか。Rackアプリになっているようなので、rackupで起動してブラウザで見てみます。

$ cd demo/
$ bundle install
$ bundle exec rackup
$ open http://localhost:9292/

結果

高速で回転する立方体が出現しました。(これはgifなので本物はもっと滑らかです) どうやら動いているようです。

demo/app/main.rbが立方体を動かすコードで(以下抜粋)、これがJavaScriptにコンパイルされてブラウザ上で動いているわけですね。

aspect_ratio = $$.innerWidth / $$.innerHeight

scene  = THREE::Scene.new
camera = THREE::PerspectiveCamera.new(field_of_view: 75, aspect_ratio: aspect_ratio, near: 0.1, far: 1000)

renderer = THREE::WebGLRenderer.new
renderer.set_size($$.innerWidth, $$.innerHeight)
$document.body << renderer.dom_element.to_n

geometry = THREE::BoxGeometry.new(width: 1, height: 1, depth: 1)
material = THREE::MeshBasicMaterial.new( color: 0x00ff00 )
cube     = THREE::Mesh.new(geometry.to_n, material.to_n)
scene.add(cube.to_n)

camera.position.z = 5

render = proc do
  $$.requestAnimationFrame(render)
  cube.rotation.x += 0.1
  cube.rotation.y += 0.1
  renderer.render(scene.to_n, camera.to_n)
end
render.call

Opalは下回りがJavaScriptである都合上、IntegerとFloatの区別がないとか、StringがmutableでないなどRubyと細かい違いはありますが、使用感は完全にRubyです。

バージョンについて

1年前のプロダクトということで、どのバージョンを使っているのか気になりますね。three.rb.gemspecを見ると、Opalは0.7.xのようです(最新は0.10.x)。three.jsの方は、demo/index.htmlを見るとCDN上のr73を読み込んでいます(最新はr82)。

ということで、three.jsのバージョンを上げるのは簡単そうです。demo/index.htmlを書き換え、r82を読み込むようにしてみます。特に問題なく動きました。

Opalのバージョンを上げる

問題はOpalの方で、gemspecの指定を0.10.0に変えてdemo側でbundle updateすると、立方体が出なくなってしまいました。うーむ、どうしましょうかね。

とりあえず最新の情報を得たいのでhttp://opalrb.org/を見ます。そうするとRack tutorialという項があり、どうやらこれが最新の書き方のようです。1

これにそってdemo/config.ruを書き直してみます。

require 'bundler'
Bundler.require

run Opal::Server.new { |server|
  server.main = 'main.rb'
  server.append_path 'app'
}

…いや、だめですね。これだとdemo/index.htmlが読み込まれないのでThree.jsがロードされません。デベロッパーコンソールにも「THREE is not defined」と出ています。

index.htmlをサーブしたい

チュートリアルの最後にLeran moreというリンクがあるので、opal-sprocketsのREADMEを見ます。これを見るとindex.htmlは必須と書いてありますが、さっきのチュートリアル手順では無くても動いたので、なんか情報が古そうな気がします。うーん、ソースを読まないとだめそうですね。

リロードしたページのソースを表示すると<title>Opal Server</title>という行があるので、opal-sprocketsおよびopal gemの中身をgrepします。2 opal gemに該当の行がありました。自前のindex.htmlを置けないという仕様は考えにくいので、index.htmlが見つからなければデフォルトのものをサーブするのだと推測します。3 このへんを読むと雰囲気的にindex_pathという設定項目な気がするので以下のようにします。

require 'bundler'
Bundler.require

run Opal::Server.new { |server|
  server.main = 'main.rb'
  server.append_path 'app'
  server.index_path = 'index.html'
}

Three.jsがちゃんとロードされるようになりました。

あと一歩

しかしまだ立方体は出ません。ロードされているリソースを見るとmain.rb自体はちゃんとコンパイルされていそうですが、mainを実行する部分が呼ばれていない雰囲気です。チュートリアルの方を別ポートで動かして(bundle exec rackup -p 8888)見比べてみると、arrayとかrangeとかその他もろもろを読み込むscriptタグが生成されていることが分かります。どうやらserver.rbのこの行がポイントのようです。

      <body>
        #{javascript_include_tag @server.main}
      </body>

lib/opal/sprockets/erb.rbというファイルがあるので、なんとなくerbが使えそうな予感がします。index.htmlをindex.html.erbにリネームし、main.jsを直接ロードしていた部分を以下に置き換えます。

<%= javascript_include_tag @server.main %>

config.ruのindex_pathも合わせてindex.html.erbにし、rackサーバを再起動すると…

回転する立方体

やったね!!

まとめ

ということで動くようにしたバージョンを https://github.com/yhara/three.rb/tree/opal_0_10 に置いておきました。なんというかThree.rb自体の解説が全くできなかったですが今回はここまでです。

Opalはドキュメントの更新に手が回っていないことがありますが、処理系自体の完成度はかなり高いので、直せば動くというか、やりたいことができる可能性はそれなりに高いです。何か動かないものがあればTwitterの@yharaにmentionしてもらえれば調査を手伝えるかもしれません。


  1. と言いたいところですがtypoがあって動かなかったのでプルリクしました 

  2. こういう時のために ln -s ~/.rbenv/versions/x.x.x/lib/ruby/gems/x.x.x/gems ~/gems としておくとgemの中身がすぐ読めて便利です 

  3. こういうマジカルな仕様にすると今回みたいに挙動を追うのが大変なので、面倒でもindex.htmlは必須にしたほうが分かりやすくて良いと思う 

« Prev Next »