対称集計ってなんなのか

cloud.google.com

Looker の対称集計という機能が気になっているのだけれども、いまいちよく分かっていない。検索してもあまり関連する文献が見つからないし、なんで対称集計という名前なのかも分からない。データウェアハウスで、非正規化されたデータの上で集計をする時に使える概念なのだとは思うのだけれども。

とりあえず、実験してみる。Lookerのドキュメントに載っているデータ例をErqで取り込む。(後々のため、データを追加している。)

load table orders(order_id integer, user_id integer, total real, order_date text) from
```csv
1   100 50.36   2017-12-01
2   101 24.12   2017-12-02
3   137 50.36   2017-12-02
4   100 59.68   2017-12-03
``` delimiter E'\t';;

load table order_item(order_id integer, item_id integer, quantity integer, unit_price real) from
```csv
1   50  1   23.00
1   63  2   13.68
2   63  1   13.68
2   72  1   5.08
2   79  1   5.36
3   78  1   50.36
4   50  2   23.00
4   63  1   13.68
``` delimiter E'\t';;

table data = orders join order_item using (order_id);;

注文の合計金額は簡単に計算できる。

erq> orders {sum(total)};;
select sum(total) from orders
["sum(total)"]
[184.52]
1 row (0.001s)

一方で、結合済みのデータを使って合計金額を計算しようとすると、正しく計算できない。表が結合されるときに、ordersテーブルの各行が複製されているからだ。

erq> data {sum(total)};;
select sum(total) from data
["sum(total)"]
[342.8]
1 row (0.001s)

そこで、ordersテーブルのキーを使って、うまいことやる必要がある。

ひとつの方法は、合計する値にいい感じにキーの情報を埋め込み、sum distinct関数を使って集約を行うことだ。ここでは、単純に、order_idの値を10000倍したものをtotalに加えることで、キーの情報を埋め込んでいる。最後に、order_idだけのsum distinct結果を引けば、order_idで区別されたtotalの合計になるはずだ。

erq> data {sum(distinct total + order_id * 10000) - sum(distinct order_id * 10000)};;
select sum(distinct total + order_id * 10000) - sum(distinct order_id * 10000) from data
["sum(distinct total + order_id * 10000) - sum(distinct order_id * 10000)"]
[184.52000000000407]
1 row (0.000s)

結果は、惜しいことになってしまった。SQLiteでは浮動小数点数を使っているために、大きい数を使って情報を埋め込むと、計算したいtotalの合計値には大きな誤差が出てしまう。

逆に、小数点以下に埋め込むと、誤差はマシになる。(そもそも、金額の計算に浮動小数点数を使うこと自体を避けるべきだろうが。)

erq> data {sum(distinct total + order_id * 1e-10) - sum(distinct order_id * 1e-10)};;
select sum(distinct total + order_id * 1e-10) - sum(distinct order_id * 1e-10) from data
["sum(distinct total + order_id * 1e-10) - sum(distinct order_id * 1e-10)"]
[184.52]
1 row (0.000s)

idを単純に定数倍して足す代わりに、ハッシュ関数に通すなど、複雑な変換を行うこともできるが、異なるキーの値が偶然衝突しないように埋め込めるなら、方法はどうやってもいい。ただ、先ほどの例のように、誤差やオーバーフロー・アンダーフローには気を付ける必要がある。また、キーを埋め込んだ値を集約したあとで、埋め込んだキーの影響を集約結果から打ち消す計算ができる、ということも必要だ。

グループ化をした場合も、同じ方法が使える。ここでは、user_idでグループ化して、合計と平均を計算してみる。

erq> data {user_id => sum: sum(distinct total + order_id * 1e-8) - sum(distinct order_id * 1e-8), avg: avg(distinct total + order_id * 1e-8) - avg(distinct order_id * 1e-8)};;
select user_id, sum(distinct total + order_id * 1e-8) - sum(distinct order_id * 1e-8) as sum, avg(distinct total + order_id * 1e-8) - avg(distinct order_id * 1e-8) as avg from data group by (user_id)
["user_id","sum","avg"]
[100,110.04,55.02]
[101,24.12,24.12]
[137,50.36,50.36]
3 rows (0.001s)

ところで、サブクエリが使えるのなら、こんな面倒なことをしなくても、何段階かに分けて集計を行えば済む話ではある。集計したいカラムを抜き出してきて

erq> data {user_id, order_id, total};;
select user_id, order_id, total from data
["user_id","order_id","total"]
[100,1,50.36]
[100,1,50.36]
[101,2,24.12]
[101,2,24.12]
[101,2,24.12]
[137,3,50.36]
[100,4,59.68]
[100,4,59.68]
8 rows (0.001s)

重複を除外する

erq> data {user_id, order_id, total} distinct;;
select distinct user_id, order_id, total from data
["user_id","order_id","total"]
[100,1,50.36]
[101,2,24.12]
[137,3,50.36]
[100,4,59.68]
4 rows (0.001s)

あとは、それを集計すればよい。

erq> data {user_id, order_id, total} distinct {user_id => sum: sum(total), avg: avg(total)};;
select user_id, sum(total) as sum, avg(total) as avg from (select distinct user_id, order_id, total from data) group by (user_id)
["user_id","sum","avg"]
[100,110.03999999999999,55.019999999999996]
[101,24.12,24.12]
[137,50.36,50.36]
3 rows (0.000s)

(まあ、なんか誤差が出ているけど、これは浮動小数点数のせいだと思うし、気にしないことにする。)

distinctじゃなくて、group byをつかって重複除外を行なってもいい。こちらの方が、行全体ではなくキーの値だけを使って重複を除外するので、計算がより軽いかもしれない。

erq> data {order_id => user_id, total} {user_id => sum: sum(total), avg: avg(total)};;
select user_id, sum(total) as sum, avg(total) as avg from (select order_id, user_id, total from data group by (order_id)) group by (user_id)
["user_id","sum","avg"]
[100,110.03999999999999,55.019999999999996]
[101,24.12,24.12]
[137,50.36,50.36]
3 rows (0.001s)

(SQLiteではgroup by句で指定されていない非集約カラムであるuser_id, totalは、order_idに対して関数従属であるので、select句に出てきても問題がないのだが、これを受け付けないSQLエンジンを使っている場合は、any_value集約関数を使うか、適当にmin集約関数を使うなどの対処をする必要がある。)

とにかく、サブクエリで group by を使って重複を除去しておく方法は素直で分かりやすいし、sumやavgのような集約関数の性質に左右されず汎用的な技法なので、特に制約がなければ、後者の多段階の集約を使った方がいいように思う。BIツールなどでは、集約のフォーマット・段階が固定であるといった制約があるために、前者の方法を使うしかないのかもしれない。

MDNにあるセマンティクスの説明を直したい

developer.mozilla.org

この記事の説明は問題があるので、できれば直したい。直すならGitHubにissueを出すのが筋だと思うが、いきなり英語で書くのは大変なので、とりあえず日本語で問題点をまとめておく。

プログラミングでは、セマンティクスとは、コードの断片の意味を指します。たとえば、「JavaScript でその行を実行すると、どのような効果があるのか?」、「その HTML 要素には、どのような目的や役割があるのか?」 (「どのように見えるのか?」ではなく)。

最初の、この説明は、とりあえずOKだと思う。細かい表現にケチをつけられるかもしれないけど。セマンティクスとはコードの意味のことで、その非形式的な実体としては、プログラムを実行した際に起きるエフェクトだとか、それが繋がっている目的や役割などだと言えるだろう。(言語学なんかでは、コンテキストから独立した意味を考える意味論と、コンテキストと意味の関係を論じる語用論とを分けるかもしれないが、ここでは、特に両者を区別していない。)

しかし、その後につづく、JavaScript, CSS, HTMLのセマンティクスについての個別の説明は、だいぶあやしい。

JavaScript でのセマンティクス

JavaScript において、文字列の引数を取り、その文字列を textContent とする <li> 要素を返す関数を想像してみてください。 build('Peach') または createLiWithContent('Peach') という関数名の場合、何をするのかを理解するためにコードを見る必要があるのはどちらでしょうか?

この説明は、どうやってコードをリーダブルにするか、という話をしている。コードのリーダビリティは、コードのセマンティクスとは関係がない。

最初の説明通り「JavaScript でその行を実行すると、どのような効果があるのか?」というのが、JavaScriptのセマンティクスだ。関数の名前をbuildにしようがcreateLiWithContentにしようがaaにしようが、それはJavaScriptで書かれたプログラムの動作に、ほぼ影響がない1。識別子にどう名づけるか自体はプログラムの動作に影響しないし、だからこそ、minifierのようなツールを使って識別子を短いものに書き換えることで、JavaScriptプログラムを小さくすることができる。

JavaScriptのセマンティクスは、主に次のような文書で規定されている。

HTML でのセマンティクス

HTMLのセマンティクスに関しては、OKな説明であるとは思う。ただ、最新のHTML仕様に合わせた説明に書きかえると、もっといいと思う。

HTML仕様 1.8 HTML vs XML syntax で、文書やアプリケーションを記述する抽象言語と、情報交換用の具体構文として「HTML構文」「XML構文」が規定されている。これらの構文によって記述されたHTML文書は、ファイルに保存されたり、HTTPで送信されたりする。

これらのHTML文書は、ブラウザーで処理される際、メモリ上では、「DOM HTML」というより抽象的な形に変換される。HTML構文については13 The HTML syntaxで、XML構文については14 The XML syntax で、それぞれDOM HTMLに変換する方法が規定されている。

HTMLのセマンティクスは 3 Semantics, structure, and APIs of HTML documents で定められている。それは、厳密に言えば、ファイルに保存されたり送信されたりするHTML構文やXML構文に対してではなく、より抽象的な内部表現であるDOM HTMLに対して、それぞれの要素、属性、属性値のセマンティクスを規定している。

Elements in the DOM represent things; that is, they have intrinsic meaning, also known as semantics.

For example, an ol element represents an ordered list.

https://html.spec.whatwg.org/multipage/dom.html#represents

要素はそれぞれ、なにか物事 (thing) を表現 (represent) している。たとえば、ol要素は、順序付きリストを表現している、ということが、仕様として書かれている。

また、HTMLの各要素にはAPIが定義されていて、そのAPIを介して、要素の機能に関する操作を、JavaScriptから行える。たとえば、<button> 要素はボタン操作に関するAPIを提供しているし、<video> 要素はビデオ再生に関するAPIを提供している。こういったAPIに関する規定は、仕様のいう "intrinsic meaning" や "semantics" というよりは、それをブラウザー上で実現するための機構のようなものといえる。しかし、JavaScriptのようなプログラムの、プログラムの振る舞いがセマンティクスだという考えからすれば、DOM HTMLのAPIも、一種のセマンティクスだと考えられるだろう。言語的な、コードによって表現される物事を指すセマンティクスと、プログラム的な、コードによって実現される機構の振る舞いを指す「セマンティクス」の両方が、仕様に規定されていることになる。

