2012年12月12日水曜日

Playing with Dart Web UI

以前DartでのWeb Componentsの記事を書いてからまだ一月ちょっとくらいしか経ってないけど、Web Componentsパッケージ側で色々と変化があったせいですでに前の情報が役立たずになってしまっている。ということで最新情報で改めて書く。

何が変わったか

以下の通り。詳しくはDartの開発者が書いた記事へ。

  • そもそもパッケージの名前がweb_componentsからweb_uiに変わった。
  • バインディングやらの記法が変わった
  • Web Componentsとは直接関係ないけど、パッケージの構成が少し変わった。

逆にdwc.dartでのコンパイル方法やらDartiumでの閲覧方法は変わってないのでそこは前回と同じでいける。

コードの修正版

パッケージにしたものをGithubのリポジトリにアップした。ここではそれぞれをもう少し詳しく見ていく。

index.html

このindex.htmlはこのサンプルのエントリーポイントで、Dartのmainが記述されてる。以前は一つのindex.htmlに全部まとめてたんだけど、コンポーネント間の依存関係がうまく解決できなかったので今回は細かくファイルを分けた。

<!doctype html>
<html>
    <head>
        <title>Dart Web UI Sample</title>
        <link rel="stylesheet" type="text/css" href="stylesheets/bootstrap.min.css">
        <link rel="shortcut icon" type="image/png" href="images/favicon.png">
        <link rel="components" href="components/animal-summary.html">
        <script type="text/javascript" src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script> 
    </head>
    <body>
    <div id="navigationArea" class="navbar">
        <div class="navbar-inner">
            <div class="container">
                <a class="brand" href="#">Dart Web UI Sample</a>
            </div>
        </div>
    </div>
    <div id="animalListArea">
        <ul>
            <template iterate='a in animalList'>
                <animal-summary model="{{a}}" ></animal-summary>
            </template>
        </ul>
    </div>
    <script type="application/dart">
    import 'package:dart_web_ui_sample/src/AnimalSummary.dart';
    import 'package:dart_web_ui_sample/src/Color.dart';

    List<AnimalSummary> animalList = <AnimalSummary>[
        new AnimalSummary("aardvark",0, new Color(140,70,20)),
        new AnimalSummary("giraffe",1, new Color(255,255,0))
        ];
  
    void main() {}
    </script>  
    </body>
</html>
dwc.dartでコンパイルしてからDartiumで開いたindex.html

記法やらを新しくしたのと、色を示すColorクラスを追加したのが前と違う点。

animal-summary.html

AnimalSummaryクラスのViewModelとViewに相当するのがこのhtmlファイル。templateタグ内のマークアップがViewで、その下にDartで書かれているクラスがViewModel。

<!doctype html>
<html>
 <head>
  <link rel="components" href="color-editor.html">
 </head>
 <body>
     <element name="animal-summary" constructor="AnimalSummaryViewModel" extends="div">
         <template>
             <div>{{header}}</div>
             <color-editor model="{{model.color}}"></color-editor>
             <canvas id="summary" width="300" height="200"></canvas>
         </template>
         <script type="application/dart">
         import 'package:web_ui/web_ui.dart';
         import 'package:dart_web_ui_sample/src/AnimalSummary.dart';

         class AnimalSummaryViewModel extends WebComponent {

             AnimalSummaryViewModel() {
             }

             String get header => "${model.name}:${model.count}";

             void increment(e) {
                 model.count++;
             }

             void created() {
                print("created.");
             }

             void inserted() {
                 _initializeCanvas("#summary");
                 print("inserted.");
             }

             void _initializeCanvas(String id) {
                 var c = query(id);
                 var ctx = c.getContext('2d');
                 ctx.fillStyle = model.color.cssString;
                 ctx.fillRect(0,0,300,200);
             }

             AnimalSummary model;
         }
         </script>
     </element>
 </body>
</html>

Viewとしてはdiv、canvas、それと別に定義したcolor-editorというComponentを使用している。Canvasに関してはViewModel側でinserted関数を実装することで挿入されたタイミングに合わせて初期化を行なっている点が重要。

試したかんじtemplate内の子要素についてはinsertedのタイミングならqueryで取得できる。ただcreatedでqueryしようとしても現状はnullしか返ってこない。

color-editor.html

color-editorはanimal-summaryのtemplateで使っている色のエディタ。本当はスライダー(input type="range")を使いたいんだけど、現状はWeb UI側がスライダーとのデータバインディングをサポートしてないみたいで今はテキストボックスを使ってる。

<!doctype html>
<html>
 <body>
     <element name="color-editor" constructor="ColorViewModel" extends="div">
         <template>
          <ul>
              <li>Red:<input type="text" bind-value="red" /></li>
              <li>Green:<input type="text" bind-value="green" /></li>
              <li>Blue:<input type="text" bind-value="blue" /></li>
          </ul>
         </template>
         <script type="application/dart">
         import 'package:web_ui/web_ui.dart';
         import 'package:dart_web_ui_sample/src/Color.dart';

         class ColorViewModel extends WebComponent {
             ColorViewModel() {
             }

             String get red => "${model.red}";
             String set red(String r) => model.red = int.parse(r);

             String get green => "${model.green}";
             String set green(String g) => model.green = int.parse(g);

             String get blue => "${model.blue}";
             String set blue(String b) => model.blue = int.parse(b);

             void created() {

             }

             void inserted() {
              print("r: ${model.red} g: ${model.green} b: ${model.blue}");
             }

             Color model;
         }
        </script>
     </element>
 </body>
</html>

現状は値をViewModel側に渡すタイミング(WPFで言うところのUpdateSourceTrigger)を制御できないので、一文字消すとその段階でViewModelへの値の引渡しが行われる。そのせいで、テキストボックス内の数字を全部消すとnullか何かが渡ってきてエラーが起きる。

Redのテキストボックスを空にした瞬間にエラーが飛ぶ

本当ならテキストボックスからフォーカスが外れた段階で値の引渡しをやりたい。WPFみたいにタイミングいじれるようになるのか?

Color.dart

ColorはColorViewModelのModelに相当するクラス。見たまんまなので特にいうことはない。あえていえばcssStringというプロパティでCSS用の文字列を返すようにした、という点。

library dart_web_ui_sample;

class Color {
    Color(this.red, this.green, this.blue);
    int red;
    int green;
    int blue;
    String get cssString => "rgb(${red},${green},${blue})";
}

AnimalSummary.dart

本サンプルの主役を張ってるクラス。これも見たまんま。

library dart_web_ui_sample;
import 'package:dart_web_ui_sample/src/Color.dart';

class AnimalSummary {
    AnimalSummary(this.name, this.count, this.color);
    String name = "No Name";
    int count = 0;
    Color color;
}

感想

着実に良くなってる感じ。記法はシンプルになったし、子要素へのqueryも前までできなかったけどできるようになった。書いてて気になったのは下の点くらい。

  • WPFでいうところのUpdateSourceTriggerみたいに値を引渡すタイミングをいじりたい。
  • WPFでいうところのConverter的な仕組みで文字列 to 数値の変換のような働きをViewModelの外に出したい。今回でいえば色の数値をViewModelの中でint.parse()してるのをConverter的なクラスにやらせて、ColorViewModelはあくまでintしかインタフェースを持たないようにしたい。
  • ColorViewModelでの色の変更をAnimalSummaryViewModelに伝えてCanvasの色も更新したいんだけどどうすりゃいいんだ。

でもやっぱhtmlでも部品ごとにhtmlファイル分けて、それぞれで完結するViewとViewModelを書けるのは気持ちいい。

2012年11月4日日曜日

Playing with Dart Web Components

2012/12/27更新:この記事は一部情報が古いので、こちらと併せて読むといいかも。

Dart Web Componentsの記事が10月にアップされて、それからしばらくはバグってたりでまともに使えなかったんだけど、今日試したらなんとか使えた。で、Web Componentsマジさいこーってなったのでちょっとここで参考用に情報を残しておこうと思う。試した環境はUbuntu Linux 12.10(64 bit)。

手順

注:最近はDartで互換性を捨てる変更がいろいろと入ってるため、タイミングによっては同じやり方でうまく行かないかもしれない

Web Componentsを試すにはいろいろと手順が必要で、順番的には以下の通りになる。

  1. Dart SDK、Dartiumの準備
  2. テスト用のパッケージの生成
  3. htmlの記述、およびdwc.dartによるコンパイル
  4. Dartiumでの閲覧

