CSSをクエリ言語として見る
(evdc.me)- セレクタとルールで対象集合を選び、プロパティを適用する CSS の構造は、集合とルールで動作する Datalog と形の上で似ている
div.awesomeのようなセレクタ結合は 積集合 を作り、Datalog では同じ変数を繰り返すことで似た join が起こる- 現在の CSS は計算済みスタイルの結果を再び選択条件にできないため、再帰的な推移クエリ や派生状態の反復伝播を直接表現しにくい
- Datalog は 再帰規則 と 不動点評価 によって、新しい事実がこれ以上生まれなくなるまで関係を拡張し、単調性のおかげで有限の範囲で計算を終えられる
- 実際の CSS は Container Queries のような機能で祖先情報を読めるが、フィードバックループ や循環を防ぐ方向を選んでおり、その代わり CSS 構文を再帰クエリに取り込む余地は残っている
CSSとDatalogの似た構造
- CSS は 対象集合の選択 と 選択された対象へのルール適用 という構造を持つ
- HTML 要素のような「Things」が先に存在し、selector が共通属性を持つ集合を指し示す
div,#child,.awesome,[data-custom-attribute="foo"]のような selector で集合を記述できるdiv.awesomeのように selector を組み合わせて 積集合 を作れる
- CSS ルールは selector と declaration を結びつけ、選ばれた要素に
colorやfont-sizeのようなプロパティを設定する- ただしこうしたプロパティは多くの場合 言語の外側の状態 を変えるだけで、その結果を再び selector 条件にはできない
div[color=red]のようにスタイル結果を再度クエリする形はブラウザが受け付けない
- Datalog も同様に 事実集合 と 規則ベースの導出 によって動作する
parent(alice, bob)のようなアトムと relation が基本単位になる- 変数
X,Yを使って条件に合う項目集合を選べる - 同じ変数を繰り返して条件をつなぐと、CSS の selector 結合に似た join が起こる
head(X, Y) :- body1(X, Z), body2(Z, Y)という構造は、CSS ルールと向きが逆なだけで形は似ている- CSS の selector は Datalog の body に近く、declaration は head に近い
div.awesome { color: red; }はcolor(X, red) :- div(X), class(X, awesome).に対応する
CSSではできない再帰的クエリ
data-theme="dark"の中にあるすべてのフォーカス要素に反転スタイルを与えつつ、途中でdata-theme="light"が現れたら止める条件は 推移クエリ を必要とする- 実際の CSS では
[data-theme="dark"] :focusや[data-theme="dark"] [data-theme="light"] :focusのようなルールで一部しか処理できない - ネスト段数が増えるとルールを追加し続ける必要があり、再帰関係を直接表現しにくい
- 実際の CSS では
- 必要なのは、その要素が effectively-dark かどうかを再帰的に判定することだ
- 自身が
data-theme="dark"なら effectively-dark になる - effectively-dark な祖先の下にある子も、途中に
data-theme="light"がなければ effectively-dark になる - この状態をもとに
.effectively-dark :focusにスタイルを適用する必要がある
- 自身が
- 仮想の CSSLog 構文では、ルールが
class: +effectively-darkのように 派生状態を追加 できる.effectively-dark > :not([data-theme="light"])が子へ状態を伝播する- ルールは目標状態に到達するまで 再帰的に繰り返し 適用される必要がある
- この種の再帰伝播は現在の CSS では表現しにくい
- 記事の末尾ではいくらか似たまねをする方法も出てくるが、同じ原理の一般解ではない
Datalogの再帰と不動点
- Datalog は既存の事実から 新しい事実を導出 する方式で動作し、再帰を基本的に扱える
ancestor(X, Y) :- parent(X, Y).ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
ancestor規則は親子関係をもとに祖先関係を段階的に拡張するparent(alice, bob)からancestor(alice, bob)がまず生まれる- 続いて
alice -> bob -> carol,alice -> bob -> daveのような経路も追加で導出される
- この計算は明示的な
forループがなくても 不動点評価 によって最後まで進む- 最初は明示された base fact だけを使う
- すべての規則の body を現在の事実集合に代入し、head を追加する
- 新しい事実がこれ以上生まれなくなったら止まる
- この方式が終了する理由は 単調性 にある
- 事実は追加するだけで削除しないため、既知の事実集合は増え続けるだけになる
- 有限の事実集合から始めれば、導出可能な事実数も有限に制限される
- 逆に事実を削除できると以前の結論が覆り、無限ループに陥る可能性がある
Container Queriesと実際のCSSの境界
- 実際の CSS の Container Queries は、祖先やコンテナのスタイルを基準にルールを適用できる
@container style(--theme: dark) { .card { background: royalblue; color: white; } }のような形をサポートする
- ただし transitive dark mode の例は、単純な祖先参照よりも強い条件を必要とする
- 各要素が自分自身が effectively-dark かどうかを知る必要がある
- その状態が子孫全体へ 推移的に伝播 しなければならない
data-theme="light"の境界で伝播が止まらなければならない
- Container Queries では 2 つ目の条件を扱えない
- 祖先の custom property は読めても、他のルールがすでに計算した 派生状態 を再度クエリすることはできない
- DOM に元からある情報は見られるが、再帰計算の結果を selector 条件にはできない
- 2015 年の 関連文書 でも、element queries が同じ問題にぶつかったと指摘している
- クエリによって設定したプロパティを再びクエリできるようにすると、ループや無限反復の危険が高まる
- CSS Working Group はこの問題を 情報フローの方向制限 によって避けてきた
- 子孫が祖先情報をクエリすることは許可する
- 逆方向のフィードバックや自分自身のスタイルへの循環は防ぐ
- そのため不動点意味論がなくても計算を有限に保てる
CSS構文を再帰クエリ言語として反転させる可能性
- Datalog の意味論を CSS に入れるより、CSS 構文を Datalog の上に載せる方向 のほうが現実的な新しい道として示されている
- Datalog の
:-、ピリオド、宣言のないアトムのような構文は、現代の言語利用者にとって参入障壁が高い - CSS はすでにツリー構造を扱うための豊かな selector 構文を持っている
- Datalog の
- 実データには ツリー形状 のものが多いと指摘する
- JSON
- AST
- ファイルシステム
- 組織図
- XML
- こうした領域では、親子関係を暗黙に扱う CSS 風構文と 不動点再帰 を組み合わせると有用になりうる
- 一般的な Datalog ではツリー構造を関係表現へ書き換える必要があり、煩雑さが大きい
- CSS selector の感覚をそのまま再帰クエリに持ち込めば、より多くのプログラマが容易に近づける
- この種のツールはまだ明確には見えていない
- 「CSSLog」という名前は仮のもので、よりよい名前の言語が登場するかもしれない
- 再帰的なツリークエリをもっとなじみある記法で扱える余地は残っている
補足論点と参考リンク
- Datalog は 1970 年代から関係データベースや当時の AI 研究の文脈で登場し、その後もさまざまな形で繰り返し再登場してきた
- 不動点計算の単純な形は naive evaluation として紹介されるが、既知の事実を毎回再計算するため非効率になりうる
- 各段階で新しく出た事実だけを使う semi-naive evaluation が代表的な改善方向としてあわせて言及される
- 単調性は分散システムでも有益な性質につながる
- custom property の継承で transitive dark mode を部分的にまねる方法もある
[data-theme="dark"] { --effective-theme: dark; }[data-theme="light"] { --effective-theme: light; }@container style(--effective-theme: dark) { :focus { outline-color: white; } }- この方法はこの特定のケースではおおむね機能するが、真の推移閉包 を一般的に提供するものではない
1件のコメント
Hacker Newsのコメント
CSSセレクタはXPathよりずっと書きやすい。
最近、PHPの新しいDOM APIがHTMLとCSSセレクタをネイティブに非常に簡単に扱えるようになったという発表もあった。以前はCSSをXPathに変換する必要があった。
[1] https://speakerdeck.com/keyvan/parsing-html-with-php-8-dot-4...
ブラウザのスタイリングを中心に発展してきたので、XPathのようなテキスト内容ベースの選択機能がないのは惜しい。
以前に提案はあったが、ブラウザのレンダリング文脈では性能上の問題が生じうるため、仕様には入らなかったと理解している。
ドキュメント編集エージェントを作る際、文書をHTMLで見せて、LLMがCSS selectorだけを指定して必要な断片をコンテキストに取り込めるようにしたところ、ほとんど魔法のようにうまく動いた。
人々が慣れたやり方をそのまま使える。
CSS文法と、CSSWGが定義する規則・関数・単位などの全体体系を分けて呼ぶ名前があるとよいと思う。
この分野にはかなり可能性があるが、別の活用例を語ったり調べたりしようとすると、結局はCSSパーサを含むGitHubコードを掘って、人々がどんな奇妙なものを作っているのか確認するしかないように見える。
軽量なノードベースのマークアップ言語と、テンプレートに何が入るかを表現するCSSセレクタ、そしてこれらの断片をどう結合するかを制御するCSS風の文法を混ぜた、奇妙なテンプレートエンジンのようなものもいじっている。
https://www.w3.org/TR/selectors-3/
DOM仕様もこれを参照している。
https://dom.spec.whatwg.org/#selectors
だからCSS selectorという総称はすでに正しく、単にselectorと呼んでもよい。
DOM selectorという名前のほうがすっきりしているかもしれないが、静的CSSで使うセレクタや、JSエンジン外の別のDOMエンジン(XML parser、PHP DOM APIなど)で使うセレクタまで考えると、かえって混乱を招くかもしれない。
さらに、
:hoverや::target-textのようにブラウザのレンダリング・ナビゲーションに直接結び付いた特殊セレクタもある。ただし、ブラウザやCSSとの結び付きが弱い最小クエリ文法の部分集合には別名があると便利かもしれない。
以前カンファレンスで見たhttps://github.com/braposo/graphql-cssを思い出した。
ジョークプロジェクトだったが、パターンを別の文脈に移植して再利用することで意外なことが可能になる、という点をよく示していてよかった。
まさにそういうふうに、異なる文脈のパターンを持ち込んで使ってみようとしているところだ。
大半は大したところまで行かなくても、ハッカー的な感覚ではかなり興味深い。
pyastgrepはhttps://pyastgrep.readthedocs.io/en/latest/で見られるように、Python文法をクエリする際にCSSセレクタを使える。
デフォルトはXPathで、たとえば
pyastgrep --css 'Call > func > Name#main'のように使える。自分が指し示したかった方向性とほぼ正確に重なっている。
これがどんなシナリオを解決するのかよく分からない。
今でも子要素に応じて親を条件付きで変えられる。たとえば
preはデフォルトのパディングが16pxで、直接の子がcodeなら&:has(> code)で0にできる。結論も「現代のCSSの限界を直すべきだ」ではなく、CSS風の文法をDatalog風のシステムに載せれば、ツリー状データを扱う仕事をより多くのエンジニアにとって親しみやすくできるのではないか、というものに近い。
つまりDOMに新しい子要素や属性を追加する方向の話である。
今のLLMはCSSをあまりうまく扱えないので、むしろこれを試すことでLLMがより単純に推論できるか確かめてみたくなる。
実用性はあまり思い浮かばないが、それでもかっこいい。
うーん……これは単にJQなのではと思う。
CSSはある程度好きだが、だんだん複雑性のクリープがひどくなっていくのは嫌いだ。
プログラミング言語が非プログラミング言語より強力になるという理屈は理解できるが、HTML・CSS・JavaScriptを延々と複雑にしていくくらいなら、いっそそれ全体を置き換える何か別のものが出てくるほうがよいと感じる。
HTML5の新要素も大半はなぜ必要なのかよく分からず、ほとんど使っていない。結局、多くのコンテナは固有IDが付いた
divにすぎないと思うようになったし、内部リンク用のhrefナビゲーションのために、そうしたIDに別名のようなものがあればいいのにとさえ思った。[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }のようなものも、頭の中で解釈するのに時間がかかりすぎて、もはや優雅で単純には感じられない。その一方で
h2 { color: red; }は今でも単純だ。ancestor(X, Y) :- parent(X, Y).のような表現も、もう考えたくなくなる。:-はいったい何なんだ、笑顔みたいに見える。@container style(--theme: dark) { .card { background: royalblue; color: white; } }を見たところで読むのをやめた。昔はうまく機能していた標準が、時間がたつほど壊れていくようで奇妙だ。
たとえば
[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }は英語風の擬似コードにすれば、data-theme="dark"な X がいて、その子 Y がdata-theme="light"で、さらにフォーカス状態なら、Y のoutline-colorを black にせよ、という意味に近い。だからこれはDatalog風に
outline-color(Y, black) if data-theme(X, "dark") and parent(X, Y) and data-theme(Y, "light") and focused(Y)のように書ける。:-をifに、カンマをandに置き換えたようなものだ。さらに
Y.outline_color := black if X.data-theme == dark and Y.parent == X and Y.data-theme == dark and Y.focusedのように書いて、attr(X, val)をX.attr == valのようなUFCS風の構文糖衣として見せることもできる。もっとALGOL系っぽく見せるなら
forall Y { Y.outline_color := black if Y.data_theme == "dark" and Y.focused and Y.parent.data_theme == "light" }のようにもできる。ここではYを明示的に導入し、結合の一つを暗黙化して、より一般的なプログラミングのように見せているが、実際にはDatalogエンジンが依存関係が変わるたびにこうしたループを効率的に回してくれる、というわけだ。