Displaying posts filed under

プログラミング

PHP5.3の__callStaticをエミュレートする

PHP 5.3.0 ではマジックメソッド __callStatic が追加された。グローバルに使用されるユーティリティクラスで、動的にメソッド名を生成したいときに便利。

class FooUtil {
  static protected $_inc = array('test1' => 1,
				 'test2' => 2);
 
  public static function __callStatic($method, $args) {
    return isset(static::$_inc[$method])
      ? static::$_inc[$method] + $args[0] : $args[0];
  }
}
echo FooUtil::test1(100); // => 101
echo FooUtil::test2(100); // => 102
echo FooUtil::testxxx(100); // => 100

同等の機能を PHP 5.2 より前の環境で再現したい場合、シングルトンパターンと __call を組み合わせるのが最も普通のやり方だと思う。

class FooUtil2 {
  protected $_inc = array('test1' => 1,
			  'test2' => 2);
  static private $__singleton = null;
 
  public static function getInstance() {
    if(self::$__singleton === null) {
      self::$__singleton = new self;
    }
    return self::$__singleton;
  }
 
  public function __call($method, $args) {
    return isset($this->_inc[$method])
      ? $this->_inc[$method] + $args[0] : $args[0];
  }
}
echo FooUtil2::getInstance()->test1(100); // => 101
echo FooUtil2::getInstance()->test2(100); // => 102
echo FooUtil2::getInstance()->testxxx(100); // => 100

この方法の欠点は「呼び出し時のコードが長い」ことである。そこで個人的にはクラス名と同じ名前のグローバル関数を使う方法を提案したい。

function FooUtil3() {
  static $singleton = null;
  if($singleton === null) {
    $singleton = new FooUtil3();
  }
  return $singleton;
}
 
class FooUtil3 {
  protected $_inc = array('test1' => 1,
			  'test2' => 2);
 
  public function __call($method, $args) {
    return isset($this->_inc[$method])
      ? $this->_inc[$method] + $args[0] : $args[0];
  }
}
echo FooUtil3()->test1(100); // => 101
echo FooUtil3()->test2(100); // => 102
echo FooUtil3()->testxxx(100); // => 100

この方法ならば __callStatic を使うのと比べて2文字長いだけだ。しかも他2つの方法よりもカプセル化の面では優れている。関数とクラスは「名前が同じ」というごく緩やかなつながりを持っているだけなので、静的な意味でも動的な意味でも、あとから自由に実装を切り替えられる。staticメソッドでなければならない、シングルトンでなければならない、といった制約はない。

これはグローバル変数や単純なレジストリ(CakePHPにおけるConfigureのような)を使うのとも違う。関数なので、どんな処理でも差し挟むことができる。関数の方がパラメータを持ってもいい。

FooUtil4($context)->test($parameters);

グローバル関数というと問題になるのは名前の衝突だが、関数名とクラス名では名前の傾向が異なるので、クラス名が衝突しないのであれば同じ名前の関数も衝突しない可能性が高いと考えられる。

あまり公のプロジェクトで採用する気にはならないが、内々に使うにはそれなりに便利なテクニックだと思う。

CakePHP 1.3RC1の新機能:プラグインdriver

せっかく私のpatchが採用されたので、ブログにも書いておきます。

CakePHP 1.3からはプラグイン内の DataSource が使えるようになりますが、1.3RC1からは DataSource の driver もプラグインから読み込めるようになりました。

たとえばGithubのCakePHP datasources pluginでは、Cakeコアに含まれない様々なデータベースに対するDboドライバが提供されていますが、RC1ではこれを次のようにして読み込むことができます。

class DATABASE_CONFIG {
  var $sqlite3 = array('driver' => 'Datasources.DboSqlite3',
                       ...);
 
}

また driver を使用する DataSource をプラグインとして提供することも可能です。拙作のKeyValueSourceは次のように使用できます。

class DATABASE_CONFIG {
  var $memcache = array('datasource' => 'KeyValueStore.KeyValueSource',
                        'driver' => 'KeyValueStore.KeyValueMemcache',
                        ...);
}

プラグインが読み込む driver を app/models/datasources 内に作ることもできます。たとえば memcache ドライバを拡張した my_memcache ドライバを作った場合、次のように読み込みます。

// app/models/datasources/key_value/key_value_my_memcache.php
 
