MIDIデータを可視化するやつを作った

Midivis screenshot

音楽でどういう音が鳴っているのかを知りたいと思ったので、MIDIデータを可視化するやつ、Midivisを作りました。

https://midivis.ryusei.dev/

Web MIDI APIが動くブラウザーGoogle ChromeMicrosoft Edge)で使うことができます。Mozilla FirefoxはWeb MIDI APIをサポートしていないので動きません。(プラグインを使えば動かせるかもしれませんが、確認していません。)

ノートは、横方向は半音、縦方向は完全4度の音程で並んでいます。ノートは横に12列並んでいるので、これだと同じ音が重複して表示されることになりますが、このように表示することで、コードのパターンが分かりやすくなります。たとえば、次の動画ではカノン進行 D-A-Bm-F♯m-G-D-G-A を演奏していますが、D→AやBm→F♯m、G→Dのような4度下行のパターンは下に、D→Gのような4度上行は上に、A→BmやF♯m→G、G→Aといった2度の上行は右に動いて感じるかと思います。そうすると、カノン進行全体ではパターンはどんどん右に動いていきますが、循環コードなので最後には元の配置に戻ります。こういう表現ができるのは音を重複して表示させているからで、これを横幅5列にして重複をなくしてしまうと、パターンが途切れてうまくアニメーションしなくなってしまいます。

www.youtube.com

(動画ではF♯はG♭と表示されていますが、Midivis最新版では音名の表示にシャープを使うよう切り替えることができます。)

おまけですが、コードネームを表示する機能もつけています。よく使われるコードは表示できると思いますが、対応していないコードは代替表示になります1異名同音も無視されるので、あくまでおまけ機能です。


  1. たとえばC, D♭, G, Bから構成されるコードがC{1,m2,5,M7}のような表示になります。

「不愉快の峠」仮説

マイノリティとマジョリティの間には、不愉快の峠があるのではないかと思う。

マイノリティ文化がマジョリティ文化に認知される過程を考えると、情報の少ない接触初期段階では、どうしても表現が歪み、不正確になってしまう。理解の深い人間の目には、この不正確で偏見混じりの描写はいかにも不愉快なものに映る。偏見をなくせ、正しく理解しろと憤る。

しかし、マイノリティ文化を最初から正確に描写することはできない。マイノリティ文化は異文化で、まず、正確に表現するための語彙さえ、マジョリティ文化には存在していない。マジョリティ文化の人間にマイノリティのことを正確に説明しようとすればするだけ、迂遠な表現で浅薄な解釈を回避することになるし、正確だが無味な描写は、結局はマジョリティ文化においては無視されることになる。偏見を許さない態度を強めれば強めるだけ、マイノリティ文化への理解が進展しなくなってしまう。そうであるとすれば、マイノリティがマジョリティに認知され、受容されるためには、少なくとも当分は、不愉快を飲んで相手の好奇心を刺激する描写に甘んじるしかないのではないか。

このような法則が、マイノリティの種別を問わず、普遍的にあるのではなかろうか。これは仮説でしかないのだけれども、明らかに不条理な仮説だ、と思う。マイノリティが無視される社会と、マイノリティが偏見にさらされる社会は、どちらも苦しい。マジョリティからの能動的な攻撃に晒されないだけ、無視されていた方がマシだ、という考えもあるだろう。しかし、無視されている状態からは、理解は進むはずがない。異文化間の不和を解消するには、不愉快の峠を乗り越え、その先へ進むしかないはずだ。

では、偏見を受け入れるべきなのか。部分的にはそうだ。しかし、極力無害な偏見を選ぶことは、できるかもしれない。致命的でない、許容できる不愉快さの偏見の峠を見つければ、不愉快さを耐えた先で、より深い相互理解を得られた未来に至ることができるかもしれない。

キーボード・ノートの対応

コンピューターのキーボードで音符を入力するとき、よくある方式だと

キー ノート
Z C3
S C♯3
X D3
D D♯3
C E3
V F3
G F♯3
B G3
H G♯3
N A3
J A♯3
M B3
K B♯3
, C4

のように対応させていて、要はピアノの鍵盤と対応させているけど、ふつうに横は半音階で並べて

キー ノート
Z C3
X C♯3
C D3
V D♯3
B E3
A F3
S F♯3
D G3
F G♯3
G A3
H A♯3
J B3
K B♯3
E C4

と割り当ててもいいのではないか。C4がEキーに当てられているのは、ベースの弦と同様に1段上が完全4度(5半音)になるように配置した結果で、実際にはQ, WキーにもB3, B♯3を重複して割り当てる。 こういうふうに、縦に完全4度、横に半音間隔で配列すると、左下のZキーから右上の0キーまでの音程は、3×5+9=24半音、ちょうど2オクターブになるので、割り当てに重複があっても、音域としては十分ではないかと思う。

竈門禰豆子の禰の字について

f:id:mandel59:20201029182505p:plain
アニメ公式での竈門禰豆子の表記。禰を表示するのに中国語繁体字の字形を使っている。

どうやら、竈門禰豆子の禰の字について、しめすへんは正式には「ネ」の形という指定が存在しているようで、公式サイトでもわざわざフォントを変えて1、禰のしめすへんを「ネ」に変えています。中国語のフォントでは、しめすへん常用漢字かどうかに関わらず、いつでも「ネ」の形をしているからですね。2

日本語フォント

中国語フォント

