async/await は Promise を置き換えない

まとめ

  • async/await 構文は、Promise で書ける処理のうち特定のケースしか表現できない
    • 特定のケースとは、ある非同期処理の前処理と後処理がそれぞれ 1 個ずつの場合のみである
  • async/await 構文は初心者に非同期処理を導入する際に適しているが、非同期処理を逐次処理として書けるという幻想を与えるので、どこかで知識をアップデートする機会を設けるべきである

この記事はなに?

少しバズったのでまとめておこうかと。

async/await 構文とは?

Promise を使った非同期処理を平易に記述するための糖衣構文。

例えば、Twitter API から指定したユーザのプロフィール画像の URL を取得する関数と、指定した URL のデータをメモリにダウンロードする関数があったとする。どちらも非同期処理なので、一つ前の処理の完了を待ち合わせたり、ネットワークエラーなどに対処する必要がある。JavaScript の場合、これを Promise を使って書くとこのようになる(型アノテーションは TypeScript の記法を流用)。

const fetchProfileImageUrl = (username: string): Promise<URL> => { ... };
const downloadUrl = (url: URL): Promise<ArrayBuffer> => { ... };

const image = fetchProfileImageUrl('okapies').then(url => downloadUrl(url));

非同期処理は、処理が完了する順番やその待ち合わせ、例外処理などの様々なケースを考慮する必要があり面倒が多い。Promise を使えば、非同期処理を「Promise を返す関数」と then の組み合わせで実装できる。

しかし、この例では一つ前の非同期処理の結果を使って処理を順番に実行したいだけなので、組み合わさる処理が増えてくると記述が煩雑になる。そこで、非同期処理を命令型の逐次処理のように書くための構文として async/await が提案されている。以下の例では、fetchProfileImageUrl の前に await キーワードを追加することで、thencatch などの特別なメソッドを使わずに、あたかも非同期処理の組み合わせを逐次処理であるかのように書ける。

const fetchProfileImageUrl = async (username: string): Promise<URL> => { ... };
const downloadUrl = async (url: URL): Promise<ArrayBuffer> => { ... };

const url: URL = await fetchProfileImageUrl('okapies');
const image: ArrayBuffer = await downloadUrl(url);

Promise は多くの言語でクラスやデータ型として表現できるが、async/await は構文の拡張が必要であり、全てのプログラミング言語で利用できるわけではない。非同期処理のユースケースが増える中で、async/await 構文を持つことは新興の言語にとって大きなセールスポイントになっている。

async/await は何ができないか

ある非同期処理について、そこに繋がる前処理や後処理が2つ以上ある場合 (N > 1) は async/await は適用できない。なぜなら、原理的に、二つ以上の非同期処理の並列実行を逐次処理 (N = 1) として記述することはできないからだ。ストリームデータ処理などの文脈では、このような接続関係を fan-in/fan-out と呼ぶ(おそらく論理回路の用語の流用)。

f:id:okapies:20201213143951p:plain

例えば、冒頭のツイートで指摘した問題は投機的実行として知られるテクニックだが、これを解くには、以下のように Promise の配列を Promise.any() に渡す必要がある。*1 *2

const asyncFn0 = async (): Promise<any> => {...};
const asyncFn1 = async (): Promise<any> => {...};
...

const results = [asyncFn0(), asyncFn1(), ...];
Promise.any(results).then(a => { ... });

また、もう少し一般的なユースケースでは「複数の API に問い合わせて、全ての結果が返ってきたら結果を集約して表示する。どれかが失敗した場合はエラー処理をする」というものがある。この場合も、同様に Promise の配列に対して Promise.all() 関数を使う。*3 *4

後処理が複数ある場合も同様だ。例えば、先ほどの問題で Twitter から取得した URL をキャッシュサーバにキャッシュしておくとして、生の Promise を使うなら単に Promise 値 imageUrl に対して then を二回呼べばよい。

const fetchProfileImageUrl = (username: string): Promise<URL> => { ... };
const downloadUrl = (url: URL): Promise<ArrayBuffer> => { ... };
const cacheUrl = (username: string, url: URL): Promise<void> => {...};

const imageUrl = fetchProfileImageUrl('okapies');
const image = imageUrl.then(url => downloadUrl(url));
imageUrl.then(url => cacheUrl('okapies', url));

対して、async/await で書くと以下のようになる。一見、async/await でも正しく記述できているように見えるが、セマンティクスが変わってしまう。つまり、downloadUrl の完了を待ってから cacheUrl しているので並列に実行できず応答性能が劣化する。*5

const imageUrl = await fetchProfileImageUrl('okapies');
const image = await downloadUrl(imageUrl);
await cacheUrl('okapies', imageUrl);

async/await をどう教えるか

昨今、「async/await は従来の難しい Promise を完全に置き換える優れたコンセプトである」という主張を散見する。しかし、以上のような限界がある以上、async/await は Promise の特定のユースケースで便利な糖衣構文、という以上のものではない。

Promise が「難しい」のは、そもそも非同期処理が難しいからだ。そして、async/await が「簡単」なのは、非同期処理の中でも簡単な部分をより簡潔に書けるようにしているからに過ぎない。

もちろん、以上のような限界を理解した上で活用するのは問題ない。特に、JavaScript のように代表的なユースケースに UI 処理やネットワーク処理などが入ってくる環境では、プログラミング入門者向けに非同期処理の必要な部分で async/await 構文を「おまじない」として教えるのは致し方ない面もある。

だが、そうした便宜上の教え方を真実と勘違いした技術者が増えるとなると話は別で、コミュニティが主体となった知識のアップデートなど、何らかの対策が必要であるように思う。

十年以上前、Protocol Buffer の RPC 機能(今日 grpc として知られているもの?)の是非について論争があった際に、反 RPC 派の Steve Vinoski 氏が以下のように述べたという。当時と比べて開発環境は大いに変化したが、残念ながら、根本的に問題になっている点はあまり変わっていないように思う。

For years we’ve known RPC and its descendants to be fundamentally flawed, yet many still willingly use the approach. Why? I believe the reason is simply convenience. Regardless of RPC’s well-understood problems, many developers continue to go down the RPC-oriented path because it conveniently fits the abstractions of the popular general-purpose programming languages they limit themselves to using. Making a function or method call to a remote or distributed function, object, or service appear just like any other function or method call allows such developers to stay within the comfortable confines of their language. Those who choose this approach essentially decide that developer convenience and comfort is more important than dealing with hard distribution issues like latency, concurrency, reliability, scalability, and partial failure.

Convenience Over Correctness :: Steve Vinoski’s Blog

補足

記事に対していただいたご意見など。

async/await の明確なメリット

制約があるということは、処理系の側で制約を使った最適化ができるということなので、async/await を使えるときは使った方が良いというのはその通りですね。

追記:

これは完全におっしゃる通りですね。Promise を使っていると変数間の依存関係を常に意識させられて煩雑な面がある(良い面もあるにせよ)わけですが、古典的な制御構造の範囲内では、そこを処理系がよしなにしてくれる。

fan-out でも Promise.all を使う

qiita.com

const imageUrl = await fetchProfileImageUrl('okapies');
const result = await Promise.all([
    downloadUrl(imageUrl),
    cacheUrl('okapies', imageUrl),
]);

