Chainer に型ヒントをつける
この記事は Chainer/CuPy Advent Calendar 2018 の六日目です。前回は @marshi さんの「Chainerで確率分布が扱えるようになりました。chainer.distributionsの紹介」でした。
最近、Chainer に型ヒント (type hint) をつけるために色々と試行錯誤をしている。これによって、Chainer を使ったソフトウェアの型検査ができるようにしたり、PyCharm などの IDE でのオートコンプリートが機能するようにしたいと考えている。ここでは、既存の Python ライブラリに型を付けるにあたってのノウハウ的な話を共有したい。
型ヒントとは?
言うまでもなく Python は動的型付け (dynamic typing) なので、近年まで静的な型検査 (static type checking) には対応していなかった。そのため、例えば、ある関数に渡した変数が正しい型であるかは実行してみるまで分からない。また、コード中に出てきた変数の型が分からないので、IDE などでオートコンプリートがうまく働かないという問題があった。
そこで、Python 3.5 では型ヒントと呼ばれる機能が導入された。具体的には、関数アノテーションという記法を使って以下のように書ける:
>>> import typing >>> def f(a: int, b: int) -> int: ... return a + b ... >>> typing.get_type_hints(f) {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
Python インタプリタは、この情報を __annotations__
属性に格納する以外には何もしない。つまり、この機能は実行時に型検査を行うためのものではない。実際に型検査を行うには、mypy などのサードパーティの静的型検査器を使う。型検査器は、与えられた Python コードを検査して型の整合性を確認する。
あるいは、関数アノテーションではなくコメントで書いてもよい(型コメント)。型検査器は # type:
で始まるコメントを認識して、関連付けられた式の型を判断する。Python インタプリタ自体は型コメントを認識しないので、上記のように get_type_hints()
などで実行時に型情報を取得することはできないが、関数アノテーション記法のない Python 2 系でも動作するコードになるというメリットがある。Chainer は Python 2 もサポートしているので、今回はこちらの記法を使うことになる。
>>> def f(a, b): ... # type: (int, int) -> int ... return a + b >>> typing.get_type_hints(f) {}
ユーザは、組み込み型以外にも自前で型を定義できる。単純なエイリアスだけでなく、ジェネリクスのような高度な型にも対応している。型ヒントに使う型の実体は単なる type
クラスのインスタンスなので、新しい型を定義する際はコード中に直接書けばよい。
from typing import Generic, TypeVar UserName = str # alias T = TypeVar('T') class Node(Generic[T]): # generic type ...
面白いのは、これ自体は普通の Python コードである点。例えば、角カッコ ([]
) は配列アクセスにも使われる合法的な文法なので Python 3.5 以前でも問題なく実行できる。定義した型は、通常の実行時にはインスタンス化されるだけで使われない。
その他の詳細については、型ヒントの仕様である PEP 484(日本語訳)を参照してほしい。仕様とは言っても分かりやすく書かれているので、静的型付き言語のユーザならば馴染みのある話が多いし簡単に読めると思う。
mypy を既存のプロジェクトに組み込む
mypy は単体でもコマンドとして実行できるが、テストや静的コード解析の一種だということを考えれば、pytest や flake8 のような他のツールと同様に CI を回したい。既存のコードベースへの組み込む際の設定方法については、このドキュメントが参考になった。
注意点としては、mypy は Python 3.4 以降でないとインストールすらできないという点。Chainer のように Python 2.7 を含む各バージョンのテストを回しているライブラリでは、下記のようにチェックしてやる必要がある。
if python -c "import sys; assert sys.version_info >= (3, 4)"; then pip install mypy mypy chainer fi
直和型 (Union Type)
既存の動的型付き言語のライブラリに型ヒントをつける際の課題として、ある関数の引数に渡される値が様々な型を取りうる、という点が挙げられる。例えば、以下の関数 double
は、数字を渡しても文字列 (str
) を渡しても内部的に int
に変換して二倍にする。
>>> double(2) 4 >>> double("2") 4
@overload
アノテーションで型ごとにシグネチャを宣言してもよいが、これだと複雑な組み合わせへの対応が大変になる。このようなケースを扱うため、typing
モジュールは Union
という特別な型を提供している。型検査器は、Union
に含まれていればどの型が渡されても正しい式として扱う。
>>> from typing import Union >>> def double(a: Union[int, str]) -> int: ... if isinstance(a, int): ... return a * 2 ... else: ... return int(a) * 2 ... >>> typing.get_type_hints(double) {'a': typing.Union[int, str], 'return': <class 'int'>}
実際の例としては、Chainer では ndarray
を受け取る関数が多数存在するが、実際には numpy と cupy の両方の場合がありえる。また、今回 ChainerX の導入によって chainerx.ndarray
も仲間に加わった。Union を使うことで、このような型もエンコードできる。
NdArray = Union[numpy.ndarray, cuda.ndarray, chainerx.ndarray]
また、同様に Union[..., None]
の簡易表現として Optional[...]
が使える。これは、以下のように None
が渡されることを許容する関数のシグネチャを書く時に使う。Optional といっても(他の言語のような)モナドではないのでそういう使い方はできないが、Optional
でない変数に None
を渡すとエラーを出してくれるので便利。
def run(config: Optional[Config]=None): if config is None: ...
ダックタイピングにプロトコルで立ち向かう
Chainer で配列の初期化処理を実装している Initializer
は function
クラスではないが、__call__
メソッドを実装しているので、あたかも関数であるかのように呼び出すことができる。また、Chainer では initializer を引数に取る関数に、ndarray
を引数に取る普通の関数を渡しても動作する。なぜなら、内部的にはダックタイピングで単に initializer(array)
(= initializer.__call__(array)
) を呼んでいるだけだからだ。
この挙動は通常は便利なのだが、今回のように型をつけようとすると問題になる。なぜなら、Initializer
は関数ではないので generate_array(initializer: Callable[[NdArray], None], ...)
とすると型が合わないからだ。
この問題は Protocol で解決できる。これは type hint とは別の仕様 (PEP 544) で導入が検討されているもので、まだ draft の段階で正式な仕様ではないが mypy などでは既に使うことができる。
プロトコルは、簡単に言うと「同じ変数やメソッドを持つクラス同士は同じ型として扱う」というものだ。いわゆる構造的部分型で、「静的なダックタイピング」という説明をされることが多い。例えば、Initializer
のプロトコルを定義するとこのようになる:
class AbstractInitializer(Protocol): dtype = None # type: Optional[DTypeSpec] def __call__(self, array: NdArray) -> None: ... def generate_array(initializer: AbstractInitializer, ...): ...
このように定義すれば、generate_array
は Initializer
と function
の両方を受け取ることができる。また、これは実行時には単なるアノテーションなので、これらをラップする共通クラスを作る場合と違って性能上のオーバヘッドもない。
循環参照の解決
若干ハマった点として循環参照の問題がある。例えば、このような chainer.backend.Device
型を含む型を chainer.types
モジュールに定義する。
from typing import Tuple, Union # NOQA from chainer import backend # NOQA DeviceSpec = Union[backend.Device, str, Tuple[str, int], ...]
そして、定義した型を使って backend
モジュールの関数に型ヒントを書く。
from chainer import types # NOQA from chainer._backend import Device # NOQA def get_device(device_spec): # type: (types.DeviceSpec) -> Device ...
ところが、こうすると import 時に循環参照を起こしてエラーになってしまう。
>>> from chainer import backend Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/home/okapies/chainer/chainer/backend.py", line 1, in <module> from chainer import types # NOQA File "/home/okapies/chainer/chainer/types.py", line 4, in <module> DeviceSpec = Union[backend.Device, str, Tuple[str, int]] AttributeError: module 'chainer.backend' has no attribute 'Device'
これを防ぐため、types
モジュールに以下のようなおまじないを追加して、型検査器が Python コードを読み込んだ時のみ chainer.backend
が import されるようにする。
try: from typing import TYPE_CHECKING # NOQA except ImportError: # typing.TYPE_CHECKING doesn't exist before Python 3.5.2 TYPE_CHECKING = False # import chainer modules only for type checkers to avoid circular import if TYPE_CHECKING: from chainer import backend # NOQA DeviceSpec = Union['backend.Device', str, Tuple[str, int], ...]
TYPE_CHECKING
は Python 3.5.2 から追加された変数で、Python インタプリタでは常に False
として扱われる。一方、mypy などの型検査器はこれを True
として扱うので、型チェックをする場合のみ import
文が動くようにできる。さらに、DeviceSpec
に定義する型を文字列として記述することで、実行時に import されていないシンボルを読み込むことによる文法エラーを回避する(型検査器は文字列を type
クラスに変換してくれる)。
まとめ
というわけでボチボチと手を動かしていが、歴史のあるプロダクトなだけに API 本数も多く、まだまだ先は長そう。気長にやっていこうと思うので、応援して頂けると幸い。
tzdata の 1887 年以前の日本標準時子午線が間違っている話
今朝、こういう話を見かけた。
tzdataの間違い見つけたかも。
— 投機的実行《アクセラレーション・ブースト》 (@yuba) 2018年10月11日
1887年以前の日本時間を +09:18:59、東経にして139度44分40.9秒としていて日本の歴史上こんな位置を基準にしていたことはないんだけど、この地点は東京天文台だったんだ。麻布台。
1887年まで基準だったのは東京天「守」台。皇居。+09:19:01 が正しい。
tzdata (tz database) というのは、世界各地域の標準時(タイムゾーン)の情報を収録したデータファイルで、様々な OS やライブラリで利用されている。例えば、Unix 系 OS で環境変数 TZ
に適当な地域を設定すると date
コマンドが適切な時刻を表示してくれるのは、この tzdata を使っているおかげ。
$ TZ=UTC date 2018年 10月 12日 金曜日 03:00:00 JST $ TZ=Asia/Tokyo date 2018年 10月 12日 金曜日 12:00:00 JST
この問題は以前から指摘されていたようで、例えばきしださんの 2015 年の記事など。この記事でも書かれているように、tzdata は Java API でも使われているので当然影響を受ける。
そこで思い出したんだけど、この天守台、正しくは旧江戸城天守台に、僕はつい最近行ってきたばかりだった。
奇しくもオリンピックに伴うサマータイム導入(つまり、標準時をいじるという話である)に反対するシンポジウムを聴講した9月2日の午後、「俺、大手町サラリーマンのくせに目の前の皇居に行ったことがないなー」ということに気づき、会場の永田町から国会議事堂を経由してテクテクと皇居東御苑へと通じる大手門へと向かったのだった。
わりと外国人観光客に人気のスポットらしく、また都会のオアシスとしてもなかなか良いロケーションだった。あとポケストップが大量にある(重要。これで、入り口の荷物検査とか諸々の面倒がなければ気軽に散歩に来れるんだけどなぁ(無理。
ところで、この天守「台」というのはちょっと奇妙なネーミングだと思わないだろうか。天守「閣」ではないのだ。普通に考えると「将軍家の居城だった旧江戸城にはさぞ立派な天守閣があったのでは?」となるだろうが、実は江戸城に天守閣が存在したのは江戸時代の初期まで。何があったかというと、
火事で天守閣が消失したので再建のために高台を築いたものの、「よく考えたらやっぱ天守いらなくね?」ってなって結局二百年近く更地のまま放置された天守閣予定地からの眺めです。 pic.twitter.com/sirxEZCAyo
— Yuta Okamoto (@okapies) 2018年9月2日
下からの眺め。 pic.twitter.com/9bmj3lArJZ
— Yuta Okamoto (@okapies) 2018年9月2日
まぁ、ツイートでは茶化して書いているけど、いちおう以下のようなまともな理由があるようだ。この保科雅之は江戸の町の防災性の向上に尽力した人物で、火除け用地として上野広小路を作ったのも彼なんだそうだ。
この寛永の天守は、明暦3年(1657)の火災で焼け落ち、翌年に加賀藩前田家の普請により高さ18mの花崗岩でできた天守台が築かれます。これが現在残る天守台ですが、四代将軍家綱の叔父である保科正之の戦国の世の象徴である天守閣は時代遅れであり、城下の復興を優先すべきであるとの提言により、以後天守閣は再建されることはありませんでした。現在、東西約41m、南北約45m、高さ11mの石積みが残っています。
その後、明治に入ってから、内務省地理局測量課がこの天守台を測量の原点と定めて三角点などが置かれたらしい。
現在はホテルオークラとなっている旧溜池葵町に置かれていた内務省地理局は、1882(明治15)年に江戸城本丸に移転した。天文台は天守台(天守閣の土台)に設けられ、この新しい天文台を経緯度の零点とした。江戸城天守台は築後200年で、堅牢・安定な地盤が観測の適地と見込まれたのである。
しかし、「東京天守台を初度とする」というお触れが出た直後、国際子午線会議の結果を受けて「東経135度を子午線とする」という勅令が出て、明石を通る現在の子午線 (GMT+9) に切り替わってしまった。だから、公式に +09:19:01
だった期間はかなり短い。この辺の細かい経緯については国立天文台の Wiki に詳しい。
というわけで、イギリス旅行の際にわざわざグリニッジ子午線を踏みに行く程度には子午線マニア(?)を自認していたわりには、足下のこういう歴史を見過ごしていたとは不覚だったなぁ、というお話でした。
ソフト屋としてさんざん痛い目を見てきた GMT の征服が完了したことをご報告します。 (@ グリニッジ天文台 - @rogastronomers) https://t.co/amozn2IurR pic.twitter.com/UY0P6R7pA3
— Yuta Okamoto (@okapies) 2017年8月19日
なお、本題の tzdata の方ですが、修正には何かしらエビデンスがあった方がいいようなので、何かご存知の方がいらっしゃったら以下のスレッドにコメントして頂けると良いかと思います。
そしたらissueでエビデンス置くだけでも良さげですね。
— 投機的実行《アクセラレーション・ブースト》 (@yuba) 2018年10月12日
そのエビデンスを集めるのが骨ですけど。国立天文台のwikiはエビデンスにならないだろうし…
自作キーボードとクセをすり合わせる話
この記事は自作キーボード Advent Calendar 2017の4日目です。前日は、@mt_mukko さんの「Nyquistを組み立てた話。Pro Microもげもあるよ!」でした(この記事は ThinkPad T460s で書いてます)。
Let's Split を組み立てた
@matsPod さんの Group Buy に参加して Let's Split のパーツを購入したものの、転職やら何やらでバタバタしていてようやく組み上げたのが 10 月…。
皆に遅れること数ヶ月、ようやく僕の #レツプリ が完成したでござる。RGB どうしようかと思ったけど、やっぱつけると綺麗ね。 pic.twitter.com/uGCEM2Zh1A
— Yuta Okamoto (@okapies) 2017年10月21日
#レツプリ 組み立て中動画(ブログ用。 pic.twitter.com/KhtjeKGMBY
— Yuta Okamoto (@okapies) 2017年12月4日
現在は職場で活躍中。机の上を広く使えるのがいい感じ。
仕事で #レツプリ を使い始めました。まだ色々と調整中。 pic.twitter.com/kaLvSGRPAM
— Yuta Okamoto (@okapies) 2017年10月30日
キー配列の調整
さて、上のツイートに謎のポストイットが写っているが、これは使っている最中に気付いた点をメモっているものだ。これを元にしてキー配列をどう変更するか考えたりしている。言うまでもなく、自作キーボードの魅力の一つは自分の好みに合わせてキー配列を自由に設定できること。
現在は、以下のような配列にしている。記号の位置についてはまだまだ調整を続けているし、Lower/Raise と無変換/変換のデュアルキー化がうまく設定できてないなど課題が多いものの、ひとまず日常業務で支障のない程度には仕上がった。
最初のツイートと見比べてもらうと分かると思うが、初期配列からそこまで大きな変更はしていない。QWERTY 配列そのものとか、Ctrl や Alt などの修飾キーの位置は、昔から愛用している HHKB に合わせるようにしている。
最も頻繁に変更しているのは、Lower/Raise と組み合わせる記号キーだ。Let's Split が 40% キーボードである以上、これらのキーをどこかに押し込む必要があるが、どこでもいいというわけではない。自分がプログラミングや日本語入力をする上で頻出するキータイプの順番というものがあるので、その流れに沿ってタイプしやすい配置を探す必要がある。
面白いのは、単純に頻出するキー同士を近くに置けば良いというものではないらしい、ということだ。経験的には、右手と左手を交互に動かしたり、手首の回転を使うような動きは安定感が高い。
こうして、キーを通常とは別の場所に設定していると「あのキーはどこに置いたっけ」となることが、まぁ…稀によくある(笑。この点については、上の写真を見てもらうと分かるように、キーキャップをアルファベットではなく Lower/Raise レイヤーの記号に対応したものにして解決している。一見すると奇妙に感じると思うが、どの道、アルファベットはブラインドタッチしているのであまり問題がないという寸法。
他にも、Lower/Raise は他のキーとは別形状のキーキャップを持ってきて角度を付けることで、親指で探りやすいようにしている。
キーボードと自分のクセをすり合わせる
上の写真を見て、わりと保守的なキー配列だなと思った人も多いと思う。これは Let's Split を使っているからといって他のキーボードを使う機会が無くなるわけではない、という単純な理由による。僕がキーボードを自作しているのは、身体への負担を減らして効率的に作業をするためなので、一般的でないキー配列を採用して効率を下げては元も子もないし。
一方で、多少のキー配置の変更は慣れで何とかなる部分も大きい。僕の場合、記号の位置は一週間か二週間もすれば体が覚えてしまうし、そこで他のキーボードに一時的にスイッチしても問題なく扱える。
自作キーボードに対するよくある懸念として、標準的でないキー配列ではまともにタイプできないのではないか、というものがある。僕は、自分の経験を通じて人間の適応性は自分自身で思っている以上に高いのでそこまで心配する必要はない、と考えるようになった。キー配置の変更、というのは市販キーボードでも論争のタネになりがちなテーマだが、この点を過大に見積もって新しい可能性に手を出さないのは人生の損失…というのは言い過ぎだろうか。
このように、自作キーボードには「自分のクセをキーボードに合わせる」方向と「キーボードを自分のクセ(習慣)に合わせる」方向の、二つの方向性があるように思う。
これは、どちらかが正しいという話ではない。二つの相反する要求のバランスを取っていく中で、自分が無意識のうちに従っていた習慣を一つ一つ発見し、何を守りどこで殻を破っていくか考えていくのが、自作キーボードという趣味の一つの醍醐味だろう。
「先生のレポートは形式的な縛りが多すぎる。自分はもっと自由に書きたい」という学生からの批判が。ひとまず「形式は、君が既に縛られているものから、君を別様に縛り直す過程で、君を相対的に自由にする道具です。縛りが無ければ自由と考える君は、死ぬまで偶然と慣習の奴隷でしょう」と答えました。
— 久保田裕之(家族社会学) (@hkubota1016) 2017年5月1日
これは、万人にとっての〈理想のキー配列〉は存在しない、ということでもある。キーボードと自分のクセをすり合わせていく際に、使ってみて課題を発見して修正する、というプロセスを回していくと、個々のキー配列はその時点での自分に合ったものでしかない、ということになるからだ。
このことは、自作キーボードの普及を考える上で悩ましいポイントではある。キー配列を編集する GUI エディタのようなものはすでにあるが、日本語で使うキーに十分に対応していないものも多い。その辺はコミュニティで解決していくべき課題なのかもしれない。
Akka HTTP クライアントを使う
この記事は Scala Advent Calendar 2017 の三日目です。前回は @poad1010 さんの「JupyterでScala」でした。
Akka HTTP を使って REST API を叩いてみようと思って色々と試したメモ。基本的には下記のドキュメントを読めば良いのだけど、サーバ側はともかくクライアント側の使い方について日本語で書かれたものが少ないので、使う側の目線での話を書き残しておくのも良いでしょうという感じ。ここでは、最も簡単な API である Request-Level Client-Side API について話をする。
少し難しい話になるが、簡単に使いたい場合は、記事の最後にラッパーコードを貼っておくのでコピペして使ってもらうと良いかと思う。
HTTP ボディを取得する
ドキュメントを見ると、初っ端からこういう感じで脅されるので嫌な予感がすると思うが、その予感は的中する。
「ストリームが一級市民」ではない HTTP クライアントに慣れている人は、最初に Akka HTTP クライアントの裏側にあるフルスタックなストリーミングの概念について説明した「リクエストとレスポンスのエンティティがストリームの性質を持つとはどういう事か」の章を読むことをお勧めします。
とは言っても、レスポンスを受け取るところまでは特に難しいところはなく、単に Http().singleRequest()
に HttpRequest
を渡すだけだ(以下では、スコープに implicit な ActorSystem
, Materializer
, ExecutionContext
が入っていることを前提に話を進める):
implicit val system: ActorSystem = ActorSystem() implicit val materializer: ActorMaterializer = ActorMaterializer() implicit val executionContext: ExecutionContext = ... val request = HttpRequest(uri = Uri("http://example.com/api/v1/users")) val response: Future[HttpResponse] = Http().singleRequest(request)
ややこしいのはレスポンスボディをメモリに読み込むところで、HttpResponse
を見てもそれらしいメソッドが見当たらない。結論から言うと、以下のようにする必要がある。
val body: Future[String] = response.flatMap(_.entity.dataBytes.runFold(ByteString.empty)(_ ++ _).map(_.utf8String))
なぜこういう書き方をする必要があるかというと、HttpEntity
から取得できる dataBytes
は Akka Streams のストリーム (Source[ByteString, Any]
) だから。
Akka HTTP はストリームベースのライブラリなので、サーバから到着したレスポンスを全てメモリに貯めこんでからユーザに引き渡すのではなく、バイト列が到着するたびにイベントを発火して処理する。これにより、例えば巨大なデータファイルや終端がない (unbounded) サーバログのようなようなレスポンスを効率的に扱うことができ、細かい制御(チャンクの区切り方とか)も容易になる。
一方で、従来のように文字列として全体を一回でアクセスできるようにするには、
- 受信した複数のバイト列を全て足しあわせて一つのバイト列に畳み込む (
.runFold(ByteString.empty)(_ ++ _)
) - バイト列を文字列に変換する (
.map(_.utf8String)
)
という処理を明示的に記述する必要がある(ちなみに、1 と 2 を逆にすると多分文字化けが起きる。理由は考えてみよう)。分かってしまえば大した話ではないが、カジュアルな API を期待していると面食らうのは確かだ。
もう一つ注意点があり、Akka HTTP は送信側のデータ送信量を TCP レベルでスロットリング (TCP back-pressure) しているので、あるレスポンスのエンティティを消費せずに途中で放り出してしまうと、その TCP 接続の送信が詰まってしまい、その接続を利用している他のリクエストの処理に影響が出る。したがって、エラーの場合でも必ず response.discardEntityBytes()
を呼び出す必要がある(将来的には自動検出できるようにしたいらしい)。
なお、全てのレスポンスをメモリに読み込む方法はもう一つあり、以下のように toStrict(timeout)
を呼ぶと Future[HttpEntity.Strict}
を取得できる。Strict
を使うと受信したバイト列を集約した data
にアクセスできるので、以下のように書ける:
import scala.concurrent.duration._ val timeout: FiniteDuration = 10.seconds val body: Future[String] = response.flatMap(_.entity.toStrict(timeout).map(_.data.utf8String))
toStrict
はタイムアウトを指定する必要があるが、レスポンスを待つ時間を明示的に指定するならこちらを使うべきだろう(runFold
を使う場合は idle timeout が成立するまで待つ)。
また、タイムアウトは FiniteDuration
という型名が示すとおり「無限」は指定できない。「タイムアウトが無限」はナンセンスである、というライブラリ作者たちの意思表示なので、いつまでも待ち続けたいお気持ちを表明する場合は適当に 100 万秒とかを指定すると良い。
JSON を変換 (unmarshal) する
メモリに読み込んだ JSON 文字列をアプリケーション内部の形式に変換するには、Akka HTTP が提供する Unmarshal(...).to[A]
を使う。ただ、Unmarshal
自体は単なるラッパーで、具体的な処理は他の JSON パーサーに委譲する仕組みになっている。なので、好きなライブラリを直接使っても構わない。
標準では spary-json のラッパーが提供されている。使うには、ライブラリの依存関係に akka-http-spray-json を追加する。
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.11"
例として、JSON を User
型にマッピング (jsonFromat2()
) して Unmarshal
してみる。akka-http-spray-json を使うには SprayJsonSupport
の配下の implicit
をスコープに入れる。こんな感じだろうか:
import akka.http.scaladsl.unmarshalling._ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import spray.json._ case class User(id: Long, name: String) trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { implicit val UserFormat = jsonFormat2(User) } class FooClient() extends JsonSupport { val res = Http().singleRequest(HttpRequest(uri = Uri("http://example.com/api/v1/users/1"))) res.entity.dataBytes .runFold(ByteString.empty)(_ ++ _) .flatMap(bs => Unmarshal(bs.utf8String).to[User]) }
spray-json ではなく、他の Jackson や circe などのライブラリを使いたい場合は、それぞれに対して Unmarshaller
を実装した akka-http-json というサードパーティのライブラリの開発が進んでいる。
ラッパーを作る
以上のノウハウをまとめて、こういう感じのラッパーを作っておくと便利なのではないかと思う。言うまでもなく、このコードで 10 GB のデータファイルとかを受け取るとヒープメモリが爆発四散するので、その場合はストリームの作法に従って書こう。
def doRequest[A](req: HttpRequest, unmarshal: String => Future[A]): Future[A] = Http().singleRequest(req).transformWith { case Success(res) if res.status == StatusCodes.OK => res.entity.dataBytes .runFold(ByteString.empty)(_ ++ _) .flatMap(bs => unmarshal(bs.utf8String)) case Success(res) => res.discardEntityBytes() Future.failed(new Exception(s"HttpRequest failed: $res")) case Failure(t) => Future.failed(t) }
追記: Unmarshal
に HttpEntity
を直接与えることで、より短く書くことができる:
def doRequest[A](request: HttpRequest) (implicit unmarshaller: FromEntityUnmarshaller[A]): Future[A] = Http().singleRequest(request).transformWith { case Success(res) if res.status == StatusCodes.OK => Unmarshal(res.entity).to[A] case Success(res) => res.discardEntityBytes() Future.failed(new Exception(s"HttpRequest failed: $res")) case Failure(t) => Future.failed(t) }
これは、FromEntityUnmarshaller[A]
の実装がバイト列の集約を実装している Unmarshaller.byteStringUnmarshaller
を呼び出すため(実装を見ると runFold(...)
による畳み込みをしているのが分かる)。
まとめ
Akka HTTP はドキュメントが充実しており、ライブラリの思想や使う上で必要なことは概ね書かれている。また、機能的、非機能的な制約が型として埋め込まれており、自分がどんなコードを書こうとしているのか、常にプログラマに意識させるような作りになっている。
ただ、それはごまかしが効きにくいということでもある。「サーバからデータをガサッと持ってきてババッて変換してさー」的なやり方は技術的にはいくつも穴があるが、ちょっとした作業では不問に付したい場合も多い。なので、Akka HTTP は日々の作業をアドホックにこなしていくツールとしては使いにくい点が多い。ただ、堅牢で高性能なアプリケーションを書く上では強力な道具になりうると感じた。
最近、SoftwareMill が開発する sttp が 1.0 になった。これは、akka-http や async-http-client などをラップしてシンプルな API を提供することを目的としている。こうしたライブラリを、作業に求められている品質に応じて使い分けていくのが良いのではないかと思う。
参考文献
- Consuming HTTP-based Services (Client-Side) • Akka HTTP
- Request-Level Client-Side API • Akka HTTP
- Akka HTTP Timeouts • Akka HTTP
- Akka HTTP, docs: Explain toStrict in more detail · Issue #206 · akka/akka-http · GitHub
- akka stream - Why HttpEntity.toStrict has no variant without a timeout? - Stack Overflow
- JSON handling in akka-http
ネットワークは「切断」しない
Disconnect という英単語を「切断」と訳すのは(特に IT エンジニアリングの文脈では)誤解の余地が大きいので良くないのではないか、という話。
Connect は「結線」しない
Connect という英単語は、一般的に「繋ぐ」とか「接続する」と訳されることが多いと思う。connect は、元々「互いに結びつける」という意味のラテン語から来ていて、初期には「物理的に一つにする」という意味で使われていたようだ。しかし今日では:
connect
- Bring together or into contact so that a real or notional link is established.
- Join together so as to provide access and communication.
- Link to a power or water supply.
- Put (someone) into contact by telephone.
- [no object] (of a train, bus, aircraft, etc.) be timed to arrive at its destination just before another train, bus, etc., departs so that passengers can transfer.
- Associate or relate (something) in some respect.
- Provide or have a link or relationship with.
- [no object] Form a relationship or feel an affinity.
connect - definition of connect in English | Oxford Dictionaries
上記のように、物理的な接続だけではなく、観念的な結びつき (link) とか関わり合い (relationship) を表すのにも使われている。
日本語で「接続する」と書くと、何となく媒体(メディア)を介して二つのモノを物理的に結線する様子を思い浮かべがちだ。しかし、どちらかというと「二者の間に関係性を作る」というニュアンスで理解するのが正しいだろう。
接続は「切断」されない
例えば、「列車の接続がいい」という表現が使われるように、connect を「接続」と訳すのは間違いではない。また、その対義語である disconnect は一般的に「切断」と訳されるが、その意味は英語でも “break the connection”、つまり connection を壊すとか断つとかいう意味になる。
だが、繋がり (connection) を断ち切るとはどういうことだろうか?
そもそも「繋がり」とは何か。ここでは、互いの存在を認識した上で、媒体を介して情報(やエネルギーや社会的な何か)を交換すること、と定義してみよう。既に見たように connection が指す「繋がり」には、物理的なものだけでなく観念的なものも含まれる。注意が必要なのは、disconnect が断つのは「繋がり」であって「媒体」ではないということだ。
あらゆる繋がりは媒体を介した交換行為を通じて実現される。そして、媒体が断たれれば繋がりも断たれることが多いので、我々は両者を同一視しがちだ。しかし、逆は真ではない。つまり、媒体が断たれなくても繋がりが断たれることはある。この世から公衆電話網が無くならなくても、友達同士は絶交できる。
disconnect を「切断」と表現すると、状態がゼロイチでキレイに切り替わる様を想像しがちだが、しかしコトはそう単純ではない。なぜなら、媒体の接続性はそれ自身で完結する問題であるのに対し、繋がりは二者の関係性、つまりお互いが相手のことをどう認識しているかの問題だからだ。
例えば、媒体に(一時的に)障害が起きると二者は一切情報を交換できなくなる。あるいは、情報の一部だけがたまたま失われて届かないような場合もあるだろう。しかし、二者の繋がりは障害が起きた途端に断たれるわけではない。お互いが諦めない限り、媒体の障害が回復した時点で再び交換が行えるようになるからだ。それでも繋がりが断たれるとしたら、例えば、以下のようなことがあった場合だろう:
- どちらかが、相手との交換の意思を捨てる
- 相手に通知して辞める場合も、そうでない場合もあるだろう
- 媒体の障害が原因の場合、一定期間待っても復活しない時点で「媒体はもう二度と復活しないに違いない」と予測して諦めたのだろう(=タイムアウト)
- どちらかが、媒体の先にいる相手が自分との交換の意思を持っていると信じられなくなる
黒やぎさんは、白やぎさんから届いた手紙を食べてしまって、しかも返事を書くのを忘れているだけなのかもしれない。ここで白やぎさんが「黒やぎさんに手紙は届かない」とか「黒やぎさんに無視された」と判断すれば、両者の関係は disconnect される。しかし、それは「白やぎがそう判断した」というだけだ。この時、媒体に物理的な変化は何も起きていない。
特に、「相手が自分との関係をどう思っているか」を知るのは難しい。それを知るためにすら、媒体を介して相手にコミュニケーションを試みるよりないからだ。コミュニケーションを取らなければ家族や恋人が何を考えているのか分からず、相手に示す適切な愛情表現を選べないのと同じように。
ネットワークは「切断」しない
ここまでの話を踏まえると、特に TCP/IP のようなパケット通信において「ネットワークが切断」という表現は、何となく不自然なものに見えてくる:
try { final URL url = new URL("http://example.com"); final InputStream in = url.openConnection().getInputStream(); System.out.println(in.read()); } catch (IOException e) { System.err.println("ネットワークが切断されました!"); // (1) }
ここでの IOException
は、多くの場合、おそらくはネットワークの経路上で輻輳が発生したために、HTTP プロトコルに基づく接続 (url.openConnection()
) や読み込み (in.read()
) の通信が正常に完了しなかったというだけだ。ネットワークという「媒体」そのものが切断されたわけではない。
もちろん、日常の業務では様々な障害発生ケースを念頭に置いた上で、その簡便な表現として「切断」という単語が使われている。しかし、我々はふとすると、ネットワーク経路がゼロイチで開通したり不通になったりすると考えたり、そもそも媒体が完全に正常でも通信の失敗が起きうる(片側のプロセスが応答しなくなるとか)ということを忘れたりしがちだ。
何をもって「通信障害が起きている」と判断すべきかは難しい。例えば、上記で (1)
のパスを通った時点で http://example.com
への経路が死んでいると判断…するのは難しい。その時、たまたまパケット損失が起きただけなのかもしれないからだ。
通信障害が発生しているか否かは、実務的には統計量に基づいて判断するしかない。従って、上記のような「一回ごとの通信」について記述しているコードでそれに対処することはできないだろう。それを忘れると、一度でも IO 例外が起きたらその経路は不通と判断する、みたいなコードを書いてしまうことになる。
まとめ
connect/disconnect とは二者の間に関係を作ったり無くしたりすることであり、二者の関係性を取り持つ媒体はその本質ではない。そして「切断」という語は、二者間の繋がりの有無がゼロイチで判定できるかのような誤解を生み出しがちなので、あまりよろしくないのではないか。
まぁ、すごく細かいことにこだわっているような気もするのだが、一度自分の考えをまとめてみるのも良いかと思って書いてみた次第。
「裏のニコニコ超会議」に行ってみた
GW中の皆さん、休暇をいかがお過ごしだろうか? え、ずっと仕事? それはご愁傷様…。
さて、我々(オタク)にとってGW期間中のビッグイベントといえば、4/29~30 に幕張メッセで開催された「ニコニコ超会議」だろう。今年も 15 万人越を動員して大盛況だったようで、今年は僕も久々に顔を出して知り合いやマストドン本の関係者に挨拶して回ったりしていたけれど、相変わらずのカオスな活気だった。
しかし、その盛況の陰で「裏のニコニコ超会議」とも言える音楽フェスが開催されていたことをあなたはご存じだろうか? ずばり、その名を Electric Daisy Carnival Japan (EDC Japan) という…。
「裏」って?
まぁ、こういうことです:
そう、正真正銘、超会議の裏が会場なのだ。期間も全く同じなので、超会議でオタクや中高生がワイワイやってる隣で、パリピの皆さんがウェイウェイやっていたというわけ。
その結果としてこんな光景も見られた:
刺青入れたパリピのカップルの隣でサーバルちゃんコスの女の子が携帯いじってる。
— Yuta Okamoto (@okapies) 2017年4月30日
EDC って?
EDC は、EDM というこの十年くらい世界的に流行しているダンスミュージックをかける DJ イベント、いわゆる EDM フェスのうちでは最大級のイベントで、世界三大フェスの一つと呼ばれているらしい。規模も桁違いで、ウン十万人分のチケットが一瞬で売り切れることも珍しくない。これまで三大フェス (Ultra Music Festival, Tomorrowland, EDC) のうち、日本で開催されていたのは Ultra Japan だけだったんだけど、今回 EDC が初来日したのだ。
japan.electricdaisycarnival.com
元々、僕は BEMANI がきっかけで電子音楽を聞くようになって、自然と流行りのジャンルである EDM にも触手を伸ばすようになり、そうした楽曲の主なパフォーマンスの場である EDM フェスにも興味があった。まぁ、僕は基本的にお祭り騒ぎを見るのが好きなので。
とはいえ、正直なところ、海外のフェスに関しては治安が悪いしドラッグが蔓延しているという話も耳にするので、わざわざ海外に行ってまで参加するのは敷居が高いナァと感じていた。そんな時、今回の日本での開催を聞きつけ、また以前から聴きたかった Zedd も出演するということで、これはいい機会だと当日券を握りしめて駆け付けた次第。
会場
この手の EDM フェスの特徴としては、とにかく設備や美術にカネがかかっていることが挙げられる。大音量のスピーカーや巨大なディスプレイの上に「おとぎの国」のような世界観を構築して、来場客の非日常感を盛り上げるための様々な仕掛けが凝らされている。
また、これはフォトジェニック(写真写りの良さ)を重視しているということでもあるようだ。この辺は、口コミでの拡散を狙った、いかにもインスタ世代らしい施策と言える。以下の動画が今回のトレイラーだけど、雰囲気は掴めるかと。
システムとしては、「踊ってみた」しかない超会議だと思えばよい。つまり、入場した後は3つある野外ステージのどれを見に行ってもいいという仕組み。出演者としては海外の人気 DJ を多数取り揃える一方で、一日目には中田ヤスタカときゃりーぱみゅぱみゅも出演していたらしい。
会場内を回ると各所にこういう装飾が置いてあり、夜はライティングされて雰囲気を盛り上げる。あと、写真の真ん中に映っているように仮装してきた人たちが結構いて、あちらこちらを闊歩している。
各ステージの様子を紹介しよう。まず、第一ステージは千葉マリンスタジアムの中に構築されてて、巨大なフクロウの像が観客を睥睨する荘厳な作りになっている。一方、第二ステージは海岸線沿いの砂浜にあって、DJ ブースの後ろに巨大なディスプレイが組まれており、演奏と同時にこれを活かした演出が行われる。
普通のコンサートと違うのは、ステージ後方の VIP エリアを除いて指定座席とかはないので、出演者のプレイが始まるとみんなワーッと DJ ブースの前に集まってきてこういう騒ぎになる:
…うーん、俺、よくここから生きて帰ってこれたな。*1
そして、次第に辺りが暗くなってくると「光」による演出が本格化してくる。ここからが、出演者的にも演出的にもフェスの本番だと言っていいだろう。
ステージは爆音で包まれ、頭上を何条ものレーザーが飛び交い、何万人という観客がすし詰め状態で踊り狂い、歌詞を合唱する。そんな中で頭をカラッポにしてひたすら恍惚感に浸るというのは、普段はなかなかできない貴重な体験だったと思う。
確かに、混じりっけ無し純度100%のリア充の中に飛び込むことになるので、我々みたいなオタクにとってかなり参加を躊躇する類のイベントなんだけど、周りに合わせていれば大体問題ないし、コミュ力も必要ないのでそういう意味では楽だった。
観客
既に紹介したように、フェスには仮装してやって来ている人たちがけっこう多い。こういう人たちを特集したユーチューバーの動画があったので紹介…するんだけど、肌の露出が多めなので一応閲覧注意。
しかし、ハロウィンもそうだけど、こういう仮装文化っていつの間にか定着したよね。一体どういう経路で浸透したんだろう。あと、動画には海外から参戦してる人がけっこう出てくるけど、これは実際にそうで、パッと周りを見渡すと必ず外人さんが視界に収まるくらいはいた。周りもちゃんと交流してて、全体的に英語スキルが高めだったという印象。真のコミュ力とは言葉の壁を超えるものなのかもしれない。
驚いたのは、観客みんなが流行りの曲の歌詞を全部覚えているらしいということ。DJ は、基本的に自分の曲だけでなく他人の曲を含めてどんどんプレイしていくので、観客は次に誰の何という曲がかかるかは予想できないんだけど、みんなイントロクイズばりに即座に曲名を割り出して大合唱が始まる。隣の人に聞いたところ「クラブの定番曲のベスト盤とかを聞いていればそのうち覚える」と言われたが…。
このように、「パリピ」について我々オタクの側からは「何も考えていない連中」みたいな偏見が広くあると思うが、これはこれでとても熱心なファン文化なんだな、ということが感じ取れたのは収穫だった。
共通点と差異
そんなわけで、今回、一日のうちにこういう両極端のイベントに参加して思ったのは、意外な感想だと思うけど「あれ、けっこう似てるな?」ということだった。
表面的な違いは山ほどあるんだけど、いろいろな場面で既視感を感じることが多かった。これは多分に感覚的なものだけど、実は世間で言われているほど根本のロジックに大きな違いはなくて、表現の手段が違うだけじゃないか、というようなことを思ったのだった(例えばコスプレ好きな事とか)。
まぁ、ウェイもオタクもお互いに鼻で笑いあってるけど(実際そういう発言は今日も現地で聞いた)ベクトルが違うだけで似たもの同士なんじゃないかと思いますけどね。どっちもコスプレや光りモノのガジェット好きだし。 https://t.co/3rZRrit3AT
— Yuta Okamoto (@okapies) 2017年4月30日
一方で、両者の最も大きな違いはビジネスモデルだろう。超会議が、何十もの「趣味」の島を束ねたロングテールモデルであるのに対して、EDC は高々2~3の島に有名アーティストを据えて集客するモデルだ。その結果として、超会議は EDC の倍近く動員することができている。
動員数 | チケット代 | 予想売上 | |
---|---|---|---|
超会議 | 15万4,601人 | 2,000 円 | 約 3 億円 |
EDC | 8万4,000人 | 16,000 円 | 約 13.4 億円 |
ただ、売上を比べると別の面も見えてくる。元々、超会議は「儲ける」ことを主眼にしたイベントではないとされているが、ここ数年は動員数が伸び悩んでいるようだ。一方で、EDC は同じロケーションで(チケット代だけ考えても)4倍以上の売上を叩き出している。そろそろ転換期なのかな、という気もするのだがどうだろう。
越境
リア充文化とオタク文化の越境、という話として、Porter Robinson(ポーター・ロビンソン)というアメリカ人アーティストを紹介して締めくくりとしたい。彼は EDC に出ていたわけではないんだけど、最近も来日公演があって大変すばらしいパフォーマンスだった(同業者からも称賛の声が相次いでいるとか)。*2
彼は、一般的に EDM の文脈で紹介されるアーティストで、世界の人気 DJ ランキング “Top 100 DJs” にランクインする実力者*3。ただ、本人の音楽性は EDM の主流とは少し雰囲気の違うもので、EDM というジャンル自体が「客を踊らせる」ことを突き詰め過ぎて単純化し多様性を失っている、という声が出ている中、次世代の旗手として期待する声も出ている、とかそういう立ち位置らしい。
個人的には、彼の曲の透明感がありつつ、どこか寂寥感を帯びた曲調が好きで愛聴している。そもそも、僕も EDM の主流のガチャガチャした雰囲気は苦手で、どちらかというと彼や Madeon のような曲調が好みなので、僕の EDM に関する知識はわりと偏っているんだけど…。
ところで、彼は日本文化に非常に造詣が深いことで知られている。造詣が深いというか、はっきり言うとガチヲタである。
そもそも、彼が音楽制作を始めたのは、言わずと知れた日本の音楽ゲーム “Dance Dance Revolution” をプレイしたのがきっかけであり*4、他にも彼に日本の深夜アニメの話を振ると熱く語り始めるとか、度々来日してオタク系の DJ イベントに出没しているらしいとか、自分の DJ パフォーマンスで欧米人相手に唐突に アニソンを 流し始める とか、そういう逸話に事欠かない。ちなみに、最近は「Re:ゼロ」のエミリア推しらしい。
楽曲制作の方でも、わざわざ許可を取って好きな声優の声をサンプリング *5したり、ボーカロイドに歌わせたりと趣味に走ってチャレンジを重ねていたのだが、盟友 Madeon とのコラボ楽曲 “Shelter” で、ついに自分の MV のためにアニメ制作を手掛けるに至った。それがこれ:
見てもらうと分かるが、欧米人が作るアニメによくあるような「バタ臭さ」が一切ない。最大の理由は制作に日本のアニメスタジオ (A-1 Pictures) が関わっているからだが、しかし全体のストーリーやキャラクターイメージの多くは Porter 自身のアイデアなんだとか。アニオタ DJ の面目躍如といったところか。
これが世界でどの程度受け入れられたかというと、現在の YouTube の再生数が 1,700 万越えを達成しており、なかなか成功した数字と言えるだろう。まぁ、先ほど挙げた Top 100 DJs 一位の Martin Garrix(彼は EDC Japan 一日目の大トリを務めた)の MV が軽々と1~10億を叩き出す世界なので、それと比べてしまうと見劣りはするのだが…。
とはいえ、日本のオタク文化が海外のオタクにウケている、という話から一歩踏み込んで、世界的なメジャーシーンの側から日本のオタク文化への越境を達成しつつあるわけで、十分に注目に値する試みと言えるだろう。インタビューによれば、Porter は今後もオーディエンスの反応を見つつ、オタク文化の良さを世界へ発信していきたいと考えているようだ。
ただ、翻って日本のオタク文化から世界のメジャーシーンへの越境ということが可能だろうかと考えると、なかなか課題は多そうだ。上で紹介した Porter の楽曲や DJ パフォーマンスを見ると、自分が好きなものを素材として積極的に使っていく一方で、(当たり前だが)内輪ネタ臭は極力取り除かれていることが分かる。それはもちろん、彼の軸足がメジャーシーンにあるからだ。
これは昔から繰り返されてきた話題なのだと思うが、オタク文化は内輪ネタとは切っても切り離せない関係がある。だから、メジャーへの露出を増やすために内輪ネタを排除する、というのは本末転倒な可能性がある。しかし、国内市場が飽和しつつある(参考: コミケの動員数)中で現状の水準を維持する方向は、新陳代謝を失って緩やかな衰退へと繋がる可能性もある。今や、2020年に向けて、様々なことが岐路にあると言うべきなのだろう。
まとめ
今回、超会議と EDC という全く性質は異なるがどこか似た所のあるイベントが並んで開催されたのは、色々なことを象徴していて面白い経験だった。面倒なこともあった(オタクだとバレて絡まれかけたりとか…)が、文化の越境というのはいつも新鮮な驚きがある。皆さんにもぜひお勧めしたい。
JAPAN 🇯🇵!!!!
— Zedd (@Zedd) 2017年4月30日
I LOVE YOU!!!!!!! ♥️♥️♥️ pic.twitter.com/5JJ7RrWRc0
Mastodon の電脳考古学、あるいは不在の中心としての Twitter について
突然ですが、5月始めに出る「マストドン本」に寄稿させて頂くことになりました。後半の技術パートの導入として、Mastodon の技術的な基盤である OStatus の章を担当しています。
本書の執筆には、mstdn.jp のぬるかるさんや pawoo.net の中の人に加えて、界隈では誰でも名前を知ってるような面々が勢ぞろいで、まさに Mastodon オールスターといった趣であり、わし本当にこんな所に混ざっていいのか…。まぁ、ともかく現在予約受付中です、買ってね!
これがマストドンだ! 使い方からインスタンスの作り方まで (NextPublishing)
- 作者: マストドン研究会
- 出版社/メーカー: インプレスR&D
- 発売日: 2017/05/12
- メディア: オンデマンド (ペーパーバック)
- この商品を含むブログ (1件) を見る
思い返すになかなかの急展開ですが、最初、降って湧いた Mastodon ブームに第二次 P2P ブームの面影を見て手を出したくなり、じゃあ Akka Streams で OStatus プロトコルを実装してみようかなと、さっそく OStatus API の挙動を調べるために Pawoo を curl
でつつき回したり、gist にまとめ記事を書いたりしていたところ、それを見ていた知人の紹介でインプレスさんから依頼を頂いたという次第です。
それが先週の中頃で、一昨日には校了したのでたった一週間で作られたことになります。当然、リアルで紙をやり取りしていては間に合わないので、原稿は Google Drive でやり取りし、著者同士の連絡調整は Facebook Messenger、書き上がったらその場で組版システムに突っ込んで出力結果を見ながら校正、という具合。出版に関わったのは初めてなんですが、最近はこんなスピード感なんですねぇ、いやはや。
担当編集者さん、さすがにこういう進行だと本当に大変そうで、時間が限られてる中で著者10人を相手に原稿集めて組版して校正してと大車輪でヒイヒイ言っておられました。おつかれさまです…。
Mastodon の電脳考古学
ところで、手元に作業時間と紙幅の関係で原稿に入れられなかった謎の図があるのですが、もったいないのでここに置いておきます。
これは、記事を書くにあたって OStatus の背景にある技術思想の歴史を押さえておいた方が良かろうと思い、関連するプロダクトや仕様書の日付を整理してみたものです。まぁ、これだけでは何のこっちゃという感じだと思うので詳しくは書籍をお買い求めください(宣伝)。
この辺の歴史は掘ってみるとなかなか面白いのですが、一つ問題があって、こうした経緯を語る資料が今まさに失われかけているってことですね。OStatus 関連、公式サイトどころか、仕様書自体がネットから消失しかけているものが多く、それを探し出すべくネットを徘徊していると「電脳考古学」という言葉を思い浮かべてしまう。たった十年前の話だというのに。
この辺、興味がある人は今のうちに資料を収集して保全しておくと良いんじゃないかしら。もう十年も経ったら何もかも電子の海の向こうへと消え去りそう。
「不在の中心」としての Twitter
最初に「今の Mastodon ブームを見ると過去の P2P ブームを思い出す」と書きましたが、ただ、ブームを駆動する動機は十年前とは大きく異なっているように思います。その根源は、ネットやそれを取り巻く社会のありようがこの十年で大きく変わったことにありそうだ、というのが僕の意見です。
ゼロ年代前半までのネットは、まだまだ社会の外部であるという意識を強く持っていたと思います。「ネットの自由」と「社会の規範」との間にある緊張関係は今よりもはるかに強く、またネット側の立場を強力に代表する人間や組織もなく、このままでは権威が介入して自由が全てスポイルされる日が来るのでは、という懸念が真剣に語られていた頃です。
そんな時代に分散システムが要請された理由は、一言で言えば「単一障害点をなくす」ということでした。責任主体の分散化、つまり責務を数多くの個人(が所有するコンピュータ)に「分権」して薄めることでシステムと参加者を守ろう、という動機があったわけです。
そして現在。今や、ネットは社会そのものとなりつつあり、また我々は Google や Twitter という中央集権的なシステムを信頼してネットを使うようになりました。そして、彼らは社会の一部に入り込んで「ネットの利益」を代表する存在となり、少なくとも民主主義陣営の国々の多くで、突然ネットに強力な規制がかけられる可能性は下がっています*1。
こうして、十年前と比べれば中央集権的な構造に対する警戒は(良くも悪くも)薄まっており、そういう切迫感が動機付けとなる状況ではなくなっています。例えば、GNU social 陣営と Mastodon 陣営の議論における微妙な温度差は、「自由」について常に危機感を持って活動してきた旧世代と、それをリスペクトしつつも切実さが薄い現行世代の違い…というまとめ方はいささか乱暴かもしれないですが、まぁそんなことも感じます。
では、こうした状況においてなお、我々が分権的なコミュニティを求める動機って何でしょうか?
僕は、「社会そのものと化したネットから一歩引く」という話なのだろうと思っています。全てがグローバルでフラットな場から撤退して、参加者や管理者の「顔」が見えやすい場を求めて移動し、ノイズが少なく心理的な安全性が高い少しだけ閉じたコミュニティを作る。この Mastodon ブームがどこまで続くかは不透明ですが、少なくとも、そういうバランスを取りたいという欲求の現れとして見ることはできそうな。
かと言って、これは Twitter の役割が終わったことを意味しないでしょう。なぜなら、今や Twitter とは概念であり、具体的に OStatus 互換実装たちが「連合」できるのはそのおかげだろうと思うからです。
記事にも少し書きましたが、僕は OStatus を構成する仕様のうち最も重要なのは、インスタンス間の通信プロトコルである PubSubHubbub や Salmon ではなく Atom フィードとその「語彙」だろうと思います。例えば、我々は「フォロー」という語彙が何を指すのか知っており、インスタンス間で「AはBをフォローする」というメッセージが伝えられた時に、利用者や異なる OStatus 実装がその意図を誤解する余地はありません*2。それに対して、そのペイロードを運ぶプロトコルはいくらでも取り換え可能だろうし、むしろもっと良い選択肢がありそうに思います。
思うに、OStatus は Twitter からの「脱出」を目指して作られたものですが、これらは不在の中心である Twitter の周りを巡っているが故に、バラバラにならずに済んでいるのだと思います。Twitter はマイクロブログの語彙を象徴する「バベルの塔」であり、仮に将来それが崩壊するようなことが起きれば、きっとまた、連合も維持できなくなるでしょう。