この、フォントを変える手法での字形変更は昔から行われていますが、中国語のフォントを使うわけなので、日本語のフォントのしめすへんとは形が少し違う問題があります。

他の方法としては、異体字セレクタと呼ばれる仕組みを使うと禰󠄁しめすへんが「示」)と禰󠄀しめすへんが「ネ」)は区別して出せます。この方法で変えられる字形は、日本語フォントの中で用意されている、他の字になじんだ字形を使うことができるメリットがありますし、異体字Unicodeで表現されているので、コピー&ペーストしても、意図通りの異体字を出すことができます。3一方で、テキストに異体字セレクタという特殊な文字が含まれることになるので、それを考慮しないソフトウェアが誤った処理をする可能性もあります。

まあ、異体字セレクタまで使って字形に拘るのは個人的にはちょっとこだわりすぎに感じてしまいますが、常用漢字外の難字であればなおさら、細かい字形の差が混乱を引き起こすということもあるかもしれず、まあ難しいですね……

(ここまで書き上げたあとでスラドWebサイトやWebサービスで異体字セレクタは使われているか | スラド IT で取り上げられていることに気づきましたが、はてなブログ異体字セレクタを使って表示する例として載せることにします。)


  1. 具体的には日本語のNoto Serif JPから繁体字中国語のNoto Serif TCに変更しています。

  2. 新字形国字標準字体の場合。

  3. 表示に使うフォントが使う異体字セレクタに対応している必要があります。

メッセージとコンテキスト

note.com

安倍総理は、星野源さんのコンテンツの趣旨に敬意を払わずに、コンテクストと星野源さんが与えたルールを無視しました。

安倍総理の動画はただくつろいでいるだけで、歌っても踊ってもいない。コンテンツの趣旨、コンテキストを無視しているように見えるし、それが憤りに繋がっている。

しかし、いったいそんなコンテキストが本当にあったのだろうか。

星野源さんのインスタグラムには、バナナマン大泉洋さんの動画も投稿されている。

www.instagram.com www.instagram.com

これらの動画は、何を伝えているだろうか。参加の形態は、必ずしも楽器の伴奏やコーラスやダンスに限らない、ということではないのか。俳優やお笑い芸人であり、音楽として関わることができなくとも、連帯してほしいということではないのか。これらの動画はニュースショーでも取り上げられていたし、これを見て、音楽でなくても大丈夫、というふうに思った人がいてもおかしくはない。

もちろん、個性的な大泉さんや、お笑い芸人のバナナマンだから、コンテキストを逸脱することが許されるのかもしれないし、星野源自身が投稿した動画であるから、彼らの動画が炎上するはずもない。一方で、一国の総理大臣が(追記:現状あまり職務を全うしていないと見なされている状況で)、彼らのように歌でも踊りでもない逸脱的メッセージを発することは、やはり問題があったのだとは思う。端的に言えば、安倍総理の動画はスベっていて、ムーブメントに水を差す結果になってしまった。

インターネットの時代、私たちは皆が皆異なるものを見ていて、コンテキストはほぼ共有されていない。この世界では、コンテキストを置き去りにして、メッセージだけが転送されてゆく。メッセージは原理的にコンテキストから乖離していく。この時代、正気でコミュニケーションをとろうとするのであれば、異文化への原理的無理解を理解し、ディスリスペクトという現象をリスペクトするべきだ。不快感に正直でありつつ、自分と異なる存在を理性的に認めるべきだ。そうでなければ、排外主義に堕してしまう。文化摩擦は、絶望というよりもチャンスであり、そういう時こそ感性を研ぎ澄まし、失われたコンテキストを集め、現象を見極めようとするべきだ。創造的な、あるいは破壊的な他者へのリスペクトが行われなければ、私たちは永遠にリスペクトされないだろう。

ウイルス感染シミュレーターを作ったので、オーバーシュートを完全に理解した

新型コロナウイルスの問題で、専門家が使う「オーバーシュート」という言葉の意味が問題になっていた。

togetter.com

興味を持ったので、自分でも感染症数理モデルを作り(グラフは参考にしたが、計算するための具体的な数式は見ていない。集団免疫率の定義を見たぐらいだ。)、シミュレーションを行ってみたところ、同様のグラフを得ることができた。ここに、簡単に説明をしておこうと思う。

留意点として、私は疫学の専門家ではないので、以下で使われている用語は疫学で使っているものと異なっているかもしれない。

docs.google.com

f:id:mandel59:20200325093514p:plain
再生産数=2のときのシミュレーション

再生産数は感染者1人がウイルスを伝染させうる人数のこと。ウイルスの性質で決まる、本来的な再生産数を基本再生産数といい、コロナウイルスではだいたい2~3ぐらいであると考えられているらしい。

1度感染した人間は免疫を獲得するとすれば、ワクチンが開発されていない限り、延べ感染者数=免疫獲得者数となる。免疫を獲得した人間にはもうウイルスが感染しないことから、次のような漸化式を立てて、感染者数の変化を考えることができる。(直接、感染者数や延べ感染者数=免疫獲得者数を扱うのは面倒があるので、それぞれ人口で割って、感染者率・延べ感染者数率=免疫獲得者率とする。)

  感染者率' = 感染者率 \times 基本再生産数 \times (1 - 免疫獲得者率)

  免疫獲得者率' = 免疫獲得者率 + 感染者率'