class DATABASE_CONFIG {
  var $memcache = array('datasource' => 'KeyValueStore.KeyValueSource',
                        'driver' => 'my_memcache',
                        ...);
}

CakePHPでSchemalessなデータを扱う

拙作のKeyValueSource(key-valueストアのためのDataSource)で、スキーマ・レスなデータを保存できるようにしてみました。

cakephp-key-value-source – GitHub
(このプロジェクトは実用より実験を重視しているので、頻繁に仕様が変わります)

モデルクラスで $looseSchema というプロパティを設定すると、プライマリキーidを除いて、どんな構造のデータでも保存できるようになります。

class SchemalessUser extends AppModel {
  var $useDbConfig = 'memcache';
  var $looseSchema = true;
}
 
// in Controllers 
$SchemalessUser->save(array('id' => 1234,
                            'name' => 'John Smith',
                            'hobby' => array('baseball', 'soccer')));
$SchemalessUser->save(array('id' => 1235,
                            'firstname' => 'John',
                            'lasttname' => 'Smith',
                            'age' => 20));

saveメソッドの fieldList オプションでキーを制限することができます。

$SchemalessUser->save(array('id' => 1234,
                            'name' => 'John Smith',
                            'hobby' => array('baseball', 'soccer')),
                      array('fieldList' => array('id', 'name')));
 
// hobby は保存されない
// array('id' => 1234, 'name' => 'John Smith')
$ret = $SchemalessUser->read(null, 1234);

$looseSchema プロパティで一部のスキーマを明示的に設定しておくこともできます。下の例ではプライマリキーを key に変更し、更新日時を入力するために updated フィールドを定義しています。

class LooseSchemaUser extends AppModel {
  var $useDbConfig = 'memcache';
  var $primaryKey = 'key';
  var $looseSchema = array('key' => array('type' => 'string', 'length' => 255),
                           'updated' => array('type' => 'datetime'));
}
 
// in Controllers 
$LooseSchemaUser->save(array('key' => 1234,
                             'name' => 'John Smith'));
 
// array('key' => 1234, 'name' => 'John Smith', 'updated' => '2010-02-07 22:23:13')
$ret = $LooseSchemaUser->read(null, 1234);

$looseSchemaを設定せず、$_schema を設定すれば通常の動作になります。

class StrictUser extends AppModel {
  var $useDbConfig = 'memcache';
  var $_schema = array('id' => array('type' => 'string', 'length' => 255),
                       'name' => array('type' => 'string'));
}
 
// in Controllers 
$StrictUser->save(array('id' => 1234,
                        'name' => 'John Smith',
                        'hobby' => array('baseball', 'soccer')));
 
// hobby は保存されない
// array('id' => 1234, 'name' => 'John Smith')
$ret = $StrictUser->read(null, 1234);

裏側では KeyValueLooseSchemaBehavior というBehaviorを使って、$_schema に存在しないデータを一旦別のフィールドに退避しておき、DataSource の中で取り出しています。退避用のフィールドはデフォルトでは _schemaless_data という名前で、DataSource の describe メソッドの中で組み込まれます。さらに(ちょっと反則気味ですが) Behavior 自体も describe メソッドの中で $actsAs に設定することで、特別な設定なしで動くようになっています。

自動設定される部分を書き下すと、おおむね次のようになります。

class User extends AppModel {
  var $actsAs = array('KeyValueLooseSchema' => array('schemalessField' => '_schemaless_data'));
  var $_schema = array('id' => array('type' => 'string', 'length' => 255),
                        '_schemaless_data' => array('type' => 'schemaless'));
}

KeyValueLooseSchemaBehavior の実装は汎用的になっているので、他の DataSource にほとんどそのまま持っていけるはずです。

CakePHP用Cicindelaライブラリ

CakePHP から レコメンデーションエンジン Cicindela にアクセスするためのライブラリを公開しました。

http://github.com/tkyk/cakephp-cicindela

使い方はREADMEを読んでください。Cicindela本体のWeb APIとほとんど同じ構造なので、特に迷うことはないだろうと思います。

ちなみに Configure::read(‘debug’) > 1 で実行すると、DboSourceと同じようにリクエスト情報が一覧表示されます。デバッグに便利です。こんな感じ:

(cicindela) 2 request(s)
NrURLStatusTook (ms)
1http://host/cicindela/recommend?set=pick&op=for_item&item_id=RubyHTTP/1.1 200 OK2887
2http://host/cicindela/recommend?set=pick&op=for_item&item_id=PHPHTTP/1.1 200 OK1346

….
以下、このライブラリを作りながら考えたこと。

このライブラリはDataSource + Behaviorという2クラス構成になっている。最初はDataSourceだけでできるかと思っていたのだが、無理だった。現在のCakeのModelクラスは、DataSourceとDboSourceの関係で言うなら”DboModel”であって、非DboのDataSourceを扱うには適していない。

現在のModelが”DboModel”であるように、XXXSourceに対してはXXXModelが存在することが望ましい。CakePHP 1.3からはDataSourceがプラグイン化できるようなので、同時にModelのベースクラスを提供するようにすれば、ある程度は改善されると思う。ただそれでも結局(プラグインの)ユーザがいちいちクラスの継承関係に気を使わなければいけない点が煩わしい。理想を言うなら、1.非常に簡素なBaseModelクラスを用意する、2.DataSourceにモデル初期化用コールバックを設ける、3.初期化用コールバックにおいてBehaviorをattachする、DataSource固有の機能はBehaviorに実装する……という構成にできれば良いのではないか。

XPCNativeWrapperとunsafeWindowの間で同期処理を行う

先日使い方を学んだ MessageEvent と、location.href+javascript:ハックとJSDeferred userscript版とを組み合わせ、XPCNativeWrapper と unsafeWindow の間で同期処理を行うコードを書いてみた。

まずは MessageEvent を使って前回 evalInPage 相当の処理を作る。キャンセル処理も入れたかったので UnsafeWrapper というオブジェクトにまとめることにした。前回より長くなったように見えるが、実際には MessageEvent の lastEventId が使えるようになった分、簡潔になっている。

var UnsafeWrapper = new (function() {
	var seqId = 0, waiting = {}, self = this, noop = function(){};
 
	function dispatch(data, id) {
	    var e = document.createEvent("MessageEvent");
	    e.initMessageEvent('GM_UnsafeWrapper_returned', true, false,
			       data, location.protocol + "//" + location.host,
			       id, window);
	    document.dispatchEvent(e);
	}
 
	function listen(func) {
	    document.addEventListener('GM_UnsafeWrapper_returned', func, false);
	}
 
	listen(function(e) {
		if(waiting[e.lastEventId]) {
		    waiting[e.lastEventId].call(self, JSON.parse(e.data));
		    delete waiting[e.lastEventId];
		}
	    });
 
	this.exec = function(func, args, callback) {
	    var userFunc = "("+ func +").apply(null,"+ JSON.stringify(args || []) +")";
	    waiting[seqId] = callback || noop;
	    location.href = "javascript:void "+
	    (function(dispatch, ret, id) { dispatch(JSON.stringify(ret), id) }) +
	    "("+ dispatch +","+ userFunc +"," + JSON.stringify(seqId) +")";
	    return seqId++;
	};
 
	this.cancel = function(id) {
	    delete waiting[id];
	};
    });

そして Deferred に対するアダプタを書く。とても簡単。

with(D()) {
    function unsafeExec(func, args) {
	var d = new Deferred();
	var id = UnsafeWrapper.exec(func, args, function(ret) {
		d.call(ret);
	    });
	d.canceller = function(){ UnsafeWrapper.cancel(id) };
	return d;
    };
}

これで例えば、GM_xmlhttpRequest で外部ドメインからデータを取得して、そのデータを整形して、unsafeWindow 側で表示処理を行って、そしてその結果をuserscript側で受け取って…という処理は次のように書くことができる。

with(D()) {
 
xhttp.get("http://example.com/")
    .next(function(res) {
            var data = res.responseText;
            //....
 
	    return unsafeExec(function(data) {
                    $.each(data, function(k,v) {
                      //...
                    });
                    //...
		    return result;
		},
		[data]);
	})
    .next(function(result) {
          //...
	});
 
}

より実用性を高めるのであれば、location.href で実行するコード全体を try {} で囲み、catch したエラーをJSONに変換して _failed イベントを dispatch して、Deferred の fail に転送すれば、unsafeWindow 側で起こったエラーをuserscript側のDeferred chainで捕捉できるようになる。

参考:

XPCNativeWrapperとunsafeWindowの間でデータを送受信する

