blockcode: ビジュアルプログラミングツールキット
ブロックベースのプログラミング言語では、プログラムの構成要素を表すブロック同士をマウスのドラッグで接続することでプログラムが作成される。単語や記号をタイプする伝統的なプログラミング言語とブロックベースのプログラミング言語は大きく異なる。
通常のプログラミング言語は打ち間違いに全く寛容でない。これが理由でプログラミングの学習が難しくなっている可能性がある。多くのプログラミング言語は大文字と小文字を区別し、奇妙な構文を持ち、セミコロンを間違った位置に配置しただけで ── もっと厄介なのは、セミコロンを一つ忘れただけで ── 実行できなくなる。さらに、多くのプログラミング言語は英語をベースに作成されており、その構文はローカライズできない。
これに対して、上手く設計されたブロック言語は構文エラーを完全に除去できる。間違った処理をするプログラムの作成は可能であるものの、間違った構文を持つプログラムは作成できない: そもそもブロックが噛み合わない。また、ブロック言語は発見可能性に優れる: 言語が持つ構文やライブラリがブロックのリストとして視覚的に表示される。さらに、プログラミング言語の意味を保ったまま任意の自然言語にブロックを翻訳することもできる。

ブロックベースの言語には長い歴史がある。主要なものとして Lego Mindstorms, Alice3D, StarLogo, そして特に知名度の高い Scratch がある。ウェブで実行できるブロックベースプログラミング用ツールもいくつかある: Blockly, AppInventor, Tynker をはじめとして、他にも多くある。
本章で紹介する言語 Blockcode のコードはオープンソースプロジェクト Waterbear のコードをある程度ベースにしている。Waterbear は言語ではなく、ブロックベースの構文で既存の言語を包む (ラップする) ためのツールである。こういったラッパーは上述した利点を持つ: 構文エラーが無くなり、利用可能な構成要素が視覚的に見えるようになり、ローカライズが簡単になる。加えて、視覚的に表されたコードの方が読解・デバッグが容易な場合があり、ブロックはキーボードを使えない子供でも操作できる (ブロックにテキストの名前だけではなくアイコンを付ければ、文字が読めない子供でもプログラムが書けるようになる。ただ、本章のコードではこの機能を実装しない)。
Blockcode でタートルグラフィックスを選択したのは、子供にプログラミングを教えるための言語として開発された Logo 言語からの伝統と言える。上述したブロックベースの言語にもタートルグラフィックスの機能を持つものがある。コード量が大きく制限される Blockcode のようなプロジェクトでも実装できる程度にタートルグラフィックスは単純である。
ブロックベースの言語がどのようなものかを知りたい場合は、GitHub レポジトリのコードを実行してみてほしい。
目標と設計
Blockcode で達成したいことはいくつかある。まず最も重要なこととして、タートルグラフィックス用のブロック言語を実装したい。それも、ブロックのドラッグアンドドロップだけで画像を作成するコードが作成できる環境を、HTML, CSS, JavaScript の機能を最低限だけ使いつつ実装するのが望ましい。次に、これも重要なこととして、Blockcode というタートルグラフィックス用の小さい言語以外の言語でもブロックという概念が利用できることを示したい。
このために、Blockcode に特有のコードは全て turtle.js
に収め、他のファイルに簡単に置き換えられるようにした。他のコード、例えばブロックを操作する block.js
, menu.js
やウェブ用のユーティリティ util.js
, drag.js
, file.js
は Blockcode に直接は関係がない。ただ、プロジェクトを小さく保つために、汎用性を犠牲にしてブロックの操作に特化させたユーティリティ関数がいくつかある。
ブロック言語を書く中で私は次の事実に気が付いた: ブロック言語では言語自体が IDE (統合開発環境) である。ブロックをテキストエディタで書くことはできないので、ブロック言語と並行して IDE も設計・開発されなければならない。この事実には利点もあれば欠点もある。使うべきエディタに関する宗教戦争が起こらなくなるのは利点であるものの、ブロック言語の作成に集中できないのは大きな欠点である。
Blockcode スクリプトの構造
Blockcode スクリプトは、他の (ブロックベースおよびテキストベースの) 言語のスクリプトと同様に、実行されるべき操作の列である。Blockcode ではスクリプトが HTML 要素から構成され、それぞれの要素に実行されるべき JavaScript 関数が結び付けられる。他のブロックを保持する (そして実行する) ブロックや、自身に結び付いた関数に渡す数値引数を持つブロックも存在する。
ほとんどのテキストベースの言語では、スクリプトはいくつかのステージを通過してから実行される: 字句解析器がテキストをトークン列に変換し、構文解析器がトークン列を抽象構文木に変換し、それから抽象構文木は言語によって機械語にコンパイルされたりインタープリタに入力されたりする。これは単純化された説明であり、これより多くのステップが存在する場合もある。Blockcode では、スクリプトに含まれるブロックが既に抽象構文木を表す構造を持っているので、字句解析と構文解析のステージは必要にならない。Visitor パターンを使ってスクリプトに含まれるブロックを走査し、各ブロックに関連付いた JavaScript 関数を呼び出していけばスクリプトが表すプログラムを実行できる。
古典的な言語が持つステージを追加できない理由があるわけではない。turtle.js
を書き換えれば、ブロックに関連付いた JavaScript 関数を呼び出すのではなく異なる仮想マシンに対するバイトコードを生成・実行するブロック言語とすることもできる。あるいは、コンパイラを使うことを前提として C++ コードを生成する言語にもできる。Waterbear プロジェクトでは、ロボットを制御する Java コードの生成、Arduino のプログラミング、Raspberry Pi で動作する Minecraft のスクリプト作成に利用できるブロック言語がそれぞれ存在する。
ウェブアプリケーション
ツールを可能な限り多くの人が利用できるようにするには、ウェブネイティブとするのが一番である。Blockcode は HTML, CSS, JavaScript で書かれているので、ほとんどのブラウザとプラットフォームで実行できる。
モダンなウェブブラウザは強力なプラットフォームであり、素晴らしいアプリケーションを構築するためのツールが豊富に揃っている。実装が複雑になり過ぎたと感じるとき、私はそれを「ウェブのやり方」ができていない兆候だとみなし、ブラウザのツールをもっと上手く活用できないかを考え直すようにしている。
ウェブアプリケーションと古典的なデスクトップ・サーバー向けアプリケーションの重要な違いの一つに、ウェブアプリケーションには main
のようなエントリーポイントが存在しない点がある。明示的な実行ループはどこにも書かれておらず、ブラウザが暗黙のうちに全てのウェブページに組み込む。ウェブアプリケーションの開発者が書くコードはページの読み込み時に一度だけパース・実行され、そのときにユーザーとの対話で利用されるコールバック関数が登録される。ページの読み込みが終了すると、その後アプリケーションとユーザーの対話は全てコールバックを通して行われる。コールバックはイベント (例えばマウスが操作されると呼ばれる)、タイムアウト (一定時間ごとに呼ばれる)、フレームハンドラ (通常は 1 秒間に 60 回の画面の再描画のたびに呼ばれる) に対して登録できる。また、ブラウザは完全な機能を持ったスレッドを公開しない (shared-nothing なウェブワーカーだけが公開される)。
コードの解説
本プロジェクトで採用した慣習やベストプラクティスがいくつかある。まず、各 JavaScript ファイルに含まれるのは一つの大きな関数の呼び出しとなっている。こうすると変数がグローバル環境に漏れる心配がなくなる。変数を他のファイルに公開する必要がある場合は、ファイル名と同じ名前のグローバル変数を一つだけ定義し、そのプロパティとして変数を公開する。この処理はファイルの末尾に書かれ、その後にイベントハンドラの設定が続く。このようにファイルを構成しておけば、ファイルの末尾に目を通すだけで公開される変数と登録されるハンドラが分かるようになる。
コードスタイルは手続き型であり、オブジェクト指向型や関数型ではない。二つのパラダイムのいずれを使っても同じ処理は書けるものの、DOM の API をパラダイムに合わせるために初期化コードやラッパーが多く必要になるだろう。最近になって策定が進んでいる Custom Elements を使えば DOM をオブジェクト指向らしい方法で扱えるようになり、関数型 JavaScript に関する素晴らしい書籍も数多く存在する。しかし、そうだとしても多少の「靴べら」は必要であり、手続き型のまま使う方が簡単だと私は感じた。
Blockcode プロジェクトは 8 個のソースファイルからなる。index.html
と block.css
はアプリケーションの基礎的な構造と見た目を定義するだけのファイルなので、以降では議論しない。また、ヘルパー関数の定義やブラウザ実装の差異解消を行う util.js
(役割は jQuery などのライブラリと同じだが、50 行しかない) と、ファイルの入出力やスクリプトのシリアライズを行う関数を定義する file.js
も以降では議論しない。
残りのファイルとその役割を次に示す:
block.js
には Blockcode の抽象表現に関するコードが含まれる。drag.js
は Blockcode で中心的な役割を果たす対話処理を実装する: 利用可能なブロックを示したリスト (「メニュー」) からブロックをドラッグして、プログラム (「スクリプト」) を組み立てる処理を実装する。menu.js
にはユーザーのプログラムを実際に実行するために必要なヘルパー関数が含まれる。turtle.js
は Blockcode 言語が実行する実際の処理 (タートルグラフィックス) を実装し、ブロックを初期化する。異なるブロック言語を作成するときは、このファイルを改変することになる。
blocks.js
ブロックはいくつかの HTML 要素から構成され、その見た目は CSS で定義される。また、ドラッグアンドドロップに対するイベントハンドラと、入力引数の改変方法を定義する JavaScript コードもブロックは持つ。blocks.js
ファイルには、こういった要素をまとめて一つのオブジェクトとして作成・管理するためのヘルパー関数が含まれる。ブロックメニューにブロックの種類が登録されるとき、Blockcode 言語でそのブロックが表す処理を実装する JavaScript 関数もブロックに関連付けられる。そのため、全てのブロックにはスクリプトの実行時に呼び出すべき関数が必ず関連付けられる。