ありがとうございます。まず、何をもって「Promise を置き換えた」と判断するかは様々な見解がありうるという前提を置いた上で、僕は Promise.all() に渡す式を評価すると Promise の配列が出てくる以上は Promise を隠蔽できていないと考えます。

これに対して、allany などの関数は async/await に付随した特殊な専用構文であり、await していない async 式を渡せる例外とみなすことは可能だと思います。しかし、この解釈だと関数呼び出しを書く場所が制約されてコードの取り回しが悪くなりますし、それならば Promise を変数として直接取り扱った方がシンプルかつ楽な方法に見えます。

あと、okazuki さんご自身も注釈されているように、Promise.all() は渡した Promise のうち一つでも失敗すると全体が失敗します。たとえば、キャッシュサーバへの通信経路が輻輳して URL のキャッシュが失敗すると、たとえ画像のダウンロードは成功していても一緒に失敗とみなされます。そこで、記事では個々の async 関数の中でエラー処理をすることで Promise.all() に例外を見せない方法を提案されていますが、それはそれでエラー処理の方法が制約されることになります(上のレイヤに例外のハンドリングを移譲する、など)。

JavaScript の場合、代わりに Promise.allSettled() を使う方法があるかもしれません。これを使うと、渡した非同期処理が一部失敗することを許容して、全て「完了」するのを待つことができます。ただこの方法でも、全体の完了時間が、渡した非同期処理のうち最も完了まで時間がかかったものに律速されます。これを避けたいなら、やはり個別の非同期処理ごとに then を書くしかないように思います。

all を使う方法がうまく行かないのは、おそらく all が集約関数だからだと思います。並列処理を逐次処理の中で書けるようにするには処理を集約するしかないですが、その代わりに個別の事情に沿った処理ができなくなってしまうのです。

any を race で代替できるか

ブコメより。

anyはexperimentalでも、raceで代替できるはず。/ thenの代替をawaitでやりたい、というモチベーションなら、raceで最初のひとつを待ってから処理した後、処理済みのpromiseを除いてからもう一度raceにかけてawaitすればいい。

azzrのブックマーク / 2020年12月14日 - はてなブックマーク

雑に書いてみたんですが、reject されると promise ではなく error が渡ってくるので、失敗した Promise を特定する方法が無さそうです。例外に自分自身の参照を埋め込むしかなさそう。

let resolved = null;
let results = [promise1, promise2];

while (!resolved) {
  try {
    resolved = await Promise.race(results);
  } catch (error) {
    results -= ???;
  }
}

*1:なお、any 関数は現時点では experimental なので Edge などのブラウザでは使用できない。

*2:追記: any は race 関数 で代替できるというコメントを頂いたが、race は最初に完了した Promise が rejected だった場合は、それ以降に fulfilled で完了する Promise があっても rejected になる。つまり、セマンティクスが異なるので置き換えられない。

*3:追記: TC39 で提案されている await.ops を使えば Promise をユーザに見せずに済む、というコメントを頂いているが、僕はこれは論旨に影響しないと考える。なぜなら、仕様書を読む限り、結局は Promise の配列を受け取って Promise.all() 等の集約関数にそのまま引き渡す薄いラッパーに過ぎないからだ。他の懸念点としては、例えば Promise が一つではなく二つ以上成功した場合を扱うユースケースがあった時に、仮に any2() という関数を生やすとして、それを毎回言語仕様に反映するのか、ということだ。

*4:追記: この記事で問題にしているのは、Promise という文字列をユーザが書かずに済むか否かという話ではなく、計算を記述するパラダイムの根本的な違いについてだ。本文中では説明を端折っているが、本質的に Promise は有向非循環な非同期計算の計算グラフ (DAG) を直接組み立てる API であり、その帰結として、N > 1 の場合は命令型のパラダイムでは記述できない。async/await で記述できるのは N = 1 の場合のみだ。これは原理的な問題なので、構文の工夫では解決できないと考える。

*5:もし処理系が Promise 同士の依存関係を正しく分析できるなら、Promise 版と同等のコードを生成してくれるかもしれない。その場合、コード上は逐次処理に見えるのに実行順序がひっくり返る、というようなことが起きるが…。

コロナ時代の VR 音楽イベント・ガイド

コロナ禍の折、皆様はいかにお過ごしだろうか。僕は、所属先の業務の多くがリモートでも遂行可能なこともあり、2月中旬からずっと家で仕事をしている。しかし、もちろん弊社は非常に幸運な例であり大きな影響を受けている業界も少なくない。

その一つが、僕も折りに触れて参加している商業音楽イベントだ。先日、ようやく緊急事態宣言が解除されたとはいえ、感染者数は一進一退であり、いわゆる「三密」の象徴とも言えるライブの類は当分難しいだろう。

そのような情勢を反映して、音楽業界では YouTube などの動画配信プラットフォームを使った音楽イベントのオンライン化が数多く試みられている。今のところは収益度外視、あるいはチャリティ名目というものが多いが、事態の長期化はほぼ間違いない以上、CD 売上の減少に伴ってライブビジネスへ転換を進めてきたと言われる音楽業界にとって、ビジネスモデルの面で新たなチャレンジを強いられることになる。

そもそも、我々は「ライブという体験」に何を求めてチケット代を払い、わざわざ会場へ足を運んでいたのだろうか? そして、オンラインで同じ価値を実現するには何が必要だろうか? この一ヶ月ほど、いくつかのオンラインライブを視聴する機会があったので、個人的に考えたことをまとめてみたい。

目次

動画プラットフォームの活用

コロナ禍で self-quarantine を余儀なくされているファンに向けて、自分のパフォーマンスを配信しようとしたアーティストが直面したのは、そもそも「会場が使えない」という問題だった。よって、ジャニーズのようにコロナ流行前から会場を押さえていた場合を除いて、自宅での収録が基本となる。

例えば、この B'z のパフォーマンスでは、各自が収録した演奏を編集で組み合わせることで、国内外のメンバーとの「おうちセッション ("HOME" session)」を成立させている。

www.youtube.com

また、このような事前に収録した動画の配信だけでなく、ストリーミング配信によるイベントも盛んに行われている。

例えば、世界最大規模の EDM フェス Tomorrowland のオンラインイベント "United Through Music" では、DJ 達のパフォーマンスが YouTube 上でライブ配信され、観客はチャット上でリアクションを返すことができた。また、本イベントは Zoom 経由でも参加でき、家のカメラの前でノリノリで聴取している「観客」たちの様子を共に配信するといった工夫も盛り込まれている。

www.youtube.com

あと、少人数なら「密」ではないということか、EDC 主催の "Virtual Rave A Thon" では、自宅ではなく無観客のスタジオを使用していた。この場合、高価な機材やライティングが使用できるので、より「非日常感」を演出しやすい。

youtu.be(個人的には、この Z-Trip の職人芸の光る DJ プレイがとても良かった。初っ端から司会者の直前の音声をミックス素材化し始めて楽しい)

日本でも、秋葉原MOGRA が Twitch で無観客の配信を続けている他、いくつかのクラブが配信のみの営業を行っている。

仮想空間でのライブ

このように、YouTube や Twitch などの配信プラットフォームの普及により、コロナ以前から、配信者−視聴者間の「チャット」や「スパチャ(投げ銭)」を介したコミュニケーションがすでに一般的なものになっていたことは、ライブのオンライン化にとって幸運だったと言える。

