1 Punkte von GN⁺ 4 일 전 | 1 Kommentare | Auf WhatsApp teilen
  • Die Struktur von CSS, das mit Selektoren und Regeln eine Zielmenge auswählt und Eigenschaften anwendet, ähnelt formal Datalog, das mit Mengen und Regeln arbeitet
  • Eine Kombination von Selektoren wie div.awesome bildet eine Schnittmenge; in Datalog entsteht ein ähnlicher Join, indem dieselbe Variable mehrfach verwendet wird
  • Aktuelles CSS kann das Ergebnis berechneter Styles nicht erneut als Auswahlbedingung verwenden, daher lassen sich rekursive transitive Abfragen oder die wiederholte Weitergabe abgeleiteter Zustände nur schwer direkt ausdrücken
  • Datalog erweitert Beziehungen mit rekursiven Regeln und Fixpunkt-Auswertung, bis keine neuen Fakten mehr entstehen; dank Monotonie lässt sich die Berechnung in einem endlichen Bereich abschließen
  • Reales CSS kann mit Funktionen wie Container Queries zwar Informationen von Vorfahren lesen, wählt aber einen Ansatz, der Feedback-Schleifen und Zyklen verhindert; zugleich bleibt Spielraum, CSS-Syntax mit rekursiven Abfragen zu verbinden

Die ähnliche Struktur von CSS und Datalog

  • CSS hat die Struktur aus Auswahl einer Zielmenge und Anwendung von Regeln auf die ausgewählten Ziele
    • „Things“ wie HTML-Elemente existieren zunächst, und ein Selektor verweist auf eine Menge mit gemeinsamen Eigenschaften
    • Mit Selektoren wie div, #child, .awesome, [data-custom-attribute="foo"] lässt sich eine Menge beschreiben
    • Selektoren lassen sich wie in div.awesome kombinieren, um eine Schnittmenge zu bilden
  • CSS-Regeln verbinden Selektor und Declaration und setzen bei ausgewählten Elementen Eigenschaften wie color oder font-size
    • Solche Eigenschaften verändern jedoch meist einen Zustand außerhalb der Sprache; ihr Ergebnis kann nicht erneut als Selektorbedingung verwendet werden
    • Eine Form wie div[color=red], die das Styling-Ergebnis erneut abfragt, akzeptiert der Browser nicht
  • Datalog funktioniert ähnlich mit Faktenmengen und regelbasierter Ableitung
    • Atome und Relationen wie parent(alice, bob) bilden die Grundeinheit
    • Mit Variablen X, Y lassen sich Mengen passender Einträge auswählen
    • Wird dieselbe Variable wiederholt, um Bedingungen zu verknüpfen, entsteht ein Join, ähnlich wie bei der Kombination von CSS-Selektoren
  • Die Struktur head(X, Y) :- body1(X, Z), body2(Z, Y) ist der Form nach ähnlich zu einer CSS-Regel, nur in umgekehrter Richtung
    • Der CSS-Selektor entspricht eher dem Body von Datalog, die Declaration eher dem Head
    • div.awesome { color: red; } entspricht color(X, red) :- div(X), class(X, awesome).