HTMLのセマンティクスは、ブラウザーの画面上での具体的な見た目(プレゼンテーション)を規定していない。HTMLの見た目は、HTML自体ではなく、HTMLに関連づけられたスタイルシートによって決まっている。

CSS でのセマンティクス

CSS において、さまざまな種類の果物を表すために li 要素でリストをスタイル付けすることを想像してみてください。 div > ul > li または .fruits__item で選択された DOM の一部が何であるか分かるのはどちらでしょうか?

本来であれば、「CSSでのセマンティクス」とは、やはり「CSSのコードがどのような意味を持つのか?」といった部分を説明するべきだろうと思う。ここでのMDNの説明は、セマンティクスではなくて、よりよいCSSを書くためのプラクティスの説明でしかない。

div > ul > li という指定は、「div要素の子ul要素のさらに子のli要素」を指定している。一方で、.fruits__itemは「class属性がfruits__itemの要素」を指定している。どちらも意味のある記述であり、どちらの方がどちらもCSSとしての意味がある。要素の意味はHTML仕様としておおむね規定されているが、クラス(class属性の値)の意味に関しては、そのHTMLの書き手が勝手に決めていることだ。class属性はHTML要素の意味を拡張する機構で、たとえばMicroformatのような仕様が、HTML仕様で規定されない意味を表現するために使っている。しかし、そういう仕様で規定されていない、独自クラス fruits__item が厳密にどのような意味をもっているのかを知ることは難しい。人間は(あるいは今日のLLMは)fruits__itemというクラス名に埋め込まれた英単語の意味を解釈できるかもしれないが、それはCSS自体のセマンティクスではなく、英単語の意味を解釈している。CSS意味の先にある人間の意図を説明しているか、という観点では、差があるだろう。ただ、それは、パスを使ったからセマンティックでないとか、クラス名だからセマンティックだ、ということにはならない。これが ul > li というセレクターだったら、「ul要素の子li要素」という意味の先にある「順序なしリストのリストアイテム」という意図は明瞭だろうし、逆に、.aaaa のようなセレクターだったら、クラス名を使っていても意図を読み取ることはできない。また、ulやliのような要素の意味はHTML仕様で規定されているが、fruits__itemクラスの要素がどのような意味であるのかを知るには、どうにか推測するしかない。

ここに書くことは特に個人的な考えになるとは思うのだけれども、CSS自体は、あまり「セマンティック」な仕組みではないかもしれない。というのも、CSSは、DOMに対して構文的にマッチし、マッチした要素に対してCSSプロパティを割り当てる機構だからだ。CSSの処理の仕組み上、HTMLの各要素が本来持っているセマンティクスは、無視して処理することになる。だから、HTMLの意味上は同じ意味になるはずでも、CSSは異なるスタイル付けをしてしまう。たとえば <div> 要素の意味は "The div element has no special meaning at all. It represents its children." のように定まっている。だからといって、CSSは<div>要素を無視して処理したりはしない。

HTML のセマンティクスの拡張

先述のように、HTMLではclass属性を使って、HTML自体に規定されていない要素の意味を表現することができる。その他に、意味の拡張を行いたいのであれば、カスタム要素を使うこともできる。カスタム要素は、class属性を使った場合と違って、カスタム要素のAPIの提供が可能になるので、HTML仕様に規定されているプリミティブの要素と近い形で、カスタム要素を規定し、実装し、利用することができる。

その他、HTMLの意味を拡張・変更しているものに ARIA in HTML がある。HTMLのセマンティクスでは、アプリケーション操作中の遷移的なステートを表現しきれていないことがある。たとえば、トグルボタンを実装しようとしたとき、たとえ、HTMLのbutton要素と、JavaScript、CSSによってトグルボタンの視覚表現を実装したとしても、そうした視覚表現が使えないユーザーにとっては、アクセシビリティーの欠如したアプリケーションになってしまう。そういう、HTMLレベルでは欠如している情報をDOM HTML上で表現するため、ARIAではセマンティクスを拡張しており、トグルボタンの例でいえばaria-pressed属性によって、トグルボタンのステートを表現し、支援技術を介して、より多くのユーザーがアプリケーションを利用することができる。


  1. ほぼ、というのは、JavaScriptには関数の名前を取得する方法が用意されていて、そういう名前の取得を行う処理を含んでいれば動作が変わるから。

記号と通信に関する抽象階層モデルのメモ

情報科学・技術と記号論を関連付けようとする試みは、いくらか存在するが、個人的には、いままで提示されているモデルで納得がいくものはあまり多くない。既存のモデルは、断片的だったり不正確だ。ここでは、より正確であるが、詳細になりすぎないモデルをメモしておく。

情報システムは、次の4つの世界観に分けて、考えることができる。低次のレイヤーから順に、

  • 物理界=場の世界
  • 通信界=信号の世界
  • 計算界=表現の世界
  • 人間界=意識の世界

と考える。

物理界は、情報システムをそのように物理的な系として解釈した世界観だ。人間や計算機は、センサーやアクチュエーターなどを介して物理的な外部環境と相互作用している。そうした過程のすべての現象は、計算過程や通信過程も含めて、実際にはすべて物理的な法則に従っている。物理界では、人間の知覚しがたい現象が作用しあい、混沌としていて、高度な数理モデルによって記述される。

通信界は、宇宙のうち、秩序だって信号を伝達できる部分を抜き出した世界観だ。そこには、伝達が望まれるシグナルと、伝達が望まれないノイズの区別が存在している。その区別は、すべてを場として扱う物理界には無かったものだ。ノイズを効果的に除去する変調技術によって通信路は延長され、現代では地球的な通信網が形成されている。

計算界は、通信によって結ばれ、表現を交換するエンティティからなる世界観だ。エンティティどうしは物理的には隔てられているが、通信網によって接続されていることで、計算界においては、直接的に接しているように見える。通信は、表現を信号に変換し、物理場を介して伝達し、また表現に戻す。これによって、通信路自体はほとんど透明であって、路というより面のように見える。これがインターフェースであり、エンティティはインターフェースを介して表現を交換しあっている。エンティティは、表現をなんらかの法則で別の表現に変換し、またインターフェースを介して交換している。

人間界は、エンティティが意識しているオブジェクトからなる世界観だ。人間は意識的に環境と接し、オブジェクトを認識する。計算界より下の世界の多くの現象、たとえばエンティティやインターフェースや表現といった計算界の現象は、無意識の存在になっている。コンピューターが処理する多くの計算は人間に意識されないが、一部の計算は、人間が意識できるオブジェクトとして提示される。

ひとつの情報システムは、こういった4つの世界観によって見ることができる。重要なのは、情報が伝達される過程を、場の世界から論じることも、心の世界から論じることもできるということだ。つまり、情報は、物理的には電場として存在しているが、それが通信上は電圧の高低によって記録・伝達される111001100101011のようなデジタル信号だったりする。これをコンピューターは、29483という数値として処理したり、あるいは"猫"という文字として処理したりする。しかし、コンピューターのそのような計算はプログラムされた機構であって、コンピューター自体は信号を数値や文字として意識することはなく、ただ機械的に計算しているだけだ。29483という数値に大小の感慨を抱くこともないし、"猫"という文字を処理しているからと言って、猫という動物を連想したりはしない。逆に、「猫」という文字を読んだ人間の多くは動物の猫に思いを馳せるのであって、この「猫」という文字自体について考え出す人は少ないという点で、人間は認知機能としては「猫」という表現を確実に処理しているにも関わらず、表現自体は意識にのぼりづらいと言えるだろう。

情報システムの見方をこういった世界観のレイヤーに分けることで、次のようなことが言えるかもしれない。

  • 高次の世界の現象は低次の世界でも解釈が可能だが、その逆は成り立たない。人間も計算機も(還元した結果を計算できるかはともかく)物理現象に還元して解釈できる。しかし、物理現象のなかで通信として解釈できるのは一部の現象だけであり、計算として解釈できるのはさらに少なく、人間の意識に昇ってくる現象はそのほんの一部分だ。

  • ヒトはそれ自体で人間的にも計算的にも通信的にも物理的にも解釈が可能なエンティティだ。人間自体も物理現象に還元されるし、通信現象に還元されるし、計算現象に還元される。人間と機械を区別せず計算を行うエンティティと考えても問題がない。

  • 人間と人間、人間と機械、機械と機械といったインターフェースでやりとりされる表現は異なる。具体的にどのような表現を使うかは、当然、情報技術分野で詳しく検討されているので、情報技術に記号論的な考察を加えるのであれば、そういった表現の体系を適切に説明できるようなものでなければならないはずだ。

  • プログラミングがなぜ難しいのかと言えば、その一因として、ふだん人間が意識しない計算界について意識せざるを得ないから、ということがあるだろう。そして、計算について意識をするようになったプログラマーも、通信に関してはあまり意識をしていないかもしれない。しかし、適切な方法でプログラムを行えば、通信を意識させずに計算をプログラムすることができる。同様に、計算の大部分を隠蔽して、意識する計算を抽象的なものに絞ることで、プログラムは人間の手に負えるものになる。

抽象クラス、インターフェース、Kotlinの記法

抽象クラスは未定義の抽象メソッドがあるクラスで、派生クラスで実装を行うことで、インスタンス化が可能になる。

abstract class AC protected constructor() {
  abstract protected fun amX(): Int
  abstract protected fun amY(): String
  fun cmZ() = "${this.amX()} ${this.amY()}"
}

val ac = object : AC() {
  override fun amX() = 42
  override fun amY() = "abc"
}

この種類の継承は、委譲に置き換えることができる場合がある。抽象メソッドを抜き出してインターフェース化し、そのインターフェースをコンストラクタにとるクラスで具体メソッドを実装する。

interface IC {
  fun amX(): Int
  fun amY(): String
}

class CC(val ic: IC) {
  fun cmZ() = "${ic.amX()} ${ic.amY()}"
}

val cc = CC(object : IC {
  override fun amX() = 42
  override fun amY() = "abc"
})

抽象メソッドを実装する具体メソッドから、スーパークラスの他のメソッドを呼び出すケースでは、こういう種類の変換はできないが、そういう、親クラスのメソッドが子クラスのメソッドに依存しつつ、子クラスのメソッドが親クラスのメソッドに依存するという、相互依存の関係になっているケースは、多くの場合は必要がないし、避けられるなら避けたいケースでもあるので、積極的に継承から委譲へ置き換える動機がある。

