
2026/04/25 2:39
# CSS をクエリ言語として捉える - セレクターは、スタイル付けの対象となる要素のスコープを定義します。 - プロパティは、対応する要素に適用されるビジュアル上のルールを指定します。 - 値は、そのプロパティにおける具体的なスタイル特性を決定します。 - スペシフィシティは、競合が生じた際にどのスタイルが優先されるかを規定します。 - 継承機能により、子要素は親要素のスタイルを自動的に受け継ぐことができます。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
本記事は、CSS を単なる視覚的なスタイリングツールではなく、再帰的な検索やツリー遍歴などの複雑なタスクを処理できる強力なクエリ言語として再考することを提唱しています。これは、ロジックプログラミングシステム Datalog の概念に触発されたものです。Datalog の原則(エンティティ(「物」)は事実として定義され、新しい真理は固定点評価を通じて規則から導出される)に基づき、標準的な CSS は再帰的な定義や計算された値に基づく要素の選択に苦慮するという問題点が指摘されています。これらの制限を安全に解決するためには、ブラウザでの危険なセマンティクス変更を強要するのではなく、既存の CSS 構文を堅牢な Datalog エンジンの上に配置するハイブリッドアプローチを採用することが提案されています。この戦略は、Datalog の モルトニシティ(事実に追加を行いつつ削除せず、無限ループを防ぐ)や、JSON やファイルシステムのような階層的データ構造を処理できる能力を活用します。現在のコンテナクエリなどの機能は祖先をクエリすることは可能ですが、仮想的な「CSSLog」と呼ばれるバージョンの CSS が新しい要素を作成するか、属性に対して推移的に変化をもたらすために必要な導出状態の伝播を完全に支えることはできません。究極的には、この進化はウェブ技術を一時的で重要なロジック処理ドメインへと拡大させ、CSS 標準では現在サポートされていない複雑な階層を定義することを可能にします。
本文
CSS をクエリ言語として、あるいはウェブページをブラウザ上にレイアウトする以外のタスクを実行するための汎用プログラミング言語として調査したものです。
問い: いったい何を理由にそんなことをするのでしょうか?CSS は非常に理解しにくく厄介なものとして有名です。しかも SQL などという問題なく動作するより優れたクエリ言語だってあるはずです。
答え: なぜなら、そこにあるからです(It's there)。
以下に CSS の基本的な原理を見てみましょう。
1. もの事柄(Things)が存在する
「もの事柄」こそがドメインエンティティ(domain entities)、あるいは「原子(atoms)」、「事実(facts)」と呼ばれるものです。これらは CSS の外にある存在であり、CSS が眺める限り、それらは既にそびえ立ち、常久し続ける永遠のものとしてそこにあります。
例えば:
<h1>Hello, World!</h1> <a href="example.com">This is a link</a> <div class="awesome" data-custom-attribute="foo"> <div id="child">This div is inside another one!</div> </div>
具体的には、ここでは「もの事柄」は HTML 要素です。
2. もの事柄の集合を記述できる
共通項を有する「もの事柄」の集合を指し示すセレクタを書くことができます。
/* 'div' という種類の全ての要素の集合 */ div /* id が "child" の要素だけの集合(これはただ一つの要素です)*/ #child /* クラス名が "awesome" の全ての要素の集合 */ .awesome /* 属性 data-custom-attribute の値が "foo" の要素の集合 */ [data-custom-attribute="foo"]
また、ドキュメント階層内での相互位置関係に基づいてもの事柄を記述することも可能です。これは「もの事柄」が HTML 要素(互いにネストし合う傾向がある)である場合、非常に役立つ機能です。
さらに、複数のセレクタを組み合わせて、それらが記述するもの事柄の集合について交差演算(intersection)を実行することもできます。これが極めて重要です:
div.awesome /* 'div' であり、かつクラス "awesome" をもつ要素全ての集合 */
3. 対して「何か」を行える
単に孤立してもの事柄の集合を記述するだけでは、あまり有用性の高い言語にはなり得ません。CSS では、要素の集合を選択するためのセレクタと、その集合内の要素に対して何を行うべきかを記述するための宣言(declarations)をペアにしたルールを定義します。
div.awesome { color: red; font-size: 24px; }
これは、「'div' でありかつクラス 'awesome' を持つ全ての要素については、これらのプロパティ(色とフォントサイズ)をこれらの値に設定せよ」という意味です。ブラウザー上では、HTML ページの該当部分が巨大な赤い文字として表示される効果を生み出します。結構面白いですね?これで 90 年代のウェブデザイナー気分でいいですよ。
3a. しかし、それほどでもない
ただし、このアプローチにはかなりの大きな制限があります。大半の場合、これらの宣言は要素そのもの(これも言語の外にある存在)のプロパティを変更するものでしかありません。言い換えるなら、「要素の色を設定することはできるが、要素の色を基準に選択することはできない」ということです:
/* ブラウザーはこのようにrejectします:*/ div[color=red] { color: blue; /* これは一体何を意味するのでしょうか?*/ }
これは確かに混乱を招くでしょう。「色の赤い全ての要素について、その色は青にせよ」と言ったらどうなるのでしょう?一瞬赤で表示された後に青に瞬くのでしょうか?それとも往復で瞬いてしまうのでしょうか?あるいは、3=4 のように矛盾として検出され、計算をあきらめるのでしょうか?
これに対する答えがあります。そこへたどり着きましょう。
3b. 実際の例
ウェブ開発者として実際に(もしかしたら)やりたいことがあるでしょう。
あなたは設計システムを構築しています。「ダークモード」対応のコンポーネント(
data-theme="dark" を持つカード)があり、その内部にあるあらゆるインタラクティブな要素に、どのレベルまで深くネストされていても反転されたフォーカススタイルを適用したいと考えています。直接の子供元素だけでなく、間接的に任意の子孫(descendant)にも適用されなければなりません。ただし、中間のコンポーネントが data-theme="light" で明確にオプトアウトしている場合は別です。(「でも、それは悪いデザインではないでしょうか?」マネージャーはそれを良しとしているし、あなたより彼女のほうが気に入られていますから。)
通常の CSS では、以下のように記述します:
[data-theme="dark"] :focus { outline-color: white; } /* 間に light テーマを持つ祖先があれば無効化 */ [data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }
これは機能しますが……ネストが一つのレベルの場合だけです。もし、ダークなページの中にライトなパネルがあり、その中にダークなカードが入っている場合どうしましょうか?追加のルールが必要になります。もう一つ。また一つ。あなたは此刻から、逐次的に指定されたトランジティブ(経由)クエリのアドホック版を手入力することになっています。
実際には言いたいのは、「要素が実質的に 'dark' であれば、それは
data-theme="dark" を持つ場合であり、または effectively-dark な祖先を持ち、かつ間に effectively-light な祖先が存在しない場合である」ということです。これは再帰的な関係性定義です。CSS はこれを表現できませんが、CSSLog は可能です:
[data-theme="dark"] { class: +effectively-dark; /* 仮想的な構文でクラスを追加 */ } .effectively-dark > :not([data-theme="light"]) { class: +effectively-dark; } .effectively-dark :focus { outline-color: white; }
二番目のルールは、明示的なライト制約に到達するまで子要素に
effectively-dark の属性を伝播させます。これは再帰的に実行され、所望の目標状態が達成されたことを自分自身で満足して停止します。現在の CSS ではこれを行うことはできません(いやまあ、後ほど触れます)。
4. しかし、もしそれが可能であれば
CSS を「CSSLog」と呼ぶバージョンを想像してください(その理由は推測できますが、やがて明らかにされるでしょう)。
通常の CSS と同様、CSSLog でも要素にマッチするセレクタを書いて属性を設定することができます。しかし、そのセレクタは以下のこともできます:
- 他のセレクターのマッチングに影響を与える要素のプロパティを設定できる(例えば
など)。class - 新しい要素を作成できる?
- 要素を破壊できる!?(おそらくできないでしょう)
以下のような記述が可能です:
div.foo { class: +bar /* クラス bar を追加 */ +<div class="baz"> /* 子要素を追加 */ } div.bar { /* 上記のルール実行後に .foo を持っていた要素は、今や .bar も持ち、ここでもマッチします!*/ }
最悪の場合に何が起きるでしょうか?
5. それ、神様の名前を呼んでみても狂っているようですが?
おそらくそうです。しかし、ご覧の通り、それはあなたが思い描くほど珍しくはありません。単によく知られている別の書き方があるだけです。
シャープな Retina スクリーンの設計者たちの世界とは異なる、灰色がった大学のラボや、奇妙な過去のカンファレンス、幸運があれば大手テック企業の内部部門に埋もれた世界において、人々は以下のように見えるコードを記述しています:
parent(alice, bob). parent(bob, carol). parent(bob, dave). ancestor(X, Y) :- parent(X, Y). ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
あれは何ですか?関数呼び出しでしょうか?
:- は何を示しているのでしょうか?ピリオドは全部で何が起きるんですか、オブジェクト指向のプロパティアクセスのようなもの……ああ神様、70 年代の SQL のような、句読点を並べて英語の文に見えるようにしようとするあの仕草ですか?それにしても alice、bob、X、Y は一体どこから来たのでしょう?変数宣言 (var や let など) を見かける記憶がありません。しかもこれが CSS とどう関わるんですか?
驚くべきことに、これはかなり似ています。順を追って見てみましょう。
5.1 もの事柄(Things)が存在する
この場合、「もの事柄」は「原子(atoms)」と呼ばれます。原子は最初に触れられた時点で存在し始め、使用前に宣言するルールはありません。
alice と bob は原子です(Ruby の記号 (:symbol) と比較できます)。
5.2 もの事柄の集合を記述できる
Datalog では、これを行えるようにするために「関係(relations)」を使います。関係とはタプル(tuple)の集合であり、これは偶然にも SQL テーブルの定義でもあります。タプルとは原子の一覧です。例えば上記の例では、
parent が関係です。parent(alice, bob) は parent 関係における一つのタプルです。parent 関係とは、「このペア内の要素 1 は要素 2 の親である」と示す (alice, bob) というようなペアの集合です。
変数を用いて、クエリにマッチするもの事柄を選択することもできます。以下の例:
ancestor(X, carol).
は「(bob, X) が parent 関係におけるタプルである全ての X」あるいは「Bob は X の親である全ての X」と読み解かれます。この場合、X は carol と dave という原子の集合として評価されます。X は変数です(慣習上、変数は大文字、原子や関係は小文字を使用します)。
CSS 同様に、集合同士を交差させることもできます。これは通常「join」と呼ばれます。ルール本体で同じ変数名を二度繰り返すことで、その変数について join を行います:
% これらは一元関係(unary relations)、つまり原子の集合です。またコメントは % で記述します。 woman(alice). man(bob). parent(alice, bob). parent(bob, carol). % "X は Y の母親である場合、X は Y の親であり、かつ X は女性である。" % 本体で X を繰り返しているので、これが join です。 mother(X, Y) :- parent(X, Y), woman(X).
上記の例は実質的に、「全ての親の集合」と「全ての女性の集合」を交差させ、「全ての母親の集合」を形成します。
Datalog のルールは以下のように見えます:
head(X, Y) :- body1(X, Z), body2(Z, Y).
:- を「もしならば(if)」と読みましょう。右側が本体(body)であり、同時に成り立つ条件のリストです。左側が頭部(head)で、本体が成立するたびに真であると主張する新しい事実です。本体の中のコンマは「かつ(and)」です。
したがって
ancestor(X, Y) :- parent(X, Y). は、「X と Y の可能なすべての値について、もし X が Y の親ならば、X は Y の祖先である」という意味です。
比較を明確にするために:
% "X が div であり、かつクラスが awesome ならば、X は赤い色を持つ" color(X, red) :- div(X), class(X, awesome).
/* "X が div であり、かつクラス awesome ならば、X は赤くなる" ですがここでは X を書かないんです。*/ div.awesome { color: red; }
Datalog と CSS は互いに似ていますが、逆の方向にあります。セレクタが本体(body)、宣言が頭部(head)です。
:- は { に相当します(まあ、sort of です)。私たちはこれ entire ずっと論理ルールを記述していたのです!
5.3 それら「もの事柄」に対して何かできる
Datalog では、「何かする」とは単に「色を設定する」だけでなく、「新しい事実を導き出す」という意味を持ちます。つまり、既存の関係に基づいて新たなタプルが存在すること主張します。
「祖先(ancestors)」の例を再度見てみましょう。これは Datalog の教科書のどこにでも登場する例です。私が伝統を破る資格があるでしょうか:
parent(alice, bob). parent(bob, carol). parent(bob, dave). ancestor(X, Y) :- parent(X, Y). ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
最初のルールは「親は祖先である」という単純なことです。二番目のルールは、「もし X が Z の親であり、Z がすでに Y の祖先と知られているならば、X も Y の祖先である」と言っています。 注意してください。二番目のルールでは
ancestor は頭部(head)にも本体(body)にも現れています。それは自分自身を参照しており、再帰的(recursive)です。
上記の事実にこれを適用すると以下のようになります:
ancestor(alice, bob). % ルール 1 から直接導出 ancestor(bob, carol). % 同上 ancestor(bob, dave). % 同上 ancestor(alice, carol). % alice -> bob -> carol、ルール 2 から ancestor(alice, dave). % alice -> bob -> dave、ルール 2 から
これは SQL が
WITH RECURSIVE キーワード導入以前にはできなかったものです。このキーワードはまさに人がこうしたことを継続して行う必要があったため存在しています。(典型的な SQL の姿勢ですが、WITH RECURSIVE は再帰計算を表現できるようにしますが、それを奇妙な構文と意味付けに無理やり嵌め込み、言語の他の部分との調和が悪いままになっています。)これは CSS には絶対にできませんが、Datalog の教科書での最初の例です。
私が ever
for ループを書くことはなかったことに気づいてください。明確に「全てを得るまで続ける」と宣言する必要はありませんでした。Datalog エンジンは単に……それを見つけ出してしまいます。どうやって?
6. 解決策は固定点(Fixpoints)である
通常の CSS では、「カスケード(cascade)」は一つのフォワードパスです:ブラウザーはすべてのルールを読み込み、どのセレクターがマッチするか判別し、宣言を適用します。フィードバックループはありません。
CSSLog(そして実際の Datalog)では、ルールは属性を設定することができ、それが別のルールの発火を引き起こし、さらに別の属性を設定して最初のルールを再び発火させることができます。したがって、単一のパスですむわけではありません。続ける必要がありますが、いつ止めるのでしょうか?
非公式な Datalog エンジンの動作方法を見てみましょう:
- 明示的に書いた事実(
などの基本的事実)から始める。parent(alice, bob) - ルールの全てを検討し、「本体(body)」を現在既知の事実に対してマッチさせ、その過程で変数に値を代入する。
- そのようなマッチのそれぞれについて、ルールの「頭部(head)」を既知の事実リストに加える。
- ステップ 3 で何か新しいことを追加した場合、ステップ 2 に戻る。
- そうでなければ止める。終わりです。
これは「naive evaluation(素朴な評価)」と呼ばれます。既知の事実の集合が成長しなくなるまで実行され、固定点(fixpoint)に達します。固定点とは、すべてのルールを適用しても新たなことは何も生まれず、もはや変化しない状態のことです。2
祖先の例では以下のように見えます:
基本的事実: parent(alice, bob) parent(bob, carol) parent(bob, dave)
一度ルールの適用:
ancestor(alice, bob) % ルール 1 より parent(alice,bob) から導出
ancestor(bob, carol) % ルール 1 より parent(bob,carol) から導出
ancestor(bob, dave) % ルール 1 より parent(bob,dave) から導出
再度ルールの適用: ancestor(alice, carol) % ルール 2 より parent(alice,bob) と ancestor(bob,carol) から導出 ancestor(alice, dave) % ルール 2 より parent(alice,bob) と ancestor(bob,dave) から導出
3 ラウンド目: (ルールの適用により新しいものは何も生まれません。固定点に達しました)
なぜこれが機能するのでしょうか?答えは「単調性(monotonicity)」と呼ばれるものです。実用的な観点から言えば、これは「事実は決して削除せず、追加のみ行う」ということを意味します。有限の事実集合から始め、そこから導き出せる事実も有限であるため、有限量の作業しか行えません。事実を除去できる場合、あるいは後続の結果が先行する結果の有効性を打ち消す場合、この性質は失われ、無限ループ(Infinite Loop Land)に陥ることに戻ってしまいます。(事実削除を許可しないのはそのためです。)3
(実は、単調性は分散システムなどの他の文脈でも有益です。4)
7. しかしどうだろうか?
さて、CSS と Datalog の類似点を私たちは見ました:両者とも「もの事柄」(HTML 要素または原子)を持ち、「もの事柄の集合」を合取クエリ(conjunctive queries)を通じて記述でき(「セレクター」または「ルール本体」)、そしてそれらに対して「何かを行う」ことができる(属性設定や新しい事実の導出)。一体なぜこんなことを entire ブログ記事にする必要があるのでしょうか?
一つには、コネクションを作るのは面白いからです。Datalog(より古い汎用 cousin プロログ(Prolog)も含む)は 70 年代から存在し、関係データベース(SQL の起源でもあり)および「AI」の研究から始まりました。当時の「AI」という言葉は今世界を支配している LLM とは全く異なる意味を持っていました。以来、数十回にわたって再発明されています——Datomic, Differential Datalog, various rule engines など。システムを記述したい場合(1)もの事柄が存在し、2) もの事柄の集合を記述でき、3) それらに対して何かを行うことができる(新しいもの事柄を作成する可能性がある方法)、あなたは常にこのような場所に行き着くことが分かります。しかし、データベース/ロジックプログラミングの人々とフロントエンドウェブ開発者の人々はいつもお互いに話していないのです。もし話せたら、私たちは一緒に素晴らしいものを考え出せるかもしれません!
また、これは実際の CSS 機能である「Container Queries(コンテナクエリ)」とも交差します。これは実在しつつ実験的な仕様に含まれています。これにより、親要素または祖先要素のスタイルをクエリし、そのコンテナのスタイルに基づいて要素にスタイルを適用できます(コンテナとは再び、親または祖先です):
@container style(--theme: dark) { .card { background: royalblue; color: white; } }
これはほとんどの実用的な目的において問題ありません。私がそれを提案していないからといって、それがダメだと言っているわけではありません!しかし、第 3 節の例では微妙に異なります:それは導き出された事実をツリー内で伝播させ、特定の境界で止める必要があります。言い換えるなら:
- 要素自身が「実質的に dark」かどうかを知る必要がある(単に祖先が dark かどうかではない)。
- その「実質的に dark」な状態は子孫を通してトランジティブに伝播する必要がある。
に到達した時点で伝播を止める必要がある。data-theme="light"
コンテナクエリはステップ 2 を行うことはできません。祖先の --theme カスタムプロパティをクエリできるが、他のルールによって既に実質的に dark と判定されているかどうかをクエリすることはできません。クエリは DOM の「as-given」から読み取っており、導き出された状態には見えません。「トランジティブにどのような祖先が --theme: dark を持ち、かつ近い祖先が --theme: light を持つかない場合、これを適用せよ」と書く方法はありません。なぜなら、それは再帰計算の結果を求めているためで、コンテナクエリには再帰がないからです。5
2015 年の記事がこの動機と制限を説明しています。 earlier の「element queries」提案も、ここで議論された理由の多くにより長期的に失敗しました。一度クエリがそのプロパティも設定しているものをクエリできるようになると、ループを引き起こす可能性があります(潜在的には無限ループ)。
CSS Working Group は数年間、「CSSLog」に近い何かへの軌道を描いてきました。彼らは「element queries」や「container style queries」を望みましたが、無限ループと固定点のセマンティクスという問題に直面し、情報の流れの方向を制限することで解決しました:子孫は祖先の情報についてクエリできますが、逆にはできません。これにより有限性を保ち、固定点のセマンティクスを持たせます(情報がツリーを下方向にしか伝播せず、あたかも新しい「基本的事実」を注入しないため)。コンテナクエリは祖先をクエリできますが、自身をスタイル化することはできません。スタイルクエリは上方へフィードバックを与えることはできません。彼らはまるで Datalog エンジンの構築の真ん中で、慎重にやめており、まるで海へと一直線に進み、波が足を触れる前に笑って走っていくようなものです。
CSSLog はただ水全体の中に頭を突っ込み、「何で 70 年代から Datalog がやっているように循環を許可し、固定点まで評価することを単純に許したらいいでしょうか?」と大きく、愚かしく問いかけます。CSS の答えは実務的には「いいえ、狂っていますか?そんなことしないでください」というものです。ブラウザーのレンダリングエンジンであり、インクリメンタルな関係データベースエンジンではありません。
8. 新しい方向性?
さて、仕方がありませんね。CSS は Datalog セマンティクスを持っていない(おそらく持たないでしょうし、持つべきではないかもしれません)。あなたのブラウサーは近いうちに CSSLog を実装することはないでしょう。
しかし、それを逆に考えてみましょう。Datalog セマンティクスを CSS に無理やり嵌める代わりに、CSS の構文を Datalog に乗っ付けるのはどうでしょうか?Datalog の構文はいつも何らかの障害になっています——現代的な言語に慣れたプログラマーたちは
:- や . など、「= 宣言なし」や「大文字小文字の区別」などを乗り越えられないことで弾み、跳ね返るからです。さらに、CSS はもともとツリー構造の概念を持っています(その降下/子/兄弟コンバイナーなどすべて込みで)、通常の Datalog が関係形式に何とか痛みを伴うようにエンコードしなければなりません。(これは古典的な「オブジェクト/関係不整合(object/relational impedance mismatch)」であり、私の意見ではこれを「ツリー形状/テーブル形状の不整合」と呼ぶことができます。)
現実世界で選択または変換したいデータは非常に多くの場合、ツリー形状になっています:JSON、ASTs、ファイルシステム、組織図、運が良ければ XML など。そのドメインをターゲットとした(より良い名前のある)「CSSLog」——固定点再帰を持ち、暗黙的な親/子関係のための CSS 風味の構文を持ち、その他の特典を含む——は、すでに多くのプログラマーが筋肉記憶を持つノテーションで再帰的ツリークエリを書くことを可能にします。
私ができる限り知っているところでは、まだ誰もそれを構築していません。もしかしたら誰かがするべきかもしれません?