しかし、我々が「ライブ」という非日常的な体験に求めているのは、単に好きなアーティストの演奏を聞いて楽しむということだけではなかったはずだ。それは例えば、光と音の洪水に投げ込まれる「没入感」だったり、隣の観客と一緒に歓声を張り上げ、歌に合わせて腕を振り上げる「一体感」だろう。

このことから、自宅に居ながらにしてあたかもライブ会場にいるかのような感覚を与えることができる、仮想空間プラットフォームを活用するアイデアが出てくるのは自然だと言える。今回のコロナ禍に対応して、既存のオンラインゲームを活用したものから専用の VR プラットフォームに至るまで、様々な試みがされている。

興味深いのは、「会場」というハコの捉え方からそこでの「観客」の位置付けに至るまで、イベントによって実に様々なアプローチがなされていることだ。以下では、いくつかの取り組みを紹介していきたい。

会場を仮想世界に写し取る

Blockeley は、コロナにより卒業式が不可能になったカルフォルニア大学バークレー校 (UC Berkeley) の学生たちが、仮想空間で卒業式を開催するため、Minecraft の中で敷地と校舎を再現するというプロジェクトだ。完成した校舎は、学外からも Minecraft クライアントでアクセスできる。当日は、学長が自身のアバターで参加して式辞を読み上げたようだ。

youtu.be

また、同時に卒業記念パーティーとして、世界各国から招聘した DJ によるバーチャル音楽フェスが開催された。もちろん同様に、実際のフェス会場を模してDJ ステージや照明が完備されたスタジアムが Minecraft 内に用意された。

音楽イベントの仮想化という点で見ると、Blockeley は最も基本的な形と言えるだろう。つまり、現実のフェス会場のレイアウトをそのままモデリングしたものになっている。観客は観客席でジャンプやエモートで盛り上がり、演者はステージに上がって音楽を流す。また、観客はステージに進入できないようになっており、観客席とシステム的に区別されている。

「第四の壁」を取り除く

ステージと観客席を区別して配置するモデリングは、現実の会場レイアウトを反映しており分かりやすい。しかし、この区別は本質的なのだろうか? つまり、物理世界で興行をトラブルなく進行するための運営上の制約に過ぎないのでは、ということだ。例えば、ステージに興奮した観客がなだれ込んだら演奏どころではなくなる。大勢の人間を一箇所に集めた際の警備をどうするか、というのは、現実のイベントでは大きな問題だ。*1

一方で、仮想空間のイベントではそのような問題はない。いや、ないわけではないが、その負担は大いに軽減される。興奮した客に楽器をひっくり返されたくないなら、主催者はそういう「コード」を実装すれば良い。あるいは、厄介な客をつまみ出すために警備員を雇わずとも、ワンボタンでミュートするなり BAN するなりすれば済む。もし、仮想空間への移行によって運営上の問題が解決され、ステージと観客席を分け隔てる必要がないのなら、もはや第四の壁は存在しない

例えば、4月末に VRChat(VR ヘッドセットを使うソーシャル VR アプリの一つ)内で開催されたこのライブイベントでは、途中で会場がライブハウス風の場所から「電脳空間」へとまるごと切り替わる。ここでは、ステージと観客席の区別が取り払われ、観客はステージの「中」で演者のパフォーマンスを体験することになる。

他にも、有志が開発した DepthField という仕組みを使い、サテライト会場なのに VR の中でメイン会場の VR が体験できるなど、先鋭的な仕組みがいくつも取り入れられていたようだ。

youtu.be(演奏は 1:29:00 あたりから)

これは有志による非商業的なものだが、もっと大規模かつ商業的に行われたものもある。人気バトロワゲーム「フォートナイト (Fortnite)」内で行われた音楽イベントも、仮想空間でのライブパフォーマンスについての興味深い取り組みの一つだ。公式発表によれば、本イベントは同時接続数が 1,230 万人に達したという。以下の記事によれば、収益面でも大きな成功を収めたようだ。

www.cinra.net

もともと、フォートナイトはシューティングゲームでありながら「建物を建てながら銃で撃ち合う」というゲーム性から、大量に用意された3Dモデル部品を組み合わせてプレイヤー自身が自由なマップを作れる「クリエイティブモード」を提供するなど、仮想空間プラットフォームとしての機能を備えている。この特性を活かしてか、フォートナイトは昨年にも世界的人気 DJ マシュメロをゲストとして同様のイベントを行っていた。

www.youtube.com

この時は Blockeley と同様の「舞台−観客席」形式だったが、人気ラッパートラヴィス・スコットを招いた今回のイベントでは、その取り組みをより進化させたものになった。イベントは、マップ全体がステージとなった広大な会場に、巨大な「神」と化したトラヴィス・スコットが観客を蜘蛛の子のように吹き飛ばして登場するのを皮切りに、曲に合わせて場面が次々と切り替わっていき、最終的に宇宙にまで達する。

このイベントは、現時点のこの種の取り組みとしては、最も完成度の高いものと言ってよいだろう。以下の動画は、実際にプレイしていなくても、その面白さを十分に楽しむことができるものになっている。

www.youtube.com

観客の抽象化

前節ではステージと観客席の融合について見てきたが、ライブ会場のモデル化については、それ以外にも様々な切り口があるように思う。その一つは「観客をどう表現するか」ということだ。その具体例として、個人的に大ファンである DJ のポーター・ロビンソンが主催した Secret Sky を取り上げたい。

これは、YouTube や Twitch 上で、ポーターが直々に指名した19組のアーティスト達が入れ替わり立ち替わり、14時間に渡って自宅などから DJ セットをプレイするという企画。当日の様子については以下の記事が詳しい。

realsound.jp

ポーターは、以前の記事でも紹介したように、アニメ好きが高じすぎて日本の大手アニメスタジオと組んで新曲のミュージック・ビデオを作ってしまうほど日本のオタク文化サブカルに造詣が深く、この日の参加アーティストにも3人の日本人(kz、長谷川白紙、キズナアイ)が含まれていた。また、彼自身のセットでも序盤から ICO のBGM や「ちょびっツ」OPテーマ曲をシレっと放り込むなど、フリーダムな選曲センスを遺憾なく発揮。

youtu.be

さて、本題だが、Second Sky ならではの取り組みとして、動画配信と並行して Web 上に VR 会場を用意したことが挙げられる。まず、下記のツイートの動画を見てほしい(音出し推奨)。

つまり、観客はアバターではなくサイリウムなのだ。確かに、会場自体は「舞台−観客席」のモデリングを採用しており、なんなら VIP エリア(会場後方の坂の部分)まで備えているが、しかし、ここには「観客とは自己表現する主体ではなくサイリウムである」という圧倒的な割り切りがある。そして、互いの動作はサーバを介してリアルタイムに同期されているので、自分を操作することで「みんなでサイリウムを振る」こともできる。

この VR ライブ会場は、アメリカのクリエイティブ集団 Active Theory が開発した技術を用いて実現された。ライブ会場のサイトはすでにクローズしているが、彼らはポーターの新作アルバム "Nurture" の PR サイトの制作にも関わっており、ここでほとんど同じものを体験できる。これも Secret Sky と同様に自分の操作がオンラインで同期されるので、たまたま同時にアクセスしている人がいれば(左下の表示が CONNECTED になる)、一緒に仮想空間を飛び回ることができる。*2

nurtu.re