しかしながら問題として、継承から委譲へ置き換えたときには、余分な識別子が増えてしまっている。もとのコードでは抽象クラスACだけを定義すればよかったところが、新しいコードでは、インターフェースのICとクラスのCCに分かれてしまっている。もちろん、クラスの内部でインターフェースを定義することで、名前空間に散らからないよう工夫することはできる。

class CC(val ic: IC) {
  interface IC {
    fun amX(): Int
    fun amY(): String
  }
  fun cmZ() = "${ic.amX()} ${ic.amY()}"
}

Kotlinの記法上の問題として、匿名オブジェクトの記法がラムダ式に比べてはるかに不格好という点がある。もし、インターフェースのメソッドが1つだけなら、これは関数インターフェースとすることができる。

class CC1(val ic: IC) {
  fun interface IC {
    fun amX(): Int
  }
  fun cmZ() = "${ic.amX()}"
}

val cc1 = CC1 { 42 }

メソッドが1つの場合と2つの場合で記法上の相違がここまで大きいのはわりと理不尽で、記法上の制約が設計上不自然な選択を取らせる可能性がある。たとえば次のように、関数を複数コンストラクタで取るような形に変えるという手法で、記法の煩雑さを迂回するかもしれない。

class CC2(val amX: () -> Int, val amY: () -> String) {
  fun cmZ() = "${amX()} ${amY()}"
}

val cc2 = CC2(amX = { 42 }, amY = { "abc" })

この方法は、もとの記法より簡潔だが、問題として、評価のタイミングがある。コンストラクタの引数は、呼び出し時点で評価されてしまうから、関数ではなく即値で渡すようなシグネチャにしてしまう可能性がある。

class CC3(val pX: Int, val pY: String) {
  fun cmZ() = "${pX} ${pY}"
}

val cc3 = CC3(pX = 42, pY = "abc")

こう定義してしまうと、あとからゲッターメソッドの実装を変えて、評価タイミングをコントロールすることが厳しくなるから、やはり適切にインターフェースを使うことが望ましい場面も存在するように思われる。

class CC4(val ic: IC) {
  interface IC {
    val pX: Int
    val pY: String
  }
  fun cmZ() = "${ic.pX} ${ic.pY}"
}

val cc4 = CC4(object : CC4.IC {
  override val pX = 42
  override val pY by lazy { println("computed!"); "abc" }
})

これが

val cc4 = CC4 {
  val pX = 42
  val pY by lazy { println("computed!"); "abc" }
}

ぐらい簡単に書けたらいいのだけれども。

それか、メソッド引数のほうをどうにかして、評価戦略を変えられるようにするとか? まあ、メソッド呼び出しの形式で書いたものが正格評価・値渡しになっていないのは、いろいろと言語の前提とかメンタルモデルとか壊していそうではある。 Ceylon言語のLazySpecifierみたいな概念を導入して、もう文法は適当なんだけど、

class CC5(lazy val pX: Int, lazy val pY: String) {
  fun cmZ() = "${pX} ${pY}"
}

val cc5 = CC5(lazy pX = 42, lazy pY = { println("computed!"); "abc" })

とか? まあ、なんかもっとマシな記法でできるといいんだけど

「IPAフォントライセンスを巡って」について思うところ 2

前から時間が空いてしまったけど、サブセットフォントは包摂基準を変えるのか、というところについて書く。

IPAフォントライセンスを巡って | 一般社団法人 文字情報技術促進協議会

フォントがどうだろうと包摂基準には関係しない

包摂基準というのは、文字図形を、符号化にあたって同定するための基準だ。要するに、文字図形→文字コードという変換のルールだ。一方、フォントは、文字を描画するためのプログラムで、文字コード→文字図形という、逆向きの変換を行う。

フォントは描画に関わるプログラムであって、符号化に関わるルールではない。そのようなルールは規格書に書かれているものであって、フォントの中に包摂基準は含まれていない。それゆえに、フォントをサブセット化するなどの改変を行ったからといって、包摂基準が変わるということもありえない。

フォントのサブセット化で包摂基準が変わると思うとしたら、それは文字コードの規格書とフォントをちゃんと区別していないということではないだろうか。

IPAmj明朝フォントと文字情報基盤は違う

フォントは、それ自体が包摂基準ではない。包摂基準のような概念は、フォント実装の外部に存在する。そのような概念を共有するのは、規格書の役割だ。たとえ規格書のコードチャートが特定のフォントで印刷されているとしても、フォントと規格は違うものだ。

実際のところ、文字情報基盤の規格書というものが存在して、正式に公開されているというわけではない。公開されているのは、OpenXML形式のMJ文字一覧表と、IPAmj明朝フォントだ。MJを利用するためには別途符号化のルールを規定した規格書が要るはずだが、簡単な利用ガイド程度の資料しか用意されていない。現状の文字情報基盤がこのような形態だから、文字情報基盤とIPAmj明朝フォントの違いが、よく分からなくなっている。

文字情報基盤を規格として捉えた上で、フォントの規格適合性を保証・認証できるようにしたい、というモチベーションがあるのはわかる。ただ、それを、フォントのライセンスであるIPAフォントライセンスでやろうとするのは、筋が悪すぎる。そもそも、IPAフォントライセンスは文字情報基盤整備事業の前にできたという時点で、派生フォントが規格に適合するかどうかということはIPAフォントライセンスと何も関係のない話だ。

包摂基準などを規定した仕様書が正式に策定されていないというのは、文字情報基盤自体の問題であるはずだ。たとえIPAmj明朝フォントの利用をライセンスで制限しようと、文字情報基盤の問題が解決するわけではない。それよりも、文字情報基盤の、文字コード規格としての不完全さを解消するべきだろう。

「IPAフォントライセンスを巡って」について思うところ 1

IPAフォントライセンスを巡って | 一般社団法人 文字情報技術促進協議会

IPAフォントライセンス1は、Open Source Initiative (OSI) から、The Open Source Definition (OSD) 2に準拠しているという認定を受けているという触れ込みだった。3 だから、ライセンスを理由にMJ明朝体フォントを利用したサービスを差し止めるというのは不可解なことに思える。いったい、これはどういうことなんだろうか。この記事を読んで、次のような疑問が浮かんだ。

  1. IPAmj明朝をWebフォントとして利用することは、IPAフォントライセンス上問題となるのか。
  2. IPAmj明朝をWOFF化するサービスは、IPAフォントライセンス上問題となるのか。
  3. サブセットフォントは包摂基準を変えるのか。

以下では、上記疑問のうち、1と2について考えてみたい。3については、別の記事にする。

以下、IPAフォントライセンスの条項を指す場合は、IPAフォントライセンスの表現に従い、列挙された項目を号ではなく項で表現する。また、OSD条文は八田真行(mhatta)による「オープンソースの定義」の日本語訳4に基づく。

1. IPAmj明朝をWebフォントとして利用することは、IPAフォントライセンス上問題となるのか。

IPAmj明朝が、複製その他の利用をされるケースとして、次の3つがある。

  1. 派生プログラムを再配布する
  2. 許諾プログラムをそのままの状態で改変することなく再配布する
  3. デジタル・ドキュメント・ファイルについて複製その他の利用をする

ケース1は2条4項または2条7項に該当して許諾が付与されるケースで、3条1項の制限を満たす必要がある。ケース2は2条6項に該当するケースで、3条2項の制限を満たす必要がある。ケース3は2条2項、3項および5項に該当するケースで、このケースでは3条1項および2項の制限を満たす必要がない。

Webフォントとしての利用がいずれのケースに該当するにしても、それぞれのケースについて必要な制限を満たしていれば、とうぜんWebフォントとして利用可能だ。

3条3項および4項に示されているのは免責条項なので、以下議論しない。

ケース1 派生プログラムを再配布する。

1のケースに該当する場合は条件が多い。

  • 3条1é …(1)「派生プログラムを再配布する際には、下記もまた、当該派生プログラムと一緒に再配布され、オンラインで提供され、または、郵送費・媒体及び取扱手数料の合計を超えない実費と引き換えに媒体を郵送する方法により提供されなければなりません。」該当するデータを提供すればよい。
  • 3条1é …(2)「派生プログラムの受領者が、派生プログラムを、このライセンスの下で最初にリリースされた許諾プログラム(以下、「オリジナル・プログラム」といいます。)に置き換えることができる方法を再配布するものとします。かかる方法は、オリジナル・ファイルからの差分ファイルの提供、または、派生プログラムをオリジナル・プログラムに置き換える方法を示す指示の提供などが考えられます。」この条項は、具体的に何を指して「置き換える」と言っているのかがよく分からない。アプリケーション上での表示フォントを置き換える、という意味であれば、サブセット化したWebフォントとローカルにインストールしたIPAmjフォントとで、表示を切り替えることができるようにする機構を入れておけば済むと思われる。
  • 3条1é …(3)「派生プログラムを、本契約書に定められた条件の下でライセンスしなければなりません。」ライセンスすればよい。
  • 3条1é …(4)「派生プログラムのプログラム名、フォント名またはファイル名として、許諾プログラムが用いているのと同一の名称、またはこれを含む名称を使用してはなりません。」該当箇所を適当にリネームすればよい。
  • 3条1é …(5)「本項の要件を満たすためにオンラインで提供し、または媒体を郵送する方法で提供されるものは、その提供を希望するいかなる者によっても提供が可能です。」制約に関する条項なのに、文が「可能です」で終わっているのは不可解だし、意味がとれない。「その提供を希望するいかなる者」というのは、提供を行うことを希望する者なのか、それとも提供を受けることを希望するものなのか、それも分からない。3条1é …(1)で示されている提供されなければならないものの提供方法はその提供の主体を問わず、他者から提供を受けることができるようにすればそれで条件を満たしているという但し書きなのだろうか。

3条1項(2), 3条1項(5)の意味に不可解な部分はあるが、満たすことは可能な条件だと思われる。

ケース2 許諾プログラムをそのままの状態で改変することなく再配布する

  • 3条2é …(1)「許諾プログラムの名称を変更してはなりません。」
  • 3条2é …(2)「許諾プログラムに加工その他の改変を加えてはなりません。」
  • 3条2é …(3)「本契約の写しを許諾プログラムに添付しなければなりません。」

「本契約の写しを許諾プログラムに添付」の部分の解釈が微妙だ。これがZIPアーカイブ化であれば、同一アーカイブ内にライセンス本文が含まれていれば、これは確実に添付したと言えるだろう。では、OpenTypeフォントファイルをオリジナルの形式でWebサーバー上にアップロードしてある場合はどうだろうか? フォントファイルをWebフォントとして参照するページ自体に許諾プログラムが記載されていれば、それは添付と言えるだろうか? あるいはライセンス条文へのリンクが記載されている場合はどうか? ここで、アーカイブに含まれているのは添付だが、ライセンス条文へのリンクは添付ではないということになると、それは直感的には不条理な制限だし、OSD 10条「ライセンスは技術中立的でなければならない」に抵触するかもしれない。ここの部分が明確であれば、OpenTypeフォントをサブセット化せずそのままウェブフォントとして使うことは、容量の問題は存在するにせよ、技術的に可能なはずだ。