ブロックが持つことのできる構造が二つある: 単一の数値パラメータ (デフォルトの値を設定可能) と、他のブロックが入るコンテナである。これは Blockcode が持つハードリミットであるものの、大規模なシステムでは緩和されることになるだろう。Waterbear ではパラメータとして渡すことができる式ブロックが存在し、種類が異なる複数のパラメータがサポートされる。Blockcode ではコード量に厳しい制約があるので、一つのパラメータだけを使う処理を実装する。
<!-- ブロックを表す HTML 要素 -->
<div class="block" draggable="true" data-name="Right">
Right
<input type="number" value="5">
degrees
</div>
指摘しておきたい重要な点として、メニュー内のブロックとスクリプト内のブロックは同じものである。ドラッグの処理は両者をどこからドラッグされたかに応じて少しだけ違うものとして扱い、スクリプトの実行では「スクリプト」エリアにあるブロックだけが実行される。しかし、本質的に両者は同じ構造をしているので、メニューからスクリプトにブロックがドラッグされたときはブロックを直接コピーできる。
createBlock(name, value, contents)
関数は Blcokcode によって利用されるプロパティを持った DOM 要素を返す。メニューに表示するブロックを作成するとき、そしてファイルや localStorage
からブロックを復元するときに利用される。この説明は非常に汎用的であるものの、実際の createBlock
関数は Blockcode の「言語」専用に作られており、いくつかの事項が仮定される。例えば、ブロックが保持する値は数値だと仮定されるので、type="number"
とした input
要素が作成される。Blockcode がそのようになっているので、このコードに問題はない。しかし、もし異なる種類の引数や複数の引数をサポートすることになったら、createBlock
関数には改変が必要になる。
function createBlock(name, value, contents){
var item = elem('div',
{'class': 'block', draggable: true, 'data-name': name},
[name]
);
if (value !== undefined && value !== null){
item.appendChild(elem('input', {type: 'number', value: value}));
}
if (Array.isArray(contents)){
item.appendChild(
elem('div', {'class': 'container'}, contents.map(function(block){
return createBlock.apply(null, block);
})));
}else if (typeof contents === 'string'){
// Add units (degrees, etc.) specifier
item.appendChild(document.createTextNode(' ' + contents));
}
return item;
}
ブロックを DOM 要素として扱うためのヘルパー関数がいくつかある:
blockContents(block)
:block
が持つコンテナ (子ブロック) を返す。block
がコンテナを持たないならnull
を返す。blockValue(block)
:block
が持つinput
要素に入力された数値を返す。block
がinput
要素を持たないならnull
を返す。blockScript(block)
:block
を表す、JSON シリアライズに適したオブジェクトを返す。返り値のオブジェクトはブロックの保存と復元に利用できる。runBlocks(blocks)
: ブロックの配列に含まれる各要素を実行する。
function blockContents(block){
var container = block.querySelector('.container');
return container ? [].slice.call(container.children) : null;
}
function blockValue(block){
var input = block.querySelector('input');
return input ? Number(input.value) : null;
}
function blockUnits(block){
if (block.children.length > 1 &&
block.lastChild.nodeType === Node.TEXT_NODE &&
block.lastChild.textContent){
return block.lastChild.textContent.slice(1);
}
}
function blockScript(block){
var script = [block.dataset.name];
var value = blockValue(block);
if (value !== null){
script.push(blockValue(block));
}
var contents = blockContents(block);
var units = blockUnits(block);
if (contents){script.push(contents.map(blockScript));}
if (units){script.push(units);}
return script.filter(function(notNull){ return notNull !== null; });
}
function runBlocks(blocks){
blocks.forEach(function(block){ trigger('run', block); });
}
drag.js
drag.js
の役割は、「メニュー」領域と「スクリプト」領域の対話を実装し、HTML 要素の静的なブロックを動的なプログラミング言語とすることである。ユーザーはメニュー領域からスクリプト領域にブロックをドラックしてプログラムを構築し、システムはスクリプト領域にあるブロックを実行する。
この処理では HTML5 のドラッグアンドドロップ機能が利用され、そのために必要な JavaScript イベントハンドラが drag.js
で定義される (HTML5 のドラッグアンドドロップについて詳しくは Eric Bidleman による記事 が参考になる)。ドラッグアンドドロップが組み込みで利用できるのは素晴らしいことであるものの、一部に奇妙な振る舞いや「(執筆時点では) どのモバイルブラウザも実装していない」など重大な欠点もある。
ファイルの先頭でいくつかの変数が定義される。これらの変数はドラッグが始まって終わるまでの間に呼ばれる様々なコールバックから参照される。
var dragTarget = null; // ドラッグしている対象
var dragType = null; // ドラッグはメニュー領域とスクリプト領域のどちらから始まったか?
var scriptBlocks = []; // スクリプト領域にあるブロック (位置でソート済み)
ドラッグの開始地点と終了地点に応じて、drop
関数は異なる動作をする:
- スクリプト領域からメニュー領域にドラッグしたなら、
dragTarget
を削除する (既存のブロックの除去)。 - スクリプト領域からスクリプト領域にドラッグしたなら、
dragTarget
を移動する (既存のブロックの移動)。 - メニュー領域からスクリプト領域にドラッグしたなら、
dragTarget
をコピーする (新しいブロックの挿入)。 - メニュー領域からメニュー領域にドラッグしたなら、何もしない。
dragStart(evt)
ハンドラでは、ドラッグが始まったブロックがスクリプトへコピーされるのか、それともスクリプトから削除または移動されるのかが判断される。また、スクリプトに含まれるドラッグされていない全てのブロックからなるリストも後で使うために取得される。evt.dataTransfer.setData
の呼び出しは本来ブラウザと他のアプリケーション (またはデスクトップ) の間でドラッグアンドドロップをするときにだけ必要となるので必要ないものの、ブラウザのバグを回避するために必要となる。
function dragStart(evt){
if (!matches(evt.target, '.block')) return;
if (matches(evt.target, '.menu .block')){
dragType = 'menu';
}else{
dragType = 'script';
}
evt.target.classList.add('dragging');
dragTarget = evt.target;
scriptBlocks = [].slice.call(
document.querySelectorAll('.script .block:not(.dragging)'));
// Firefox のバグを回避するために必要
// 本来は必要ない
evt.dataTransfer.setData('text/html', evt.target.outerHTML);
if (matches(evt.target, '.menu .block')){
evt.dataTransfer.effectAllowed = 'copy';
}else{
evt.dataTransfer.effectAllowed = 'move';
}
}
ドラッグ中にドロップ可能な領域をハイライトするなどの方法で視覚的ヒントをユーザーに与える処理では、dragenter
, dragover
, dragout
イベントが利用できる。もちろん、Blockcode では dragover
イベントを利用する。
function dragOver(evt){
if (!matches(evt.target, '.menu, .menu *, .script, .script *, .content')) {
return;
}
// ドロップ処理を独自に実装するために必要
if (evt.preventDefault) { evt.preventDefault(); }
if (dragType === 'menu'){
// 詳しくは DataTransfer オブジェクトに関する節を参照
evt.dataTransfer.dropEffect = 'copy';
}else{
evt.dataTransfer.dropEffect = 'move';
}
return false;
}
ユーザーがマウスから手を離すと drop
イベントが発火し、ここで魔法が起こる。最初にドラッグの開始地点 (dragStart
関数で設定された dragType
) と終了地点 ev.target
が確認され、その後ブロックのコピー・移動・削除のいずれかが実行される。ブロック固有のロジックは util.js
で定義される trigger
関数を使ってカスタムイベントとして発火されるので、スクリプトが更新された場合でも更新できる。
function drop(evt){
if (!matches(evt.target, '.menu, .menu *, .script, .script *')) return;
var dropTarget = closest(
evt.target, '.script .container, .script .block, .menu, .script');
var dropType = 'script';
if (matches(dropTarget, '.menu')){ dropType = 'menu'; }
// ブラウザにリダイレクトを止めさせる。
if (evt.stopPropagation) { evt.stopPropagation(); }
if (dragType === 'script' && dropType === 'menu'){
trigger('blockRemoved', dragTarget.parentElement, dragTarget);
dragTarget.parentElement.removeChild(dragTarget);
}else if (dragType ==='script' && dropType === 'script'){
if (matches(dropTarget, '.block')){
dropTarget.parentElement.insertBefore(
dragTarget, dropTarget.nextSibling);
}else{
dropTarget.insertBefore(dragTarget, dropTarget.firstChildElement);
}
trigger('blockMoved', dropTarget, dragTarget);
}else if (dragType === 'menu' && dropType === 'script'){
var newNode = dragTarget.cloneNode(true);
newNode.classList.remove('dragging');
if (matches(dropTarget, '.block')){
dropTarget.parentElement.insertBefore(
newNode, dropTarget.nextSibling);
}else{
dropTarget.insertBefore(newNode, dropTarget.firstChildElement);
}
trigger('blockAdded', dropTarget, newNode);
}
}
次に示す dragEnd(evt)
関数はユーザーがマウスから手を離したとき、drop
イベントの後に呼ばれる。この関数はクリーンアップを行い、関連する要素からクラスを削除し、次回のドラッグに備える。
function _findAndRemoveClass(klass){
var elem = document.querySelector('.' + klass);
if (elem){ elem.classList.remove(klass); }
}
function dragEnd(evt){
_findAndRemoveClass('dragging');
_findAndRemoveClass('over');
_findAndRemoveClass('next');
}
menu.js
menu.js
ファイルはブロックを実行したときに呼び出される関数とブロックの関連付け、そしてユーザーが作成したスクリプトの実行を行うコードが含まれる。スクリプトは変更されると自動的に再実行される。
Blockcode の「メニュー」は多くのアプリケーションが持つドロップダウンやポップアップのメニューではなく、ユーザーがスクリプトに追加できるブロックを並べた領域を指す。menu.js
はメニューを構築し、汎用的な (そのためタートルグラフィックスと独立する) ループブロックをメニューに追加する。このファイルは「その他いろいろ」のファイルでもあり、他のファイルに収まらないコードも含まれる。
アプリケーションのアーキテクチャが発展途上の間は、収まりの悪い関数をとりあえず置いておくファイルを一つ用意すると便利である。家に物を散らかさないコツは何でも入れておける「物置き」を用意することであり、これはアプリケーションのアーキテクチャでも変わらないと私は考えている。どこに書くべきか現時点では明らかでないコードは「物置き」代わりの一つのファイル (またはモジュール) に入れておけばよい。このファイルが大きくなっていく中で生じるパターンに注目することが重要となる: 関連性の高い関数は個別のモジュール (あるいは単一の一般的な関数) に切り出せる可能性がある。この「物置き」はコードを整理する正しい方法が判明するまでの一時的な保管所であり、際限なく大きくするべきではない。
menu.js
では menu
と script
という参照が (ファイルの中で) グローバルに保持される。両者は多用されるので、使用するたびに DOM を検索するのは無駄が大きい。メニューに含まれるスクリプトは scriptRegistry
オブジェクトが保持する。scriptRegistry
は名前と関数を結び付ける非常に単純なマップであり、名前が同じで振る舞いが異なるブロックやブロックの名前変更はサポートされない。複雑なスクリプト環境では scriptRegistry
より柔軟性の高い仕組みが必要になるだろう。
scriptDirty
変数はスクリプトが最後に実行されてから変更されたかどうかを表す。同じスクリプトを何度も実行するのを避けるために利用される。
var menu = document.querySelector('.menu');
var script = document.querySelector('.script');
var scriptRegistry = {};
var scriptDirty = false;
次回のフレームハンドラでスクリプトを実行するようシステムに指示するときは、scriptDirty
フラグを true
に変更する runSoon
関数を呼ぶ。システムは各フレームで run
関数を呼び出すものの、この関数は scriptDirty
が false
のときすぐに処理を終える。scriptDirty
が true
ならスクリプトを実行し、さらにスクリプトを実行した後に実行すべき言語固有のハンドラも呼び出す。ツールキットとしてのブロックとタートル言語を分離することで、ブロックのコードは再利用可能となる (見方を変えれば、タートル言語は様々なフロントエンドに接続できるとも言える)。
スクリプトを実行するとき、スクリプトに含まれるブロックのそれぞれに対して runEach(evt)
が呼ばれる。この関数はブロックにクラスを設定してから対応する関数を検索・実行する。このためスクリプトの実行に時間がかかるときは、どのブロックに時間がかかっているかを視覚的に確認できる。
以下のコードで使われている requestAnimationFrame
関数はアニメーションのためにブラウザによって提供される。requestAnimationFrame
に渡された関数は、ブラウザがフレームをレンダリングする直前に呼び出される。フレームのレンダリングは通常 1 秒間に 60 回の頻度で行われるものの、スクリプトの実行に時間がかかると頻度が減る可能性もある。
function runSoon(){ scriptDirty = true; }
function run(){
if (scriptDirty){
scriptDirty = false;
Block.trigger('beforeRun', script);
var blocks = [].slice.call(
document.querySelectorAll('.script > .block'));
Block.run(blocks);
Block.trigger('afterRun', script);
}else{
Block.trigger('everyFrame', script);
}
requestAnimationFrame(run);
}
requestAnimationFrame(run);
function runEach(evt){
var elem = evt.target;
if (!matches(elem, '.script .block')) return;
if (elem.dataset.name === 'Define block') return;
elem.classList.add('running');
scriptRegistry[elem.dataset.name](elem);
elem.classList.remove('running');
}
メニューへのブロックの追加は menuItem(name, fn, value, contents)
で行う。この関数は新しいブロックを作成し、そのブロックと関数の関連付けを作成し、メニューの子要素としてそのブロックを追加する。
function menuItem(name, fn, value, units){
var item = Block.create(name, value, units);
scriptRegistry[name] = fn;
menu.appendChild(item);
return item;
}
repeat(block)
関数と "Repeat"
ブロックは menu.js
で、つまりタートル言語の外側で定義される。繰り返し処理は他の言語でも利用されるためである。もし条件分岐や変数の読み書きを行うブロックが存在するなら、それらもここで (もしくは個別の「言語横断ブロック」モジュールで) 定義されることになる。ただ、Blockcode 言語で定義される汎用ブロックは繰り返し処理しか存在しない。
function repeat(block){
var count = Block.value(block);
var children = Block.contents(block);
for (var i = 0; i < count; i++){
Block.run(children);
}
}
menuItem('Repeat', repeat, 10, []);
turtle.js
turtle.js
はブロックベースのタートル言語を実装する。このファイルは他のファイルに変数を一切エクスポートしないので、他のどのファイルも turtle.js
に依存しない。そのため、Blockcode のコアを変更しなくても turtle.js
を改変するだけで新しいブロック言語を作成できる。
タートルプログラミングはグラフィックスプログラミングの一種であり、Logo 言語によって初めて有名になった。タートルプログラミングではペンを持った仮想的なカメが画面上に配置され、そのカメに対してユーザーは「ペンを画面に置く (移動するときに線を引く)」「ペンを画面から離す (移動するときに線を引かない)」「指定された歩数だけ進む」「指定された角度だけ方向転換する」の指示を出せる。この四つの指示とループを組み合わせるだけで、非常に入り組んだ豪華な画像を作成できる。
Blockcode で実装したタートル言語では、この四つ以外にもブロックを追加している。例えば「右を向く」ブロックがあれば「左を向く」ブロックは厳密には必要にならない (符号を反転させた「右を向く」に等しい) ものの、利便性のため「左を向く」ブロックも存在する。同様に「前に進む」ブロックと「後ろに進む」ブロックの両方が存在する。