それぞれ順を追って説明する。

Dart SDK、Dartiumの準備

まずすべきは最新のDartiumとSDKを落としてくること。Editorごと落とすと全部含まれているのでそれが楽かも。場所はここ(64bit版)。なぜ最新版でないといけないかというと、安定版では最新のDartの変更が取り込まれていないため、あとの手順で失敗してしまうから。(2012/11/4現在)

Editorを落としたら解凍してでてきたdartディレクトリにパスを通すんだけど、自分は~/binの下にdartディレクトリをおいて、~/.bashrcに以下のように書いてる。

export PATH=$PATH:~/bin/dart/dart-sdk/bin

これで必要な実行ファイル、特にdartとpubにパスが通る。

テスト用パッケージの生成

Dartはコードのパッケージ化を推進していて、今回使うWeb Componentsももちろんパッケージ化されている。パッケージ化されたコードを使う側もパッケージ化したほうが何かと都合がいいので、その作法に従うことにする。パッケージ化の詳細な手順は公式なドキュメントに任せるとして、ここでは最低限必要な部分だけを説明する。

  1. パッケージのルートディレクトリの生成

    パッケージ関連のファイルを格納する適当なディレクトリを作る。今回はdart-web-components-testとする。

  2. pubspec.yamlの作成

    pubspec.yamlというファイルはパッケージの情報、例えば名前やら依存する他のパッケージやらの情報を記述するファイルで、Web Componentsのパッケージを持ってくるためにこれを記述しなければならない。といっても内容は単純で、以下のとおり。

    name: dart-web-components-test 
    dependencies:
      web_components: any 
    

    パッケージの名前と、あとweb_componentsというパッケージに依存してることを示している。内容はこれだけ。

  3. pubによる依存パッケージのダウンロード

    今度は先ほど作ったpubspec.yamlを使って依存するパッケージをダウンロードする。dart-web-components-testのしたにpubspec.yamlを置いて、以下のコマンドをうつ。

    $ pub install
    Resolving dependencies...
    Downloading web_components 0.2.5+2 from hosted...
    Downloading js 0.0.7 from hosted...
    Downloading html5lib 0.0.19 from hosted...
    Dependencies installed!
    $ 
    

    これでWeb Componentsを使うのに必要なパッケージがダウンロードされて、自動的に作られたpackagesディレクトリの下に置かれる。ラクチン。ちなみにpackagesの中身は下のようになってるはず。

    $ ls packages/
    args  html5lib  js  logging  unittest  web_components
    

htmlの記述、およびdwc.dartによるコンパイル

必要なパッケージがそろったので、今度は実際にコードを書いていく。Web Componentsなコードの書き方はこの記事に詳しく書いてあるのでそっちを見るのをおすすめする。MVVM(Model-View-View Model)なコードに経験がある人ならあっさり理解できると思うけど、そうでない人にはもしかしたらちょっとわかりづらいかも。幸い俺は経験があるので問題なかった。

index.html

以下にサンプルのコードを示す。内容は動物のリストを表示するという単純なもの。中ではWeb Componentsの要素(element, template, iterate等)を色々と使ってる。

<!doctype html>
<html>
    <head>
        <title>Dart Web Components</title>
        <link rel="stylesheet" type="text/css" href="dart-web-components-test/bootstrap.min.css">
        <script src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script> 
    </head>
    <body>
    <!-- Summary of Animal -->
    <element name="animal-summary" constructor="AnimalSummaryViewModel" extends="div">
        <template>
            <div>{{header}}</div>
            <button data-action="click:increment">Click to increment</button>
            <input type="text" data-bind="value:name" placeholder="type name here">
        </template>
        <script type="application/dart">
        import 'package:web_components/web_component.dart';

        /// Model class of animal
        class AnimalSummary {
            AnimalSummary(this.name, this.count);
            String name = "No Name";
            int count = 0;
        }

        /// View Model class of animal
        class AnimalSummaryViewModel extends WebComponent {

            /// read-only header
            String get header => "${_animal.name}:${_animal.count}";

            void increment(e) {
                _animal.count++;
            }

            String get name => _animal.name;
            String set name(String s) => _animal.name = s;

            /// accessor for the underlying model
            AnimalSummary get model => _animal;
            AnimalSummary set model(AnimalSummary ts) => _animal = ts;

            AnimalSummary _animal;
        }
        </script>
    </element>

    <div id="navigationArea" class="navbar">
        <div class="navbar-inner">
            <div class="container">
                <a class="brand" href="#">Dart Web Components</a>
            </div>
        </div>
    </div>
    <div id="animalListArea">
        <ul>
            <template iterate='a in animalList'>
                <animal-summary data-value="model: a"/>
            </template>
        </ul>
    </div>
    <script type="application/dart">

    // create a list of animals to show
    List<AnimalSummary> animalList = <AnimalSummary>[
        new AnimalSummary("aardvark",0),
        new AnimalSummary("giraffe",1)
        ];
  
    void main() {}
    </script>  
    </body>
</html>

コードが書けたら今度はそれを一旦コンパイルしないといけない。これは最終的には不要になる(と期待している)けど、現状は必要みたい。ということで以下のコマンドをうつ。

$ dart --package-root=packages/ packages/web_components/dwc.dart index.html 
Total time spent on index.html                               -- 270 ms
$ ls
_index.html.animal_summary.dart  _index.html_bootstrap.dart  packages
_index.html.dart                 bootstrap.min.css           pubspec.lock
_index.html.html                 index.html                  pubspec.yaml

これでindex.htmlの解析がおこなわれて、無数のよくわからないファイル(_index.html*)が生成される。

Dartiumでの閲覧

これで必要なファイルが揃ったのでいよいよDartiumを起動して結果を確認する。Dartiumを起動するときは下のコマンドを使用する。

$ ~/bin/dart/chromium/chrome --user-data-dir --enable-experimental-webkit-features --allow-file-access-from-files &

これでWeb Componentsが有効化されたDartiumが起動する。ついでにローカルファイルアクセスを有効化するフラグもつけた。この状態で先ほど生成された_index.html.htmlを開くと下のようになる。

テキストボックス上で名前をいじれば表示されている名前も変わるし、ボタンを押せば数字も増える。

AnimalSummaryViewModelのプロパティと各種のView、例えば名前とテキストボックスがバインド(Web Component的には別の用語かも)されていて、名前が変更された場合は自動的にその変更が反映されるようになっている。

またulの中のli要素も"iterate='a in animalList'"という書き方でリストから自動的に生成させることができる。その生成される要素もelementとして定義したanimal-summary要素で、すごいすっきりと宣言的に書くことができている。

まとめ

ということでDart Web Componentsを試してみた。JavaScriptではBackbone.jsとかで似たようなことはできるっぽいけど、やっぱり公式にサポートしてもらえると大変助かる。あと最近はC# + WPF + MVVMを書くことが多かったので、Webでも似たような書き方ができるというのは非常にありがたい(というかそうでないとめんどくさすぎてヤル気が起きないくらい)。今後もWeb Componentsには頑張ってほしい。

2012年8月18日土曜日

Playing with Play!

ここ何回か紹介してたWebSocketのサンプル、今度はScala (Playframework) + Dartに移植したので公開する。Playframeworkを使ってる関係でファイルが多いのでGithubのリポジトリにアップしてある。

わざわざGoで書いたのになんで今度はPlayframeworkに移植したかというと、ローカルで遊ぶんじゃなくてどこかのサーバーでプログラムを動かしたいと思ったから。で色々調べたらHerokuが良さそうだ!ってんでHerokuにGo版をアップしたら、実はHerokuがWebSocketにまだ対応してないことが発覚。しょうがないから他の候補を探したらdotCloudというサービスを発見して、でもそこはGoには対応してないからじゃあ今度はScala(というかPlayframework)で書きましょう、という流れになった。

Scalaとの戦い