XPCNativeWrapperの外側でスクリプトを評価する関数。Firebugも同じことをしている。unsafeWindowを触らないので安全。

function evalInPage(fun) {
  location.href = "javascript:void (" + fun + ")()";
}

SmartLDR更新 – 素人がプログラミングを勉強するブログ

これは面白い!Function.prototype.toString が関数のソースコードを返すことを利用した、巧妙にして簡潔なハック。

次のように引数を渡せるように改良すればさらに強力になる(引数に渡せるのはJSON化可能な値のみ)。

function evalInPage(func, args) {
    var argStr = JSON.stringify(args || []);
    location.href = "javascript:void "+ func +".apply(null,"+ argStr +")";
}

GM_xmlhttpRequest を使って別ドメインから得たデータを、unsafeWindow 上のライブラリを使って表示する、といったコードが安全かつ自然に書けるようになる。

// データの取得は Greasemonkey で行う
GM_xmlhttpRequest({ method:"GET",
                    url: "http://example.com/api",
                    onload: function(response) {
                      var data = response.responseText;
                      //...
                      evalInPage(render, [data]);
                    }
                  });
 
// データの表示は unsafeWindow 上で行う
// jQueryなど unsafeWindow 上のライブラリが使える
function render(data) {
  $.each(data, function(k,v){
    //...
  })
}

これだけでも十分便利なのだが、「返り値が利用できない」「Firefoxのlocation.hrefは非同期で実行される」といった難点があるらしい。それを回避するためにunsafeExec on JSDeferred – 枕を欹てて聴くにおいてはJSDeferredというライブラリを使う方法が紹介されているのだが、コードを見る限り unsafeWindow に対するアクセスが必要になるようだ。

unsafeWindow にアクセスせずにGreasemonkeyに制御を戻すには、DOM Eventを使うのが良いと思う。以下、DOMNodeInserted イベントを使って実装してみた(返り値として利用できるのはJSON化可能な値のみ)。

追記: Constellationさんからコメントをいただきました。DOMNodeInsertedなどのイベントを使わなくても、直接createEvent/dispatchEventでイベントを生成・データを送受信する方法があるようです。 詳細はこのページの Constellationさんのコメントや、『Greasemonkey スクリプトとイベントで通信: Days on the Moon』など参照してください。

function getUniqId() {
    do {
        var id = "_tmp" + String(Math.random()).slice(2);
    } while(document.getElementById(id));
    return id;
}
 
function evalInPage(func, args, callback) {
    var argStr = JSON.stringify(args || []),
        userFunc = "("+ func +").apply(null,"+ argStr +")";
 
    if(!callback) {
        location.href = "javascript:void "+ userFunc;
        return;
    }
 
    var div = document.createElement('div'),
        id = getUniqId();
    div.id = id;
    div.style.display = 'none';
    document.body.appendChild(div);
    div.addEventListener('DOMNodeInserted', function(e){
            callback(JSON.parse(e.target.nodeValue));
            div.parentNode.removeChild(div);
        }, false);
 
    location.href = "javascript:void "+ (function(ret, id) {
            document.getElementById(id)
            .appendChild(document.createTextNode(JSON.stringify(ret)));
        }).toString() + ".call(null,"+ userFunc +"," + JSON.stringify(id) +")";
}

JSDreferredを使う方法と同じく、戻り値を受け取るコールバック関数を指定する。

evalInPage(function(obj, num) {
        //この中のコードは unsafeWindow で実行される
        return { answer: obj.a + num };
    },
    [{a:99}, 1],
    function(ret) {
        //この中のコードは Greasemonkey で実行される
	alret(ret.answer);
    });

多分まじめに作り込めば一種のRPCシステムみたいになると思う。

JavaScriptで確実にglobalオブジェクトを得る方法

確実にグローバル関数/変数にアクセスしたいとき。あるいはglobalオブジェクトの名前が分からないときに。

var global = (function(){ return this })();

ブラウザ上では window がglobalオブジェクトを指すことは暗黙の前提になっているが、そうでない環境もあり得る。

ちなみに Greasemonkey と Chrome user-script ではともに window でglobalオブジェクトにアクセスできるようだ。

Behavior の適用範囲を広げる $mapMethods

(CakePHP 1.2.5)
ビヘイビアに定義されたメソッドは、モデル自身のメソッドであるかのように呼び出すことができます。