この式を眺めれば分かるが、感染者率は基本再生産数 \times (1 - 免疫獲得者率)が1を超えていればどんどん増えるし、1を下回ればどんどん減っていく。その境界となる免疫獲得者率の値を、集団免疫率という。

基本再生産数 \times (1 - 集団免疫率) = 1

式変形すれば

集団免疫率 = 1 - \frac{1}{基本再生産数}

これらの式から、免疫獲得者率=延べ感染者率が集団免疫率を下回っている間は、感染者は増え続け、延べ感染者率が集団免疫率を上回ったところで、ようやく感染者数が減少に転じるということがわかる。(わからなければ、シミュレーション結果のグラフを見て確認してほしい。上掲のグラフは基本再生産数が2の場合のシミュレーションで、集団免疫率は50%だ。免疫獲得率が50%のところで、感染者数が減少に転じている。)何もしなければ、社会の人口のうち集団免疫率の分の人間がウイルスに感染するまで、ウイルスの勢力は強まり続けるという、避けがたい運命が示されている。ウイルスの封じ込めができなくなった以上、どんな政策を行おうと、いずれ延べ感染者数は、人口の50%~66%に至るのだ。たとえ、新型コロナウイルスの致死率が数%程度だったとしても、大量の人間が死ぬことになる。

もはや、延べ感染者数の人口比が集団免疫率に至ることが避けられないのであれば、私たちは速やかに集団免疫率を目指して、感染するようにすべきなのだろうか?いや、違う。何もしなければ、集団免疫率を大幅に超えて感染者が発生してしまうことになるのだ。

集団が集団免疫を獲得した時点、すなわち、延べ感染者率が集団免疫率に達し、感染者数が減少に転じた時点で、自然と流行に終息に向かうというのは正しい。しかし、流行が終息に向かうというのは、直ちに終息するわけではなく、感染者数が0になるまでは、あらたな感染者が出続ける。シミュレーションでは延べ感染者率は約87%に至っており、集団免疫率とは大きな差が生じている。これこそが、「オーバーシュート」と呼ぶべき現象だ。

しかし、オーバーシュートを回避する方法はある。実効再生産数(感染者が実際に感染を広げる人数)を下げれば良い。つまり、衛生管理を徹底する、外出を避ける等の施策によって、人為的に実効再生産数を下げることができれば、延べ感染者率はより低い値に収束し、その時点で感染者数がほぼ0になる。延べ感染者率が集団免疫率に至った時、感染者がほぼ0である状況であれば、ここから元の生活に戻り、実効再生産数が上昇したとしても、免疫獲得者の割合が多く実効再生産数は1を下回るのでウイルスの流行はもはや拡大しない。(延べ感染者率が集団免疫率を下回った状態ではいけない。延べ感染者率が集団免疫率より少ない状況で、実効再生産数が基本再生産数に戻れば延べ感染者率が集団免疫率より少ない状況では、実効再生産数が1以上に戻りうるので、そこからまた流行が拡大することになる。)

実効再生産数を下げれば、感染者数の最大値も下がる。医療のキャパシティが有限である以上、このことも施策上重要な要素ではあるのだが、感染者数が医療のキャパシティを超えることをオーバーシュートと呼ぶわけではない。オーバーシュートは、延べ感染者数の割合が集団免疫率を超えて増えすぎてしまうことを指すのだ。延べ感染者数×死亡率=総死者数なのだから、延べ感染者数を最小化することは非常に重要だ。

以上が、是非とも実効再生産数を下げる政策を積極的に行わなければならない理屈だ。しかし、このような理屈をまともに書いている記事はほとんどない。医療従事者さえ、この理屈を正しく理解できていないから、オーバーシュートを誤用しているのだろうと思う。これは問題だ。自分でシミュレーションをやってみれば、上の理屈は容易に理解できるはずだ。

ウイルス感染シミュレーターといっても、Google Sheetsで簡単な数式を並べてグラフを作成するだけの、やり方を知っていれば誰でも作れるものだ。実際に自分でも、シミュレーションをして、パラメーターを変えて感染状況の変化を確認してみてほしい。

追記:

追記2 (2020-03-27): 上の記述で、「実効再生産数」が適切に使われていなかったので、訂正し、より正確な表現にした。

オープンソース概念の無意識な風化への抵抗

このツイートが目に触れた。これは問題だ。

リプライにぶら下がっているリンクから、<利用ガイドライン>に飛ぶことができた。

本日より放送開始30周年となる2028年7月6日までの間において、以下の「利用ガイドライン」と「利用規約」の両方に同意いただくことを条件に、日本国内に居住されている個人の方に限り、アニメーション作品「serial experiments lain」(以下「本作品」といいます)の二次創作の利用を商用・非商用にかかわらず無償で許諾します。監修を受ける必要もありません。

また、個人の集合体となるファン・コミュニティによるOpen Source Projectであれば法人格を有しない限り、同様に許諾します。

www.nbcuni.co.jp

Open Source という言葉は、1998年に生まれた。パーソナル・コンピュータが大衆化し、人々の相互接続性が次第に高まっていく、そういう時代だ。奇しくも、あるいは必然か、serial experiments lainが発生した年でもある。(私はそれが存在することを知っているが、触れたことはない。)

Open Source は、概念だ。概念はとらえどころのない存在だが、言葉で定義されることで、社会全体に客観的に理解できる、確固としたものとなる。Open Sourceは、Open Source Initiative によって定義されていて、次のページで定義を見ることができる。