Rekursive Abfragen, die CSS nicht kann

  • Die Bedingung, allen fokussierten Elementen innerhalb von data-theme="dark" einen invertierten Stil zu geben, aber bei einem zwischengeschalteten data-theme="light" anzuhalten, erfordert eine transitive Abfrage
    • In realem CSS lässt sich ein Teil davon mit Regeln wie [data-theme="dark"] :focus und [data-theme="dark"] [data-theme="light"] :focus behandeln
    • Nimmt die Schachtelungstiefe zu, müssen ständig weitere Regeln ergänzt werden; eine rekursive Beziehung lässt sich schwer direkt ausdrücken
  • Die benötigte Bedingung ist eine rekursive Bestimmung, ob ein Element effectively-dark ist
    • Hat es selbst data-theme="dark", ist es effectively-dark
    • Auch ein Kind unter einem effectively-dark-Vorfahren ist effectively-dark, sofern dazwischen kein data-theme="light" liegt
    • Auf Basis dieses Zustands müsste dann ein Stil auf .effectively-dark :focus angewendet werden
  • In einer hypothetischen CSSLog-Syntax könnten Regeln mit class: +effectively-dark abgeleitete Zustände hinzufügen
    • .effectively-dark > :not([data-theme="light"]) würde den Zustand auf Kinder weitergeben
    • Die Regeln müssten rekursiv wiederholt werden, bis der Zielzustand erreicht ist
  • Eine solche rekursive Weitergabe lässt sich im heutigen CSS schwer ausdrücken
    • Gegen Ende des Textes werden auch einige Methoden erwähnt, die etwas Ähnliches nachahmen, aber sie sind keine allgemeine Lösung nach demselben Prinzip

Rekursion und Fixpunkt in Datalog

  • Datalog arbeitet, indem aus bestehenden Fakten neue Fakten abgeleitet werden, und behandelt Rekursion als Grundprinzip
    • ancestor(X, Y) :- parent(X, Y).
    • ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
  • Die ancestor-Regeln erweitern die Vorfahrenbeziehung schrittweise auf Basis der Elternbeziehung
    • Aus parent(alice, bob) entsteht zunächst ancestor(alice, bob)
    • Anschließend werden auch Pfade wie alice -> bob -> carol und alice -> bob -> dave zusätzlich abgeleitet
  • Diese Berechnung läuft auch ohne explizite for-Schleife per Fixpunkt-Auswertung bis zum Ende
    • Zunächst werden nur die angegebenen Basisfakten verwendet
    • Der Body aller Regeln wird auf die aktuelle Faktenmenge angewendet und der Head ergänzt
    • Sobald keine neuen Fakten mehr entstehen, stoppt der Prozess
  • Warum das endet, liegt an der Monotonie
    • Fakten werden nur hinzugefügt, nicht entfernt; die Menge bekannter Fakten wächst also nur weiter
    • Beginnt man mit einer endlichen Faktenmenge, ist auch die Zahl ableitbarer Fakten endlich begrenzt
    • Könnten Fakten dagegen entfernt werden, würden frühere Schlussfolgerungen rückgängig und es könnte zu Endlosschleifen kommen

Container Queries und die Grenzen von realem CSS

  • Die realen Container Queries in CSS können Regeln auf Basis des Stils von Vorfahren oder Containern anwenden
    • Unterstützt wird eine Form wie @container style(--theme: dark) { .card { background: royalblue; color: white; } }
  • Das Beispiel mit transitive dark mode verlangt jedoch stärkere Bedingungen als eine einfache Vorfahrenabfrage
    • Jedes Element müsste wissen, ob es selbst effectively-dark ist
    • Dieser Zustand müsste transitiv weitergegeben werden, und zwar an alle Nachfahren
    • An einer data-theme="light"-Grenze müsste die Weitergabe stoppen
  • Container Queries können die zweite Bedingung nicht erfüllen
    • Man kann zwar Custom Properties von Vorfahren lesen, aber keinen abgeleiteten Zustand, den andere Regeln bereits berechnet haben, erneut abfragen
    • Informationen, die ursprünglich im DOM vorhanden waren, sind sichtbar, aber das Ergebnis rekursiver Berechnungen kann nicht als Selektorbedingung dienen
  • Ein einschlägiger Artikel von 2015 weist ebenfalls darauf hin, dass Element Queries an demselben Problem scheiterten
    • Wenn man per Abfrage gesetzte Eigenschaften erneut abfragbar macht, steigt das Risiko für Schleifen und unendliche Wiederholungen
  • Die CSS Working Group hat dieses Problem bislang durch eine Beschränkung der Informationsflussrichtung umgangen
    • Dass Nachfahren Informationen über Vorfahren abfragen, ist erlaubt
    • Feedback in die Gegenrichtung oder Zyklen mit dem eigenen Stil werden verhindert
    • Dadurch bleibt die Berechnung auch ohne Fixpunkt-Semantik endlich