ケース3 デジタル・ドキュメント・ファイルについて複製その他の利用をする

このケースに該当する場合は、上記3条1項および2項の制限に従う必要がない。

WebアプリケーションにおけるWebフォントとしての利用は、どのケースに該当するのか

ライセンスでは、デジタル・コンテンツおよびデジタル・ドキュメント・ファイルは以下のような定義になっている。

  • 1条4項「「デジタル・コンテンツ」とは、デジタル・データ形式によってエンド・ユーザに提供される制作物のことをいい、動画・静止画等の映像コンテンツおよびテレビ番組等の放送コンテンツ、ならびに文字テキスト、画像、図形等を含んで構成された制作物を含みます。」
  • 1条5項「「デジタル・ドキュメント・ファイル」とは、PDFファイルその他、各種ソフトウェア・プログラムによって製作されたデジタル・コンテンツであって、その中にフォントを表示するために許諾プログラムの全部または一部が埋め込まれた(エンベッドされた)ものをいいます。フォントが「エンベッドされた」とは、当該フォントが埋め込まれた特定の「デジタル・ドキュメント・ファイル」においてのみ表示されるために使用されている状態を指し、その特定の「デジタル・ドキュメント・ファイル」以外でフォントを表示するために使用できるデジタル・フォント・プログラムに含まれている場合と区別されます。」

Webアプリケーションは、定義上は「デジタル・データ形式によってエンド・ユーザに提供される制作物」に該当するから「デジタル・コンテンツ」であるし、それにフォントを埋め込めば、それは「デジタル・ドキュメント・ファイル」となると思われる。しかし、これは本来ライセンスが意図した定義ではないかもしれない。すなわち、Webアプリケーション自体はとうぜん制作物であるにしろ、それによって表示されるテキストはWebアプリケーションに由来するとは限らず、たとえばチャットアプリとして、送信されたテキストをIPAmjフォントで表示するというようなケースでは、アプリに含まれないテキストを表示するためにフォントが使われることになる。そのようなアプリケーションは、制作物でありながらメディアなのだから、IPAフォントライセンス制定者の意図としてはケース3から除外したいもののように推察されるが、それにしては、ライセンス上の定義は不適切だと思われる。

「デジタル・コンテンツ」および「デジタル・ドキュメント・ファイル」の定義がそもそも不適切なので、フォントが埋め込まれたWebアプリケーションはおよそケース3に該当してしまうという解釈はできる。しかし、そう解釈しないにしても、本来想定されていたであろう外部のテキストの表示にフォントを用いない、真に「デジタル・ドキュメント・ファイル」と呼べるWebアプリケーションも存在する。そもそも、HTMLというのはアプリケーション以前に、文書を表現するためのフォーマットであるはずなのだから、Webアプリケーションのうち、真に「デジタル・ドキュメント・ファイル」であるものは存在する。

WebフォントがHTMLファイルの外部に保存されていて、そのファイルにリンクされている場合は、条文中の「エンベッド」に該当しない可能性も残ってはいる。しかし、Webフォントをdata URLの形でHTMLファイル中に埋め込むことも可能であり、この場合は「エンベッド」に該当することに争う余地はないだろう。

(そもそも細かいことを言えば、リンクの場合はエンベッドに該当しないという解釈を取ることは、どうなのだろう? HTMLファイルにdata URLをを埋め込むことと、単なるリンクにすることの間で、フォント抽出の難しさはほとんど差がなく、別の文書での再利用も簡単だ。ここで、data URLの場合は埋め込みで、リンクの場合は埋め込みではないということにする合理性はどれほど存在するだろうか?)

2. IPAmj明朝をWOFF化するサービスは、IPAフォントライセンス上問題となるのか。

IPAmj明朝のWOFF化は、ケース1に該当するのだから、3条1項の条件を満たせば、IPAmj明朝をWOFF化するサービスは可能だと思われる。そのサービスから提供されたWOFFフォントをWeb文書に埋め込んで利用する場合、これはケース3に該当し、3条1項および2項の条件によらず、利用できる。このとき、単にWOFFファイルへのリンクを埋め込む形では文書への埋め込みに該当しないという解釈がありうるが、バックエンドサーバーでWOFFサブセット化サーバーからフォントを取得して文書に埋め込むという処理を行うことでより確実にケース3に該当する形でWebフォントを利用できる。そもそも、今のブラウザーの仕組みではセキュリティ上の問題で、異なるサイト5 6間ではもはやWebフォントのキャッシュは共有されない7 8のだから、リンクを使ったWebフォント提供サービスというのは、もはやキャッシュ効率の点では価値がないので、その文書・そのページで使われるフォントを直接HTMLに埋め込む(インライン化する)という選択を取ることもあるだろうと思う。

MJ文字情報一覧 部首・内画数の誤りについて

MJ文字情報一覧の部首・内画数に誤りがあるので、現時点で気づいているものをここにメモしておく。(部首が異体字の部首になっているもののうち、内画数と総画数が一致しているものを含む。内画数に-が指定されているものは含んでいない。)

MJ000026
㐭
亠部6画
广部8画

MJ000499
㙄
土部7画
阜部10画

MJ000946
㠯󠄁
人部5画
己部2画

MJ001318
㦲
口部8画
戈部4画

MJ003904
ä‘‘
人部18画
臣部12画

MJ003925
䑨
木部11画
舟部5画

MJ006832
倐󠄂
人部9画
犬部11画

MJ007269
å…©ó „‚
人部6画
入部8画

MJ007496
刦
刀部5画
力部7画

MJ007952
厩󠄊
厂部12画
广部14画

MJ009348
壐󠄂
玉部17画

MJ010288
ð¡­—
小部2画
爻部1画

MJ010857
帰󠄄
刀部8画
巾部7画
彑部7画
止部10画

MJ013046
æ–‹
文部6画
示部10画

MJ016562
熔󠄃
火部10画
金部14画

MJ016795
牕󠄂
片部11画
穴部15画

MJ019072
稉󠄂
禾部7画
米部12画

MJ019186
穤
禾部14画
米部19画

MJ019768
籠󠄃
竹部16画
而部16画

MJ019851
粦󠄂
火部12画
米部6画

MJ021301
臯
白部12画
自部6画

MJ021531
芔󠄂
十部9画
艸部3画

MJ023019
藁󠄂
禾部18画
艸部14画

MJ024216
覊󠄂
网部25画
襾部19画

MJ024997
貟
口部9画
貝部2画

MJ026090
遡
水部14画
辵部10画

MJ026250
邨󠄃
木部7画
邑部4画

MJ028027
鞸
革部11画
韋部20画

MJ028062
韌󠄂
革部12画
韋部3画

MJ029194
鯖󠄃
宀部16画
魚部8画
鹿部8画

MJ029360
é±’
魚部12画
麻部12画

MJ029862
麿󠄃
魚部7画
麻部7画

MJ030124
龍󠄇
立部0画
龍部0画

MJ033043
𡌛󠄂
里部10画

MJ059404
ð­…¼
邑部10画

MJ059587
𫝸󠄀
彡部9画
立部4画

MJ059875
𤏁󠄂
火部13画
日部13画

MJ060005
𮄂
水部13画

MJ060014
𮄿
立部30画

MJ060316
ð«•…
阜部8画
足部11画

㋿のヒストリー

ハックとデジタル社会

コンピューターで作られたシステムに問題があって、困っているとしよう。システムを作った人はここにいないし、一から作り直す時間はなかったり、そもそも触る権限がなかったりする。そういうシステムを、どうにかうまく使えるようにすることを、ハックという。

ここでいう「ハック」は、コンピューターを壊すとか、システムに侵入するという意味ではないし、単純にコンピューターに精通するというニュアンスでもない。責任の境界を超越して、システムをうまく動くようにしてしまう行為のことを言っている。いいかえれば、他人に依頼して直してもらうめんどくささを回避して、自分でシステムを直してしまうことだ。

たとえば、macOSの標準ブラウザーのSafariには「サイト固有のハックを無効にする」という開発者向けの設定項目がある。これは、どういう意味だろうか?

世の中のWebサイトは、コンテンツを配信するWebサーバーと、それを受け取って表示するブラウザーの組み合わせでできている。Webサーバーとブラウザーでどうやってやりとりするかは取り決めがあって、それに従うことで、システムは機能している。だけど、ばあいによっては、Webサーバーとブラウザーのどちらかに問題があって、Webサイトが壊れる場合がある。

もしサーバーに問題があるとしても、Safariを使っている人はSafariが壊れていると思うだろうし、Safariを作っている人たちが世の中のWebサーバーを直してまわるわけにもいかない。手っ取り早く、サイトごとに特殊な取り扱いを追加することで直してしまうことを、ここでは「サイト固有のハック」と呼んでいるのだ。

Safariの開発者向け設定画面。「互換性:サイト固有のハックを無効にする」という設定項目がある。(オレンジ色で囲って強調した部分)

人間社会は責任の境界がある。そういう社会でシステムを直す「まっとうな方法」は、責任をもっている人に対してうまく交渉を行って、直してもらうということだろう。でも、世の中には、誰が責任を持っているのか分からないシステムや、誰も責任を持っていないシステムというものがある。交渉しても、失敗して目的が達成できないかもしれない、という不安がある。でも、そこを迂回して、「ハック」を行ってしまえば話は簡単になる。だれか他人の顔色をうかがうことなく、目的を達成できてしまうので、楽だし、効率的で、安くつく。むかし、コンビニのコーヒーメーカーのデザインが分かりづらすぎて、テプラの修正まみれになったこともあった。つまり、そういうことである。

ソフトウェアのハックには、ソースコードを共有する文化も重要だ。テプラがあるから簡単に製品の文言を修正できるのと同じように、ソースコードがあるから、ソフトウェアを簡単に修正できる。ソースコードがなければないでうまくやる方法はあるけど、難しくなってくる。ソースコードをオープンソースの形で公開することは、私の了承など取らずとも自由にハックしてくださいという表明で、このようにしてハックを自由にやらせることが、結果的には有用であることを経験的に知っている。そして、ハックを自由にさせる文化は、いろいろな価値観と結びついている。