今回はじめてScalaに触ったけど、これが想像以上に苦戦した。苦戦した点としては以下。

  • 型推論のおかげで(せいで?)色んなところが省略できてしまう。戻り値があるのにreturnは書いても書かなくてもいいとか、引数の無い関数はカッコが不要とか、そういった諸々のルールのおかげで何が型で何が関数かすら最初はわかりづらかった。
  • 記号の羅列、たとえば">>>"とか"?"とかも関数として定義できるから、importしたライブラリのリファレンスをちゃんと探さないとプログラムの意味がわからない。今回のPlayframeworkで言えば"a ? b"という記述の意味するところがわからなくて(結局はAkkaのaskという関数コールを意味してた)、その上検索もしづらいからかなり解読に苦労した。
  • いきなりPlayframeworkのコードはハードル高かった。フレームワーク自体が色々高度なことをやってるから、Scalaはじめたばっかりの人間が見ても厳しいものがある。最初はobject定義とclass定義の違いすらわからなくて、本当はobject定義の方のリファレンス見なきゃいけないのにずっとclassの方の定義をみてて、関数の定義のってねーなーどういうこと? とかやってた。

Application.scala

サーバー側で行う各種の処理を記述してあるのがこのApplication.scalaで、ここで定義された関数をroutesファイルでURLと結びつけることでウェブサイトを構築していく。

indexはルートのURLにアクセスしてきたクライアントに対してUUIDを生成して、そのUUIDを埋めこんだhtml+scriptを返すということをやっている。また、connectはWebSocketでアクセスしてきたクライアントに対してIterateeとEnumeratorの組を生成して返す、ということをやっている。

package controllers

import play.api._
import play.api.mvc._
import play.api.data._
import play.api.libs.json._
import play.api.data.Forms._
import models.Room
import models.Remark

object Application extends Controller {

  private val initialString = """
      _,,,,._                  、-r
   ,.','" ̄`,ゝ _,,,_   _,,,_   _,,,,__,. | |  _,,,,,_
  { {   ,___ ,'r⌒!゙! ,'r⌒!゙! ,.'r⌒!.!"| l ,.'r_,,.>〉
  ゝヽ、 ~]| ゞ_,.'ノ ゞ_,.'ノ ゞ__,.'ノ | l {,ヽ、__,.
   `ー-‐'"   ~    ~  〃 `゙,ヽ ̄` `゙'''"
                 ゙=、_,.〃
  """
 
  def index = Action { implicit request =>
    val uuid = java.util.UUID.randomUUID.toString
    Logger.info("Generated uuid: "+uuid)
    Ok(views.html.index("Remark Presenter", uuid, initialString))
  }

  def connect(userID: String) = WebSocket.async[JsValue] { request  =>
    Logger.info("userID from client: "+userID)
    Room.join(userID) 
  }
}

Room.scala

クライアントとやりとりを行うアクターを定義している。基本的にやっていることは単純で、クライアントをリストに追加して誰かが発言するたびにその内容を全員に伝播する、というもの。

package models

import akka.actor._
import akka.util.duration._

import play.api._
import play.api.libs.json._
import play.api.libs.iteratee._
import play.api.libs.concurrent._

import akka.util.Timeout
import akka.pattern.ask

import play.api.Play.current

object Room {

  lazy val default = Akka.system.actorOf(Props[Room])

  implicit val timeout = Timeout(1 second)

  def join(userID:String):Promise[(Iteratee[JsValue,_],Enumerator[JsValue])] = {
    (default ? Join(userID)).asPromise.map {
      
      case Connected(enumerator) => {
      
        // Create an Iteratee to consume the feed
        val iteratee = Iteratee.foreach[JsValue] { event =>
          val content = (event \ "Remark").as[String]
          val duration = (event \ "Duration").as[String]
          val path = (event \ "Path").as[String]
          val doRotate = (event \ "Rotate").as[Boolean]
          val remark = Remark(content, duration, path, doRotate)
          default ! Talk(userID, remark)
        }.mapDone { _ =>
          default ! Quit(userID)
        }

        (iteratee,enumerator)
      }
        
      case CannotConnect(error) => {
      
        // Connection error

        // A finished Iteratee sending EOF
        val iteratee = Done[JsValue,Unit]((),Input.EOF)

        // Send an error and close the socket
        val enumerator =  Enumerator[JsValue](JsObject(Seq("error" -> JsString(error)))).andThen(Enumerator.enumInput(Input.EOF))
        
        (iteratee,enumerator)
      }
    }
  }
}

case class Remark(content: String, duration: String, path: String, rotate: Boolean)
case class Join(userID: String)
case class Quit(userID: String)
case class Talk(userID: String, remark: Remark)

case class NotifyJoin(userID: String)
case class Connected(enumerator:Enumerator[JsValue])
case class CannotConnect(msg: String)

class Room extends Actor {
 private var clients = Map.empty[String, PushEnumerator[JsValue]]

 def receive = {
  case Join(userID) => {
      // Create an Enumerator to write to this socket
      val channel =  Enumerator.imperative[JsValue]( onStart = self ! NotifyJoin(userID))
      if(clients.contains(userID)) {
        sender ! CannotConnect("This userID is already used")
      } else {
        clients = clients + (userID -> channel)
        
        sender ! Connected(channel)
      }
  }

    case Quit(userID) => {
      clients = clients - userID
      Logger.info(userID + " leaved")
      //notifyAll("quit", userID, "has leaved the room")
    }

  case Talk(userID, remark) => {
      Logger.info(userID + ":" +remark.toString)
   // send to all clients
      val msg = Json.toJson(
          Map(
          "Remark" -> Json.toJson(remark.content),
          "Duration" -> Json.toJson(remark.duration),
          "Path" -> Json.toJson(remark.path),
          "Rotate" -> Json.toJson(remark.rotate)
        )
      ) 
      //Logger.info(msg.toString);
      clients.foreach { 
        case (_, channel) => channel.push(msg)
      }
   }
    
    case NotifyJoin(userID) => {
      Logger.info(userID + " joined")
      // notifyAll("join", userID, "has entered the room")
    }
  }


  // def notifyAll(kind: String, user: String, text: String) {
  //   val msg = JsObject(
  //     Seq(
  //       "kind" -> JsString(kind),
  //       "user" -> JsString(user),
  //       "message" -> JsString(text),
  //       "clients" -> JsArray(
  //         clients.keySet.toList.map(JsString)
  //       )
  //     )
  //   )
    
  //   clients.foreach { 
  //     case (_, channel) => channel.push(msg)
  //   }
  // }
}

routes