opensource.org

日本語で読みたい人は、次の八田真行訳を読んでもいい。

opensource.jp

第5条には、こういう定義がある。

  1. No Discrimination Against Persons or Groups The license must not discriminate against any person or group of persons.

  2. 個人やグループに対する差別の禁止 ライセンスは特定の個人やグループを差別してはなりません。

ライセンスに「法人格を有しない」という条件を課すことは、当然、差別にあたる。これは、私には(おそらく、他のオープンソースソフトウェア・コミュニティーの人間にとっても)明らかにOpen Sourceとは認められない。

オープンソースとは、個人の集合体という意味ではない。個人であろうと、法人であろうと、差別されず参加できる、そういう仕組みを実現するための言葉なのだ。

だいたい、「法人格を有しない」という条件があるのであれば、法人格を有するいちから株式会社の運営するバーチャル配信者グループ「にじさんじ」に所属する、月ノ美兎は、どうなるのだ。月ノ美兎は、個人なのか、法人なのか?

考えれば、法人という概念自体が、組織をバーチャルな人間とみた概念とも言えるだろう。とらえどころのない組織に対してインターフェースとして与えられた、法的にバーチャルな人間、それが法人だ。月ノ美兎自体は、いちからではなく、いちからの運営するにじさんじに所属するバーチャルライバーではあるし、月ノ美兎は、個人であるように見える。しかし、月ノ美兎の活動は © Ichikara Inc. の付く、いちからの知的財産でもあり、月ノ美兎の、月ノ美兎としての活動は、いちからの活動ということにもなるはずだ。

いったい、この状況で、月ノ美兎は岩倉玲音の凸を受けることができるだろうか? いや、そもそも、Open Source の考えは、そんなことで悩ませたりせず、自由な参加を促すためのものだったんじゃないのか?(これはただの私見だし、私は悩むのが好きだけれども)

Open Source 文化は、日本の同人文化やネット文化と似た面もあるが、本質的に異文化だ。おそらく、日本の同人文化が個人と法人の区別による黙認で二次創作を正当化してきた一方、Open Source 文化は明示的なライセンスによる許諾で二次創作を正当化してきたという文化的背景が、利用ガイドラインに無意識に反映されてしまっているのだろう。しかし、自分が Open Source だと言葉にするなら、Open Source 文化を正しく理解するとまでは言わずとも、少しはOpen Source 文化に触れてみてほしい。社会では色々な属性の人間やグループが、それぞれの意思と目的をもって活動しているが、それにも関わらず、Open Source文化では協同することができる。たとえ競合関係にある企業同士でさえ、その経済的原理から Open Source License のもとで協同することができる。(わたしはこういう、ドライで感情を伴わない状態から、原理に従って協力構造が生まれるという構図が、とても好きだ。)Open Source は、そうしてできあがった営みを指している。

lain は真に Open Source となることを願うだろうか。

概念をフォローする

概念をフォローする

本質的に、わたしたちは概念をフォローしたいのである。

あなたがツイッターで、その人をフォローする理由はなんだろうか。その人自身が本当に好きなのだろうか。単に、好みの記事や、写真や、動画をRTしてくれるから、ではないだろうか。実際に、おもしろ動画botのようなものがあり、それをフォローする人間はたくさんいる。

人間ではなく、概念をフォローするには、どうすればいいだろうか。たとえば、ハッシュタグを追うこともできる。ハッシュタグは、すなわち直に概念に結びついた記号だ。しかし、今のツイッターは、話題を検索することはできても、ハッシュタグを直接フォローすることはできない。

ハッシュタグは、書き手が恣意的に設定しなければならないという点で、ウェブページに設定されたキーワードと同じような、簡易な仕組みだ。それは、機械が直接文章から概念を抽出することができない、あるいは抽出するコストが高いということが前提にある。しかし、わたしたちは現に文章から概念を抽出している。それは、全文検索 Full Text Search という技術がやっていることだ。

全文検索システムは、その名からは想像が付きづらいが、直接文字列を検索するのではない。そうではなく、文章を「語」ごとに分割し、語によってデータの転置操作を行い、索引を作成する。そうすることで、語が与えられると、効率的に、語に結びついた文章を引いてくることができる。そして、語はやはり概念に結びついた記号である。つまり、全文検索を実現する段階で、わたしたちは文章から概念を抽出しているのである。

わたしたちは文章からある程度の精度で概念を抽出する技術を持っているのだが、本来は概念をフォローすることができてもおかしくないのだが、そうはなっていない。わたしたちは概念をフォローするのではなく、ボットをフォローしている。しかし、検索システムは任意のフレーズで自由に検索できるのに対し、ボットは限られた概念しかRTをしてくれない。これは非常に不自由なことだ。人間は有限だが、概念は無限だという問題がある。

分散概念検索システム

わたしたちは、分散SNS上で、概念のフォローを実装できるかもしれない。ActivityPubで、人間のかわりに、概念をフォローできる仕組みが作れるはずだ。インスタンスは、各個人のoutboxの公開アクティビティーから概念を抽出し、概念のoutboxに入れる。この概念のoutboxを、フォローできるようにすればよい。また、概念のoutboxそれ自体が転置インデックスの役割を果たし、インスタンスに対する全文検索機能を提供する。概念のoutboxは仮想的に無限に存在するが、その無限のoutboxを一括でフォローする仕組みがあれば、作成された転置インデックスを他のインスタンスに転送することもできる。もちろん、興味がある概念だけをフォローしてもよい。