ハッカー文化は「私たちは問題に対して、責任の境界を気にせず直接的に取り組むことができる」という感覚に支えられている。その一方で、「問題の解決方法は、元の責任者に対してフィードバックされるべきだ」という感覚も兼ね備えている。無秩序なハックはさらなる追加のハックを困難にしてしまうし、ソフトウェア更新のたびに、ハックを適用するのは大変なので、最終的には、オープンソースソフトウェアの開発で「アップストリーム」と呼ばれる、元の開発責任者に対して、自分の行なった変更を適用してもらうように依頼する。テプラの例えでいえば、テプラを貼ってそれでよしとするのではなく、もとのコーヒーメーカーのデザインに反映してもらうような感じだ。その過程で、結局は交渉が必要になってくるんだろうけど、交渉して他人に直してもらうのと、自分で直した後に反映してもらえるよう交渉するのとだったら、後者の方が簡単な場合も多い。「直して」ではなく、「直しました」という形で、提案を行うのだ。何かを取り決めてから課題に取り組むのではなく、課題に取り組んでから取り決めるという順序の逆転を許容することが、より迅速で効果的な課題解決につながっている。

インターネットの根幹はハックに支えられている。世の中の無数に存在する、責任者の不明なシステムどうしをうまく繋げて通信できるようにする必要があるわけで、その技術は上から下まで、最初から考えられて設計されているわけではなく、テプラの修正のような後付けの技術にあふれている。もちろん、インターネットにもいろいろな標準規格が存在しているけど、規格で決めたこと以上に実際に動くことが大事だから、規格に合わせてシステムを直すという力だけではなく、システムに合わせて規格に直すという力が働くことになる。ASCIIしか想定していない古いシステムを置き換えずに世界中の文字を扱いたいから、UTF-8だとか、パーセントエンコーディングだとか、そういう技術が生み出されてくるし、そういうものが、規格になっていく。インターネットの世界で使われる規格というのは、国や団体が議論を尽くしてキレイな規格を定めたから広まるというよりは、既存のエコシステムに適応した、優秀なハックであるかどうかという点が有利に働くようにみえる。

そういうハックが支える技術や文化が背景知識としてあると、登大遊の「けしからん」「超正統派インチキ」というキーワードや、オードリー・タンがレナード・コーエンのAnthemを引用していう "There is a crack, a crack in everything. That's how the light gets in." というフレーズについての理解も、深まるのではないだろうか。責任境界の超越を問題とせず、自由に課題に取り組めないことを問題とする思想。問題がいつまでも解決されず絶望するのではなく、問題を自分で解決につなげられるという希望。システムを壊さないよう人間を管理するのではなく、人間が自発的にシステムを直せるよう自由にするという思考。そういったことを言っているのであり、そういった、ハックに親和的な考えを企業や社会に取り入れる重要性がそれぞれの考え方に表れているように、私は思う。

デジタル社会の実現には、技術に精通した優秀なハッカーが必要なわけではない。必要なのは、技術への精通ではなく、課題へのアクセシビリティだ。技術への精通は、課題に取り組み、ハックした結果として得られるものであって、ハックする前に必要なのではない。ハックは、最初は身近で簡単なところからはじめていけばよい。

デジタル社会を形成するハックという考え方は、そのままでほかの分野には適用できないかもしれない。リスクが高い領域だから自由にさせられないとか、システムは入札で調達するから取り決める前に取り組ませるのは難しいとか、いろいろと課題はあると思う。しかし、そういった課題だって、解決する方法は存在するのではないだろうか? ハックという行為を社会に組み込むというメタな課題について、ハックしてみるべきかもしれない。

所在表現と所有表現

これは、 位置の外延的表現・内包的表現の区別と考察ノート の続きのような話で、数学とプログラミングは、ときに、あるいみでは、物事をまったくあべこべに捉えているのではないだろうか? という話を書く。

前回の話について簡単に説明すると、プログラミングでは、位置を表現する方法には「外延的表現」と「内包的表現」があるという話をした。JSONで書けば { "8": { "4": "王将" } } が「外延的表現」、 { "王将": [4, 8] } が「内包的表現」で、その間にどういう違いがあるか簡単に考察を行なっている。

話をより一般的にすると、位置の表現に限らず、情報の表現全般には、位置を使った「所在表現(外延的表現)」と、属性を使った「所有表現(内包的表現)」が考えられる。コンピューターであれば、メモリアドレスによって表現される位置と、その位置であらわされるセルに格納された電荷などの属性の組み合わせを使って情報を表現しているのだから、表したい情報のいくらかは位置によって、残りの情報は属性によって、表現されている。

情報の表現には所在表現と所有表現がある考えると、次の図を2通りに、所在と所有それぞれの解釈で読み解けることに気づく。

flowchart LR subgraph A direction TB subgraph B direction RL C end end

この図は、オイラー図であるように見える。それは「*Xは*Bである」という命題を「XがBにある」図形で表す、所在表現だ。そして、Aの内部にBを、Bの内部にCを配置することで「*Cならば*Bであり、*Bならば*Aである」ことを表現する。(ここで、Aと書いたのは図の領域のことで、*Aと書いたのは、領域が表す物事だ。ここでのアスタリスク記号「*」は、記号のデノテーションを表すものだと考えてほしい。)

しかし、この図を逆さまに所有表現として読んで、「XがBをもつ」図形が「*Xは*Bである」という命題を表すと見ればどうか。Aの内部にBを、Bの内部にCを配置することで「*Aならば*Bであり、*Bならば*Cである」ことを表現している、と読める。これでも、つじつまはあっている。また、これは、プログラミングにおけるオブジェクトの表現によく似ている。

type A = { b: B }
type B = { c: C }
type C = {}
const p: (a: A) => B = (a) => a.b
const q: (b: B) => C = (b) => b.c
const r: (a: A) => C = (a) => a.b.c

日常生活で考えれば、「*Xである」という情報を「Xがある」という形で表現することは多い。「私は弁護士である」という情報を「私は弁護士バッジを持っている」という形で示すことがあるだろうし、「きのこに投票する」という情報を「投票用紙に『きのこ』と書く」という形で示すことがあるだろう。もちろん、「*Xである」という情報を「Xにある」という形で表現することも多くあり、「私の名前は弁護士会の名簿にある」とか「『きのこ』と書いてある枠にシールを貼る」という例が考えられる。

ここまでの話は抽象的だが、UIデザインやデータの可視化においては、考慮すべきことに思える。たとえば散布図などはまさに属性を位置に変換して表現するものだと言える。人間にとって理解しやすい表現を行うためには、どのように両者の変換を行うのかの考察は重要になるだろう。

漢字データベースを使って漢字ベン図を作問する

漢字ベン図は、QuizKnockがやっていた漢字クイズです。条件が3つ与えられるので、複数の条件に当てはまる漢字を答えていきます。

www.youtube.com

この記事では、漢字情報データベース Mojidata を活用して、漢字ベン図を作問してみようと思います。

github.com

MojidataはSQLiteというデータベースエンジンで使うことができるデータベースになっていて、情報をSQLで取得することができます。

データベースを使う準備

Mojidataを使うには、Node.jsとSQLiteをインストールしてあると楽です。

その後、ターミナルで次のコマンドを実行して、moji.dbをダウンロードし、sqlite3を起動してください。

# 作業用のディレクトリを作る
mkdir kanji-venn
# カレントディレクトリを変更する
cd kanji-venn
# npm パッケージの初期化(node_modulesを作業用ディレクトリに作成するため)
npm init -y
# mojidataパッケージのインストール
npm install @mandel59/mojidata
# SQLiteの起動
sqlite3 node_modules/@mandel59/mojidata/dist/moji.db

SQLiteが起動すると、次のようなプロンプトが表示されます。

SQLite version 3.40.0 2022-11-16 12:10:08
Enter ".help" for usage hints.
sqlite> 

個人的にはSQLiteはCLIからだと少し使いづらいと思うので、普段は自作のErqというツールを使っています。これは補完機能が使え、SQLより簡単に書けるErqクエリ言語を使って情報を取得できます。開発途中で、マニュアル等はないのですが、Erqを使ってみたい場合は、こちらもnpmでインストールして使うことができます。次のコマンドでErqをインストールします。

# Erqのインストール
npm install github:mandel59/erq
# Erqの起動
npx erq node_modules/@mandel59/mojidata/dist/moji.db

Erqを起動すると、次のようなプロンプトが表示されます。

Connected to node_modules/@mandel59/mojidata/dist/moji.db
erq> 

他にDuckDBを使って読み込む方法や、GUIのツールを使う方法もあります。

漢字情報を取得してみる

作問するにあたって、漢字の次のような情報が取得したいです。

  • 常用漢字の一覧
  • 漢字の読み
  • 漢字の総画数
  • 漢字の部首
  • 漢字の構造

常用漢字の一覧と読みは、常用漢字表のデータを格納した joyo テーブルに保存されています。また、総画数はMJ文字情報一覧のデータを格納した mji テーブルから、部首は mji_rsindex テーブルから、漢字の構造は ids テーブルから、それぞれ取得できます。

erq> joyo limit 10;;
select * from joyo limit 10
["漢字","音訓","例","備考"]
["亜","ア","[\"亜流\",\"亜麻\",\"亜熱帯\"]",""]
["哀","アイ","[\"哀愁\",\"哀願\",\"悲哀\"]",""]
["哀","あわれ","[\"哀れ\",\"哀れな話\",\"哀れがる\"]",""]
["哀","あわれむ","[\"哀れむ\",\"哀れみ\"]",""]
["挨","アイ","[\"挨拶\"]",""]
["愛","アイ","[\"愛情\",\"愛読\",\"恋愛\"]","愛媛(えひめ)県"]
["曖","アイ","[\"曖昧\"]",""]
["悪","アク","[\"悪事\",\"悪意\",\"醜悪\"]",""]
["悪","オ","[\"悪寒\",\"好悪\",\"憎悪\"]",""]
["悪","わるい","[\"悪い\",\"悪さ\",\"悪者\"]",""]
10 rows (0.011s)

読み情報ビュー kanji_reading を定義して、必要な情報だけを使いやすくします。

erq> view temp.kanji_reading = joyo {k: 漢字, r: 音訓};;
create view `temp`.kanji_reading as with kanji_reading as (select 漢字 as k, 音訓 as r from joyo) select * from kanji_reading
ok (0.002s)
erq> kanji_reading limit 10;;
select * from kanji_reading limit 10
["k","r"]
["一","ひと"]
["一","ひとつ"]
["一","イチ"]
["一","イツ"]
["丁","チョウ"]
["丁","テイ"]
["七","なな"]
["七","ななつ"]
["七","なの"]
["七","シチ"]
10 rows (0.000s)

総画数と部首、構造を取得するビューも定義します。

総画数情報ビュー kanji_strokes の定義

erq> view temp.kanji_strokes = mji[漢字施策='常用漢字']{k: 実装したUCS, s: 総画数};;
create view `temp`.kanji_strokes as with kanji_strokes as (select 実装したUCS as k, 総画数 as s from mji where (漢字施策 = '常用漢字')) select * from kanji_strokes
ok (0.000s)
erq> kanji_strokes limit 10;;
select * from kanji_strokes limit 10
["k","s"]
["一",1]
["丁",2]
["七",2]
["万",3]
["丈",3]
["三",3]
["上",3]
["下",3]
["不",4]
["与",3]
10 rows (0.001s)