Application.scalaで記述されている関数と実際のURLを紐づけているのがこのroutesファイル。一番下の行はpublicディレクトリの下の各種CSS・スクリプトとURLを対応付けしている。

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                           controllers.Application.index
GET     /:userID                    controllers.Application.connect(userID: String)
GET     /assets/*file               controllers.Assets.at(path="/public", file)

main.scala.html

クライアント側で実行されるスクリプトのmain部分が含まれたテンプレート。Application.scalaで生成されたuserIDをもとにWebSocketへ接続を行うといった処理がDart側で行なわている。

@(title: String, userID: String)(content: Html)(implicit request: RequestHeader)

<!doctype html>
    <head>
        <title>@title</title>
        <link rel="stylesheet" type="text/css" href="@routes.Assets.at("stylesheets/bootstrap.min.css")">
        <link rel="stylesheet" type="text/css" href="@routes.Assets.at("stylesheets/client.css")">
        <link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
        <script src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script> 
    </head>
    <body>
    <script type="application/dart">
    #import("@routes.Assets.at("scripts/App.dart")");
    #import("@routes.Assets.at("scripts/SVGRemarkDisplayer.dart")");

    void main() {
        // initialize displayers
        final int MAX_NUMBER_OF_REMARKS = 15;
        var displayer = new SVGRemarkDisplayer();
        displayer.initialize("#stage", MAX_NUMBER_OF_REMARKS);

        PanelLayoutParameter param = new PanelLayoutParameter("#inputPanel", "#displayArea", "#remarkPanel", "#hideInputPanel", "#hideRemarkPanel");

        var url = "@routes.Application.connect(userID).webSocketURL()";
        print(url);

        App app = new App();
        app.initialize(url, displayer, param);
    }
    </script>
        @content
    </body>
</html>

サーバー動作

上記のキモとなるロジックの部分とその他フレームワーク的に必要なものを用意してPlayframeworkに実行させ、Dartiumでlocalhost:9000にアクセスすると実行されているのを確認することができる。下は実際にPlayframworkを実行させたときのログ。

$ play
[info] Loading project definition from /home/masato/desk/programs/xclamm-j/project
[info] Set current project to xclamm-j (in build file:/home/masato/desk/programs/xclamm-j/)
       _            _ 
 _ __ | | __ _ _  _| |
| '_ \| |/ _' | || |_|
|  __/|_|\____|\__ (_)
|_|            |__/ 
             
play! 2.0.3, http://www.playframework.org

> Type "help play" or "license" for more information.
> Type "exit" or use Ctrl+D to leave this console.

[xclamm-j] $ run

--- (Running the application from SBT, auto-reloading is enabled) ---

[info] play - Listening for HTTP on port 9000...

(Server started, use Ctrl+D to stop and go back to the console...)

[info] play - database [default] connected at jdbc:h2:mem:play
[info] play - Application started (Dev)
[info] application - Generated uuid: a447101a-df11-45c5-8c64-97f0d1a96eb0
[info] application - userID from client: a447101a-df11-45c5-8c64-97f0d1a96eb0
[info] play - Starting application default Akka system.
[info] application - a447101a-df11-45c5-8c64-97f0d1a96eb0 joined

2012年5月28日月曜日

WebSocket client with Dart, WebSocket server with Go

ちょっと前の話になるけど、5/12に開催されたDartのハッカソンに参加して、そこでWebSocketを用いてウェブアプリをつくるチームに参加した。自分はここで公開してるSVGアニメーション用のコードがあったので、それを用いたアプリを作ってきた。

その場ではサーバーもクライアントもDartで書いたんだけど、個人的にはサーバーサイドはGoで書きたかったので改めてGoで書きなおしてみた。ちなみにハッカソンではGithubを使って開発したので、それを自分のところへForkして拡張してる。場所はここね。

server.go

基本的にはmanageClientという関数がクライアントに関するリソースを管理するアクターで、serveClient内での各クライアントとのやりとりはすべてこのmanageClientに対して転送される。例えば新規にクライアントがつながってきた場合はAddClientRequestがserveClientからmanageClientに対して送られ、クライアント一覧に相当するmapにクライアントとの接続が格納される。

ちなみにちょっと苦労したのがscriptやらCSSやらをGoのhttpサーバー経由でどうやってホストするのか、という点。それは色々調べたところhttp.ServeFile()という関数を使うとファイルに対するリクエストは大体いい感じに処理してくれることがわかったので、下のコードはそれを採用している

package main

import (
 "code.google.com/p/go.net/websocket"
 "net/http"
 "fmt"
)

type RequestType int
type ResponseType int

var (
 requestQueue = make(chan Request)
 responseQueue = make(chan Response)
)

const (
 // Request
 GENERATE_CLIENT_ID RequestType = 0
 ADD_CLIENT RequestType = 1
 REMOVE_CLIENT RequestType = 2
 BROADCAST RequestType = 3

 // Response
 CLIENT_ID ResponseType = 0
)

type RequestParameter struct {
 clientID int
 Connection *websocket.Conn
}

type DisplayParameter struct {
 Remark string
 Duration string
 Path string
 Rotate string
}

type Request interface {
 Type() RequestType
 ClientID() int
 Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn
}

type GenerateClientIDRequest struct {
 RequestParameter
}

type BroadcastRequest struct {
 RequestParameter
 DisplayParameter
}

type AddClientRequest struct {
 RequestParameter
}

type RemoveClientRequest struct {
 RequestParameter
}

type Response interface {
 Type() ResponseType
}

type GenerateClientIDResponse struct {
 ClientID int
}

func (gcr GenerateClientIDResponse) Type() ResponseType {
 return CLIENT_ID
}

func (gcm GenerateClientIDRequest) ClientID() int {
 return gcm.clientID
}

func (gcm GenerateClientIDRequest) Type() RequestType {
 return GENERATE_CLIENT_ID
}

func (gcm GenerateClientIDRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn {
 res := GenerateClientIDResponse {
  *currentClientID,
 }
 queue <- res
 // proceed to next id
 *currentClientID++
 return clientList
}



func (acm AddClientRequest) ClientID() int {
 return acm.clientID
}

func (acm AddClientRequest) Type() RequestType {
 return ADD_CLIENT
}

func (acm AddClientRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn {
 clientList[acm.clientID] = acm.Connection
 fmt.Printf("Added client. %d clients existing.\n", len(clientList))
 return clientList
}

func (bm BroadcastRequest) ClientID() int {
 return bm.clientID
}

func (bm BroadcastRequest) Type() RequestType {
 return BROADCAST
}

func (bm BroadcastRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn  {
 // send a text message serialized as JSON.
 for id, client  := range clientList {
  fmt.Printf("Sending to %d\n", id)
  err := websocket.JSON.Send(client, bm.DisplayParameter)
  if err != nil {
   fmt.Printf("error: %s\n",err)
   // remove client
   delete(clientList, id)
  }
 }
 fmt.Println("Broadcasted")
 return clientList
}

func (rcm RemoveClientRequest) ClientID() int {
 return rcm.clientID
}

func (rcm RemoveClientRequest) Type() RequestType {
 return REMOVE_CLIENT
}

func (rcm RemoveClientRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn  {
 // remove client
 delete(clientList, rcm.clientID)
 fmt.Printf("Removed client ID:%d. %d left\n", rcm.clientID, len(clientList))
 return clientList
}

/**
  * Message loop for the client manager
  */
func manageClient(reqQueue chan Request, resQueue chan Response) {
 var currentClientID int = 0
 clientList := make(map[int]*websocket.Conn, 0)
 fmt.Println("Entering client manage loop")
 for {
  select {
  case msg := <- reqQueue:
   fmt.Printf("client:%d type:%d\n", msg.ClientID(), msg.Type())
   clientList = msg.Process(clientList, resQueue, &currentClientID)
  }
 }
}

/**
  * Get a new client ID from the client manager
  */
func generateClientID(reqQueue chan Request, resQueue chan Response)  int {
 msg := GenerateClientIDRequest {}
 reqQueue <- msg
 res := <- resQueue
 v, _ := res.(GenerateClientIDResponse)
 return v.ClientID
}

/**
  * Function that serves each client
  */
func serveClient(ws *websocket.Conn, reqQueue chan Request, resQueue chan Response) {
 id := generateClientID(reqQueue, resQueue)
 defer func() {
  // remove client when exiting
  msg := RemoveClientRequest {
   RequestParameter {
    id,
    ws,
   },
  }
  reqQueue <- msg
 }()

 // add client first
 msg := AddClientRequest {
  RequestParameter {
   id,
   ws,
  },
 }
 reqQueue <- msg
 fmt.Println("Entering receive loop")
 for {
  var param DisplayParameter
  err := websocket.JSON.Receive(ws, &param)
  if err != nil {
   fmt.Printf("receive error: %s\n",err)
   break
  }
  fmt.Printf("recv:%#v\n", param)

  // broadcast received message
  msg := BroadcastRequest {
   RequestParameter {
    id,
    ws,
   },
   param,
  }
  reqQueue <- msg
 }
}

/**
  * Handler for the websocket json server
  */
func echoJsonServer(ws *websocket.Conn) {
 fmt.Printf("jsonServer %#v\n", ws.Config())
 serveClient(ws, requestQueue, responseQueue)
}

/**
  * Serve all files (html, dart, css)
  */
func mainServer(w http.ResponseWriter, req *http.Request) {
 path := req.URL.Path[1:]
 fmt.Printf("path: %s\n",path)
 http.ServeFile(w, req, path)
}

func main() {
 // start an actor that processes requests from every client
 go manageClient(requestQueue, responseQueue)

 // setup the handlers
 http.Handle("/echo", websocket.Handler(echoJsonServer))
 http.HandleFunc("/", mainServer)
 fmt.Println("serving...")
 port := 8080
 err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
 if err != nil {
  panic("ListenAndServer: " + err.Error())
 }
}

client.dart

クライアント側で実行されるDartのコードが以下。いままでのサンプルと違うのは、他のクライアントとポスト内容を共有するためポストボタンをクリックしたら即websocketで内容を送っているところ。実際の表示処理はサーバー側からブロードキャストされてきたメッセージに対してon.messageのハンドラで実行することになる。

#import("dart:html");
#import("dart:json");
#import("RemarkDisplayer.dart");
#import("SVGRemarkDisplayer.dart");
#import("Unit.dart");

