コンテンツにスキップ

Ruby、データ、ジョブ

ここまでは、若草が足した部分の話でした。 このページは、若草が足していない部分の話です。 Ruby 自身のライブラリと、データベースと、ハンドラの外で処理を進める二つのやり方です。

Ruby 自身の標準ライブラリ

Ruby 自身のライブラリは、どちらの実行でも使えます。 FileDirJSONCSVTimeMathNet::HTTP、ソケット、スレッド、format、正規表現、それに Enumerable が答えることまで、どれもそのまま動きます。 これを若草のライブラリで置き換えてはいないので、覚え直すものもありません。

  require "json"

  def load
    @rows = JSON.parse(File.read(PATH))
  end
  @spread = format("mean %.1f median %d", mean, median)
  @tally  = @votes.tally.sort_by { |name, n| [-n, name] }
  @steps  = @scores.each_cons(2).map { |a, b| b - a }
  @stamp  = Time.at(1_700_000_000).utc.strftime("%Y-%m-%d %H:%M:%S UTC")

どちらの実行でも書くのは Ruby のライブラリですが、実際に答えているのは、片方が CRuby の実装、もう片方がコンパイラの実装です。 その二つが同じ答えを返すことを確かめるのが、ゲートです。 実装が二つあるライブラリについて確かめられるのは、そこまでです(二つの実行)。 demo/stdlib.rbdemo/files.rbdemo/reader.rb は、それを確かめるために置いてあります。

データベース

データベースだけは例外です。 どちらの実行も同じファイルを同じように読まなければ、意味がないからです。 どちらの実行も、エンジンを通して同じ sqlite に届きます。

  sqlite_exec(DB, "CREATE TABLE IF NOT EXISTS notes(body TEXT)")
  sqlite_exec(DB, "INSERT INTO notes VALUES (?)", [@draft])
  rows = sqlite_rows(DB, "SELECT rowid, body FROM notes ORDER BY rowid")
  bodies = sqlite_column(DB, "SELECT body FROM notes")

文には ? を書き、値は別の引数として渡します。 こう書けば、人が打った文字が文の一部になることはありません。 o'brien という品目はアポストロフィであって、SQL の一部にはなりません。 demo/ledger.rb が、ゲートを通るたびにそれを打ち込んでいます。

sqlite_rows は行の一覧を返し、その一行は列の一覧です。 sqlite_column は、各行の最初の列だけを一次元のリストで返します。 読み出した値はすべて文字列で、書き込むときは列の型に従って変換されます。

タイマー

くり返したい処理は every に渡します。

app = Clock.new
every(1.0) { app.tick }
run(app, title: "clock")

タイマーも run の前に書きます。 一度書いたタイマーは、アプリが終わるまで刻み続けます。 どちらの実行も、一つの時計で刻みます。 その時計を進めるのは、ウィンドウではフレーム、スクリプトでは advance:<ms> です。 だから、どちらの実行でも同じ回数だけ呼ばれます。 demo/dashboard.rb が、スクリプトで動かすタイマーです。

ウィンドウの外のジョブ

ハンドラの中で待ってはいけません。 待っているあいだ、ウィンドウは固まります。 時間のかかる処理は task に渡し、終わったあとにすることを on_done に書きます。

  def start
    @status = "working"
    job = task do
      # わざと遅く、わざと算術だけにしてある。
      # 何を返すかで、二つの実行が一致しなければならないからだ。
      total = 0
      i = 0
      while i < 300_000
        total += i % 7
        i += 1
      end
      total
    end
    # このブロックが、ジョブが終わったあとにウィンドウの側で走る。
    # 中の `task_answer` が、ジョブの答え。
    on_done(job) do
      @answer = task_answer
      @status = "done"
    end
  end

どちらの呼び出しも、ジョブが終わるのを待ちません。 task はジョブを始めて番号を返し、on_done はあとで何をするかを登録するだけです。 ハンドラはそこで終わり、ウィンドウは動き続けます。

ジョブの中からアプリの状態や画面に触ってはいけません。 規則はそれだけです。 ジョブの答えが on_done に渡される形になっているのも、そのためです。

ダイアログも、人を待つという意味では同じ種類のジョブです。 だから task を通します。

  job = task { open_dialog("choose a file") }
  on_done(job) { @path = task_answer }

普通のスレッド

動かし続けたい処理は、普通の Thread にします。 監視するもの、待ち受けるもの、アプリが自分のために立てるサーバがそれにあたります。 その結果は、そのスレッドからアプリの状態に書き込むのではなく、タイマーの側で拾います。

ここは二つの実行で違いが出ますが、速いのはリリースしたほうです。 コンパイルされた実行では、二本のスレッドが二つのコアで走ります。 どちらのスレッドも二秒かかる計算なら、全体でも二秒で終わり、ウィンドウはいつもの速さで描き続けます。 同じアプリを CRuby で動かすと倍かかります。 インタプリタはスレッドを一度に一本しか走らせないので、処理が終わるまでウィンドウが止まります。 重い計算は、書いているあいだのほうが、リリースしたあとより悪く見えます。

そのスレッドがある時点までにどこまで進んでいるかは、二つの実行で一致しません。 片方はマシンの時計で動き、もう片方はスクリプトが進める時計で動くからです。 task があるのはそのためで、エンジンが答えを待つので、次の手順に進む前に二つの実行が同じところまで進みます。

書いているあいだ

wakakusa run はアプリのファイルを監視しています。 保存すると、その変更がウィンドウに反映されます。 ファイルが読み直され、クラスも定義し直されますが、ウィンドウが持っているオブジェクトはそのクラスのインスタンスのままです。 だから、新しい view を返しながら、それまでの値をすべて保ちます。

そのオブジェクトの initialize は走り直しません。 状態が生まれるのはそこなので、走り直さないことに意味があります。 構文が通らないファイルを保存しても、ウィンドウは直前の表示のままで、端末にそのことが表示されます。

ファイルを読み直すので、その一番下も走ります。 そこで作られるオブジェクトは二つめで、すぐ捨てられます。 initialize がデータベースを開いたりスレッドを立てたりしているなら、それは保存のたびに起きます。 run より前に宣言したものは、ウィンドウが開いたときのまま残ります。 タイマーは渡された周期のまま動き続け、ウィンドウを開いたまま足したタイマーは、次に起動したときから効きます。

次に読むもの

  • 検証と配布:ウィンドウなしの実行、ゲート、一本のバイナリ。
  • 二つの実行:それぞれの実行が何でできていて、どこが同じでないか。