この仕組みの興味深い点は、WebGL で実装されているので、スマホを含む現代のモダンな Web ブラウザでならまず動くということだ。また、観客は単なるなので、多数の観客を収容しても高価な GPU は必要ないし、何ならスマホタブレットからもアクセスできる。つまり、ゲームなどの専用クライアントを必要とする他の方式と比べて、圧倒的に多くのファンにリーチできる。*3

音楽イベントの本質とは何か

この記事で挙げた二つの論点、つまり「ステージと観客席の融合」と「観客の抽象化」*4は、どちらも「多人数が参加するイベントにおいて必然的に発生する物理的制約とどう向き合うか」という議論に関係している。

前者は、会場レイアウトを制約していた「イベント警備」が必要でなくなった時に起こりうる変化を示している。これは、今まで制約に縛られてきたクリエイティブな想像力を解き放つ土壌となるに違いない。一方で、後者は「観客側のデバイスが積んでいる GPU 性能」という、短期的にはいかんともしがたい制約をどう解くかということだ。

特に後者については、なかなか難しい問題を孕んでいる。VRChat をやると分かるのだが、作り込まれた 3D モデルやアバターが存在する仮想空間にアクセスすると、あっという間に GPU メモリが悲鳴を上げ始める。なので、上で紹介した VRChat のイベントを見ると分かるのだが、この手の大人数のイベントでは観客を強制的に「手」とか「カカシ」にする運用になっているようだ。それでも、同じ空間に数百人を詰め込むのは難しいのではないかと思う。

これは、自己表現の一環として、自身のアバターを装飾する 3D モデルを互いに売買することでクリエイターを中心とした経済を駆動することを狙っている、多くの VR プラットフォームのビジネスモデルとは率直に言って矛盾してしまう。3D空間を使ったオンラインフェスが、Minecraft やフォートナイトなどの既存のゲームをインフラとして使っているのは、ゲームをやるような人間は強い GPU を持っているということもあるだろうし、他にもあらかじめ最適化が施されたモデルを使わないと現実的な性能で描画できない、という話もありそうだ。*5

だから、できるだけ多くの観客に一体感を共有してもらうことを重視するなら、Secret Sky の方式はかなり正解に近いのではないかと思う。例えば、これにワンボタンでエモートできる機能とか音声入力の機能を持たせたりすると、かなり面白いことができそうだ。

一方で、ある種の参加者にとってライブは自己表現の場でもある、というのも事実である。そうでなければ、誰も色とりどりのフラッグを持ち込んだり凝ったペイントをして臨んだりはしないだろう。だとするなら、真に必要なのは、あらゆるご家庭にプレステ5や NVIDIA の高性能 GPU を積んだテレビを配ることなのかもしれない。

現在は、オンラインイベントにとって、あらゆる意味で過渡期であることは間違いない。音楽ファンの一人として、業界を盛り上げるようなとびきり面白いアイデアが出てくることを期待している。

*1:Wikipedia の「興行」の項目では、芸能界とヤクザが結びつきやすい背景として「狭い区域にたくさんの観衆を集めるという構造上の特質から、暴力による妨害に弱いため、古くから不良を手なずける意味もあって、ヤクザ者・暴力団との腐れ縁があり、またヤクザ自身が興行を手がけることも多かった。」という解説がある。

*2:この方式について、このツイートでは「風ノ旅ビト」というゲームの影響を示唆している。ちょっと調べてみた限りでは、たしかに同じセンスを感じる。

*3:当日は、観客が殺到したことで Google Cloud に DDoS 判定されてアクセス拒否されたりとか、色々あったみたいだが…w

*4:この表現は、こちらのツイートから頂いた。

*5:この辺りのチューニングについてはあまり詳しくないのだが。

Apple/Google が開発中の接触者追跡 API を調べた

先日シンガポール政府が開発した接触者追跡技術 "BlueTrace" について調べたが、今日は引き続き、AppleGoogle が共同開発中としている Contact Tracing API について調べてみる。

そもそも、現状では iOS 上での制限により実用レベルで Bluetooth ベースの接触者追跡技術を実装するのは難しく、Apple の対応待ちという状況であるため、事実上、本 API が本命になると予想する。

www.apple.com

www.blog.google

アーキテクチャの概要については下記のツイートに掲載されている図が分かりやすいので、この記事ではもう少し細かいところを見ていくことにする。読み違えている部分があるかもしれないのでツッコミ歓迎。

中央サーバ方式の問題点

Apple/Google が提供する Contact Tracing の最も大きな特徴は、接触者の探索をサーバではなくクライアント端末上で行うことだ。

BlueTrace では、接触者調査員 (Contact Tracer) がコロナ陽性と診断されたユーザの濃厚接触者を発見するために、一度、端末内の接触履歴を(本人の承認を得た上で)保健当局のサーバにアップロードさせ、サーバ上で履歴にある ID を保健当局のみがもつ暗号鍵で復号して接触者の電話番号を得るようになっている。

この方式は、調査の網羅性という点では優れているが、懸念すべき点がないわけではない。例えば、以下のようなケースが思いつく:

  1. いわゆる「夜の街クラスタ」では聞き取り調査が難航し、クラスタ対策において大きな障害になったと言われる。これは、店側や客側が「特定の客が高級クラブや風俗店に出入りしていること」を隠そうとしたのが原因と言われている。このような状況においては、自分や接待相手の素性が明らかになると困る層は、やはり自分のスマホへの Contact Tracing アプリのインストールを忌避するだろう
  2. 接触履歴のアップロードに端末の所有者の同意が必要な手順になっているとはいえ、当局が「誰が誰と会っていたか」という情報を容易に入手可能な仕組みであることに変わりはない。例えば、一部の国家が自国民や旅行者に対して「感染症対策」の名目でアプリを常に起動することを求め、感染の有無に関わらず、接触履歴の提出を事実上強制するような「運用」がなされる可能性は否定できない

Diagnosis Key による非中央集権的な接触者検出

一方で、Apple/Google が提案するシステムにおいては、コロナ陽性と診断されたユーザは、端末に蓄積された接触履歴はアップロードせずに、代わりに Diagnosis Key(診断キー)のみをサーバにアップロードする。

あとで説明するように、Diagnosis Key は各ユーザを一意に識別する Tracing Key から算出されるが、逆に Diagnosis Key から Tracing Key を算出することはできない。また、Diagnosis Key は毎日更新される。つまり、三者は Diagnosis Key からユーザを特定することはできない

接触者検出は、サーバではなくユーザの端末の中で行う。端末に蓄積した接触履歴はこのために(のみ)使われる。ユーザ端末にインストールされたアプリは、保健当局のサーバから陽性確認されたユーザの Diagnosis Key の一覧をダウンロードして、それを端末の Contact Tracing API に渡して曝露検出 (exposure detection) を要求する。API は、渡された Diagnosis Key に対応する接触履歴の有無をアプリに対して回答する。

最終的に、コロナ陽性者との接触があったと判定された場合は、アプリを通じて保健当局に申告を行うことになると思われる。

接触履歴に対するアクセス制御