個人は特定のインスタンスにしか存在しないが、概念はあらゆるインスタンスに存在する。そこで、上の考えの発展として、概念のフォローは、インスタンスを特定しない形式によるフォローができることが望まれる。インスタンスを特定せず、すべてのインスタンスの特定の概念をフォローできるようにする、あるいは、概念のフォローに関する情報を、連合インスタンス全体に伝播させる仕組みがあれば、わたしたちは、透過的に、純粋な概念をフォローすることができる。

人間やボットを介せず、純粋な概念によって構成されたタイムラインを、わたしは見てみたい。

TopShell: シェル再考

GitHubのExplore repositoriesにたまたま表示されていた TopShell が気になったので、ここで紹介する。

github.com

TopShell開発の動機は TopShell: Reimagined Terminal and Shell · topshell-language/topshell Wiki · GitHub に書いてあるが、要点をまとめると「古典的なUnixシェルを使うのはつらい。いいところだけを抜き出して、全くシェルを考えたら、どうなるだろうか?」ということらしい。

  • Unixシェルのだめなところ
    • 未定義の変数を使ってもエラーにならない【デフォルトで。set -u を使えばエラーになる。】
    • コマンドがエラーになってもスルーされる【デフォルトで。set -e とか set -opipefail を使えばエラー時に中断される。】
    • 全部のデータが文字列
      • sedawk の不思議なコードを覚えないといけない
      • 不完全なパーサーを書いて処理することになる
  • Unixシェルのいいところ
    • システムへの実用的でインタラクティブなインターフェースを提供している
    • パイプを使ってコマンドを合成することで、素早く問題を解決できる
  • TopShell
    • 純粋計算と作用(副作用)を分離する純粋関数型プログラミング
    • モダンな型システム
    • 細かいコード断片を書けば、直ちに評価され、結果を見ることができる
      • 結果はテキストのみならず、画像でもアニメーションでもよい
    • 非同期タスクとリアクティブ ストリームによるプログラムの合成
    • これらを努力なしに使うことができる環境

理屈はともかく、TopShellはブラウザから使うことができる。次のリンクからプレイグラウンドを開いて、試してみよう。

http://show.ahnfelt.net/topshell/

https://github.com/topshell-language/topshell#http-example のサンプルコードを画面左側のエディタに入力すると、直ちに評価結果が右側に表示される。といっても、副作用が発生するタスクやストリーム(バインド文 x <- e で宣言されているもの)は、行にカーソルをあわせて Ctrl + Enter で1文ずつ実行するか、実行ボタンを押すまでは実行されない。感覚的には、シェルというより Jupyter Notebook に近い気もする。

json <- Http.fetchJson {url: "https://reqres.in/api/users?page=2"}

people : List {id: Int, "first_name": String, "last_name": String, avatar: String} = 
    Json.toAny json.data

htmlImage = url -> Html.tag "img" [Html.attributes ["src" ~> url]]

peopleWithImages = people |> List.map (
    p -> {image: htmlImage p.avatar, name: p."first_name" + " " + p."last_name"}
)

peopleWithImages |> View.table

f:id:mandel59:20190902225844p:plain
HTTP example

f:id:mandel59:20190902230537p:plain
HTTP exampleの実行結果

ストリームのサンプルとして https://github.com/topshell-language/topshell#stream-example も試してみよう。この例では、時計のアニメーションが動く。

言語仕様についてはまた今度書く。

『新記号論』メモ 記号の正逆ピラミッドとOSI参照モデル

記号の正逆ピラミッドのうち、少なくとも逆ピラミッドは、OSI参照モデルのレイヤー構成と対応すると考えられる。逆ピラミッドは基底部から頂点へ向けて順にアナログ信号/デジタル信号/プログラムとなっているが、これがOSI参照モデルの第1層 物理層が逆ピラミッドのアナログ信号、第2層 データリンク層~第6層 プレゼンテーションまでがデジタル信号、第7層 アプリケーション層がプログラムと、対応している。

OSI参照モデルの規格書 ISO/IEC 7498-1:1994 は、次のリンクからダウンロードすることができる。

Information technology – Open Systems Interconnection – Basic Reference Model: The basic model

OSI参照モデルにおいて、各システムは物理層によって相互接続されている。この層は物理的な信号を流すことができるが、その信号は、減衰するしノイズも混じる。また、数えきれない機器が接続されているから、どのようにして目的の機器に情報を伝達するかという問題もある。OSI参照モデルはその問題を解決し、遠隔地の任意のプロセスどうしが相互に通信できる仕組みのモデルとなっている。

3階層の記号の(逆)ピラミッドと比べて、OSI参照モデルは7階層もあるのは、OSI参照モデルでは各レイヤーごとに通信を実現する上で求められる機能を、各層ごとに細く定義しているからだが、現実にインターネットで使われている通信技術はOSI参照モデルのように行儀よく積み上がっているわけではなく、複雑に組み合わさっているので、その点はOSI参照モデルに縛られず考えてもよい。