部首情報ビュー kanji_radical の定義

erq> view temp.kanji_radical = mji[漢字施策='常用漢字'] -:MJ文字図形名:> mji_rsindex -:部首:> radicals {k: 対応するUCS, rad: 部首漢字};;
create view `temp`.kanji_radical as with kanji_radical as (select 対応するUCS as k, 部首漢字 as rad from mji join mji_rsindex on mji.MJ文字図形名 = mji_rsindex.MJ文字図形名 join radicals on mji_rsindex.部首 = radicals.部首 where (漢字施策 = '常用漢字')) select * from kanji_radical
ok (0.000s)
erq> kanji_radical limit 10;;
select * from kanji_radical limit 10
["k","rad"]
["一","一"]
["丁","一"]
["七","一"]
["万","一"]
["丈","一"]
["三","一"]
["上","一"]
["下","一"]
["不","一"]
["与","一"]
10 rows (0.001s)

構造情報ビュー kanji_ids の定義

erq> view temp.kanji_ids = ids[UCS in joyo{漢字}]{k: UCS, ids: IDS} distinct;;
create view `temp`.kanji_ids as with kanji_ids as (select distinct UCS as k, IDS as ids from ids where (UCS in (select 漢字 from joyo))) select * from kanji_ids
ok (0.000s)
erq> kanji_ids limit 10;;
select * from kanji_ids limit 10
["k","ids"]
["一","一"]
["丁","⿱一亅"]
["七","〾⿻乚一"]
["万","⿸丆𠃌"]
["丈","⿻𠂇乀"]
["三","三"]
["上","⿱⺊一"]
["下","⿱一卜"]
["不","⿸丆⿰丨丶"]
["不","⿻丆卜"]
10 rows (0.003s)

クイズを作問する

ここまでできれば、あとは、条件に当てはまる漢字を取得するクエリを作るだけです。先の動画の例題で言えば、「さんずい」「9画」「「せ」から始まる」といった条件は、漢字をxとすれば、SQLとErqではそれぞれ次のように表現できます。

  • さんずい
    • SQL: x in (select k from kanji_ids where ids glob 'â¿°æ°µ*')
    • Erq: x in kanji_ids[ids glob 'â¿°æ°µ*']{k}
  • 9ç”»
    • SQL: x in (select k from kanji_strokes where s = 9)
    • Erq: x in kanji_strokes[s = 9]{k}
  • 「せ」から始まる
    • SQL: x in (select k from kanji_reading where r glob 'せ*' or r glob 'ã‚»*')
    • Erq: x in kanji_reading[r glob 'せ*' or r glob 'ã‚»*']{k}

各常用漢字 x についてそれぞれ判定し、複数の条件に当てはまるものを表示すれば作問ができそうです。Erqでクエリを作ってみます。

/* 常用漢字を x という名前で取り出す */
joyo {x: 漢字} distinct
/* 各漢字について、条件を判定する */
{
  x,
  `さんずい`: x in kanji_ids[ids glob '⿰氵*']{k},
  `9ç”»`: x in kanji_strokes[s = 9]{k},
  `「せ」から始まる`: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k}
}
/* 複数の条件にあてはまる漢字のみ残す */
[`さんずい` + `9画` + `「せ」から始まる` >= 2]
/* あてはまる条件でグループ化 */
{ `さんずい`, `9画`, `「せ」から始まる` => group_concat(x) }
;;

これを入力すると:

erq> /* 常用漢字を x という名前で取り出す */
joyo {x: 漢字} distinct
...> /* 各漢字について、条件を判定する */
...> {
...>   x,
...>   `さんずい`: x in kanji_ids[ids glob '⿰氵*']{k},
...>   `9ç”»`: x in kanji_strokes[s = 9]{k},
...>   `「せ」から始まる`: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k}
...> }
...> /* 複数の条件にあてはまる漢字のみ残す */
...> [`さんずい` + `9画` + `「せ」から始まる` >= 2]
...> /* あてはまる条件でグループ化 */
...> { `さんずい`, `9画`, `「せ」から始まる` => group_concat(x) }
...> ;;
select `さんずい`, `9画`, `「せ」から始まる`, group_concat(x) from (select x, x in (select k from kanji_ids where (ids glob '⿰氵*')) as `さんずい`, x in (select k from kanji_strokes where (s = 9)) as `9画`, x in (select k from kanji_reading where (r glob 'せ*' or r glob 'セ*')) as `「せ」から始まる` from (select distinct 漢字 as x from joyo) where (`さんずい` + `9画` + `「せ」から始まる` >= 2)) group by (`さんずい`), (`9画`), (`「せ」から始まる`)
["さんずい","9画","「せ」から始まる","group_concat(x)"]
[0,1,1,"宣,専,政,施,星,染,泉,牲,狭,省,窃,背"]
[1,0,1,"清,潜,瀬"]
[1,1,0,"洋,洞,津,洪,活,派,浄,海"]
[1,1,1,"æ´—,æµ…"]
4 rows (0.017s)

コマンドラインからクエリを実行する

先ほどは手でクエリを入力していましたが、毎回同じように手で入力するのは面倒なので、次のクエリを kanji-venn.erq ファイルに保存しておき、コマンドで実行してみます。

view temp.kanji_reading = joyo {k: 漢字, r: 音訓};;
view temp.kanji_strokes = mji[漢字施策='常用漢字']{k: 実装したUCS, s: 総画数};;
view temp.kanji_radical = mji[漢字施策='常用漢字'] -:MJ文字図形名:> mji_rsindex -:部首:> radicals {k: 対応するUCS, rad: 部首漢字};;
view temp.kanji_ids = ids[UCS in joyo{漢字}]{k: UCS, ids: IDS} distinct;;
/* 常用漢字を x という名前で取り出す */
joyo {x: 漢字} distinct
/* 各漢字について、条件を判定する */
{
  x,
  p1: x in kanji_ids[ids glob 'â¿°æ°µ*']{k},
  p2: x in kanji_strokes[s = 9]{k},
  p3: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k}
}
/* 複数の条件にあてはまる漢字のみ残す */
[p1 + p2 + p3 >= 2]
/* あてはまる条件でグループ化 */
{p1, p2, p3 => group_concat(x)}
;;
npx erq node_modules/@mandel59/mojidata/dist/moji.db < kanji-venn.erq
$ npx erq node_modules/@mandel59/mojidata/dist/moji.db < kanji-venn.erq            
Connected to node_modules/@mandel59/mojidata/dist/moji.db
view temp.kanji_reading = joyo {k: 漢字, r: 音訓};;
view temp.kanji_strokes = mji[漢字施策='常用漢字']{k: 実装したUCS, s: 総画数};;
view temp.kanji_radical = mji[漢字施策='常用漢字'] -:MJ文字図形名:> mji_rsindex -:部首:> radicals {k: 対応するUCS, rad: 部首漢字};;
view temp.kanji_ids = ids[UCS in joyo{漢字}]{k: UCS, ids: IDS} distinct;;
/* 常用漢字を x という名前で取り出す */
joyo {x: 漢字} distinct
/* 各漢字について、条件を判定する */
{
  x,
  p1: x in kanji_ids[ids glob 'â¿°æ°µ*']{k},
  p2: x in kanji_strokes[s = 9]{k},
  p3: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k}
}
/* 複数の条件にあてはまる漢字のみ残す */
[p1 + p2 + p3 >= 2]
/* あてはまる条件でグループ化 */
{p1, p2, p3 => group_concat(x)}
;;
create view `temp`.kanji_reading as with kanji_reading as (select 漢字 as k, 音訓 as r from joyo) select * from kanji_reading
ok (0.024s)
create view `temp`.kanji_strokes as with kanji_strokes as (select 実装したUCS as k, 総画数 as s from mji where (漢字施策 = '常用漢字')) select * from kanji_strokes
ok (0.000s)
create view `temp`.kanji_radical as with kanji_radical as (select 対応するUCS as k, 部首漢字 as rad from mji join mji_rsindex on mji.MJ文字図形名 = mji_rsindex.MJ文字図形名 join radicals on mji_rsindex.部首 = radicals.部首 where (漢字施策 = '常用漢字')) select * from kanji_radical
ok (0.000s)
create view `temp`.kanji_ids as with kanji_ids as (select distinct UCS as k, IDS as ids from ids where (UCS in (select 漢字 from joyo))) select * from kanji_ids
ok (0.000s)
select p1, p2, p3, group_concat(x) from (select x, x in (select k from kanji_ids where (ids glob '⿰氵*')) as p1, x in (select k from kanji_strokes where (s = 9)) as p2, x in (select k from kanji_reading where (r glob 'せ*' or r glob 'セ*')) as p3 from (select distinct 漢字 as x from joyo) where (p1 + p2 + p3 >= 2)) group by (p1), (p2), (p3)
["p1","p2","p3","group_concat(x)"]
[0,1,1,"宣,専,政,施,星,染,泉,牲,狭,省,窃,背"]
[1,0,1,"清,潜,瀬"]
[1,1,0,"洋,洞,津,洪,活,派,浄,海"]
[1,1,1,"æ´—,æµ…"]
4 rows (0.032s)

数列の内包的記法について

公理的集合論についてほとんどなにも知らないからこれから書くことは間違っているのかもしれないけど、古典的なクラスや集合の記法は公理的集合論の元では複数の公理に対応していると理解している。ZF公理系の分出公理に対応する記法として

\left\lbrace x \in X \,\middle|\, P(x) \right\rbrace

というような記法が考えられる。これがふつうは内包的記法と呼ばれているんだけど、別の置換公理に根拠を持つ記法も考えられて、

\left\lbrace f(x) \,\middle|\, x \in X \right\rbrace

のような書き方ができる。この両者を内包的記法と呼ぶのは紛らわしいから、分出公理に基づく記法を分出記法、置換公理に基づく記法を置換記法を置換記法と呼ぶことにする。(x \in Xは命題であると考えれば、命題が左側に来る分出記法は元来のクラスの記法のルールを破っているように思えるが、左辺に来るx \in Xは分出記法の一部として扱われる。それと同様に、置換記法の右辺に来るx \in Xも命題ではなく、置換記法の一部として扱われる。)

ところで、数列があったとして、その数列の各項を2倍した数列をどう書けばいいだろうか。

まあふつうに

a = \left( a_n \right)_{n \in \mathbb{N}}

b = \left( 2 a_n \right)_{n \in \mathbb{N}}

でもよいのだが、内包的記法のように、簡単に書きたい。Pythonであれば、

a = [1, 2, 3]
b = [2*x for x in a]