もう一つの特徴は、どうやらアプリ自身も端末が蓄えた接触履歴に自由にアクセスできるわけではない、ということだ。仕様書によると、Contact Tracing API が提供するメソッドは以下である:

  • CTStateGetRequest / CTStateSetRequest
    • バイスの Contact Tracing 機能のオン・オフを制御する
  • CTExposureDetectionSession
    • 端末内の接触履歴に対して API に曝露検出を要求し、該当する key の件数を取得する
  • CTSelfTracingInfoRequest
    • ユーザの認可を経た上で、端末から Daily Tracing Key (Diagnosis Key) を取得する

つまり、接触履歴はベンダーが提供する Contact Tracing Framework の内部で管理されており、アプリがユーザの許可なしに Diagnosis Key を取得してアップロードすることはできない。また、接触履歴にアクセスできないので、アプリ側で「誰と誰が会っていたか」という情報は取得できない。

このことから、スマホ OS ベンダー提供の Framework は接触履歴の蓄積と検索を担当し、保健当局が提供するアプリは Diagnosis Key 広告サーバとの通信や、接触者の相談窓口への誘導を行うといった役割分担が想定されているようだ。

鍵の生成とローテーション

Contact Tracing Framework で使う鍵は、鍵の更新期間ごとに左から右へ導出される。逆に、右から左へ遡って算出することはできない*1

Tracing Key → Daily Tracing Key(1日毎) → Rolling Proximity Identifier(10分毎)

Tracing Key は、ユーザ(端末)を一意に特定する鍵で、暗号論的乱数生成器 (CRNG) で生成した 32 バイト (256-bit) の値、とされている。仕様書では "securely stored on the device" とされているので、おそらく端末内にある暗号鍵保管用の耐タンパ性チップに保存されるのではないか。その場合、Tracing Key は端末の所有者自身も確認できない。

Daily Tracing Key は、Tracing Key と Day Number から日替わりで生成される 16 バイト (128-bit) の値。コロナ陽性と診断された場合は、生成した Daily Tracing Key の一部(過去14日分)を Diagnosis Key として保健当局のサーバへ提出する。

Rolling Proximity Identifier (RPI) は、Daily Tracing Key を秘密鍵として、1 日を 10 分単位で分割した値である Time Interval Number を HMAC 関数に適用して得られる値。この値を、Bluetooth を通じて他の端末と交換して端末内に蓄積する。Bluetooth 通信の際、匿名性を保つために MAC アドレスはランダムなものを使うが、MAC アドレスと RPI は常に一緒に更新する。

すでに説明したとおり、10 分間のみ有効な RPI から Tracing Key は逆算できないため、この値を Bluetooth で放送 (broadcast) しても個人は特定できず匿名性は保たれる*2

端末で曝露検出を行う際は、各端末はサーバから取得した Daily Tracing Key を使って、可能性のある Time Interval Number に対して総当りで RPI を計算し直し*3、端末内に保存された RPI との一致を見ることで、コロナ陽性者との接触の可能性を判定する。

まとめ

 そしてもう一つはコロナ禍後の社会のヴィジョンがほとんど語られないことだ。コロナは人類全体を滅ぼすほどのウイルスではない。ほとんどのひとは生き残る。そのときどんな社会を残すかも考えるべきである。いまマスコミでは命か経済かと選択を迫る議論が多い。でも本当の選択は「現在の恐怖」と「未来の社会」のあいだにもある。こんな監視社会の実績を未来に残していいのか。

東浩紀「緊急事態に人間を家畜のように監視する生権力が各国でまかり通っている」 〈AERA〉|AERA dot. (アエラドット)

Apple/Google の Contact Tracing システムは、BlueTrace と比べると若干複雑な仕組みだが、「どこで会ったか」という情報だけでなく「誰と誰が会ったか」という情報を明らかにせずともコロナ陽性者との接触を検出できるなど、プライバシー保護の面では多くの点で BlueTrace より優れているように見える。

当局が接触者を検出する手段はあくまでアプリを通じた各自の自己申告による、という点がデメリットだが、裏返せば、先に論じたように「夜の街クラスタ」のような人たちが導入する際の心理的抵抗を減らせるという意味ではメリットになりうる。

一方で、プライバシーの担保が Contact Tracing Framework に多くを依存しており、ここにセキュリティホールがあると多くの前提が崩れることになる*4。また、アプリが独自に位置情報や近くにいるユーザの情報を集めていないかにも注意する必要がある*5Apple/Google が提供するから安心とするのではなく、設計で期待されている要件が実装においても達成されているかは、我々第三者による十分な検証が必要だろう。

*1:こういった暗号理論に基づく議論に興味がある方は、Wikipedia暗号学的ハッシュ関数のページなどが参考になると思う。

*2:基本的には。もちろん、監視カメラなどの複数の情報源を組み合わせると、理論上は紐付けが可能である。一方、RPI の有効期限は 10 分間なので、それが問題になるユースケースとは何か、という点を検討することになるか。

*3:明示的には書かれていないが、おそらくそういう意味だろう。

*4:例えば、何らかの回避手段を使うとアクセス制御を破れるとか、暗号化されてない接触履歴を読み出せるとか。

*5:AppleGoogle は、ストア審査でここを担保するつもりかもしれない。

BlueTrace: シンガポール発のコロナ接触者追跡アプリ

今日、コロナウィルスの濃厚接触者を把握するためのスマホアプリの導入を政府が検討しているという報道が出た。

www3.nhk.or.jp

何日か前に、コロナ対策専門家会議でクラスタ対策を主導している西浦教授から以下の発言が出たように、感染症の専門家からも、医学的な検査を補完する手段としてスマホによる電子的な接触者追跡を活用したいという期待があるようだ。

この報道でも言及されているように、これはシンガポールの政府機関の一つ Government Technology Agency (GovTech) がコロナ感染者の接触者追跡のために開発した TraceTogether を日本向けに改修して利用するという計画らしい。

www.tracetogether.gov.sg

調べてみると、彼らはつい数日前に、TraceTogether のクライアント・サーバ間プロトコルBlueTrace と名付けて仕様化すると共に、参照実装である OpenTraceオープンソース化していた。OpenTrace には、iOS および Android 向けのクライアント実装と、Node.js ベースのサーバ実装が含まれており、すぐに利用できるようになっている。

bluetrace.io

一方で、当局主導で行われる個人の行動追跡はプライバシーの観点で大きな懸念がある。いま、単に「プライバシー」と書いたが、公権力である国家による行動追跡は、民間企業によるそれと比べても数段階上の配慮が求められる。万が一、目的外使用に繋がることがあれば、民主主義国家である日本に別種の危機を招き寄せることになりかねない。

この点に関して、BlueTrace は「ユーザのプライバシーを守りながら接触者追跡を行う」と謳っている点が大きな特徴である*1。具体的には、BlueTrace は接触者追跡にあたって GPS などの位置情報を使わない。代わりに、至近距離に存在するスマホ同士で Bluetooth を介して一時 ID を交換し、各端末のストレージに蓄える(かつてのすれちがい通信と類似の仕組みと思われる)。そして、所有者の感染が明らかになった段階で、本人の承認を経てスマホに蓄積した一時 ID をサーバ側へと送信し、履歴に含まれる ID の該当者へ警告を行う仕組みになっている。

また、同様の仕組みは、AppleGoogle が共同で開発している Bluetooth ベースの接触者追跡システムでも採用予定だとされている。

wired.jp