Die Möglichkeit, CSS-Syntax in eine rekursive Abfragesprache umzudrehen

  • Statt Datalog-Semantik in CSS einzubauen, wird ein neuer Weg als realistischer vorgeschlagen: CSS-Syntax auf Datalog aufzusetzen
    • Die Syntax von Datalog mit :-, Punkten und Atomen ohne Deklaration ist für Nutzer moderner Sprachen eine hohe Einstiegshürde
    • CSS besitzt bereits eine ausdrucksstarke Selektorsyntax für Baumstrukturen
  • Es wird darauf hingewiesen, dass viele reale Daten baumförmig sind
    • JSON
    • AST
    • Dateisysteme
    • Organigramme
    • XML
  • In solchen Bereichen könnte die Kombination aus CSS-artiger Syntax, die Eltern-/Kind-Beziehungen implizit behandelt, und Fixpunkt-Rekursion nützlich sein
    • Allgemeines Datalog ist umständlich, weil Baumstrukturen erst in relationale Darstellungen übersetzt werden müssen
    • Wenn man das Gefühl von CSS-Selektoren direkt auf rekursive Abfragen überträgt, könnte das für mehr Programmierer leichter zugänglich werden
  • Ein solches Werkzeug ist bislang noch nicht klar erkennbar
    • Der Name „CSSLog“ ist nur vorläufig; eine Sprache mit besserem Namen könnte noch entstehen
    • Es bleibt Raum, rekursive Baumabfragen mit vertrauterer Notation zu behandeln

Ergänzende Punkte und weiterführende Links

  • Datalog entstand seit den 1970er Jahren im Kontext relationaler Datenbanken und der damaligen KI-Forschung und tauchte später in verschiedenen Formen immer wieder auf
  • Eine einfache Form der Fixpunktberechnung wird als naive evaluation eingeführt, kann aber ineffizient sein, weil bekannte Fakten immer wieder neu berechnet werden
    • Als typische Verbesserung wird semi-naive evaluation genannt, die in jedem Schritt nur neu entstandene Fakten nutzt
  • Monotonie führt auch in verteilten Systemen zu nützlichen Eigenschaften
  • Mit Vererbung von Custom Properties lässt sich transitive dark mode teilweise nachahmen
    • [data-theme="dark"] { --effective-theme: dark; }
    • [data-theme="light"] { --effective-theme: light; }
    • @container style(--effective-theme: dark) { :focus { outline-color: white; } }
    • Diese Methode funktioniert in diesem speziellen Fall meist, bietet aber keinen echten transitiven Abschluss im Allgemeinen