と書くのだから、数式でも

b = \left( 2 x \,\middle|\, x \in a \right)

とするかとも思うが、しかし、x \in aというのは気持ちが悪いと言うか、aは数列であり、数列は写像なのだから、x \in aと書いたときのxは、aの数列としての項ではなくaの集合としての元であり、aの集合としての元とは添え字と項のペアであるのでは、という気持ちがある。そうすると

b = \left( 2 x \,\middle|\, (k, x) \in a \right)

と書くのか。ここまでするのなら、いっそ

b = \left\lbrace (k, 2 x) \,\middle|\, (k, x) \in a \right\rbrace

と書いてしまって、ふつうに集合の置換記法で書いてしまってもいい気がする。しかし、操作したいのは数列なのに、これでは数列を表す写像を表す集合を操作していることになってしまう。x \in aの気持ち悪さが問題であれば、矢印にしてしまって

b = \left( 2 x \,\middle|\, x \leftarrow a \right)

でもいいかなあ。わかりづらいか。

Tutorial DとErqの比較

Tutorial DとErqは部分的に似た文法を持っているが、その目的は異なっている。Tutorial Dは、その目的が数学的により純粋な関係代数を実現することであるのに対し、ErqはSQLのセマンティクスを保ったまま文法を異なるものにしている。

この記事では、Tutorial DとErqを簡単に比べる。なお、この記事でTutorial Dとして例示するもは、Project:M36 Relational Algebra Engineが実装している記法である。

(Project:M36を実際に動かして試したかったが、手元の環境でビルドに失敗して試せていない。)

セマンティクスの違い

純粋な関係か、多重関係か

Tutorial Dは関係代数に忠実なセマンティクスを持っている。そこで扱われる関係はタプルの集合であって、重複したタプルを持たないし、属性の順番は重要ではないし、関係に含まれるタプルの間に暗黙の順序はない。

一方でErqのセマンティクスは基本的にはSQLと同じであって、扱われるテーブルは純粋な関係とは限らず、重複したレコードを許すし、カラムの順番は重要で、暗黙の順序を持っている。

NULL・三値論理を採用するか

SQLはTRUE/FALSE/UNKNOWNの三値論理を採用している。(SQLiteの場合、UNKNOWNの代わりにNULLを使う。)そのせいで、値にNULLが絡んできた場合の対応が面倒なことになっている。

演算の比較

Tutorial DとErqで、個々の演算を比較してみる。データとして、TutorialD Tutorial for Project:M36で使われているものと同じ、Chris Dateのサンプル関係データを用いる。

関係(テーブル)s (suppliers) の内容

s#,sname,status,city
S3,Blake,30,Paris
S4,Clark,20,London
S5,Adams,30,Athens
S1,Smith,20,London
S2,Jones,10,Paris

関係(テーブル)p (products) の内容

p#,pname,color,weight,city
P6,Cog,Red,19,London
P5,Cam,Blue,12,Paris
P1,Nut,Red,12,London
P4,Screw,Red,14,London
P3,Screw,Blue,17,Oslo
P2,Bolt,Green,17,Paris

関係(テーブル) sp (supplierProducts) の内容

s#,p#,qty
S1,P1,300
S1,P2,200
S1,P3,400
S1,P4,200
S1,P5,100
S1,P6,100
S2,P1,300
S2,P2,400
S3,P2,200
S4,P2,200
S4,P4,300
S4,P5,400

Erq/SQLでは値にNULLが入る場合があるが、それを考慮すると複雑になってしまうので、ここではNULLが入らない場合だけについて考えることにする。

関係自体の表示

Tutorial DもErqも、関係それ自体を表すのに余計なキーワードを必要としない。関係(テーブル) p を表示したいのであれば、単に p をクエリすればよい。

属性の改名

Tutorial Dの場合は属性(カラム)の改名のシンタックスが存在して、

s rename {city as town}

のようにすると、属性cityをtownに改名できる。

Erqは、少なくとも現状ではカラムの改名の記法は存在しないので、ブレース記法で残りのカラムを選択する必要がある。

s{`s#`,sname,status,town: city}

å°„å½±

Tutorial DとErqで、射影の記法は似ている。どちらもブレースを使って、属性を選択することができる。

p{color,city}

しかしTutorial Dは関係代数に忠実であるのに対し、ErqはSQLと同じセマンティクスを持っている。すなわち、Tutorial Dの場合はタプルの重複は除去されるので、この結果は4件になる一方で、Erqの場合は重複が除去されず、結果は6件になる。

Erqで重複タプルを除去するには、明示的にdistinctをつける必要がある。

p{color,city} distinct

結合

Tutorial Dではjoinは自然結合のこと。

s join sp

ErqではSQL同様natural joinを使う。

s natural join sp

射影の略記

s join sp から関係 s に含まれる属性すべての射影をとるとき、Tutorial Dでは {all from s} と書く。Erqでは {s.*} と書く。

(s join sp){all from s}
s natural join sp {s.*} distinct

æ‹¡å¼µ

Tutorial Dではこう書く。@は属性を表す。

s:{status2:=add(10,@status)}

Erqでは射影と区別せず、同じブレース記法を使えばよい。

s{s.*, status2: status + 10}

ユニオン

Tutorial Dではunion演算子を使う。

s union s

Erqでは ; でSQLのunion all相当になる。

s; s

distinctを最後につけると、unionになる。

s; s distinct

å·®

s minus s

Erqには現状SQLのexcept相当の構文が存在しないが、データにnullが入っていなければ not in を使って対処できる。

s[{`s#`, sname, status, city} not in s];;

セミジョイン

Tutorial D の s semijoin sp は (s join sp){all from s} と同じ。

s semijoin sp

Erqにセミジョインの記法はないが、ブラケット記法(where句)とin演算子でセミジョインを表現できる。

s[{`s#`} in sp{`s#`}]

ただし、多重集合を許すErq/SQLのセマンティクス上では、x natural join y {x.*} distinct と x[{c} in y{c}] は同じ結果になるとは限らない。

アンチジョイン

s のタプルのうち、セミジョイン s semijoin sp に含まれないものからなる関係が s antijoin sp。

s antijoin sp

すなわち

s minus (s semijoin sp)

と同じ。

Erqでは

s[{`s#`} not in sp{`s#`}]

と書けばよい。

制限

Tutorial D

s where lt(@status, 30)

Erq

s[status < 30]

グループ・アングループ

Tutorial Dでは、グループ化するとサブリレーションを値に持った属性が作られる。Aggregate Queries