では、これらの仕組みは本当に設計者たちの期待通りにプライバシーを守りつつ、接触者追跡という目的を十分に達成できるだろうか? 幸い、BlueTrace の設計者たちはこれらの技術的詳細を論文ソースコードとして公開しており、外部の技術者が検証できるようになっている。この記事は、BlueTrace の技術的な紹介を通じて、今後数週間のうちに大きなトピックになるであろうスマホを利用した接触者追跡」についての議論を喚起することを狙いとしている。

よく、「技術は政治や思想から自由である/あるべきだ」という主張がなされることがあるが、本件はそれに対する最も明瞭な反例と言えるだろう。特に、この接触者追跡技術については、公共政策、感染症学、そしてソフトウェア技術という、従来はなかなか交わることのなかった領域を跨いだ総合的な視点が必要だ。ソフトウェア技術者のコミュニティは、今後始まるであろう議論に備えておくべきだと思うし、それが我々の専門家としての「社会的責任」を果たすこと繋がると思う。

BlueTrace とは何か

BlueTrace のトップページから引用する:

BlueTrace とは?

接触者追跡 (contact tracing) は、COVID-19 に代表される感染症の蔓延を減らすための重要なツールです。 BlueTrace は、Bluetoothバイスを用いて、プライバシーを保護しながらコミュニティ主導の接触者追跡を行うプロトコルであり、グローバルな相互運用性があります。

BlueTrace は、非中央集権的な近接ロギング (proximity logging) を、公衆衛生当局による中央集権型の接触追跡で補うように設計されています。 Bluetooth による近接ロギングは、手作業の接触者追跡の限界、すなわち個人の記憶に頼るため、知り合いや会ったことを覚えている接触者しか追跡できないという課題を解決します。すなわち、BlueTrace により、よりスケーラブルかつ少ないリソースで接触者追跡が可能になります。

BlueTrace はプライバシーを中心に設計されています

#1: サードパーティは BlueTrace 通信を使用したユーザの継続的な追跡はできません

バイスの一時 ID を頻繁に変更 (rotate) することで、悪意ある参加者が BlueTrace メッセージを傍受して個別のユーザを継続的に追跡するのを防ぎます。

#2: 個人を特定可能な情報の収集を制限

個人を特定可能な情報として収集されるのは電話番号のみです。これは保健当局によってセキュアに保管されます。

#3: 遭遇履歴はローカルストレージに

各ユーザの遭遇履歴 (encounter history) は、ユーザ自身のデバイスにのみ保管されます。保健当局がこの履歴にアクセスできるのは、感染者が履歴を共有することを選んだ場合のみです。

#4: 同意は取り消し可能

ユーザーは自分の個人データを管理できます。ユーザが同意を撤回すると、保健当局に保存されている個人を特定可能なデータはすべて削除されます。これにより、すべての遭遇履歴はユーザーにリンクされなくなります。

BlueTrace プロトコルOpenTrace コードを用いて実装できます。 TraceTogether は 2020/03/20 にシンガポールでローンチしました。これは、世界初の国家的な Bluetooth 接触者追跡アプリであり、OpenTrace コードを用いて BlueTrace プロトコルを実装します。

BlueTrace 技術の詳細

政府指導者や政策立案者向けの資料では、より具体的な仕組みが説明されている:

  • ユーザ登録時に電話番号を提供すると、保健当局のサーバはそれをランダムな User ID と結びつける
  • アプリが生成する一時 ID は User ID を暗号化したものであり、保健当局のみが復号できる
  • アプリをインストールした端末間で Bluetooth 通信が確立された場合、BlueTrace プロトコルにより一時 ID、携帯の型式、電波強度の3つを記録する
    • 携帯の型式と電波強度は、接触者同士の距離を推定するのに使う
    • 設計者たちが使っている calibration table はシンガポールで普及している端末に限定されているので、他の国で展開する場合は国ごとに試験が必要。将来的には全世界の端末を網羅したものを作りたいらしい
  • ユーザの感染が確認された場合、保健当局の接触者追跡員はユーザに PIN を提供する(スクリーンショットでは6桁)
  • ユーザはアプリに PIN を入力することで、過去21日分の接触履歴のアップロードに同意する
  • 接触者追跡員は(一時 ID を復号して?)得た電話番号を用いて接触者へ連絡する
  • アプリは、Bluetooth をオフにしていない限り、バックグラウンドで周囲の Bluetoothバイスをスキャンし続ける
    • ただし、現状では iOS はバックグラウンドの Bluetoothバイススキャンを許可していないので、常にフォアグラウンドで起動する必要がある。ただし、Wired の記事によれば、Apple はこの制限を近日中に撤廃する方向で動いている模様
    • だが、こうしたBluetooth方式の追跡システムは、これまで大きな壁に直面していた。アップルがiOSに制限をかけることで、バックグラウンドで稼働するアプリがBluetoothを無制限に利用できないようにしていたのだ。これはプライヴァシーの保護と省エネルギーを考慮した措置だった。

    • ところがアップルは今回、濃厚接触を追跡するアプリに限ってその規制を撤廃する。アップルとグーグルはスマートフォンのバッテリー駆動時間を考慮して、最小限度の電力しか使わないようにするという。「このアプリは24時間ずっと休むことなく稼働するので、消費電力を徹底的に抑える必要があります」と、共同プロジェクトの広報担当者は説明している。

  • データ解析ダッシュボードについては、調査員や医療専門家と協同しながらプロトタイピングを進めている
  • 各国の BlueTrace 互換アプリ同士で接触者情報を共通に収集できるようにしたい。これにより、国境を跨いだ接触者追跡が可能になる

白書

より技術的詳細について書かれた白書(white paper)が公開されている。

まとめ

white paper やソースコードについても紹介したかったが、もう午前2時なので機会があれば後日ということで。

ざっと眺めた感想としては、要素技術はなかなか興味深いと思う。Apple/Google が同様の技術を採用する方向で動いているということで、この方式がスタンダードになっていくのかもしれない。

ただ、一つ注意しておきたいのが、そもそもこの仕組みが接触者追跡という目的に対して十分に効果を発揮するかはまだよく分からないということだ。それは、彼らのドキュメントの中でも明確に宣言されている。プライバシー上の大きな懸念を呼ぶシステムである以上、その辺も含めて検証が必要だろう。

また、なんとなくだが、収集された情報の利用については技術面というより法律面、運用面での制約を加える必要を感じる(まず思いつくのは、特定の地域に「任意の第三者」がビーコン収集端末を並べて監視カメラと突き合わせると、事実上、個人の詳細な行動追跡ができるのでは、とか。あと、一時 ID の生成って暗号化よりもいい方法がありそう)。

あと、日本版の BlueTrace 実装の開発においてはオープンソース化を強く求めたい。これは、日本のソフトウェア技術者のアイデアを活かすという意味でも、仕様が正しく実装され運用されていることをチェックする意味でも非常に重要だと思う。

続き

okapies.hateblo.jp

追記1 (4/14 14:00)

www.itmedia.co.jp

NHK の報道で言及されていた「民間団体」って、もしかして Code for Japan なのかな?(東京都のコロナ情報サイトを運営しているソフトウェア技術者のボランティア団体)

この記事の通りだとすると、すでに開発はスタートしていて Apple/Google接触者追跡 API を使う方向で動いているのだろう。上記でも述べたように、オープンソース化に期待したいところ。

政府は民間団体による日本版の開発を待って、近く実用実験に乗り出すことにしています。

追記2 (4/16 10:00)