図 3 に示した画像は「前に進む」と「右に曲がる」を二重のループで囲んだスクリプトから得られる。それぞれのブロックのパラメータは満足する画像が得られるまで私が調整した値である。
var PIXEL_RATIO = window.devicePixelRatio || 1;
var canvasPlaceholder = document.querySelector('.canvas-placeholder');
var canvas = document.querySelector('.canvas');
var script = document.querySelector('.script');
var ctx = canvas.getContext('2d');
var cos = Math.cos, sin = Math.sin, sqrt = Math.sqrt, PI = Math.PI;
var DEGREE = PI / 180;
var WIDTH, HEIGHT, position, direction, visible, pen, color;
reset
関数は全ての状態変数にデフォルトの値を設定する。複数のカメをサポートしたい場合は、これらの値をオブジェクトにカプセル化する必要があるだろう。また、UI では角度を度数法で扱うのに対して描画関数では角度を弧度法で扱うので、ヘルパー関数 deg2rad(deg)
が用意される。最後に、drawTurtle
関数がカメを描画する。デフォルトでは単なる三角形であるものの、芸術的に美しいカメに変更することはいつでもできる。
drawTurtle
関数はタートルによる描画の実装で使うのと同じ基礎的操作 (プリミティブ) を使っている点に注目してほしい。異なる抽象レイヤーにまたがるコードの再利用は多くの場合で避けるべきであるものの、その意味が明確ならば再利用によってコードサイズとパフォーマンスを大きく改善できる。
function reset(){
recenter();
direction = deg2rad(90); // 「上」を向く
visible = true;
pen = true; // pen が true なら移動時に描画し、false なら移動時に描画しない
color = 'black';
}
function deg2rad(degrees){ return DEGREE * degrees; }
function drawTurtle(){
var userPen = pen; // save pen state
if (visible){
penUp(); _moveForward(5); penDown();
_turn(-150); _moveForward(12);
_turn(-120); _moveForward(12);
_turn(-120); _moveForward(12);
_turn(30);
penUp(); _moveForward(-5);
if (userPen){
penDown(); // restore pen state
}
}
}
現在のマウス位置に特定の半径の円を描画する特別なブロックが存在する。この処理は drawCircle
関数で特別に処理される。「前に 1 だけ進む」と「1 だけ右を向く」を 360 回繰り返すことでも円は描画できるものの、そうすると円の大きさの制御が非常に難しくなる。
function drawCircle(radius){
// この処理の数学的説明: http://www.mathopenref.com/polygonradius.html
var userPen = pen; // save pen state
if (visible){
penUp(); _moveForward(-radius); penDown();
_turn(-90);
var steps = Math.min(Math.max(6, Math.floor(radius / 2)), 360);
var theta = 360 / steps;
var side = radius * 2 * Math.sin(Math.PI / steps);
_moveForward(side / 2);
for (var i = 1; i < steps; i++){
_turn(theta); _moveForward(side);
}
_turn(theta); _moveForward(side / 2);
_turn(90);
penUp(); _moveForward(radius); penDown();
if (userPen){
penDown(); // restore pen state
}
}
}
タートル言語のメインのプリミティブは次に示す moveForward
関数である。初等的な三角関数を使った座標の計算と、ペンが画面に付いているかどうかのチェックがある。
function _moveForward(distance){
var start = position;
position = {
x: cos(direction) * distance * PIXEL_RATIO + start.x,
y: -sin(direction) * distance * PIXEL_RATIO + start.y
};
if (pen){
ctx.lineStyle = color;
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(position.x, position.y);
ctx.stroke();
}
}
これ以外のタートル言語のコマンドはこれまでに実装してきたコマンドを使って簡単に定義できる。
function penUp(){ pen = false; }
function penDown(){ pen = true; }
function hideTurtle(){ visible = false; }
function showTurtle(){ visible = true; }
function forward(block){ _moveForward(Block.value(block)); }
function back(block){ _moveForward(-Block.value(block)); }
function circle(block){ drawCircle(Block.value(block)); }
function _turn(degrees){ direction += deg2rad(degrees); }
function left(block){ _turn(Block.value(block)); }
function right(block){ _turn(-Block.value(block)); }
function recenter(){ position = {x: WIDTH/2, y: HEIGHT/2}; }
白紙の状態に戻したいときは、clear
関数を呼べば全てが初期状態に戻る。
function clear(){
ctx.save();
ctx.fillStyle = 'white';
ctx.fillRect(0,0,WIDTH,HEIGHT);
ctx.restore();
reset();
ctx.moveTo(position.x, position.y);
}
Blockcode が初めて読み込まれ実行されるときは、reset
と clear
による初期化の後にカメの描画が行われる。
onResize();
clear();
drawTurtle();
その後、menu.js
が公開する Menu.item
関数とこれまでに定義を示してきた関数を使って、ユーザーがスクリプトを構築するためのブロックがメニューに登録される。ユーザーはメニューに登録されたブロックをドラッグすることで自身の思い描くプログラムを作成できる
Menu.item('Left', left, 5, 'degrees');
Menu.item('Right', right, 5, 'degrees');
Menu.item('Forward', forward, 10, 'steps');
Menu.item('Back', back, 10, 'steps');
Menu.item('Circle', circle, 20, 'radius');
Menu.item('Pen up', penUp);
Menu.item('Pen down', penDown);
Menu.item('Back to center', recenter);
Menu.item('Hide turtle', hideTurtle);
Menu.item('Show turtle', showTurtle);
教訓
MVC を使わない理由
モデル・ビュー・コントローラー (MVC) は 1980 年代に生まれた設計パターンであり、Smalltalk プログラムを書くときに重宝された。一部のウェブアプリケーションでも MVC に似た設計が利用できる場合がある。しかし、MVC は全ての問題を解決できる万能のツールではない。ブロック言語ではブロック要素に全ての状態 (MVC の「モデル」) を持たせることができるので、モデルを JavaScript で表現する意味はあまりない。ただし、モデルに他の用途 (例えばネットワーク上のユーザーによるコードの共同編集) があるなら話は変わる。
初期バージョンの Waterbear では、JavaScript で書かれたモデルと DOM を同期させた状態に保つ処理に大変な労力をかけていた。気が付くと、コードの半分とバグの 90% がモデルと DOM の同期処理に関連していた。両者を一つにすることでコードは単純かつ柔軟になり、さらに DOM 要素に全ての状態が関連付くことで開発者ツールから DOM を眺めるだけでバグを見つけられるようにもなった。このため、Waterbear の開発では HTML/CSS/JavaScript が提供する以上の MVC の分離を実装する意味はほとんどなかったと言える。
「おもちゃ」に対する変更が本当の変更につながる
自分が作っている大規模なシステムの範囲を絞った小規模バージョンを作成することは興味深い経験だった。システムが大規模だと、影響が大きすぎるという理由で変更する気になれない事態がときとして起こる。しかし、小規模な「おもちゃ」バージョンのシステムであれば自由に実験でき、そこで得られた結果を大規模なシステムに逆輸入することもできる。私にとって「大規模なシステム」とは Waterbear であり、この Blockcode プロジェクトは Waterbear の構築方法に大きな影響を与えた。
実験が小さいと失敗しても気にならない
Blockcode という機能の制限されたブロック言語で私が行えた実験を次に示す:
- HTML5 のドラッグアンドドロップ機能を使う。
- DOM を走査し、関連付いた関数を呼び出すことでブロックを「直接」実行する。
- 実行されるコードと DOM を明確に分離する。
- ドラッグ時の衝突検出を単純化する。
- ベクトルとスプライトを扱う極小ライブラリを作る。
- スクリプトが変更されたときに結果をすぐに表示する (ライブコーディング)。
実験で重要なのは、成功しなくても構わないということである。普段の仕事では失敗が学習における重要な一歩として扱われることなく、失敗すれば罰せられる。そのため上手く行かなかった実験や行き詰まった実験にもっともらしい言い訳を付けてごまかす場合が多い。しかし、物事を前に進める上で失敗は不可欠である。Blockcode では HTML5 のドラッグアンドドロップ機能が動作するものの、この機能は現在モバイルブラウザでは動作しないので Waterbear では使えない。コードを分離してブロックに関連付け、ブロックを走査することでコードを実行する方式は非常に上手く行ったので、このアイデアは既に Waterbear の一部に取り入れられ、テストとデバッグで大きく役立っている。単純化された衝突検出 (を少し改変したもの) やベクトルとスプライトを扱う極小ライブラリも Waterbear で使われている。ライブコーディングの機能は現在の Waterbear には実装されていないものの、現在作業中の変更が安定したら実装するかもしれない。
作っているものの本質は何なのか?
大規模なシステムの小規模バージョンを作ると、本当に重要な部分に焦点が鋭く当たることになる。歴史的な理由で存在しているだけで何の用途も持たない要素 (もっと悪いのは、ユーザーを混乱させるだけの要素) はどれだろうか? 誰も使っていないのにメンテナンスはしなければならない機能は? ユーザーインターフェースを合理化できないだろうか? こういった質問は大規模なシステムの小規模バージョンを作る上で非常に重要となる。また、小さなプロジェクトではレイアウトの変更などの根本的な変更を他の部分への影響を考えずに手軽に試すことができ、それが複雑なシステムをリファクタリングする上での指針となる可能性さえある。
プログラムは過程であり、物ではない
この Blockcode プロジェクトでは試せなかったものの、将来 Blockcode のコードベースを使って試してみたいことがいくつかある。既存のブロックから新しいブロックを作成する「関数」ブロックの実装は興味深い。環境が制限されているので、undo/redo の実装は単純になるだろう。複雑性を大きく高めることなくブロックが複数の引数を受け取れるようにできれば便利である。それから、ブロックスクリプトをオンラインで共有する様々な方法を提供できれば、Blockcode の「ウェブらしさ」が最大限に高まるだろう。