とにかく、機械間の通信階層を考える重要なのは、下層で取り扱うノイズが混じったり欠損したりする信号が、各通信階層の提供するエラー訂正やパケットへの分割・再送、輻輳制御といった機能によって、上層では文字列の理想的の転送として見えるようになることが大事だ。すなわち、プレゼンテーション層のレベルでは、ある機器で入力した文字列表現が、遠隔地の好きな機器に、そのままの形で、あたかもテレポートしたかのように出力される。

このようにして確立した文字列の転送の上に構成されているのがアプリケーション層の諸プロトコルで、SMTPやHTTP/1.1のように、このレイヤーの表現は、テキストを伝送する限りにおいては、人間が読んでも容易に理解できるプロトコルも多い。(マルチメディアが伝送されるようになると、話は変わってくる。)

ひとつ問題として、OSI参照モデルが機械間の通信のモデルであり、人間・機械間の通信を図示していない。人間を図に加えるとするならば、どうなるべきだろうか。人間も機器と同様に、物理層で他のシステムと接続されていることは確かだ。機器どうしは電線や無線で接続されている一方、人間は各種入出力デバイスを介して接続されている。そして人間の認知システムを通して、人間はその記号を認識する。この部分の構造は、機械とそれほど変わるようには思えない。記号の正逆ピラミッドは両ピラミッドを逆向きに置いているが、底を揃えて、並置する形で図示してもよいはずだと思う。(下のツイートで言及した、ISO/IEC 7498-1:1994の図のような感じ。)

上述したとおり、プレゼンテーション層の文字列表現は人間が読むことができる。(おそらく、そういった理由でプレゼンテーション層と呼ばれているのではないかと思う。)それは、この層の表現は、人間の思う文字列表象を、文字コード表を相互変換表として使うことで、表現できるように作られているからだ。(プレゼンテーションが表現している文字列表現は、抽象文字に対応するものであって、字形に対応するものではない。原則として、ローマン体とイタリック体の差異といった文字の形の差異は、このレイヤーでは捨象されている。)人間の思う文字列の情報が保存されているから、プレゼンテーション層の表現を(文字コード表を介して)人間がそのまま読むことができるし、その文字列の上に人間や機械が処理するアプリケーション層の言語を実装することができる。(もちろん、現在よく使われているUnicode文字列の処理を安易に容易だというのは憚られるが、Unicodeの処理が難しいというのは技術上の問題以前に、世界中の文字の多様性と複雑さが反映されているからという側面もある。)

マルチメディアを伝達する場合はどうか。例えば音声を伝達する場合は、録音装置が、環境の音をディジタル信号に変換する。それは、文字列(厳密にはオクテット列)だが、文字コード表のかわりに、サンプリング定理を介して、連続的な音声信号と結び付けられている。サンプリング定理は、標本化周波数が、元の信号の最大周波数の2倍より大きい場合に、元の信号が復元できるというものだ。実際のところ、アナログ信号をディジタル信号に変換するには、標本化の他に量子化も行うが、いずれにしろ数値に変換できてしまえば、それは通信で送ることができる。人間も、見た画像や聞いた音声を言語化して伝えることは可能だが、機械が行うそれと比べると、画像や音声の再現度は大きく落ちるだろう。(ただ、たとえば証言から似顔絵を描いて犯人を探すというようなことを考えると、犯人を特定するのに十分な情報が含まれていて、それを他人が認識できればいいわけで、機械の符号化した表現が実用上は過剰だったり、ノイズとして有害に作用する場合も考えられる。)

追記:機械の場合は、文字列表現を正確に転送するという目的から設計された層が基盤にあり、その層を活用して、その上に諸プロトコルが実装されることが多い。(絶対的に文字列表現である必要性はなく、データを細かい塊に分割したデータグラムを基礎としたプロトコルなどもある。ただ、文字列上で、人間が読めるように作られたプロトコルは、流れてくる情報を人間が見て意味が理解しやすいという利点がある。)一方で、人間の表象は機械ほど正確に転送することはできない(たとえば伝言ゲームでも徐々に変わってしまう)

位置の外延的表現・内包的表現の区別と考察ノート

オブジェクトの位置の表現方法には、大きく分けて2種類ある。ここではそれを、外延的表現と内包的表現と呼びわけ、ボードゲームの駒の位置の表現を具体例にして、どのような違いがあるかを考えてみる。

今、将棋盤上においてある、王将の位置を表したい。どのような表現が考えられるか。(王将なので、手駒や成りについては考えないこととする。)

ひとつは、将棋盤を2次元配列として用意し、駒の位置に該当する要素として、駒の識別子を代入するものが考えられる。

// コード1
let 将棋盤 = Array.from({ length: 9 }, () => Array.from({ length: 9 }, () => null))
将棋盤[8][4] = "王将"

コード1を実行した結果、 将棋盤 は次のような配列になる。駒の位置は、将棋盤配列上の特定の要素として駒を表す識別子 "王将" を格納することで表現している。このような表現方法を、位置の外延的表現と呼ぶことにする。

[ [ null, null, null, null, null, null, null, null, null ],
  [ null, null, null, null, null, null, null, null, null ],
  [ null, null, null, null, null, null, null, null, null ],
  [ null, null, null, null, null, null, null, null, null ],
  [ null, null, null, null, null, null, null, null, null ],
  [ null, null, null, null, null, null, null, null, null ],
  [ null, null, null, null, null, null, null, null, null ],
  [ null, null, null, null, null, null, null, null, null ],
  [ null, null, null, null, "王将", null, null, null, null ] ]