void main() {
    // initialize displayers
    final int MAX_NUMBER_OF_REMARKS = 15;
    final int MAX_SPEED = 100;
    var displayer = new SVGRemarkDisplayer();
    displayer.initialize("#stage", MAX_NUMBER_OF_REMARKS);

    // create websocket and add handlers
    WebSocket webSocket = new WebSocket("ws://localhost:8080/echo");
    Element status = document.query("#statusArea");
    webSocket.on.message.add((event) {
            // display remark with given parameters
            var message = event.data;
            Map map = JSON.parse(message);
            DisplayParameter param = new DisplayParameter.map(map);
            displayer.display(param);
        });

    // connect post button with websocket
    TextAreaElement textNode = document.query("#remarkText");
    TextAreaElement pathNode = document.query("#pathString");
    Element postButton = document.query("#postRemark");
    InputElement speedNode = document.query("#displaySpeed");
    InputElement rotateNode = document.query("#rotateRemark");
    postButton.on.click.add((Event e) {
        String message = textNode.value;
        String path = pathNode.value;

        bool rotate = rotateNode.checked;
        var num = speedNode.valueAsNumber;
        var duration = (MAX_SPEED - num.toInt()) * 100;
        var param = new DisplayParameter(message, duration, Unit.Millisecond, path, rotate);
        String paramJson = JSON.stringify(param.toMap());
        webSocket.send(paramJson);
    });
}

感想

Dartで書いたときと比べてGo版はかなりコード量が増えた気がする。大半はmanageClientとどういったメッセージをやりとりするかの定義なので、アクターモデルをやるからにはしょうがないのかもしれない。ただやっぱり厳密に書こうとするとアクターモデルはめんどう。もっと規模が大きくなればアクターモデルの恩恵がわかる、かも?

サーバー側の出力例

サーバー側をビルド・実行して、上記のDartクライアントからGoogleのアスキーアートをPostした場合の出力が下のログ。参考に掲載しておく。

$ go build server.go 
$ ./server 
serving...
Entering client manage loop
path: 
jsonServer &websocket.Config{Location:(*url.URL)(0x188abd80), Origin:(*url.URL)(0x188abdc0), Protocol:[]string(nil), Version:13, TlsConfig:(*tls.Config)(nil), handshakeData:map[string]string(nil)}
client:0 type:0
Entering receive loop
client:0 type:1
Added client. 1 clients existing.
path: favicon.ico
recv:main.DisplayParameter{Remark:"\u3000\u3000\u3000\u3000_,,,,._\u3000\u3000\u3000\u3000\u3000 \u3000 \u3000 \u3000 \u3000 \u3000 \u3000 、-r\n\u3000 \u3000,.','\" ̄`,ゝ\u3000_,,,_\u3000\u3000 _,,,_\u3000\u3000 _,,,,__,. | |\u3000 _,,,,,_\n\u3000\u3000{ {\u3000\u3000 ,___\u3000,'r⌒!゙! ,'r⌒!゙! ,.'r⌒!.!\"| l ,.'r_,,.>〉\n\u3000\u3000ゝヽ、\u3000~]|\u3000ゞ_,.'ノ\u3000ゞ_,.'ノ\u3000ゞ__,.'ノ\u3000| l {,ヽ、__,.\n\u3000\u3000\u3000`ー-‐'\"\u3000\u3000 ~\u3000\u3000\u3000 ~\u3000\u3000〃 `゙,ヽ ̄` `゙'''\"\n\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000 ゙=、_,.〃 ", Duration:"5000&ms", Path:"M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120 A15 10 170 0 1 10 150", Rotate:"false"}
client:0 type:3
Sending to 0
Broadcasted

2012年5月4日金曜日

SVG Animation in Dartium

ブラウザ上で何らかのアニメーションをさせようとした場合、選択肢が色々ある。以前はCSS3を使ったし、他にもCanvasを使ったりSVGを使ったりスクリプトベースのアニメーション(要素の位置を定期的に更新する等)もやろうと思えばできる。今回はそのなかでもSVGを用いたアニメーションをDartでやってみた。

SVGでアニメーションする理由

SVGでアニメーションさせようとした理由は主に表現の簡単さから。

SVGはxmlの一種なので、マークアップの形で比較的簡単に文字列を回転させたり指定されたパスの上を通るようなアニメーションを記述することができる。Canvasやスクリプトベースのアニメーションのように1フレームごとにちょっとずつ処理をする、といった下回りを記述する必要もなく、アニメーションの定義だけ書けばあとはブラウザがよろしくやってくれるのはかなり楽。加えてアニメーションの描画に関してもすべてブラウザ側に委託されているので、ブラウザがかしこく描画してくれさえすれば人間が細々チューニングする必要もない。

DartでSVGアニメーション

dartからSVGアニメーションさせようとした場合はいくつか落とし穴があって、そのなかでも一番大きい穴はDartium側のバグだったんだけど、とりあえずうまくいったのでサンプルを示す。

html

まえつくったCSS3のアニメーションとほぼ同等の動きをSVGで再現したつもり。一点違うのがスライダーで速度の調整をできるようにした点。ちなみに、bodyの閉じタグの前に入ってるscriptは現状のDartiumのバグを回避するためのもの。

<!doctype html>
<head>
    <meta charset="utf-8">
    <title>dartlang test</title>
    <link rel="stylesheet" type="text/css" href="./css/bootstrap.min.css" />
    <link rel="stylesheet" type="text/css" href="./css/client.css" />