class FooBehavior extends ModelBehavior {
  function foo($model){ /* ... */ }
}
 
class Bar extends AppModel {
  var $actsAs = array('Foo');
}
 
// in Controllers
$this->Bar->foo();

しかしこの方法では Dynamic Finder のように動的に名前が変化するメソッドは実装できません。

ModelBehavior クラスの $mapMethods プロパティを使用すると、この制約を乗り越えることができます。$mapMethods は配列で、次のような構造をしています:

array( '/メソッド名に対する正規表現/' => '実際に呼び出されるメソッド'  )

例えば '/^list/' => '_genericList' という設定を行った場合、listから始まる全てのメソッド呼び出しが、ビヘイビアの _genericList メソッドの呼び出しに変換されます。このとき _genericList メソッドの引数には元のメソッド名と引数が渡されます。ちょうど __call によるメソッドのオーバーロードと同じです。

この機能を活用して、あるカラムの値を配列として取得するための listColumn メソッドを作ってみます(Column はカラム名)。このメソッドは ListingBehavior に実装することにしましょう。

class ListingBehavior extends ModelBehavior {
  var $mapMethods = array('/^list[a-zA-z0-9]+$/' => '_listColumn');
 
  function _listColumn($model, $methodName, $query=array()) {
    $column = substr($methodName, 4);
    $query = am($query, array('fields' => array($model->alias.".".$column)));
    $arr = $model->find('all', $query);
    if(is_array($arr)) {
      return Set::extract("/{$model->alias}/{$column}", $arr);
    }
    return $arr;
  }
}

モデルにこのビヘイビアを組み込むと、任意のカラムに対して listXXX メソッドを呼び出すことができるようになります。

class User extends AppModel {
  var $actsAs = array('Listing');
}
 
// in Controllers
$this->User->listId();
$this->User->listUsername(array('conditions' => 'active = 1'));
$this->User->listGroup_id();

メソッド名は小文字に変換されて渡されるので(これが仕様なのかバグなのか微妙なところです)、残念ながら Inflector::underscore を使うことはできません。

ow.ly や oneclip.jp のフレームを外す user script

ow.lyoneclip.jpなど、元コンテンツをiframe内に表示するタイプのURL短縮サービスから、自動的に元URLにリダイレクトするuser scriptを作りました。

Redirect to the iframed URL

同様の機能を持つものとしてiframe redirectorがあり、今まで私も使用していたのですが、対象サービスのURLやHTML構造に依存するコードが含まれているため、対象サービスの構成が変化すると動かなくなってしまいます(少し前に am6.jp の仕様が変わって動かなくなりました。現在は修正されています)。

そのため拙作のスクリプトではURLやHTMLに依存するコードは使わず、「面積比でbodyの4分の1を占めるiframe」を元コンテンツと判断するようにしました。おそらくほとんどのケースで問題なく動作するものと思われます。

デフォルトでは以下のURLで動作します。

  • http://ow.ly/*
  • http://oneclip.jp/*
  • http://am6.jp/*

今後新たに同種のサイトが登場した場合は(登場してほしくないのですが)、Greasemonkeyの管理ダイアログから「ユーザスクリプトを実行するページ」にURLを追加してください。スクリプトの修正は不要です。

Firefox 3.5.7 + Greasemonkey 0.8.2 および Mac版 Google Chrome 4.0.295.9 で動作確認しています。

参考

URL短縮サービス ow.ly は実URLが表示されずに危険 → グリモン書いた – まちゅダイアリー(2009-11-05)

Cicindelaを使ったAjaxアプリケーションサンプル 2

ライブドア製レコメンデーション・エンジン Cicindela の動作を確かめるために、簡単なAjaxアプリケーションを作ってみました。

前回の続きです。

JavaScript: Logger

Cicindela との連携部分を作る前に、簡単な Logger を作っておきます。ログの出力先には TEXTAREA 要素を想定しています。

var Logger = {
    clear: function() {
        $('#log').val("");
    },
    append: function(text) {
        var dom = $('#log').get(0);
        dom.value+= text + "\n";
        dom.scrollTop = dom.scrollHeight;
    }
};

Cicindela への入力

まずは入力部分から作っていきます。このアプリケーションにおいて必要な操作は「あるユーザがアイテムを選択した」、「あるアイテムをあるカテゴリに所属させる」の2つです。これらの操作を抽象化するために、Cicindela Web APIに対する Proxy となるオブジェクトを作ります。