これは 将棋盤[8][4] == "王将" である点で次の表現と大差ないので、簡潔にこちらを使って考えてもよい。

{ "8": { "4": "王将" } }

もうひとつの方法では、将棋盤を連想配列として用意し、駒に該当する要素として、駒の座標を代入する。

// コード2
let 将棋盤 = {}
将棋盤["王将"] = [4, 8]

コード2を実行した結果、 将棋盤 は次のようなオブジェクトになる。駒の位置を座標で表現するこの方法を、位置の内包的表現と呼ぶことにする。

{ "王将": [4, 8] }

位置の外延的表現と内包的表現の違いですぐ分かるのは、配列の添字と要素の関係が入れ替わっていることだ。外延的表現では、座標が添字、識別子が要素となっている。一方、内包的表現では識別子が添字、座標が要素だ。

このことは、情報へのアクセスの容易さと関わってくる。外延的表現では、位置からオブジェクトを得ることは簡単だが、オブジェクトの位置を得るには配列をスキャンしなければならない。内包的表現ではその逆で、オブジェクトの位置はすぐ分かるが、どの位置に何があるかは、オブジェクト全体をスキャンする必要がある。両方向での参照を高速に行うために、場合によっては、索引を作る必要が出てくる。

アクセスの容易さの他に、添字の空間の違いもある。外延的表現では添字が座標なので、2次元配列を使っている。一方、内包的表現では添字が識別子であるため、連想配列を使っている。つまり、添字の空間構造次第で、使うべきコンテナデータ構造が異なってくる。配列や連想配列を言語機能として備えている言語は多いが、添字空間が広大だったり連続的であるならば、R木のような構造が必要な場合もあるだろう。

オブジェクト指向のデータ表現では、オブジェクトを基準にデータが凝集されるため、素朴に設計すると内包的表現を選びがちではないかという気がする。しかし、それは空間上の現象を上手く表現することが難しいという問題がある。空間上で隣り合ったオブジェクトに作用するといった処理を書くには、将棋盤というオブジェクトを意識し、外延的表現を使う必要が出てくる。

昔作ったリポジトリがフォークされていた話

昔、SQLiteをWebAssembly向けにビルドする例をGitHubに置いていたんだけど、 github.com

先月ごろフォークされて Uno.sqlite-wasm というリポジトリができていた。 github.com

Unoは、UWPアプリをiOSAndroid, WebAssembly上で動かすプラットフォームらしい。

github.com

デモも公開されている。

https://github.com/nventive/Uno#live-webassembly-apps

練習で作ったリポジトリだったので、ドキュメントやコメントはほぼ無かったのに、よく拾い上げてハックしたな、と思った。まあ、シンプルな構成だからドキュメントがなくても理解できるとも思う。

(当のsqlite-wasm, TypeScriptで書いた部分には微妙なこだわりを出していて、普通に型を付けたらポインタが全部number型になるのを避けるために、前回紹介したPhantom property patternを使っていたりする。)

Phantom property pattern

TypeScriptは、JavaScriptエンジンの動的セマンティクス上に、静的な型システムのセマンティクスが重なっているものです。ここで、JavaScriptとしては正しく実行できても、TypeScriptの型システム上ではちゃんと型が付かないという場合もあります。ときには、TypeScriptで型がつくように大幅に書き換えないといけない場合も出てきたりして、そうなると大変です。それでも、TypeScriptの型システムは高度な機能を持っているので、大体の場合は、うまく表現してやると、JavaScriptらしい書き方のままで型をつけることができてしまいます。今回は、JavaScriptでよくある、プリミティブ値をそのまま取り回すパターンに型を付けたいと思います。

プリミティブ型を扱うプログラミングは、素朴でわかりやすいですが、型の考えからするとかなり悪いものです。こういうプログラミングを行うと、あらゆるデータを文字列で表す様子から、ときに”stringly typed”と揶揄されます。文字列は、表したいものはとりあえず何でも表現できるのでとても便利ですが、いろいろな種類のデータが一つの型で表されてしまうのなら、型が役立たずになってしまいます。すべてがstring型のプログラムで、どうやってその文字列の中身がテキストなのか、URLなのか、メールアドレスなのかを判断するのでしょうか? 文脈を読み込んで察するか、ハンガリアン記法に回帰するか、さもなくばエスパーで当てるしかないでしょう。

この問題に対処するひとつの「まっとうな」方法は、プリミティブ値を直接使わず、適宜ラッパーオブジェクトを作り、その上でプログラミングを行うことでしょう。プリミティブ値ではなくオブジェクトを使えば、メソッドを追加することも容易です。しかし、実際にJavaScriptでそういうことが行われづらい原因として、ひとつはプリミティブ値をコンストラクタでラップするが手間だという点、ひとつは===による同一性比較がオブジェクト型に対しては工夫を加えなければ意味上の同値性比較にならない点、ひとつは性能上の問題点、ひとつはシリアライズ・デシリアライズの自明さが失われる点が挙げられます。要するに、プリミティブ値をオブジェクトで包むのはわりとデメリットもあるわけです。

今ここに、この問題へのもうひとつの対処方法、幽霊プロパティパターン phantom property pattern を提唱します。このパターンを使うと、プリミティブ値の型を好きなだけ定義できる。つまり、言語組み込みのプリミティブ値を取り回すプログラムでありながら、各プリミティブ値を別々の型として取り扱うことができるようになります。

幽霊プロパティパターンでは、まず、次のような型を定義します。