</head>
<body>
    <script type="application/dart"  src="./script/svgtest.dart"></script>
    <div class="navbar">
        <div class="navbar-inner">
            <div class="container">
                <a class="brand" href="#">dartlang test</a>
            </div>
        </div>
    </div>
    
    <div class="container-fluid">
        <div class="row-fluid" >
            <div class="span4">
                <textarea class="input-xxlarge" rows="23" id="remarkText">
        ___|二ニー-、、;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:|;::;:;:;:;:;:;:;:;:;:;:;:;:l
        /rヽ三三三三三─‐-- 、;:;:;:;:;:;:;:|;:;:;:;:;:;:;:;:;:;:;:;:;:;l
        ',i ,-三三三三三、   _,.ニ、ー-、!;: -‐二 ̄彡′
        ',、、ヾ三三'" ̄ ̄   `ー‐"    ヾ-'"  .〉′
        ヽ ヽヾ三,'    :::..,. -‐- 、     _,,..-‐、、,'
         `ー',ミミ     ::.弋ラ''ー、   i'"ィ'之フ l
         /:l lミミ     ::::.. 二フ´   l ヽ、.ノ ,'     
      ,.-‐フ:::::| |,ミ             l      /       
     /r‐'":::::::::| |ヾ        /__.   l    /      
 _,. -‐"i .|::::::::::::::::::',.',. \        ⌒ヽ、,ノ   /ヽ,_             
"    l ヽ:::::::::::::::::ヽヽ. \   _,_,.、〃  /l |    ___,. -、
     ',\\:::::::::::::::ヽ\  \  、. ̄⌒" ̄/:::::| |    ( ヽ-ゝ _i,.>-t--、
     \\\;::::::::::::\\  `、.__  ̄´ ̄/::::::::::l |    `''''フく _,. -ゝ┴-r-、
       ヽ \`ー-、::::::ヽ ヽ    ̄フフ::::::::::::::ノ ./   ,.-''"´ / ̄,./´ ゝ_'ヲ
          `ー-二'‐┴┴、__/‐'‐´二ー'".ノ   / _,. く  / ゝ_/ ̄|
               ̄`ー─--─‐''" ̄      / にニ'/,.、-t‐┴―'''''ヽ
                              /  /  .(_ヽ-'__,.⊥--t-⊥,,_
                              /  /  /   ̄   )  ノ__'-ノ
                             /      /    ゝニ--‐、‐   |
                            /           /‐<_   ヽ  |ヽ </textarea>

                <!-- setting panel -->
                <div class="setting">
                    <ul>
                        <li>
                            <label>Display speed</label>
                            <input type="range"  min="0" max="99" value="50" step="1" id="displaySpeed">
                        </li>
                    </ul>
                    <button class="btn-primary" id="postRemark" type="button">Post</button>
                </div>
            </div>

            <div class="span8 offset4">
                <div id="stage">
                </div>
            </div>
        </div>
    </div>
    <!-- workaround for SVG animation bug http://code.google.com/p/dart/issues/detail?id=2856 -->
    <script>navigator.webkitStartDart()</script>
</body>
</html>

svgtest.dart

mainにあたる部分。ここでは別のファイルに定義されているクラスを生成して、あとはボタンのハンドラに表示処理をつなげている。

#import("dart:html");
#import("RemarkDisplayer.dart");
#import("SVGRemarkDisplayer.dart");
#import("Unit.dart");

void main() {
    final int MAX_NUMBER_OF_REMARKS = 10;
    final int MAX_SPEED = 100;

    var displayer = new SVGRemarkDisplayer();
    displayer.initialize("#stage", MAX_NUMBER_OF_REMARKS);

    var postButton = document.query("#postRemark");
    InputElement speed = document.query("#displaySpeed");
    TextAreaElement textNode = document.query("#remarkText");
    postButton.on.click.add((Event e) {
            var num = speed.valueAsNumber;
            var duration = (MAX_SPEED - num.toInt()) * 100;
            var param = new DisplayParameter(textNode.value, duration, Unit.Millisecond);
            displayer.display(param);
    });
}

SVGRemarkDisplayer.dart

SVGでの表示処理を行うクラス。initializeで各文字列の親にあたるg要素を生成しておいて、実際に表示を行うdisplayでは渡された文字列をtspanで複数行に分解して、アニメーションをくっつけて表示している。アニメーション自体は左から右に要素を移動させる単純なもの。スライダーによって指定された秒数でアニメーションの速度を調整することができる。

#library("SVGRemarkDisplayer");
#import("dart:html");
#import("RemarkDisplayer.dart");

/**
 * Class that displays remarks via SVG
 */
class SVGRemarkDisplayer implements RemarkDisplayer {
    SVGRemarkDisplayer() {
        _remarkList = new List<SVGElement>();
        _currentRemark = 0;
        _numberOfRemarks = 0;
    }

    /**
     * Create remark nodes under the tag with the given ID
     */
    void initialize(String stageID, int numberOfRemarks) {
        _numberOfRemarks = numberOfRemarks;
        var stage = document.query(stageID);

        var svg = new SVGSVGElement();
        for (int i=0; i<numberOfRemarks; i++) {
            SVGGElement g = new SVGElement.svg("<g></g>");
            _remarkList.add(g);
            svg.nodes.add(g);
        }

       stage.nodes.add(svg);
       _root = svg;
    }

    /**
     * Display given remark at the current node
     */
    void display(DisplayParameter parameter) {
        var node = _remarkList[_currentRemark];

        // delete any nodes we already have
        node.nodes.clear();

        // convert text to svg
        var lines = parameter.remark.split("\n");
        SVGTextElement text = new SVGElement.svg("<text font-family='IPA モナーPゴシック' y='300'></text>");
        for (int i=0; i<lines.length; i++) {
            var line = lines[i];
            SVGTSpanElement span = new SVGElement.svg("<tspan x='0' dy='15'>${line}</tspan>");
            text.nodes.add(span);
        }
        
        node.nodes.add(text);
        var duration = "${parameter.duration}${parameter.durationUnit.toString()}";
        SVGAnimationElement animation
            = new SVGElement.svg("<animateTransform attributeName='transform' type='translate' from='0' to='1300' dur='${duration}' fill='freeze' begin='indefinite' />");
        node.nodes.add(animation);
        animation.beginElement();

        // proceed to next remark
        _currentRemark++;
        if (_currentRemark >= _numberOfRemarks) {
            _currentRemark = 0;
        }
    }

    int _numberOfRemarks;
    List<SVGElement> _remarkList;
    int _currentRemark;
    SVGSVGElement _root;
}

RemarkDisplayer.dart

文字列の表示をつかさどるクラスのインタフェース定義。CSS3を使ってアニメーションするクラスの方(今回は未使用)もおなじインタフェースを実装している。

#library("RemarkDisplayer");
#import("dart:html");
#import("Unit.dart");

/**
 * Parameter for displaying
 */
class DisplayParameter {
    DisplayParameter(this.remark, this.duration, this.durationUnit);
    String remark;
    int duration;
    Unit durationUnit;
}

/**
 * Class that manages remark display
 */
interface RemarkDisplayer {
    /**
     * Display given remark
     */
    void display(DisplayParameter param);
}

Unit.dart

表示単位を隠蔽するためのクラス。本当はenumを使用したいものの、現状のdartにはenumが無いためconstつきコンストラクタで代用している。

#library("Unit");

/**
  * Unit for animation
  */
class Unit {
    const Unit(int id) : _id = id;

    String toString() {
        switch (_id)
        {
        case PixelID:
            return "px";
        case MillisecID:
            return "ms";
        default:
            return "px";
        }
    }

    final int _id;

    static final int PixelID = 0;
    static final int MillisecID = 100;
    static final Pixel = const Unit(PixelID);
    static final Millisecond = const Unit(MillisecID);
}

結果

PostをクリックするとSVGアニメーションが左から右へ流れる。またスライダーを左に寄せるとゆっくり、右に寄せると速いアニメーションで表示することができる。

2012年2月21日火曜日

Playing with Dartium part 2

ひき続きDartiumでお遊び。今回はIsolateを試してみた。が、結論から言うと目論みははずれてうまくいなかったです。

Isolate

Dartはつねにシングルスレッドで実行される(仕様書にも"Dart code is always single threaded."て書いてある)んだけど、何らかの処理を並行して実行したい場合というのが出てくる。他の言語だったらスレッドなりの出番なんだけど、dartではIsolateという仕組みを使うみたい。

Isolate自体はスレッドのようなものだと考えれば良くて(実際にスレッドをつくるかどうかはIsolateの設定次第)、使いかたはIsolateクラスをextendしてmain関数を定義するだけ。まあその辺の作法の詳細はドキュメントサンプルを参照。

RemarkDisplayerIsolate

ということで前回つくったRemarkDisplayerをIsolate化して使おうと思ったんだけど、その過程でちょっと予想外なエラーにぶちあったった。Dartiumごとクラッシュしたのでログだけ載せる。

Unhandled exception:
DOM access is not enabled in this isolate
 0. Function: 'Utils.window' url: '/mnt/data/b/build/slave/dartium-lucid64-inc/build/src/dart/client/dom/src/native_DOMImplementation.dart' line:20 col:3
 1. Function: '::get:window' url: '/mnt/data/b/build/slave/dartium-lucid64-inc/build/src/dart/client/dom/src/native_GlobalProperties.dart' line:6 col:36
 ...

IsolateからはDOMに触れないって話。エラーメッセージ読むかぎりだと設定で有効化できそうな雰囲気もあるけど、予想ではたぶんできない。C#でGUIを作るときもそうだけど、バックグラウンドのスレッドからは直接UI要素に触らせてくれない場合が多い。

感想

まあとりあえずIsolateの使いかたそのものは何となくわかったのでよしとする。でもIsolate自体はあんまり使いやすくないな…わかりづらい。GoのChannelみたいにPortが双方向だともっとスッキリしてわかりやすいと思うんだけども。

コード

途中で例外飛ぶけど、失敗例としてコードを掲載しておく。

  • RemarkDisplayer.dart
    #library("RemarkDisplayer");
    
    #import("dart:html");
    
    class MessageTypes {
        static final int ERROR = -1;
        static final int OK = 0;
        static final int SETUP = 1;
        static final int INIT = 2;
        static final int DISPLAY = 3;
    }
    
    class DisplayerState {
        static final int SETUP = 1;
        static final int INITIALIZED = 2;
        static final int DISPLAYING = 3;
    }
    
    /**
     * Class that attempted to manipulate DOM in an Isolate.
     */
    class RemarkDisplayerIsolate extends Isolate {
        RemarkDisplayerIsolate() {}
    
        void main() {
            port.receive((message, SendPort replyTo) {
                    dispatch(message, replyTo, port.toSendPort());
            });
        }
    
        void dispatch(var message, SendPort replyTo, SendPort myPort) {
            int action = message['action'];
            var arg = message['arg'];
            switch(action) {
            case MessageTypes.SETUP:
                _displayer = new RemarkDisplayer(arg);
                replyTo.send(DisplayerState.SETUP, myPort);
                break;
            case MessageTypes.INIT:
                _displayer.initialize(arg); // EXCEPTION FIRED HERE
                replyTo.send(DisplayerState.INITIALIZED, myPort);
                break;
            case MessageTypes.DISPLAY:
                _displayer.display(arg);
                replyTo.send(DisplayerState.DISPLAYING, myPort);
                break;
            }
        }
    
        RemarkDisplayer _displayer;
    }
    
    /**
     * Class that manages remark display
     */
    class RemarkDisplayer {
        RemarkDisplayer(int numberOfRemarks) {
            _numberOfRemarks = numberOfRemarks;
            _remarkList = new List<Element>();
            _currentRemark = 0;
        }
        
        /**
         * Create remark nodes under the tag with the given ID
         */
        void initialize(String stageID) {
            var stage = document.query(stageID);
            for (int i=0; i<_numberOfRemarks; i++) {
                var tag = '<pre class="remark" id="remark${i}" draggable="true"/>';
                var elem = new Element.html(tag);
                stage.nodes.add(elem);
                _remarkList.add(elem);
            }
        }
        
        /**
         * Display given remark at the current node
         */
        void display(String remark) {
            var node = _remarkList[_currentRemark];
            var durationMS= 5000;
            node.innerHTML = remark;
            node.style.visibility = "visible";
            node.style.animationName = "fade, hslide";
            node.style.animationDuration = "${durationMS}ms";
            node.style.animationTimingFunction = "linear";
            node.style.animationFillMode = "forwards";
            
            // Replace node with a clone to restart animation
            var newNode = node.clone(true);
            node.replaceWith(newNode);
            _remarkList[_currentRemark] = newNode;
            
            // proceed to next remark
            _currentRemark++;
            if (_currentRemark >= _numberOfRemarks) {
                _currentRemark = 0;
            }
        }
        
        int _numberOfRemarks;
        get numberOfRemarks() => _numberOfRemarks;
        
        List<Element> _remarkList;
        int _currentRemark;
    }
    
  • main.dart
    #library('displayer');
    
    #import("dart:html");
    #import("RemarkDisplayer.dart");
    
    void dispatch(var message, SendPort replyTo, SendPort myPort) {
        switch(message) {
        case DisplayerState.SETUP:
            var msg = { 
                "action" : MessageTypes.INIT,
                "arg" : "#stage"
            };
            replyTo.send(msg, myPort);
            break;
        case DisplayerState.INITIALIZED:
            var msg = { 
                "action" : MessageTypes.DISPLAY,
                "arg" : "adsfasfas"
            };
            replyTo.send(msg, myPort);
            break;
        case DisplayerState.DISPLAYING:
            replyTo.close();
            break;
        }
    }
    
    void main() {
        final int MAX_NUMBER_OF_REMARKS = 10;
        final receivePort = new ReceivePort();
        receivePort.receive((var message, SendPort sendPort) {
                print("received ${message}");
                dispatch(message, sendPort, receivePort.toSendPort());
        });
    
        new RemarkDisplayerIsolate().spawn().then((SendPort sendPort) {
                var msg = { 
                    "action" : MessageTypes.SETUP,
                    "arg" : MAX_NUMBER_OF_REMARKS
                };
                sendPort.send(msg, receivePort.toSendPort());
         });
    }
    

2012年2月19日日曜日

Playing with Dartium

前にjavascriptで書いてた処理をdartに移植してDartiumで動かしてみた。やらんとしていたのはニコニコもどきだけど、書いてる途中にjavascriptに嫌気がさして投げたので色々と中途半端。とりあえず処理の内容は以下。

  1. 特定のid(今回だと"stage")を持つノードにいくつかノードを追加する。
  2. ボタンがクリックされるたびにTextArea内の内容を(最初に追加された)ノードにわりあてて、CSSアニメーションをくっつけて表示する。

完成図

Postをクリックするたびに麻呂がフェードアウトしながら右にスライドしていきます。スバラシイ。

html + dart

dartのコードはclassの使い方を試す意図もあったので、あえてclassにして書いてる。その過程で学んだことを数点書くと以下の通り。

  • Dart Style Guideには従っとけ。
  • 変数名の頭に"_"をつけるとprivateな変数になる。つけないと自動的にpublicになる。
  • intの変数を文字列表現に変換したいような場合は"$value"や"${value}"という書きかた(string interpolation syntax)をすると自動的に文字列変換してくれる。こういう仕様になった事情はDartの言語仕様を見ると書いてあって、抜粋すると以下の通り。

    The string interpolation syntax is designed to be familiar and easy to use, if somewhat awkward to parse. The intent is to encourage its use over alternatives such as s1 + s2. In a dynamically typed language, the use of the + operator requires dynamic dispatch. In contrast, in the case of string interpolation we can statically determine that the string concatenation operation is required, making the operation more efficient. Even more importantly, it helps the system to determine if other uses of + are numeric, helping the implementation speed up those operations. This is especially crucial for a language that must be efficiently compiled into Javascript.

    要するに対象の変数が文字列なのか数字なのかがより明確にできるから、周辺の処理を高速化しやすくなる、という話みたい。

<!doctype html>
<head>
    <meta charset="utf-8">
    <title>dartlang test</title>
    <link rel="stylesheet" type="text/css" href="./css/bootstrap.min.css" />
    <link rel="stylesheet" type="text/css" href="./css/client.css" />
</head>
<body>
    <script type="application/dart" >
        #import("dart:html");
        /**
          * Class that manages remark display
          */
        class RemarkDisplayer {
            RemarkDisplayer(int numberOfRemarks) {
                _numberOfRemarks = numberOfRemarks;
                _remarkList = new List<Element>();
                _currentRemark = 0;
            }
            
            /**
             * Create remark nodes under the tag with the given ID
             */
            void initialize(String stageID) {
                var stage = document.query(stageID);
                for (int i=0; i<_numberOfRemarks; i++) {
                    var tag = '<pre class="remark" id="remark${i}" draggable="true"/>';
                    var elem = new Element.html(tag);
                    stage.nodes.add(elem);
                    _remarkList.add(elem);
                }
            }

            /**
              * Display given remark at the current node
              */
            void display(String remark) {
                var node = _remarkList[_currentRemark];
                var durationMS= 5000;
                node.innerHTML = remark;
                node.style.visibility = "visible";
                node.style.animationName = "fade, hslide";
                node.style.animationDuration = "${durationMS}ms";
                node.style.animationTimingFunction = "linear";
                node.style.animationFillMode = "forwards";

                // Replace node with a clone to restart animation
                var newNode = node.clone(true);
                node.replaceWith(newNode);
                _remarkList[_currentRemark] = newNode;

                // proceed to next remark
                _currentRemark++;
                if (_currentRemark >= _numberOfRemarks) {
                    _currentRemark = 0;
                }
            }
            
            int _numberOfRemarks;
            get numberOfRemarks() => _numberOfRemarks;

            List<Element> _remarkList;
            int _currentRemark;
        }

        void main() {
            final int MAX_NUMBER_OF_REMARKS = 10;
            var displayer = new RemarkDisplayer(MAX_NUMBER_OF_REMARKS);
            displayer.initialize("#stage");
            var button = document.query("#postRemark");
            TextAreaElement textNode = document.query("#remarkText");
            button.on.click.add((Event e) {
                    displayer.display(textNode.value);
            });
         }
    </script>
    <div class="topbar">
        <div class="fill">
            <div class="container">
                <a class="brand" href="#">dartlang test</a>
            </div>
        </div>
    </div>
    
    <div class="container">
        <div class="content" >
            <form action="">
                <textarea class="xxlarge" rows="23" id="remarkText">
        ___|二ニー-、、;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:|;::;:;:;:;:;:;:;:;:;:;:;:;:l
        /rヽ三三三三三─‐-- 、;:;:;:;:;:;:;:|;:;:;:;:;:;:;:;:;:;:;:;:;:;l
        ',i ,-三三三三三、   _,.ニ、ー-、!;: -‐二 ̄彡′
        ',、、ヾ三三'" ̄ ̄   `ー‐"    ヾ-'"  .〉′
        ヽ ヽヾ三,'    :::..,. -‐- 、     _,,..-‐、、,'
         `ー',ミミ     ::.弋ラ''ー、   i'"ィ'之フ l
         /:l lミミ     ::::.. 二フ´   l ヽ、.ノ ,'     
      ,.-‐フ:::::| |,ミ             l      /       
     /r‐'":::::::::| |ヾ        /__.   l    /      
 _,. -‐"i .|::::::::::::::::::',.',. \        ⌒ヽ、,ノ   /ヽ,_             
"    l ヽ:::::::::::::::::ヽヽ. \   _,_,.、〃  /l |    ___,. -、
     ',\\:::::::::::::::ヽ\  \  、. ̄⌒" ̄/:::::| |    ( ヽ-ゝ _i,.>-t--、
     \\\;::::::::::::\\  `、.__  ̄´ ̄/::::::::::l |    `''''フく _,. -ゝ┴-r-、
       ヽ \`ー-、::::::ヽ ヽ    ̄フフ::::::::::::::ノ ./   ,.-''"´ / ̄,./´ ゝ_'ヲ
          `ー-二'‐┴┴、__/‐'‐´二ー'".ノ   / _,. く  / ゝ_/ ̄|
               ̄`ー─--─‐''" ̄      / にニ'/,.、-t‐┴―'''''ヽ
                              /  /  .(_ヽ-'__,.⊥--t-⊥,,_
                              /  /  /   ̄   )  ノ__'-ノ
                             /      /    ゝニ--‐、‐   |
                            /           /‐<_   ヽ  |ヽ </textarea>
                <button class="btn primary" id="postRemark" type="button">Post</button>
            </form>
        </div>

        <div id="stage"></div>
    </div>
</body>
</html>

css

bootstrapなcssを除き、自分で新たに定義したcssも掲載する。

html, body {
    background-color: #eee;
}
body {
    padding-top: 40px; /* 40px to make the container go all the way to the bottom of the topbar */
}
.container > footer p {
    text-align: center; /* center align it with the container */
}

.remark {
    background : rgba(0,0,0,0); /* transparent background*/
    position: absolute;
    width: auto;
    height: auto;
    font-family: "IPA モナーPゴシック", monospace;
}

/* Fading animation */
@-webkit-keyframes fade {
  0%   { opacity: 1; }
  100% { opacity: 0; }
}

/* vertical scroll animation */
@-webkit-keyframes vslide {
  0%   { -webkit-transform: translateY(0px); }
  100% { -webkit-transform: translateY(100px); }
}

/* horizontal scroll animation */
@-webkit-keyframes hslide {
  0%   { -webkit-transform: translateX(0px); }
  100% { -webkit-transform: translateX(1000px); }
}

感想

classに変数やら処理をまとめて隠蔽できるのは気持ちいいっすね。

2012年2月18日土曜日

Trying Dartium on Ubuntu Linux 11.10

Dartium(DartのVMが搭載されたChromium)のバイナリが公開されたので、早速試してみた。環境は32bitのUbuntu Linux 11.10で、unameは↓ね。

検証環境

$ uname -a
Linux masato-xubuntu 3.0.0-16-generic-pae #28-Ubuntu SMP Fri Jan 27 19:24:01 UTC 2012 i686 i686 i386 GNU/Linux

環境構築

バイナリを用意してくれたおかげで非常に簡単。

  1. 適切なバイナリを落としてくる。今回はこれを使用。dartium-lucid64.zipとかいう名前だから一瞬64bitじゃないとダメかと思ったけど、32bit OSでも普通に動いてる
  2. zip解凍してDartium起動する。zip解凍すると色々でてくるけど、重要なのはchromeだけ。ファイラーからだとchromeが共有ライブラリに見えて実行できなかったけど、普通にコマンドラインから起動すれば問題なかった。
  3. chromiumの情報を確認する。"chrome://version/"というURLを打つと色々情報が見れる。そこにDartVMという項目が出てればちゃんとDartiumが動いてるはず。自分が落としてきたのは下の表示だった。

コード書く

こことかこことかこの辺を参考にしながら書いてみた。コツがつかめれば実際のリファレンスの方をみるといいかも。特にdart:htmlのElementは良く読むべし。

<!doctype html>
<head>
    <meta charset="utf-8">
    <title>dartlang test</title>

    <link rel="stylesheet" type="text/css" href="./css/bootstrap.min.css" />
    <link rel="stylesheet" type="text/css" href="./css/client.css" />
</head>
<body>
    <script type="application/dart">
    #import("dart:html");
    void main() {
        var msg = "Testing dart!";
        window.alert(msg);
        print(msg);
        Element btn = document.query('#button');
        btn.on.click.add((Event e) {
            Element stage = document.query('#stage');
            stage.innerHTML = "OMGOMGOMGOMG";
        });
    }
    </script>
    <div class="topbar">
        <div class="fill">
            <div class="container">
                <a class="brand" href="#">dartlang test</a>
            </div>
        </div>
    </div>
    
    <div class="container">
        <div id="stage">AAAAAAAAAAAAAAAAAAAA</div>
        <div id="button">Click ME!</div>
    </div>
</body>
</html>

やってることはわりと単純。

  1. window.alertでアラートウィンドウにメッセージを出力する
  2. printでデバッグメッセージを出力する
  3. buttonというidを持つ要素のクリックハンドラでstageというidを持つ要素の中身を書きかえる

コードを埋めこんだhtmlができたら、あとは普通にDartiumで表示する。もし構文に何らかのエラーがあった場合はデベロッパーツールのConsoleに表示されるから見るといい。画像は上がクリック前、下がクリック後。

型チェック

ドキュメントにも書いてあるけど、Dartiumの実行時に環境変数を設定してやるとDartの型チェック機能を有効化できる

$ DART_FLAGS='--enable_type_checks --enable_asserts' ../lib/dartium-lucid64-inc-4350.4350/chrome &

この状態で意図的に型エラーが出るソースに書きかえた(Event eのところをint eにした)ところ、ちゃんとエラーが検出された。

感想

  • バイナリのおかげでかなり簡単にDartが試せる。
  • Consoleにエラーが出るからデバッグはまあまあやりやすい。まあこれはjavascriptでも一緒だけど。
  • 型チェックは常にONでもいいかも。

2012年2月11日土曜日

Trying "dart:io" with dart

ブラウザ側の処理を書くためちょっと試しにjavascriptを書こうとしてたものの、そのあまりに気持ちわるい言語仕様に挫折してしまった。Coffee Scriptも試したけどやっぱり文法が受けつけなかったので、早々にdartを試してみることにした。

dart:io

dartはブラウザの中だけじゃなく普通のスクリプト言語的にも実行できるようなので、ちょっと前に追加されたというioライブラリでちょろっと書いてみた。環境の構築方法はこの辺を参照。APIリファレンスを見るかぎり〜Syncていう同期的に実行する関数と非同期的に実行する関数の二種類があるみたい。Sync無しの関数がデフォルトっぽいので、ひとまず全て非同期で書いてる。

#import('dart:io');

void onOpen(RandomAccessFile opened) {
    opened.closeHandler = () => print("Closed stream");
    opened.errorHandler = (err) => print(err);
    opened.noPendingWriteHandler = () => opened.close();
    print("Writing to stream");
    opened.writeString("aardvark\n");
}

void main() {
    var name = "./aardvark";
    var f = new File(name);
    print("Attempting to write "+f.name);
    f.existsHandler = (bool b) {
        if (b) {
            print(f.name+" already exists!");
        } else {
            print(f.name + " doesn't exist...");
            f.openHandler = onOpen;
            f.open(FileMode.WRITE);
        }
    };
    f.exists();
}

"=>"記号で関数を定義するのはC#で慣れてるからどってことない。それより全て非同期で書こうとするとハンドラーだらけで変な感じ。

実行結果

$ dart --enable_type_checks test.dart
Attempting to write ./aardvark
./aardvark doesn't exist...
Writing to stream
Closed stream
$ dart --enable_type_checks test.dart
Attempting to write ./aardvark
./aardvark already exists!
$ cat aardvark 
aardvark
$ 

実行結果は期待通り。ついでに"--enable_type_checks"オプション型チェックを試してみた(existsHandlerの引数をbool bじゃなくてint bにしてみた)ところ、下のような結果になった。

$ dart --enable_type_checks test.dart
Attempting to write ./aardvark
Unhandled exception:
'dart:io': Failed type check: line 1888 pos 31: type '(int) => Dynamic' is not assignable to type '(bool) => void' of 'handler'.
 0. Function: '_File@14117cc4.set:existsHandler' url: 'dart:io' line:1888 col:31
 1. Function: '::main' url: '/home/masato/dart/test/test.dart' line:15 col:21

"--enable_type_checks"無しで実行すると何も言わないので、一応ちゃんと動いてるみたい。

まとめ

javascriptよりはるかにまっとうな言語なので早く普及してください。ちなみにsynonym.dartlang.orgにはjavascriptとdartを比較した色々な例が出てて、javascriptがいかに気持ち悪い言語かが良くわかる。