やはり Code for Japan が開発に関わっているとのこと。上記でも述べているように、適当なタイミングで仕様とソースコードを公開して、プロトコルの詳細やプライバシーを担保する方法についてオープンな議論ができるようにして頂きたい。

prtimes.jp

追記3 (4/16 16:00)

4月の初め頃に Anti-Covid-19 Tech Team (ACTT) なる枠組みが立ち上がっていたらしい。プロジェクト例として「シンガポールのTrace Togetherアプリケーション日本版の実装検討」にも触れられている。

cio.go.jp

 官庁のIT対応能力を強化すべく、コロナウイルス感染症対策担当大臣をチーム長として、IT政策担当大臣及び規制改革担当大臣が連携し、内閣官房内閣府総務省経済産業省厚生労働省等、関係省庁からなるテックチームを組成。 テックチームは、民間企業や技術者の協力を得ながら、諸外国の状況も踏まえ、考えられるITやデータの活用を検討し、TECH企業による新たな提案も受けながら、迅速に開発・実装できることを目的とする。

追記4 (4/16 16:00)

Apple/Google が検討してる接触者追跡の技術的詳細について。こちらは、ID のマッチングは中央サーバではなく、感染者の匿名化された ID を広告して各端末内で行うようになっているようだ。こちらの方がプライバシー上の懸念について説明が容易だし、筋が良い気がするな。

Apple のプレスリリースと仕様書はこの辺。

www.apple.com

www.apple.com

*1:失礼ながら、シンガポールがどういう国であるかということを考えると非常に興味深い取り組みに思える。

QMK Firmware で Raise/Lower と変換/無変換を同じキーに割り当てる

自作キーボード向けのオープンソースファームウェアの QMK Firmware は、LT(layer, kc) という特殊なキーコードを用意している。これを使うと、通常のキーコード(A とか)とレイヤー切り替えキーを同じキーに同時に割り当てることができるので、例えば、レイヤー切替の LOWER キーと「無変換」を一つのキーに収容するといったことができる。

ところが、この LT キーを押してから離す操作を非常に素早く行うと、レイヤーの切り替えがうまく行われずに誤入力が発生する問題がある。これはキーマップ (keymaps) の設定ではどうにもならないが、代わりに keymap.c の process_record_user 関数に手を加えることで解決できる。

この方法はあぷろさんに教えて頂いたのだが、知見を共有する意味で記事として残しておくことにしたい。具体的な実装例は本文の最後に掲載する。

なぜ親指のキー配置が重要か

僕は WindowsLinux を使う際に、日本語入力の IME オフを「無変換」キーに、IME オンを「変換」キーに割り当てるようにしている。この二つのキーの配置は Mac の JIS キーボードにおける「かな」キーや「英数」キーと同じであり、ホームポジションに置いた親指で操作できるのでアクセスしやすい。また、「半角/全角」キーと違って、現在の入力モードが英語なのか日本語なのかを気にせずに所望のモードへ確実に切り替えることができる。

Windows では歴史的経緯により別の機能が割り当てられてきたが、このようなメリットが考慮されたのか、Windows 10 の次期バージョンではこのキーアサインを既定にすることも検討されている。

blogs.windows.com

僕にとっては、この「確実に切り替えられる」という点が重要で、プログラムを書いたりコマンドを打ち始める前に、とりあえず無変換キーを連打するクセがついてしまっているほどだ。そのようなわけで、自作キーボードにおいても「無変換」と「変換」をなるべく親指でアクセスしやすい位置に置いておきたいという要求がある。

一方で、Planck や Let's Split に代表される 40% キーボードでは RaiseLower の二つのレイヤー切り替えキーが重要な役割を果たす。この種のキーボードではホームポジションから遠いキーをバッサリと削った結果、数字や記号は Raise/Lower との組み合わせで入力するようになっているので、これらもなるべく親指付近に置いておきたい。

その他にも親指で操作したいキーにはスペースやエンターなどもあるわけで、親指でアクセスしやすい一等地である↓の辺りはいよいよ混み合ってくる。

f:id:okapies:20190202133823j:plain
ErgoDash の親指クラスタ

ところが、親指という指は一般に考えられているほど器用ではないし、可動域もさほどではない。個人的には、親指で使いこなせるキーはせいぜい三つで、理想的には二つに抑えたいという感じがする。だから、使いたいキーが三つあるなら、うち二つのキーを一つの物理キーに収容して使い分けたくなる。

そこで、「変換/無変換」と「Raise/Lower」はそれぞれ単発押し (tap) と長押し (hold) なので、キーの押され方をファームウェアで識別すれば二つのキーを一つの物理キーで扱えるんじゃないか、というアイディアにたどり着く。

LT キーの問題点

QMK Firmware には、LT(layer, kc) という特殊なキーコードが用意されている。これを設定した物理キーを単発押しすると kc で指定したキーコードを送るが、長押しすると代わりに layer に切り替わる。これを使うと単発押しと長押しを一つの物理キーに収められるので、以下のように設定してやれば問題は解決…

#define KC_LOMH LT(_LOWER, KC_MHEN)
#define KC_RAHE LT(_RAISE, KC_HENK)

…しない。

これ、動くには動くのだが、おそらく多くの人間が期待していない動作をする。簡単に説明すると、QMK は長押しを「キーが 200 ミリ秒押され続けているか否か」で判定しているのだが、例えば「RAISE 押す → E 押す → RAISE 離す」を非常に素早く入力すると長押しの判定が成立せず、# を入力したいのに E になってしまう上に IME オンも働いてしまい、とても残念なことになる。

これを防ぐには RAISE を押して一呼吸おいてから文字の入力を始める、というような人間側のワークアラウンドが必要で、とてもストレスフルだ。

長押し判定の秒数は TAPPING_TERM という定数で決まっているので、これを小さく設定すれば問題は軽減するが根本的な解決ではない。その上、副作用として、通常の単発押しの受付時間も短くなってしまうので、変換キーを押して 100 ms 以内に離さないと入力が失敗するといったことが起きる。これはこれでつらい。

この件について、一年ほど前に Reddit で相談した際に QMK コントリビュータの人からも話を聞けたのだが、結論としては「あまりいい解決策はない」ということになってしまった。私の理解では、これは QMK の設計の根幹に関わる問題*1なので、残念ながら当分は改善されないと思われる。

www.reddit.com

解決編

この問題が未解決のまま一年ほど我慢して使っていたのだが、今年に入って ErgoDash mini を作ったのを機に「本格的にどうにかせねばなぁ」という機運が高まってきた。そこで自作キーボード Discordファームウェアに詳しい人たちに相談したところ、あっさりと解決してしまった。最初から聞いておけば…。

上記の通り、QMK の機能では対応していないので keymaps 配列の設定を工夫しても解決しない。そこで、同じ keymap.c にある process_record_user というキーの上げ下げが通知されるイベントハンドラを自分で実装すれば、Raise/Lower の長押しに関する挙動をカスタマイズできる(よく読み返すと、上記の Reddit スレでも軽く言及されている)。

github.com

おそらく、多くのキーボードの keymap.c には以下のようなデフォルト実装が書かれていると思う。

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  return true;
}