export type Tag<X, R extends keyof any, Y> = X & {
    [rel in R]: Y
}

x: Tag<X, R, Y>は「xはXで、xのRはYである」というような意味になります。

次に、幽霊シンボルを定義します。

// This is a phantom symbol, which does not exist at run time
declare const _unit: unique symbol
export type unit = typeof _unit

_unitは幽霊シンボルで、宣言しますが、実際には定義しません。_unitの型はユニークシンボル型ですが、このままでは型に名前がなく使いづらいので、unitというエイリアス名を付けます。

ここに定義したTag型コンストラクタと幽霊シンボルを組み合わせると、プリミティブ値の性質を幽霊プロパティとして表現できます。

例として、角度の単位を幽霊プロパティとして付加した型を定義してみましょう。

export type Scale<X, U> = Tag<X, unit, U>
export type Radian = Scale<number, "radian">
export type Degree = Scale<number, "degree">

ここで定義したRadian, Degreeは両方ともプリミティブ値の数値を元とする型ですが、実際には存在していない[_unit]プロパティの型が異なるため、相互に代入不可能となっています。

{
    const x: Degree = 30 as Degree
    const y: Radian = x // type error!
    console.log(x, y)
}

いいですね。これを使って、たとえば三角関数双曲線関数に単位をつけてあげるといい感じになります。

export const sin = Math.sin as (x: Radian) => number
export const tanh = Math.tanh as (x: Radian) => number
export const asin = Math.asin as (x: number) => Radian
export const atanh = Math.atanh as (x: number) => Radian

{
    const x = 30 as Degree
    const y = sin(x) // type error!
    console.log(y)
}

弧度法で渡すべきところをうっかり度数法で渡すミスを、これで防げますね。 相互に変換する関数も用意してあげましょう。

// Ref: https://tauday.com/tau-manifesto
const τ = 2 * Math.PI

/** converts radian to degree */
export function toDegree(x: Radian): Degree {
    return (x / τ * 360) as Degree
}

/** converts degree to radian */
export function toRadian(x: Degree): Radian {
    return (x / 360 * τ) as Radian
}

{
    const x = 30 as Degree
    const y = sin(toRadian(x)) // ok
    console.log(y)
}

例をもう一つ、今度はUnix時間とISO8601を扱ってみます。 今度は単位ではなくデータフォーマットの話なので、新しくdataformat型を定義しています。

declare const _dataformat: unique symbol
export type dataformat = typeof _dataformat

export type UnixTime = Tag<Unit<number, "millisecond">, dataformat, "unixtime">
export type ISODateTime = Tag<string, dataformat, "iso8601">
export const now = Date.now as () => UnixTime
export const toISODateTime = (t: UnixTime): ISODateTime => new Date(t).toISOString() as ISODateTime
export const toUnixTime = Date.parse as (d: ISODateTime) => UnixTime
export function isISODateTime(s: string): s is ISODateTime {
    return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(s)
}

{
    const t = 1514810096000 as UnixTime
    console.log(toISODateTime(t))
    const d = "2018-01-01T12:34:56Z"
    if (isISODateTime(d)) {
        console.log(toUnixTime(d))
    }
}

UnixTime型はデータフォーマットだけでなく、単位も幽霊プロパティで付加していますね。一口にUnix時間と言っても、実際にはミリ秒単位のものと秒単位のものが混ざっていることがよくあるのですが、こういう工夫が事故を防いでくれるはずです。

ISODateTime型の方は、型ガード関数を用意しました。こういう関数を用意して使うほうが、直接as ISODateTimeでダウンキャストするよりも安全です。

欠点

  • 本当ならinterface Tag<X, R, Y> extends X { [rel in R]: Y }とでも定義したいところですが、現状のTypeScriptではプリミティブ型を拡張できないため、やむなく交叉型 intersection type で表現しています。
  • 幽霊シンボルそれ自体はexportしないほうがいいでしょう。実在しない識別子なので、アクセスしても型エラーになりませんが実行時エラーになる可能性があります。型レベルで使うだけでシンボル自体はなくてもいいので、シンボルの型に名前を付けて、そちらをexportするといいです。
  • interfaceではないため、スニペットコンパイラのエラーメッセージ等で、型エイリアスが展開されて表示されてしまいます。unique symbolと表示されるだけなのは非常にまずいです。
    • f:id:mandel59:20180901034739p:plain
    • ワークアラウンドとして、幽霊シンボル自体にタグをつけるように書き換えると、なんとか分かるようになります。
      • f:id:mandel59:20180901035321p:plain
declare const _unit: unique symbol
export type unit = Tag<typeof _unit, "name", "unit">
export type Unit<X, U> = Tag<X, unit, U>

declare const _dataformat: unique symbol
export type dataformat = Tag<typeof _dataformat, "name", "dateformat">

まとめ

幽霊プロパティパターンを使うと、実行時コストに影響を与えることなく(ゼロコストで)、プリミティブ値にユーザー定義の型をつけることができるようになります。

2019/08/20追記: この記事を書いた時点では、先行事例があるということをちゃんと調査していませんでしたが、実は"Branding"と呼ばれて、それなりに使われている技法であることを知りました。(TypeScriptのソースコードでも使われています。

以下の記事では、Brandingや、その変形であるFlavoringについて解説されています。 Need Flexible Nominal Typing for TypeScript? Use Flavoring, not Branding