1 Kommentare

 
GN⁺ 4 일 전
Hacker-News-Kommentare
  • CSS-Selektoren sind viel einfacher zu schreiben als XPath
    Kürzlich gab es auch einen Vortrag darüber, dass die neue DOM-API in PHP HTML und CSS-Selektoren nativ sehr einfach handhabbar macht. Früher musste man CSS nach XPath umwandeln.
    [1] https://speakerdeck.com/keyvan/parsing-html-with-php-8-dot-4...
    Schade ist, dass sich CSS vor allem rund um Browser-Styling entwickelt hat und deshalb Funktionen wie Auswahl anhand von Textinhalten fehlen, wie man sie aus XPath kennt.
    Soweit ich weiß, gab es früher Vorschläge dazu, sie kamen aber wegen möglicher Performance-Probleme im Browser-Rendering-Kontext nicht in die Spezifikation.

    • LLMs kommen auch ziemlich gut mit CSS-Selektoren klar
      Beim Bauen eines Agenten zur Dokumentbearbeitung habe ich Dokumente als HTML dargestellt und das LLM nur CSS-Selektoren angeben lassen, um die nötigen Fragmente als Kontext zu holen — das funktionierte fast magisch gut.
    • Auf der Client-Seite sind querySelector/querySelectorAll ohnehin extrem weit verbreitet, daher freut es mich, dass das jetzt auch in der neuen PHP-DOM enthalten ist
      So können Leute genau die Arbeitsweise nutzen, die sie bereits kennen.
  • Es wäre schön, wenn es einen Namen gäbe, der CSS-Syntax von dem gesamten System aus Regeln, Funktionen und Einheiten trennt, das die CSSWG definiert
    Da steckt einiges an Potenzial drin, aber wenn man über andere Anwendungsfälle sprechen oder sie untersuchen will, scheint man am Ende doch GitHub-Code mit eingebautem CSS-Parser durchforsten zu müssen, um zu sehen, welche seltsamen Dinge Leute damit bauen.
    Ich bastle auch an etwas, das fast wie eine seltsame Template-Engine wirkt: eine Mischung aus einer leichten knotenbasierten Markup-Sprache, CSS-Selektoren zur Beschreibung dessen, was in Templates hineinkommt, und einer CSS-ähnlichen Syntax zur Steuerung, wie diese Teile kombiniert werden.

    • In den Standards ist das aus meiner Sicht bereits ziemlich klar getrennt
      https://www.w3.org/TR/selectors-3/
      Die DOM-Spezifikation verweist ebenfalls darauf
      https://dom.spec.whatwg.org/#selectors
      Daher ist CSS-Selektor als Sammelbegriff bereits korrekt, und man kann auch einfach Selektor sagen.
      DOM-Selektor könnte als Bezeichnung sauberer wirken, aber wenn man auch Selektoren berücksichtigt, die in statischem CSS oder in anderen DOM-Engines außerhalb von JS-Engines verwendet werden (XML-Parser, PHP-DOM-API usw.), könnte das eher noch verwirrender sein.
      Außerdem gibt es besondere Selektoren wie :hover oder ::target-text, die direkt an Browser-Rendering und Navigation gebunden sind.
      Für eine minimale Teilmenge einer Abfragesyntax, die weniger eng an Browser oder CSS gekoppelt ist, könnte ein eigener Name aber nützlich sein.
  • Das erinnert mich an https://github.com/braposo/graphql-css, das ich einmal auf einer Konferenz gesehen habe
    Es war ein Spaßprojekt, aber ich mochte es, weil es gut zeigte, wie das Verpflanzen und Wiederverwenden von Mustern in andere Kontexte unerwartete Dinge möglich machen kann.

    • Das ist ja interessant
      Genau auf diese Weise versuche ich gerade, Muster aus verschiedenen Kontexten zu übernehmen und auszuprobieren.
      Meistens führt das zwar nicht besonders weit, aber vom Hacker-Spirit her ist es ziemlich spannend.
  • pyastgrep kann, wie unter https://pyastgrep.readthedocs.io/en/latest/ zu sehen ist, CSS-Selektoren zum Abfragen von Python-Syntax verwenden
    Standardmäßig wird XPath genutzt, aber möglich ist zum Beispiel pyastgrep --css 'Call > func > Name#main'.

    • Das ist wirklich gut
      Das trifft fast genau die Richtung, auf die ich hinauswollte.
  • Ich bin mir nicht sicher, welches Szenario das eigentlich lösen soll
    Schon jetzt kann man Eltern abhängig von ihren Kindern konditional verändern. Zum Beispiel hat pre standardmäßig 16px Padding, und wenn das direkte Kind code ist, kann man es mit &:has(> code) auf 0 setzen.

    • Tatsächlich begann das eher damit, dass sich zwei unterschiedliche Ideen zunächst ähnlich anfühlten, und ich diese Verbindung dann in verschiedene Richtungen weitergedacht habe
      Die Schlussfolgerung ist weniger „Wir sollten die Grenzen von modernem CSS reparieren“, sondern eher: Wenn man eine CSS-ähnliche Syntax auf ein Datalog-ähnliches System setzt, könnte der Umgang mit baumförmigen Daten für mehr Ingenieure vertrauter werden.
    • Gemeint ist hier keine Lösung durch eine einzige Stilberechnung, sondern eine Syntax, die die zugrunde liegenden Daten selbst der Ziele verändert, die auf einen Selektor matchen
      Also eher das Hinzufügen neuer Kindelemente oder Attribute im DOM.
  • Die heutigen LLMs sind im Umgang mit CSS eher nicht besonders gut, daher würde ich das fast gerade deshalb gern ausprobieren, um zu sehen, ob LLMs damit einfacher schlussfolgern können.

  • Mir fällt kein klarer praktischer Nutzen ein, aber cool ist es trotzdem.

  • Hm ... ist das nicht einfach JQ?

  • Ich mag CSS bis zu einem gewissen Grad, aber ich mag nicht, wie stark der Complexity Creep zunimmt
    Ich verstehe die Logik, dass Programmiersprachen mächtiger werden als Nicht-Programmiersprachen, aber statt HTML, CSS und JavaScript immer weiter aufzublähen, fände ich es besser, wenn etwas anderes käme, das das Ganze ersetzt.
    Auch die neuen Elemente in HTML5 verstehe ich größtenteils nicht und benutze sie fast nie. Am Ende denke ich ohnehin oft, dass viele Container einfach nur div-Elemente mit einer eindeutigen ID sind, und ich hätte mir sogar so etwas wie Aliasse für diese IDs gewünscht, damit href-Navigation zu internen Links leichter wird.
    Dinge wie [data-theme="dark"] [data-theme="light"] :focus { outline-color: black; } brauchen bei mir viel zu lange, bis ich sie im Kopf aufgelöst habe, und fühlen sich dadurch nicht mehr elegant und einfach an.
    Dagegen ist h2 { color: red; } immer noch simpel.
    Bei Ausdrücken wie ancestor(X, Y) :- parent(X, Y). habe ich schon jetzt keine Lust mehr weiterzudenken. Was soll :- überhaupt sein, das sieht aus wie ein Smiley.
    Bei @container style(--theme: dark) { .card { background: royalblue; color: white; } } habe ich aufgehört zu lesen.
    Es wirkt seltsam, dass ein Standard, der früher gut funktioniert hat, mit der Zeit immer kaputter zu werden scheint.

    • Meine Absicht ist weniger, mehr Syntax und Semantik zu CSS hinzuzufügen, sondern eher, Ideen aus CSS zu stehlen und die Ähnlichkeit zu logischen/relationalen Abfragesprachen zu nutzen, um etwas Neues zu schaffen
      Zum Beispiel bedeutet [data-theme="dark"] [data-theme="light"] :focus { outline-color: black; } in englischartigem Pseudocode ungefähr: Wenn es ein X mit data-theme="dark" gibt und dessen Kind Y data-theme="light" hat und fokussiert ist, dann setze outline-color von Y auf black.
      In Datalog-Form könnte man das also als outline-color(Y, black) if data-theme(X, "dark") and parent(X, Y) and data-theme(Y, "light") and focused(Y) schreiben.
      Dabei wurde :- einfach durch if und das Komma durch and ersetzt.
      Man könnte noch weitergehen und es als Y.outline_color := black if X.data-theme == dark and Y.parent == X and Y.data-theme == dark and Y.focused schreiben, sodass attr(X, val) wie UFCS-artiger Syntax-Zucker in der Form X.attr == val aussieht.
      Wenn es noch mehr nach ALGOL-Familie aussehen soll, wäre auch etwas wie forall Y { Y.outline_color := black if Y.data_theme == "dark" and Y.focused and Y.parent.data_theme == "light" } möglich.
      Hier wird Y explizit eingeführt und ein Join implizit gemacht, sodass es mehr wie allgemeine Programmierung aussieht, aber tatsächlich würde eine Datalog-Engine solche Schleifen effizient immer dann neu ausführen, wenn sich Abhängigkeiten ändern.