s group ({s#,sname,status} as subrel)

サブリレーションに対してアングループを行うと元に戻る。

s group ({s#,sname,status} as subrel) ungroup subrel

(個人的には、グループ化の基準になるcityが陽に指定せず、都市以外の属性を列挙することになるのが気になる。{all but city}と書けるから別にいいのだろうか。)

Erq/SQLiteではサブリレーションは存在しない。代わりにjson_group_arrayを使ったグループ化が行える。

s{city => subrel: json_group_array(json_array(`s#`, sname, status))}

JSONからのアングループも、長くなるが、一応可能となっている。

s{city => subrel: json_group_array(json_array(`s#`, sname, status))} join j: json_each(subrel) {`s#`: j.value->>0, sname: j.value->>1, status: j.value ->> 2, city}

集約

Tutorial Dではリレーション関数が用意されているので、グループ化した後に、リレーション関数を適用した属性を追加すればよい。

s group ({s#,sname,status} as subrel):{citycount:=count(@subrel)}

Erqでは、集約関数を使う。

s{city => citycount: count(*), subrel: json_group_array(json_array(`s#`, sname, status))}

簡単関係照会言語 Erq で快適なデータベース分析生活を送る

Erq(アーク)は、SQLの代わりにアドホックなデータ分析に用いることを主目的とした、新しいデータベース言語です。リレーショナルデータベースは便利ですが、アドホックなデータ分析を行う上で、SQLの文法は面倒なものです。Erqは、SQLのセマンティクスは極力そのままに異なる文法を採用することで、簡単にクエリを書けるようになっています。

SQLクエリの実例

私はSQLiteデータベースに漢字の文字情報を入れて、複雑な検索や分析ができるようにしているのですが、実際にそのデータベースを使ったクエリ例を見てみましょう。使っているMojidataデータベースは、次のリポジトリからビルドできます。

まず、漢字の読みを集めたmji_readingテーブルの内容を全部表示するために、SQLで次のように照会します。(末尾のセミコロン ; は、SQLite CLIにおける文の終端記号です。)

select * from mji_reading;

データは全部で122148件あるのですが、冒頭のデータはこんな感じになっています。MJ文字図形名は、文字情報基盤における図形番号です。

"MJ文字図形名","読み"
MJ000001,"おなじ"
MJ000001,"くりかえし"
MJ000001,"のま"
MJ000002,"しめ"
MJ000004,"キュウ"
MJ000004,"おか"
MJ000005,"テン"
MJ000006,"ã‚­"
MJ000006,"よろこぶ"
MJ000007,"ã‚«"

ここで、簡単な分析として、読みごとに件数をカウントし、多い順に10件表示してみましょう。

select 読み, count(*) from mji_reading group by 読み order by count(*) desc limit 10;
"読み",count(*)
"コウ",2775
"ショウ",1985
"ソウ",1732
"ã‚·",1730
"トウ",1675
"ã‚­",1536
"カン",1515
"セン",1476
"キョウ",1437
"ケン",1279

カラムを追加し、読みに対応する漢字の例をいくつか表示してみましょう。mji_readingに格納されているのはUnicodeではなくMJ文字図形名なので、Unicodeの漢字を表示するには、別のテーブル mji と結合して照会する必要があります。UnicodeとMJ文字図形名は1対多対応なので、重複するUnicodeを排除するために、select句にdistinctキーワードを使います。また、表示する漢字を最大5つに制限するために、サブクエリを二重に使って、limit句で制限をかけたデータに対してgroup_concat()集約関数で集約を行うことにします。そうすると、クエリはこのようになります。

select
  読み,
  count(*),
  (
    select group_concat(c)
    from (
      select distinct 対応するUCS as c
      from mji
      natural join mji_reading as r
      where r.読み = mji_reading.読み
      limit 5
    )
  ) as 例
from mji_reading
group by 読み
order by count(*) desc
limit 10;
"読み",count(*),"例"
"コウ",2775,"㐬,㒶,㓂,㓚,㓛"
"ショウ",1985,"㐮,㐮,㐼,㑱,㒉"
"ソウ",1732,"㐮,㑿,㒎,㔌,㔿"
"シ",1730,"㑥,㒋,㒾,㓨,㓼"
"トウ",1675,"㑽,㓊,㓱,㓸,㔁"
"キ",1536,"㐂,㑧,㑶,㒫,㔳"
"カン",1515,"㒈,㓧,㔋,㔶,㖤"
"セン",1476,"㑒,㒄,㒨,㒰,㔊"
"キョウ",1437,"㐩,㓋,㓏,㓙,㕳"
"ケン",1279,"㐸,㒽,㓩,㓺,㔓"

上記の例は単純ですが、SQLの冗長性・煩雑性がよく表れています。

  • select句とgroup by句やorder by句に重複して書くことになる。
  • select句はクエリの先頭、group by句やorder by句はクエリの末尾にあるので、カーソル移動が面倒くさい。
  • サブクエリにも都度selectキーワードを書くので、多重のサブクエリは記述量がすごく多くなってしまう。
  • 処理の流れ上は後にくるselect句が先頭にあるので、処理の流れがクエリ上で行ったり来たりしてしまう。
  • テーブル名やカラム名の別名を式の後に書くので、後から読むとき、特に長い式の場合に、見づらい。

Erqクエリの実例

今度は同じ分析をErqで行ってみましょう。テーブルの全件取得は、Erqではテーブル名を書くだけです。(Erq CLIでは、文の終端記号に";;"を使っています。)

mji_reading;;

読みごとに件数をカウントし、多い順に10件表示するには、次のように書きます。

mji_reading {読み => count(*) desc} limit 10;;

ブレース・アロー記法 { ... => ... } はErqにおける集約クエリの書き方で、アローの左側にグループに使うカラムを、アローの右側に集約関数のカラムを書きます。また、カラムの後に asc/desc を指定することもできます。この記法によって、SQLのselect句・group by句・order by句の指定を一度に行えるので、Erqでは集約を書くのが簡単になっています。

サブクエリはどうでしょうか。SQLのときと同様に、漢字の例のカラムを追加してみます。

mji_reading
{
  読み =>
  count(*) desc,
  例:
    from mji
    natural join r: mji_reading
    [r.読み = mji_reading.読み]
    {c: 対応するUCS}
    distinct
    limit 5
    {group_concat(c)}
}
limit 10;;
  • Erqでは、サブクエリの先頭にfromを書きます。サブクエリを括弧で括る必要はありません。(トップレベルのクエリにもfromをつけて良いのですが、省略できます。サブクエリではテーブル名とカラム名の区別のため、基本的にはfromキーワードが必要です。)
  • カラム名やテーブル名の別名は、式の前に書きます。
  • ブラケット記法 [...] はwhere句・having句に相当します。
  • ブレース記法 {...} はselect句に相当しますが、from句の後に書きます。
  • distinctキーワードは、Erqでは独立したdistinct句です。
  • ブラケット記法やブレース記法は、クエリに複数書いても問題ありません。

Erqのこれらの特徴により、SQLでは二重のサブクエリとして書いていたクエリを、すっきりとした直列的なサブクエリとして記述できました。

そのほかのクエリ例

他にもいくつかクエリ例を載せてみます。Erq CLIではErqクエリから変換されたSQLを出力するので、どういう変換が行われるか分かるようになっています。

ブラケット記法がhaving句に変換される例

erq> unihan_variant[property='kTraditionalVariant']{s: UCS => t: group_concat(value, '')}[count(*)>1] limit 10;;
select UCS as s, group_concat(value, '') as t from unihan_variant where (property = 'kTraditionalVariant') group by (UCS) having (count(*) > 1) limit 10
["s","t"]
["䴘","鷈鷉"]
["䴙","鷿鸊"]
["么","幺麼麽"]
["云","云雲"]
["伪","偽僞"]
["余","余餘"]
["冲","沖衝"]
["出","出齣"]
["历","曆歷"]
["发","發髮"]
10 rows (0.015s)

共通テーブル式とユニオン

erq> with t(a, b) as (`kdpv_cjkvi/non-cognate`{subject, object}) (t{a, b}; t{b, a}) join unihan_kTotalStrokes on a = UCS {a, b, s: cast(value as integer) asc}[s = 1];;
with t(a, b) as (select subject, object from `kdpv_cjkvi/non-cognate`) select a, b, cast(value as integer) as s from (select a, b from t union all select b, a from t) join unihan_kTotalStrokes on a = UCS where (s = 1) order by (cast(value as integer)) asc
["a","b","s"]
["乀","乁",1]
["乀","乁",1]
["乁","乁",1]
["乙","𠃉",1]
["乁","乀",1]
["𠃉","乙",1]
["乁","乀",1]
["乁","乁",1]
8 rows (0.015s)

共通テーブル式を使った再帰クエリ

erq> with g(i) as ({i: 1}; g{i + 1} limit 10) g;;
with g(i) as (select 1 as i union all select i + 1 from g limit 10) select * from g
["i"]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10]
10 rows (0.000s)

再帰クエリを使ってグラフを辿る

erq> with v(a, b) as (mjsm natural join mji {対応するUCS, 縮退UCS})
...> with w(a, b) as (v; v{b, a})
...> with g(a, b) as ({null, '刈'}; g join w on g.b = w.a {w.a, w.b} distinct)
...> g {a => group_concat(b)};;
with v(a, b) as (select 対応するUCS, 縮退UCS from mjsm natural join mji), w(a, b) as (select * from v union all select b, a from v), g(a, b) as (select distinct null, '刈' union select distinct w.a, w.b from g join w on g.b = w.a) select a, group_concat(b) from g group by (a)
["a","group_concat(b)"]
[null,"刈"]
["㓼","刹"]
["㔑","刹"]
["䒳","䒳,朵,朶,𣎾,𣎿,𣏻"]
["䓭","刹,苅"]
["刈","刈,苅,𠚫,𠛄,𭃅,𭃆"]
["刴","刹,朶"]
["刹","㓼,㔑,䓭,刴,刹,剎,𠛴,𠞻"]
["剎","刹"]
["朵","䒳,朶"]
["朶","䒳,刴,朵,朶,𣎾,𣎿,𣏻"]
["苅","䓭,刈,苅,𠛄,𫟌"]
["𠚫","刈"]
["𠛄","刈,苅"]
["𠛴","刹"]
["𠞻","刹"]
["𣎾","䒳,朶"]
["𣎿","䒳,朶"]
["𣏻","䒳,朶"]
["𫟌","苅"]
["𭃅","刈"]
["𭃆","刈"]
22 rows (0.123s)

in演算子とorder by句の例

erq> mji natural join mji_reading[対応するUCS in joyo{漢字}]{漢字: 対応するUCS => 読み: group_concat(distinct 読み)} order by count(distinct 読み) desc limit 10;;
select 対応するUCS as 漢字, group_concat(distinct 読み) as 読み from mji natural join mji_reading where (対応するUCS in (select 漢字 from joyo)) group by (対応するUCS) order by count(distinct 読み) desc limit 10
["漢字","読み"]
["明","メイ,ミョウ,ミン,ベイ,ボウ,あかり,あかるい,あかるむ,あからむ,あきらか,あける,あく,あくる,あかす,ひかり"]
["生","セイ,ショウ,ソウ,いきる,いかす,いける,うまれる,うむ,おう,はえる,はやす,き,なま,うぶ"]
["行","コウ,ギョウ,アン,ゴウ,カン,ガン,いく,ゆく,おこなう,まさに,みち,めぐる,やる,ゆくゆく"]
["上","ジョウ,ショウ,うえ,うわ,かみ,あげる,あがる,のぼる,のぼせる,のぼす,たっとぶ,たてまつる,ほとり"]
["下","カ,ゲ,ア,した,しも,もと,さげる,さがる,くだる,くだす,くださる,おろす,おりる"]
["白","ハク,ビャク,ベ,ハ,ヒャク,シ,ジ,しろ,しら,しろい,しらげる,しらむ,もうす"]
["薄","ハク,ヘキ,ホ,うすい,うすめる,うすまる,うすらぐ,うすれる,せまる,すすき,バク,ビャク,ブ"]
["重","ジュウ,チョウ,ジュ,ズ,トウ,シュウ,シュ,え,おもい,かさねる,かさなる,おもんじる,はばかる"]
["反","ハン,ホン,タン,ヘン,ベン,そる,そらす,かえす,かえって,かえる,そむく,たん"]
["懐","カイ,エ,ふところ,なつかしい,なつかしむ,なつく,なつける,いだく,おもい,こころ,おもう,ふところにする"]
10 rows (0.021s)

ローバリュー演算

erq> with u(s, t) as (unihan_variant[property='kTraditionalVariant']{UCS, value})
...> u[{s, t} not in tghb_variants{规范字, 繁体字}] limit 10;;
with u(s, t) as (select UCS, value from unihan_variant where (property = 'kTraditionalVariant')) select * from u where ((s, t) not in (select 规范字, 繁体字 from tghb_variants)) limit 10
["s","t"]
["㐷","傌"]
["㐹","㑶"]
["㐽","偑"]
["㑈","倲"]
["㑔","㑯"]
["㑩","儸"]
["㑺","儁"]
["㓥","劏"]
["㔉","劚"]
["㖊","噚"]
10 rows (0.006s)

Erq実装について

現状はNode.js/JavaScriptでSQLiteのErqクライアントを実装し、個人的に利用しています。

将来的にはRustなどで実装しなおすかもしれませんが、現状でもそれなりに便利に使えています。リポジトリには公開していないので、GitHubからインストールしてください。次のコマンドを実行すると、erqコマンドがインストールされます。

npm install -g github:mandel59/erq

UnicodeのSmall Kana Extensionに関する文書

Small Kana Extension - Wikipedia に記載のない文書も追加。

  • L2/10-468R2/N3987 Lunde, Ken (2011-02-09), Proposal to add two kana characters
  • L2/16-334 Sim, Cheon Hyeong (2016-11-04), Hiragana and Katakana (Small Letters)
  • L2/16-354 Yamaguchi, Ryusei (2016-11-07), Proposal to add Kana small letters
  • L2/16-358R/N4803 Lunde, Ken (2016-11-22), L2/16-334 & L2/16-354 Feedback (small kana)
  • L2/16-325 Moore, Lisa (2016-11-18), "C.14 Kana", UTC #149 Minutes
  • L2/16-381 Suignard, Michel (2016-12-08), Additional repertoire for ISO/IEC 10646:2016 (5th ed.) Amendment 1.2
  • L2/17-016 Moore, Lisa (2017-02-08), "Consensus 150-C18", UTC #150 Minutes
  • N4523 The Japan National Body (2017-04-01), Japanese National Body Contribution on Small Kana Characters
  • N4953 "M66.07i", Unconfirmed minutes of WG 2 meeting 66, 2018-03-23
  • L2/17-353 Anderson, Deborah; Whistler, Ken (2017-10-02), "N.1. Small Kana Extension code block and code point changes", WG2 Consent Docket
  • L2/17-362 Moore, Lisa (2018-02-02), "Consensus 153-C13", UTC #153 Minutes