var Cicindela = {
    base: '/cicindela/',
    set:  'sample',
    _insert_op_url: function(op, item) {
        return this.base +"record?set="+ this.set +"&item_id="+ item + "&op="+ op;
    },
    _send_req: function(url, cb) {
        Logger.append("send Request to " + url);
        $.ajax({ type: "GET",
                 url: url,
                 success: cb,
                 complete: function(xhr, status) {
                     Logger.append("receive Response["+ xhr.status +"] from "+ url);
                 }
               });
    },
    set_category: function(item, category) {
        this._send_req(this._insert_op_url("set_category", item) + "&category_id="+ category);
    },
    insert: function(user, item) {
        this._send_req(this._insert_op_url("insert_pick", item) + "&user_id="+ user);
    }
};

insert がアイテム選択、set_category がカテゴリ登録のためのメソッドです。実際にリクエスト送信を行うのは _send_req メソッドで、送信先URLと成功時に呼び出されるコールバック関数とを引数にとります。set プロパティには集計セットの名前を書いておいてください。

あとは「insert」ボタンが押されたときに、これらの API を呼び出します。

  // $(document).ready において
  $('button#insert').click(
      function(e) {
        var user_id = $('#username').val();
        if(user_id == "") {
          return false;
        }
        $.each(data, function(category_name, category_data) {
                   var category_id = category_data.id;
                   var container = $("#"+ category_name);
                   var values = container.val();
                   $.each(values,
                          function(i, v) {
                              Cicindela.set_category(v, category_id);
                              Cicindela.insert(user_id, v);
                              $('#username').val("");
                          });
               });
        return false;
    });

このアプリケーションの場合、カテゴリ登録はデータの初期化時にまとめて行った方が効率的なのですが、「同じアイテムを、何度同じカテゴリに登録しても問題ない」ことを確かめるために、あえてアイテム選択時に毎回登録を実行しています。

これで Cicindela への入力準備が整いました。ユーザ名を入力して、適当にアイテムを選択して「insert」ボタンを押してみます。成功すれば次のようなログが出力されるはずです。

send Request to /cicindela/record?set=sample&item_id=Ruby&op=set_category&category_id=1
send Request to /cicindela/record?set=sample&item_id=Ruby&op=insert_pick&user_id=cicindela
receive Response[204] from /cicindela/record?set=sample&item_id=Ruby&op=insert_pick&user_id=cicindela
receive Response[204] from /cicindela/record?set=sample&item_id=Ruby&op=set_category&category_id=1

またMySQLサーバにログインして、categories テーブルや picks テーブルにデータが入っていることを確認します。

Cicindela からの出力

続いて Cicindela からレコメンデーション・データを取り出してみます。必要なのは「特定アイテムに対するレコメンデーション(関連アイテム)取得 (=item to item)」なので、次のような get_for_item メソッドを Cicindela オブジェクトに追加します。

var Cicindela = {
    /* ...略... */
 
    get_for_item: function(item, category, cb) {
        var url = this.base +'recommend?set='+ this.set
            +'&op=for_item&item_id='+ item +"&category_id="+ category;
        this._send_req(url, cb);
    }
};

そして「get」ボタンが押されたときにこのメソッドを呼び出します。第3引数として渡すコールバック関数は、正常にレスポンスが受信できたときに受信データを引数として呼び出されます。Cicindela からのレスポンスは改行区切りの文字列データです。ここでは単に出力用の要素に html メソッドで書き込んでおきます。

  // $(document).ready において
  $('#get').click(
      function(){
          var item_id = $('#items').val();
          $.each(data, function(category_name, category_data) {
                     Cicindela.get_for_item(item_id,
                                            category_data.id,
                                            function(data) {
                                                $("#output-"+ category_name).html(data)
                                            });
                 });
      });

以上で一通りの機能が完成しました。ユーザ名と選択アイテムを変えて何度か入力を行い、レコメンデーションを取得してみましょう。その際には自分なりに”シナリオ”を決めて、そのシナリオに沿った結果になるか試してみると面白いです。例えば:「Perl好きはEmacsやVimを使う」、「WindowsユーザにもMacユーザにもLinuxユーザにもJava好きはいる」、「EmacsとVimを両方ともよく使うユーザは少ない」などなど。

完全なソースを gist に置いておきます。