2008年9月22日月曜日

学校でしか教えてくれないプログラミング言語のこと

Scheme(スキーム)というプログラミング言語について初めて知ったのは、大学2年生のときです。理学部情報科学科に進学し(東大には進学振り分け(通称:進振り)という制度があって、大学2年生の前半までは一般教養を学び、それ以降から専門課程に進みます)萩谷先生担当のプログラミング演習がSchemeとの出会いでした。(当時のものとは大分違いますが、参考までにSchemeの講義資料へのリンクです。http://www-ui.is.s.u-tokyo.ac.jp/~hara2001/scheme/)

おそらく、ここで学ばなかったらSchemeのように一般に知られていない言語には触れることもなかっただろうし、Emacs Lispのプログラム(Schemeと同様の構文です)を自分で書くこともなかったように思います。Emacsというテキストエディタは、自分好みの機能をLispを用いて実装することが醍醐味です。今でも、簡単な計算はLispで書いて、Emacsを電卓代わりにしています。(以下は、階乗の計算をちょこっとLispで書いてみた例。xのn乗を再帰で計算する関数pow(x, n)):
括弧が多くて「何だこれは?」と思われそうですが、括弧の対応付けはエディタが視覚的にサポートしてくれるので、式そのものの見た目ほど入力は大変ではありません。

実用性という意味では、Schemeは非常にマイナーな言語だと思います。けれど、再帰であったり、リスト構造であったり、環境(変数などの状態を運ぶもの。継続とかもそう)であったり、λ関数(クロージャと言ったほうが良いかな?)など、プログラミングで重要な概念を学ぶのには良い言語だと思います。型を学ぶにはOCamlなどML系の言語の方が適しているので、そちらを授業に使う大学も多いようです。


Schemeに関しては思い入れが深く、僕にとってはEmacs(Windows版はMeadow)を使い始めたきっかけでもあるし、情報科学科のCPU実験で、Scheme言語を使って、Scheme言語のコンパイラ(自分達で作るCPU用のアセンブリを生成します)を作ってしまうくらい深く関わった言語でもあります。言語の仕様書(当時はR5RS)もそれなりに読みました。

それを最後に、久しくSchemeから離れていたのですが、思い出させてくれたのが以下の公開質問
「プログラミングに詳しい人に質問です。大学でプログラミング経験の学部一年生向けにプログラミングを教えることを想定しています。週1コマ×半年程度の限られた時間で、プログラミングとはどういうものかという本質を教えたいのですが、どの言語を使うのが適切でしょうか。」
ここでは、プログラミング言語の種類をあまり知らない人を解答対象からはじくために、以下のような質問肢が用意されています。一般の人よりは僕はSchemeを知っていると踏んで、これを読んだときの僕の思考過程(赤文字)も併記しました。

* Schemeは1.5からオートボクシングの機能をサポートした
auto-boxingってJava…
* Schemeはインデントによってブロックを表現する
そんなわけない。括弧だよ
* Schemeは多くのレンタルサーバに標準でインストールされている
もしそうだったら凄い!
* Schemeでは関数がファーストクラスのオブジェクトである
Schemeで言うオブジェクトって何?単にデータという意味だとすると'A (文字を表すsymbol)とかあるし…。(cons 1 2)だと、関数が1と2のペア表すデータ型(オブジェクト)と捕らえることができるけど。。。
* Schemeの文の終わりはセミコロンである
違う違う。
* Schemeは純粋関数型言語であり、副作用はモナドでくるむ必要がある
副作用がない関数型言語って、関数の外側の変数を変更できないってことだよね。そんなことはなかったような、、そもそも、モナドって何?と焦りグーグル検索。。これHaskellに出てくるのか。。
* Schemeは型に厳格なため整数の加算と浮動小数点数の加算の演算子が異なる
そういえばSchemeで型を意識したことがない。。。
* Schemeは関数の呼び出し時に括弧を省略することが出来る
そんなsyntaxあったかなぁ。。。たぶんない。
* Schemeのマクロ定義には#defineを使う
C/C++だよ、それ。(define f (x) ...)はあるけど。
* Schemeの言語仕様はキューマシンとしての実装に適しているため並列化が容易である
キューマシン -> 並列化が容易というロジックにとっても飛躍を感じるなぁ。副作用無しの純粋関数型の範囲なら、Googleのmap-reduceの概念のベースになっているように簡単に分散できるけど。。。
* Schemeのブロックはbeginで始まりend.で終わる
これは違う。
* SchemeのコンパイラとしてはGHCが有名である
GHCって何?聞いたことない。Googleで検索…またHaskellですか。。。知らないよ(涙)
と、まぁ、大変でした。なぜって外部情報で補完しないと正解が出せない質問がたくさんあるから!「多くのレンタルサーバーに」って。。。「多くのレンタルサーバー」の現状を知らないとわからないし。本格的なSchemeの使用例が見当たらないって理由で推測はできるけれど。他にもSchemeにはまだ僕の知らない構文があるのかも…とか不安に思ったり。それでも、Googleで検索すればそれぞれ1分と経たずにわかる程度のことだと思います。とても懐かしく、かつ、知らないことも多くあったので知的な刺激になりました。

プログラミング教育用としての関数型言語

こと、教育のためのプログラミング言語の選択に関しては、ネットワークに接続したり、データベースを使ったプログラミングが必要なら、現時点では迷いなくJavaを選択します。動作させやすいこと、使っている人が多いためEclipseのような使いやすい開発環境が整っていることが選択の理由になります。

Scheme, Lispのような関数型言語の肝は、副作用がないという点だと思っているのですが。たとえば、Googleが超並列計算に使用しているMapReduceのフレームワークでは、

map(入力を2倍にする関数f, {1, 2, 3, 4, 5}) => {f(1), f(2), f(3), f(4), f(5)} = {2, 4, 6, 8, 10}
reduce(入力を足し合わせる関数h, {2, 4, 6, 8, 10}) => 30

というmap, reduceという2つの手続きで計算を行います。map部分をScheme(Lisp)的に書くと、
上のようになります。map関数では、入力リストの先頭(car)から順に関数fを適用していきます。たとえば、以下のような式

の結果は、{2, 4, 6, 8, 10} になります。関数の引数に関数f(lambda文を使ってxを2倍する関数を定義している)を渡して、内部では{(* 1 2), (* 2 2), (* 3 2), ... }を計算しています(+演算は前置記法)。ここで大切なのが、map関数内で、関数fの実行順に制約がないということです。つまり、個々の関数fの実行では副作用がない(!)ので、どの順番で実行してもよく、この部分を言語の実装次第で並列化することができます。

GoogleのMapReduceではこのような副作用がないというプログラムの意味(semantics)を活用して、個々のmap操作を別々のノードに計算させて超並列化を可能にしています。関数型言語を用いるとこの副作用のない部分を明確に記述できますし、驚くほど多くの計算がmap-reduceの組で書けるようです。例えば、forループで先頭から順に辿らなくても良い場合が見つけやすいと思います。Googleは並列化を促進するために、入力listのコピーをいつくかのノードに分散して配置する工夫も行っています。検索エンジン用の索引構築なども、このMapReduceのフレームワークで再実装したとMapReduceの論文には書かれています。

再帰の概念は他言語でもよく見かけますが、関数型言語の特徴である副作用がない(データすら関数で表現されている)コードの書き方というのは、手続き型言語であるJava, C/C++など、実際の開発現場でよく使われるプログラムの書き方から入った人には、とても気付きにくいものだと思います。関数に関数を渡すという概念も、何十年も前の関数型言語の研究から、他に移殖されたものです。

これらの通常では気付きにくい概念や、既に一般に当たり前のように使われていることの裏にある理論を教えるのが大学のあるべき姿であると考えます。特に、簡潔な記述で速く動くことに主眼に置くcomputer scienceの分野では、プログラミングの授業も、言語を問わず、一般の技術者向けの勉強本では深く触れられていないような、本質の部分をカバーすべきでしょう。そうすることで、近い将来、新しいプログラミング言語が出てきても、プログラミングの歴史と比較して、新しい部分、重要な技術などをはっきりと認識できるようになり、言語を使いこなす能力をもったプログラマを育てることにつながるのではないでしょうか。

(追記)

そもそもプログラミングの本質って?

プログラミングの本質をどこに据えるかで言語の選択は変わりますね。省メモリ・並列化が主眼なら副作用、同期などが重要なのでアセンブリ、C/C++などlow levelに近いもの。オブジェクト指向ならJavaなどデザインパターンの威力が見せられる言語が良いと思う。簡潔さ、手軽さが大切ならPerl, Rubyとか。関数型という意味では、Scheme, Lispも面白いけれど、Ocamlも良さそう。Python, Haskellとかは詳しくないので…。

教育で大切なこと

実際、講義の中や在学中に本質を理解しきる必要はないと思うのです。僕自身、関数型言語とMapReduceの関係なんて、卒業した後に初めて理解したものです。気付く人もいれば、数学と同じでまったく使わない人もいる。ただ後になってもう一度学習しようと思ったときに、入り口の部分を見せてもらっているのとそうでないのとでは、学習効率(学ぶことへの抵抗感)が相当違います。だから、1から10までを教えるのではなくて、1を見せることが「教育」で本当に大切な事だと感じています。

2008年9月10日水曜日

Google Street Viewが持ちえない情報

Google Street View(ストリートビュー)が、写してはいけないものまで公開していることが多くのサイトで話題になっています。今日、「不適切画像の削除作業は小鳥並の知能で行われる」:高木浩光@自宅の日記のエントリをみて、根本的な問題がようやく整理できました。

Google Street Viewが検索エンジンのロボットやウェブ上のブログなどと根本的に違うのは、人のフィルターを通さない情報をネットに晒している」ことに尽きます。

もちろんGoogle Street Viewのデータを収集するには、路上の写真を撮影する実際の作業員がいて、撮るべき場所の最低限の判断はしているのでしょう。(私道に入り込んでいたり、とそれすらも怪しいですが)。写ってしまった人の顔をぼかすなどの処理もしているようです。けれど、ネットに公開すべき情報とそうでない情報の判断を、98%の精度でスパムメールを弾くのと同じ方法で行ってはいけない事例が多々存在します。

例えば、写ってはいけない人が、その場所に写っていた場合。この写真をウェブに公開してもよいかという判断は、写っている場所以外の外部情報を使わないと通常はできません。なぜなら、顔が伏せられていたとしても、背格好、服装や行動範囲などでその人を特定できる情報を持っている人にとっては、ただの写真以上の情報がそこにあります。たとえ洗濯物が映ってしまったという程度のことでも、女性の一人暮らしであることがわかってしまうなど、写された住居に住む人の危険を招くことさえありえます。

「問題のある画像だから、画像を消す」という現在のGoogleの対応では、「消したという事実」によって、消された場所に何があったのか、という好奇心を刺激し、削除依頼主の意にそぐわない事態を招くことが十分考えられます。

インターネットの普及に伴い、ネットの存在意義もプライバシーの問題も議論されてきました。ネットの検索エンジンがけしからん、といういう人が減ってきたのは、利便性の認知とともに「Webに個人情報などを公開しない」という選択肢と、そのための情報の管理方法が存在しうるからです。つまり「人によるフィルター」を介在させる余地がWebに情報を送る前に残されているのです。けれど、ストリートビューにはそれがない。

目に見える情報がすべてではないのです。株のインサイダー取引のように、内部情報を知りうるものだけに利益を与え、他の人の利益を不正に大きく損ねてしまう。

ストリートビューは、今までなかなか見られなかった情報を収集し、目的地や町の雰囲気を容易に知ることができるようなったという点で、その存在意義や利便性は高く評価しています。ただ、それを人や機械の目による判定や、報告だけで安全にできるというGoogleの現在の主張には、非常におこがましいものを感じます。当事者にしかわからない情報というのは常に存在するものです。

今までのウェブ情報の収集とは違って、ストリートビューでは「人によるフィルター」の情報が根本的に抜け落ちています。「ウェブに存在しない・載せていない」という事実だって立派な情報です。フィルターを通したあとの情報を扱ってきたから、Googleには何をしても怖くないという強みがありました。ストリートビューのように、フィルターを通す前のレイヤー(層)の情報を扱いだしてから、急に対応の陳腐さが目立つようになってきたのがとても残念です。

2008年9月9日火曜日

Google Web Toolkitを本格的に使う

昨日、Google Web Toolkit (GWT)に触れたので、備忘録という意味も含めて、もう少し詳細を書いてみます。僕自身、GWTを使って、ゲノムブラウザを作ってみたり、講義のレポート提出用サイトを作成するなど、ここ数年大活躍しているツールで、GWTによるWebインターフェースで、5万行は優に超えるだけのコードを書いています。

Google Web Toolkitは、JavaからJavaScriptに変換するコンパイラと、ブラウザで表示するHTMLを生成するWidgetクラスを含んでいます。Javaの構文でプログラムを記述すれば、Webブラウザで動くJavaScritpコードを作成してくれるというものです。

実際の開発ではHTMLを書くことはなく、FlexTable, Buttonなど、HTMLでよく使うtableやformタグをクラスとして表現してくれているので、再利用可能な形でHTML要素を設計しやすくなっています。GUIインターフェースのプログラミングと似た感覚でウェブアプリケーションの開発ができる、と考えるとよいかもしれません。Perl、PHP、Ruby on RailsなどHTMLのテンプレート中心の開発と趣が異なるので、テンプレートに慣れた人にとっては最初のうちはコードを組みにくいかもしれませんが、HTML生成のまどろっこしい部分を気にせず開発できることに真価があります。

コンパイル後の最終的なコードはJavaScriptになるのですが、GWTではJavaの標準的なライブラリをエミュレーションすることで、JavaのArrayList, HashMapなどよく使うデータ構造を使えるようにしてくれています。GWT1.5からは、JavaのGenericsの構文や、forループの簡略記法を使えるようになったので、ますます開発が簡単になりました。

3種類のJAR   GWTにおける開発では、3種類のJARファイルを使います。
  • gwt-user.jar  GWTでは、client(ブラウザ上で実行されるコード)と、sever(サーバー上で実行されるservletなどのコード)の2種類のフォルダ(Javaのパッケージ)を使って開発します。サーバーとの通信に必要なservlet周りのパッケージ(javax.servlet)がgwt-user.jarに同梱されていて、自分でTomcatをインストールしてこれらのライブラリをclasspathに設定する手間が省けます。gwt-user.jarは開発時のみに使うものです。
  • gwt-servlet.jar  一方、こちらは、TomcatなどのWebサーバーに作成したWebアプリケーションを設置するときに、WEB-INF/libフォルダに含めておくものです。gwt-user.jarから、javax.servletのパッケージを取り除いたものです(javax.servletはTomcatに標準装備されているパッケージです)。deploy時には、gwt-servlet.jarのみを使います。
  • gwt-dev-windows.jar, gwt-dev-mac.jar, gwt-dev-linux.jar  これらのJARファイルには、GWTのコンパイラが含まれており、JavaコードをJavascriptに変換するときに使います。また、GWTのコードをコンパイルせずにJavaコードのまま実行するための、GWT Shellが含まれています。GWT ShellはTomcatサーバーをローカルに立ち上げるGUIプログラムなので、OS毎に必要なJARファイルが異なり、gwtの配布パッケージに含まれている.dll、.jnilib、.soなどが実行するのに必要になります。デバッグ時には、クラスパスの先頭にgwt-dev-*.jarを追加しておきます。(gwt-user.jarなどはJavaScriptコードを含んでいるので、Javaとしては動作しません)
GWTのアプリケーションをデバッグしたり、コンパイルするには、ソースコードのフォルダ(srcとか、src/main/javaなど)と、Javaのソースコードクラスが含まれているフォルダ(binとか、target/classesなど)がクラスパスに含まれている必要があります。これはJavaのソースコードそのものがコンパイルなどに必要なためです。

GWTのためのJAR作成 したがって、GWTのコードをJARのライブラリにして再利用するときも、ソースコード(*.java)もclassファイルと同じ位置に含めて作成する必要があります。普段から、ソースコード同梱のJARを作っておくと、EclipseでJARのソースコードがすぐ参照できるというメリットもあります。

GWTによる開発を即座に始める  こう並べてGWT開発の注意事項を並べて書くと、はまりどころが多くて、設定もとても面倒なのですが、僕はこれらの設定を含めて、GWTによる開発を数秒で始めることができるようにしています。

現在開発中の、UTGB Shellというゲノムブラウザを作るためのツールでは、utgb create, utgb gwtのコマンド2つで、GWT、データベース、サーブレット機能を含めたプロジェクトの雛形を作成します。埋め込み型Tomcatサーバーを立ち上げることで、Tomcatなどのインストールなしに、ローカルマシン上でJavaによるWebアプリケーション開発ができるようにもなっています。現在は、生物系の研究者が、面倒なしにゲノムブラウザを作れるようにする趣旨のツールなのですが、僕自身、ゲノム関係以外の一般のWebアプリケーション開発にも使っています。

UTGB Shellの舞台裏ではMaven(パッケージ管理ツール)が動いています。そのインストール作業を省くために、Maven自身もUTGB Shellに埋め込まれています。また、gwt-dev-*.jarのライブラリも、本家で配布されているものとは違って、dllなどのライブラリも同梱させています。これによって、OSごとに1つのJARファイルをダウンロードするだけで済むのですが、これらのライブラリをOSの種類に応じてダウンロードしたり、展開したり、GWTのコンパイラを起動するなどもすべてMavenのスクリプトとしてまとめているので、ユーザー、開発者が、詳細を気にする必要がありません。本番用サーバー上のTomcatへのdeployもコマンド一つでできるようになっています。

このような複雑なプロジェクト管理ができるようになったのも、Javaの魅力の1つ。C++などバイナリのOS・コンパイラ間の互換性が乏しい言語の場合は、ソースコードの最新版をネットからダウンロードして、dllをコンパイルする必要があります。コンパイルエラーがでる、実行時にリンクエラー、multi-threadライブラリのタイプが一致していなくてメモリリークが起こるなど、など、慣れていない人には解決しようのない、はまりどころもたくさんでてきます。

Javaではこのような面倒が軽減され、Mavenのようなパッケージ管理ツールのおかげで、典型的な作業を繰り返さずにすむようにできるようになったのが嬉しいですね。

2008年9月8日月曜日

Google Web Toolkitでcanvasを使って画像を描く方法

Google Web Toolkit(GWT)は、Java言語からJavaScriptのコードを生成するものです。ブラウザ間のJavaScriptの動作の違いを吸収してくれるなど、ウェブアプリケーション開発の苦労をかなり軽減してくれます。GWTはもう十分実用的なのですが、まだまだ成長途中でgwt本体のtrunkに正式に組み込まれる前の機能などは、Google Web Toolkit Incubatorというプロジェクトで開発されています。

gwt-incubatorの中で目玉なのが、GWTCanvasというライブラリ。これは、次期HTML5には標準搭載されるはずのcanvasタグの機能を使って、ブラウザ自身に画像を描画させるというものです。こちらのデモを見てもらえれば、意外と複雑な図が描けたり、アニメーションが作れるなど、そのすごさがよくわかると思います。Flashプラグインなどを使わない限り、今まで画像をブラウザで見せるにはサーバーから画像データを直接送る以外に方法がありませんでしたが、今後は描画のために必要な座標、色などの情報をサーバーが送るだけで、画像を描くなどサーバーにとって重い処理はブラウザ側に任せてしまえるようになります。

実はこのcanvasという機能、IEはサポートしていません。けれど、IEにはVMLという独自の画像を描くための仕様があります。GWTCanvasは、IEのときはVMLを使って画像を描き、canvas機能が既に使えるFirefox, Google Chrome, Safari, OperaなどのWeb標準を大事にするブラウザでは、canvasを直接使う、というトリックを使って、ほとんどのブラウザで画像を描くことを可能にしています。ただし、IEのVMLを使った描画は動作が重いのでご注意。

厳しく言えば、IEがWeb標準を追おうとしてないから、こんなライブラリが必要になっている節があります。どのOS・ブラウザでも動くことを大事にするWeb Developerにとって、IEは本当に憎らしい存在。Webアプリケーションを作る動機って「どこでも動くアプリケーション」を作ることに尽きますから。IEのためだけに必要なwork aroundのなんと多いことか。

ソースコードからjarファイルを作るにはこちらを参照して、ant distとすればいいですが、僕がJARパッケージにしたものもこちらからダウンロードできます(GWT1.5.2用です)canvasで遺伝子構造を描いてみたりしています。ブラウザで画像を描き始めるとGoogle ChromeのJavaScriptの動作が本当に速いことがよく実感できます。


2008年9月3日水曜日

Googleの新しいブラウザChrome レビュー

Googleが開発しているブラウザGoogle ChromeのBeta版がリリースされました。最初から、AJAXを使ったものを含むいろいろなサイトが問題なく動くのはさすが、というところ。(Google にはChrome Botというのがいて、何百万というWebページを自動でテストしてるらしいです。)

Googleにとって、ブラウザ技術というのはサービスを提供するための基盤技術という意味で、まさにOSです。ブラウザの開発は過去にNetscapeが過去のコードを捨てて一から書き直すなどの苦労をしていたように、とても大変な領域なのですが、基盤技術を自分達で作るという正攻法に素直に取り組んで実現までしてしまうのがGoogleらしいですね。

使い始めて驚き。まず、検索バーがありません。Google Chromeのおかげで、検索バーは、本来必要ないものだったことがわかります。使い方は簡単。まず、Ctrl+Lを押してアドレスバーに飛びます。ここで、覚えているURLの一部分なり、キーワードを入れると、過去の閲覧履歴やら、Google Suggestやら、ブックマークやらを検索して、見つけてきてくれます。検索バーなどが統合された分、全体として余計なボタンが省かれたすっきりしたブラウザになっています。


新しいタブを開く操作は、IE7, Firefoxと同じCtrl+Tです。すると、普段よく見てい
るサイトがスクリーンショットとともに一覧表示されます。これは便利。反対に閉じるときはCtrl+Wと、タブを切り替えるときは、Ctrl+Tabを使います。



マウスで選択してもよいし、タブを開くとすぐアドレスバーにフォーカスが移るので、そのままキーボードで検索を開始できます。このあたりのつながりの良さが快適。

各タブ毎にプロセスを生成しているので、重いサイトがあっても、他のページは不自由なく見続けられるなど、技術的にもOS好き、プログラマにとっては興味深いことばかり。ブラウザ中にデータベースを保持できるGoogle Gearsも最初から組み込まれているのもうれしいところ。技術者には、ここにあるGoogle Chrome紹介のコミックがお勧め。(JavaScriptのGarbage Collectionを作り直しているとか、すごい正攻法。これもGmailなど, 自前のGoogle Web Toolkitで作成されている部分の高速化につながるので、自分の財産の価値をさらに高めています)

Web Developer向けの機能として、HTMLソース中の各タグの構造やスタイルを表示してくれるinspector機能がうれしい。FirefoxでFireBugsプラグインと同じような機能が最初から使えるのです。ありがとう、Google。Web開発者の気持ちをよく理解してくれています。


タブをブラウザの外側にドラッグすると、独立したウィンドウになるので、他のページを参照しながら、ブログを書くということも、簡単。

ページ表示の履歴を残さないシークレットモードもあります。のアイコンには笑ってしまいました。スパイ活動に使うのか、なるほどw。リンクを開くときに右クリックで「シークレットモード」を選べます。

IMEのon/off関係なしに、スペースキーで画面のスクロールができるし、わりと細かいところもしっかり実装されていて感心。デフォルトのブラウザに設定して、しばらく使ってみることにします。

(追記)
その他のショートカットキー
  • Ctrl+F ページ内単語検索 これもシームレスで速い!
  • Ctrl+U ページのソースコード表示
  • Ctrl+H 閲覧履歴のページ表示
  • Ctrl+N 新しいwindowを開く
  • Ctrl+R ページの再読み込み
  • Ctrl+J ダウンロード履歴の表示
  • アドレスバーでの検索結果をAlt+Enterで新しいタブで開く
  • backspaceで一つ前のページ、Shift+backspaceで一つ先のページ
  • 意外と便利なCtrl+Shift+Tで閉じたタブを復元。フォームの入力内容も復元されます。何回か押すと、1つ前、2つ前に閉じたタブを復元していきます。
Shift+ESCキーで、各ページがどれくらいメモリとCPUを使っているかもわかります。すごい。



about:network でネットワークのアクセス状況、about:memoryでメモリの使用状況の詳細も見られます。AJAXアプリケーションなどバックグラウンドで通信を行う様子を確認できて便利。


雑感 GoogleがGearsを統合したブラウザを開発し、僕が2年くらい前に予測していた「ブラウザがデータベースを持つ日」がようやく実現されましたが、使う側にとっても、開発者側にとっても、データを扱うという点については、まだまだ不自由が多いと思います。例えば、Twitterの過去ログを追う、などの問題。Twitter APIからログを取得して保存しておくような媒介サービスはたくさんあるけれど、これがブラウザ上でユーザーの手で実現できるところまでブラウザは進化すべきだと僕は考えています。ここからが競争。

License

Creative Commons LicenseLeo's Chronicle by Taro L. Saito is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 2.1 Japan License.