あるいは、最初から以下のようなコードが書かれているかもしれない(update_tri_layer という関数の説明はこの辺にある。ここでは関係ないので無視してよい)。

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  switch (keycode) {
    case LOWER:
      if (record->event.pressed) {
        layer_on(_LOWER);
        update_tri_layer(_LOWER, _RAISE, _ADJUST);
      } else {
        layer_off(_LOWER);
        update_tri_layer(_LOWER, _RAISE, _ADJUST);
      }
      return false;
      break;
      ...
  }
  return true;
}

これを書き換えて、LT に頼らずに LOWER キーを押して離した際の挙動をカスタマイズする。例えば:

static bool lower_pressed = false; // (1)

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  switch (keycode) {
    case LOWER:
      if (record->event.pressed) {
        lower_pressed = true; // (2)

        layer_on(_LOWER);
        update_tri_layer(_LOWER, _RAISE, _ADJUST);
      } else {
        layer_off(_LOWER);
        update_tri_layer(_LOWER, _RAISE, _ADJUST);

        if (lower_pressed) { // (4)
          register_code(KC_MHEN); // macOS の場合は KC_LANG2
          unregister_code(KC_MHEN);
        }
        lower_pressed = false;
      }
      return false; // (5)
      break;
    ...
    default: // (3)
      if (record->event.pressed) {
        // reset the flag
        lower_pressed = false;
      }
      break;
  }
  return true;
}

ポイントとしては以下のような感じ。

  • (1): LOWER キーの上げ下げを記録する lower_pressed 変数(値を保管する箱)を用意する
  • (2): LOWER キーが押された(record->event.pressedtrue)ら、それを lower_pressed に記録する
  • (3): 他のキーが押されたら lower_pressed をリセットする(return はしない)
  • (4): LOWER キーを離した時に lower_pressed がリセットされていない時だけ「無変換」キーを入力する
    • LOWER -> E」と組み合わせて押した場合は lower_pressed がリセットされた状態なので無変換キーは入力されない
  • (5): return false; と書く(LOWER キーについて、以降の QMK 本体側での処理を行わないことを示す)

もう少し複雑な制御(LOWER を長押ししたら、他のキーと組み合わせない場合も「無変換」を入力しない)を実装したバージョンを gist に置いておくので、もしよければご参考までに。

まとめ

このように、QMK は細かいカスタマイズをしようとすると途端に難しくなる場合が多い(これを非プログラマの人にやってもらうのはなかなか厳しい)。この辺りが、「新しい自作キーボードファームウェアを作りたい」という話が出てくる一つの動機になっている。自作キーボード Discord の #arm-keyboard-firmware チャンネルでは、この辺りの話題について議論が交わされているので、興味のある人は覗いてみてほしい。

補足

他の方法について指摘を頂いたので紹介させて頂きたい。特に MACRO_TAP_HOLD_LAYER は有用そうなのでいずれ試してみたい。

*1:TMK/QMK はキーマトリクスの走査結果をバッファせず、直接イベントハンドラをコールバックするアーキテクチャになっており、前後のキー入力イベントに依存して出力を変えるのが難しい。

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 コードを検査して型の整合性を確認する。

mypy-lang.org

あるいは、関数アノテーションではなくコメントで書いてもよい(型コメント)。型検査器は # 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 で配列の初期化処理を実装している Initializerfunction クラスではないが、__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_arrayInitializerfunction の両方を受け取ることができる。また、これは実行時には単なるアノテーションなので、これらをラップする共通クラスを作る場合と違って性能上のオーバヘッドもない。

循環参照の解決

若干ハマった点として循環参照の問題がある。例えば、このような 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_CHECKINGPython 3.5.2 から追加された変数で、Python インタプリタでは常に False として扱われる。一方、mypy などの型検査器はこれを True として扱うので、型チェックをする場合のみ import 文が動くようにできる。さらに、DeviceSpec に定義する型を文字列として記述することで、実行時に import されていないシンボルを読み込むことによる文法エラーを回避する(型検査器は文字列を type クラスに変換してくれる)。

まとめ

というわけでボチボチと手を動かしていが、歴史のあるプロダクトなだけに API 本数も多く、まだまだ先は長そう。気長にやっていこうと思うので、応援して頂けると幸い。

github.com

tzdata の 1887 年以前の日本標準時子午線が間違っている話

今朝、こういう話を見かけた。

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 でも使われているので当然影響を受ける。

d.hatena.ne.jp

そこで思い出したんだけど、この天守台、正しくは旧江戸城天守台に、僕はつい最近行ってきたばかりだった。

奇しくもオリンピックに伴うサマータイム導入(つまり、標準時をいじるという話である)に反対するシンポジウムを聴講した9月2日の午後、「俺、大手町サラリーマンのくせに目の前の皇居に行ったことがないなー」ということに気づき、会場の永田町から国会議事堂を経由してテクテクと皇居東御苑へと通じる大手門へと向かったのだった。

わりと外国人観光客に人気のスポットらしく、また都会のオアシスとしてもなかなか良いロケーションだった。あとポケストップが大量にある(重要。これで、入り口の荷物検査とか諸々の面倒がなければ気軽に散歩に来れるんだけどなぁ(無理。

ところで、この天守「台」というのはちょっと奇妙なネーミングだと思わないだろうか。天守「閣」ではないのだ。普通に考えると「将軍家の居城だった旧江戸城にはさぞ立派な天守閣があったのでは?」となるだろうが、実は江戸城天守閣が存在したのは江戸時代の初期まで。何があったかというと、

まぁ、ツイートでは茶化して書いているけど、いちおう以下のようなまともな理由があるようだ。この保科雅之は江戸の町の防災性の向上に尽力した人物で、火除け用地として上野広小路を作ったのも彼なんだそうだ。

この寛永天守は、明暦3年(1657)の火災で焼け落ち、翌年に加賀藩前田家の普請により高さ18mの花崗岩でできた天守台が築かれます。これが現在残る天守台ですが、四代将軍家綱の叔父である保科正之の戦国の世の象徴である天守閣は時代遅れであり、城下の復興を優先すべきであるとの提言により、以後天守閣は再建されることはありませんでした。現在、東西約41m、南北約45m、高さ11mの石積みが残っています。

天守台 - 千代田区観光協会

その後、明治に入ってから、内務省地理局測量課がこの天守台を測量の原点と定めて三角点などが置かれたらしい。

現在はホテルオークラとなっている旧溜池葵町に置かれていた内務省地理局は、1882(明治15)年に江戸城本丸に移転した。天文台天守台(天守閣の土台)に設けられ、この新しい天文台を経緯度の零点とした。江戸城天守台は築後200年で、堅牢・安定な地盤が観測の適地と見込まれたのである。

本初子午線をゆく Part II 東京編

しかし、「東京天守台を初度とする」というお触れが出た直後、国際子午線会議の結果を受けて「東経135度を子午線とする」という勅令が出て、明石を通る現在の子午線 (GMT+9) に切り替わってしまった。だから、公式に +09:19:01 だった期間はかなり短い。この辺の細かい経緯については国立天文台の Wiki に詳しい。

というわけで、イギリス旅行の際にわざわざグリニッジ子午線を踏みに行く程度には子午線マニア(?)を自認していたわりには、足下のこういう歴史を見過ごしていたとは不覚だったなぁ、というお話でした。

なお、本題の tzdata の方ですが、修正には何かしらエビデンスがあった方がいいようなので、何かご存知の方がいらっしゃったら以下のスレッドにコメントして頂けると良いかと思います。