Vendo CSS como uma linguagem de consulta
(evdc.me)- Seletores e declarações de CSS já se alinham estruturalmente com o Datalog no sentido de selecionar um conjunto de elementos já existente e aplicar propriedades ao resultado, assim como o Datalog consulta relações e produz fatos
- O CSS comum não oferece suporte a cálculo recursivo que reutiliza o resultado da seleção como condição de seleção, então não consegue expressar diretamente propagação de estado, como um tema dark que se transmite aos descendentes e para em uma fronteira light
- No CSSLog hipotético, seria permitido adicionar classes que afetam o matching dos seletores, propagando recursivamente estados derivados como
.effectively-darke repetindo o cálculo até que não surjam mais resultados novos - Esse tipo de cálculo é explicado em Datalog por fixpoint e monotonicity, e a avaliação repetida só pode terminar de forma finita se os fatos forem apenas adicionados, nunca removidos
- Até mesmo os container queries do CSS real conseguem ler o estado de ancestrais, mas não consultar estados recursivos derivados; assim, mesmo chegando perto do Datalog, o CSS não ultrapassa os limites do motor de renderização do navegador
Correspondência básica entre CSS e Datalog
- O CSS parte do pressuposto de que já existem objetos preexistentes, que aqui são os elementos HTML
- Elementos como
h1,aedivjá existem fora do CSS, e o CSS não os declara do zero - O texto usa como exemplo elementos com atributos como
class,idedata-custom-attribute
- Elementos como
- Os seletores CSS apontam para conjuntos com condições em comum, e é possível restringir o alvo por nome de tag, id, classe e valor de atributo
- Aparecem como exemplo seletores como
div,#child,.awesomee[data-custom-attribute="foo"] - Também é possível expressar o alvo pela posição na hierarquia do documento, e combinar seletores para formar interseções
- Aparecem como exemplo seletores como
- Um seletor combinado como
div.awesomefaz uma interseção de conjuntos, selecionando apenas elementos que sejamdive ao mesmo tempo.awesome- Essa ideia de interseção se conecta depois ao conceito de join no Datalog
- Uma regra CSS junta seletor e declaração para aplicar valores de propriedades ao conjunto selecionado
- No exemplo
div.awesome { color: red; font-size: 24px }, isso definecolorefont-sizedesses elementos - No navegador, o resultado é a renderização de texto grande e vermelho
- No exemplo
Limites do CSS e o problema de consultas recursivas
- O CSS comum é forte para alterar propriedades fora da linguagem, mas não consegue usar diretamente o resultado dessas mudanças como condição de seleção
- Dá para definir a cor de um elemento, mas o navegador rejeita algo como
div[color=red]usando a própria cor como condição do seletor - Uma regra que reaplicasse
color: blueem elementos comcolor: redficaria semanticamente ambígua
- Dá para definir a cor de um elemento, mas o navegador rejeita algo como
- O exemplo de modo escuro em um design system é apresentado como um problema que exige propagação transitiva de estado
- A ideia é aplicar contorno de foco branco a todos os elementos interativos dentro de um cartão com
data-theme="dark" - Mas, se houver um
data-theme="light"no meio, a propagação deve parar abaixo dele
- A ideia é aplicar contorno de foco branco a todos os elementos interativos dentro de um cartão com
- No CSS real, dá para tratar só parte do caso adicionando exceções
- Cria-se uma regra base com
[data-theme="dark"] :focus { outline-color: white; } - E reverte-se na fronteira light com
[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; } - Mas essa abordagem exige continuar acrescentando regras conforme a profundidade de aninhamento aumenta
- Cria-se uma regra base com
- O problema exige uma definição relacional recursiva, e o CSS não consegue expressá-la
- É preciso algo como “é effectively-dark se for dark por si só, ou se tiver um ancestral effectively-dark sem um ancestral effectively-light no caminho”
- O texto chama isso de recursive relational definition e afirma que o CSS não consegue representá-la
Sintaxe hipotética do CSSLog
- O CSSLog é apresentado como uma versão hipotética que mantém seletores e definição de propriedades como no CSS comum, mas também permite mudar propriedades que afetam o matching dos seletores
- No exemplo aparece uma sintaxe como
class: +barpara adicionar uma classe - Também se supõe algo como
+<div class="baz">para criar novos elementos filhos - Remover elementos é mencionado apenas como “provavelmente não” e sem mais detalhes
- No exemplo aparece uma sintaxe como
- Depois que uma regra
div.fooé executada, o mesmo elemento pode passar a casar também comdiv.bar, de modo que o resultado da execução das regras passa a influenciar os próximos matchings- Nesse ponto, já não basta uma única aplicação para frente; é preciso cálculo iterativo
- Ao reescrever o exemplo do modo escuro em CSSLog, torna-se possível fazer propagação recursiva de classes derivadas
- Começa com
[data-theme="dark"] { class: +effectively-dark; } - Propaga aos filhos com
.effectively-dark > :not([data-theme="light"]) { class: +effectively-dark; } - E aplica o estilo final com
.effectively-dark :focus { outline-color: white; }
- Começa com
- A segunda regra faz a propagação recursiva até a fronteira light, parando quando chega ao estado desejado
- O texto observa que esse comportamento não é possível no CSS atual e volta a mencionar algumas alternativas parciais no final
A estrutura do Datalog e sua semelhança com CSS
- No Datalog, os objetos são chamados de atoms e passam a existir no momento em que são mencionados pela primeira vez
- Nomes como
aliceebobpodem ser usados diretamente, sem declaração prévia - O texto os compara com
:symbolsdo Ruby
- Nomes como
- Conjuntos e relações são representados por relations e tuples
parent(alice, bob)é uma tupla dentro da relaçãoparentparenté descrita como o conjunto de pares em que o primeiro objeto é pai do segundo
- Variáveis são usadas para matching de consultas e seleção de conjuntos
parent(bob, X)significa todos osXdos quais Bob é pai- Nesse exemplo,
Xé avaliado comocaroledave - Por convenção, variáveis usam maiúsculas, e atoms e relations usam minúsculas
- Repetir o mesmo nome de variável produz um join
mother(X, Y) :- parent(X, Y), woman(X).cria a relação de mães pela interseção entre o conjunto de pais e o conjunto de mulheres- O texto descreve isso como a interseção entre “todos os pais” e “todas as mulheres”
- O
:-das regras em Datalog é lido como if: quando todas as condições do corpo à direita são verdadeiras, o fato do cabeçalho à esquerda é adicionado como verdadeiro- A vírgula no corpo é lida como and
ancestor(X, Y) :- parent(X, Y).significa que, se X é pai de Y, então X é ancestral de Y
- CSS e Datalog são comparados como estruturas semelhantes com a forma invertida
color(X, red) :- div(X), class(X, awesome).significa “a cor de X é vermelha se X fordive tiver a classe awesome”- Isso corresponde semanticamente a
div.awesome { color: red; }no CSS - O texto resume dizendo que o seletor é o body, e a declaração é o head
Recursão e fatos derivados
- Em Datalog, “fazer” alguma coisa significa derivar novos fatos
- O sistema funciona adicionando novas tuplas a relações com base em fatos já existentes
- O exemplo de
ancestoré apresentado como um caso clássico de regra recursivaancestor(X, Y) :- parent(X, Y).torna ancestral toda relação de parentalidade diretaancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).estende a relação seguindo os ancestrais do pai
- A segunda regra contém recursão autorreferente, porque
ancestoraparece tanto no head quanto no body- É ela que permite derivar relações indiretas como
alice -> bob -> carolealice -> bob -> dave
- É ela que permite derivar relações indiretas como
- O resultado do exemplo apresenta os cinco fatos abaixo
ancestor(alice, bob)ancestor(bob, carol)ancestor(bob, dave)ancestor(alice, carol)ancestor(alice, dave)
- O texto observa que, antes de
WITH RECURSIVE, o SQL também não conseguia fazer esse tipo de cálculo, e que esse recurso surgiu justamente por causa da demanda por computação recursiva- Ainda assim, acrescenta que a sintaxe e a semântica recursivas do SQL nem sempre se compõem bem com o restante da linguagem
- No Datalog, sem escrever um loop
for, o motor continua calculando até que todos os resultados necessários apareçam- A próxima seção liga isso ao conceito de fixpoint
Fixpoint e monotonicity
- O cascade do CSS comum é descrito como uma aplicação única para frente
- O navegador lê as regras, calcula o matching dos seletores, aplica as declarações e termina
- Não há loop de feedback
- No CSSLog e no Datalog real, o resultado de uma regra pode satisfazer novamente as condições de outra
- Uma regra muda uma propriedade, essa propriedade faz outra regra disparar, e isso pode até voltar a afetar a primeira
- Um motor ingênuo de Datalog repete a aplicação das regras até que não surjam mais fatos novos
- Começa pelos fatos-base explicitamente declarados
- Faz o matching do body de todas as regras com o conjunto atual de fatos
- Quando há correspondência, adiciona o fato do head
- Se apareceu algum fato novo, repete; se não, encerra
- Esse ponto de parada é chamado de fixpoint
- É o estado em que reaplicar todas as regras não produz mais nenhum resultado novo
- O exemplo de
ancestoré resumido em três rodadas- Na primeira, os fatos
parentgeram três relações diretas de ancestralidade - Na segunda, surgem duas relações indiretas para
alice - Na terceira, nenhum fato novo aparece e o sistema chega ao fixpoint
- Na primeira, os fatos
- O motivo de a execução terminar está na monotonicity
- Como os fatos nunca são removidos e apenas adicionados, o conjunto de fatos conhecidos só cresce
- Com fatos iniciais finitos e número finito de derivações possíveis, a avaliação para após uma quantidade finita de trabalho
- Se fosse permitido remover fatos, essa propriedade se perderia, porque resultados posteriores poderiam invalidar resultados anteriores
- O texto chama isso de Infinite Loop Land e usa isso para argumentar que é melhor o CSSLog não permitir remoção de elementos
- Em sistemas distribuídos, a monotonicidade também aparece em uma nota como uma propriedade ligada a consistência sem coordenação cara
- Consistency As Logical Monotonicity link 1: sistemas distribuídos e monotonicidade
- Artigo relacionado link 2: material complementar
Por que isso importa
- Comparar CSS e Datalog revela a mesma estrutura em áreas diferentes
- Em ambos os casos existem objetos, consultas sobre conjuntos desses objetos e uma ação que aplica algo ao resultado ou produz novos fatos
- Datalog e Prolog surgiram desde os anos 1970 em bancos de dados relacionais e pesquisas de IA da época, e depois foram recriados de várias formas
- Datomic
- Differential Datalog
- Também são citados diversos rule engines
- Há uma observação de que, quando se constrói um sistema em que há objetos, é possível descrever conjuntos e realizar operações sobre esses objetos, ele tende a convergir para pontos parecidos
- As áreas de bancos de dados/lógica e desenvolvimento web de front-end não costumam se conectar com frequência
- O texto fecha essa parte com a expectativa de que mais contato entre elas possa produzir algo novo
Container Queries e os limites do CSS real
- A discussão também toca em um recurso real do CSS, os Container Queries
- Eles permitem aplicar estilo ao elemento atual com base no estilo do pai ou de ancestrais
- O exemplo dado é
@container style(--theme: dark) { .card { background: royalblue; color: white; } }
- Porém, o problema do modo escuro transitivo exige um cálculo mais forte do que apenas consultar ancestrais
- Cada elemento precisa “saber” se ele próprio é effectively dark
- Esse estado precisa ser propagado transitivamente aos descendentes
- E a propagação deve parar na fronteira
data-theme="light"
- Os container queries não conseguem ler estado derivado
- Eles podem consultar o valor de propriedades customizadas dos ancestrais, mas não um estado como effectively-dark já calculado por outras regras
- Só conseguem ler estado que já existe no DOM, não o resultado de uma computação recursiva
- Portanto, uma consulta como “aplique se algum ancestral for dark transitivamente e não houver um ancestral light mais próximo” exige recursão e não pode ser implementada
- O texto afirma explicitamente que container queries não têm recursão
- O artigo de 2015 é citado para explicar por que element queries continuaram fracassando por motivos parecidos
- Se uma consulta puder ler de volta a propriedade que ela mesma define, surgem loops e até loops infinitos
- O CSS Working Group é apresentado como tendo resolvido esse tipo de problema por meio de restrições na direção do fluxo de informação
- Descendentes podem consultar informações dos ancestrais, mas o caminho inverso não é permitido
- Isso preserva finitude sem precisar de semântica de fixpoint
- A informação só se propaga para baixo na árvore, sem introduzir novos fatos-base
- O texto descreve esse movimento como um CSS que chega perto de um motor de Datalog, mas deliberadamente não vai até lá
- O CSSLog permitiria ciclos e avaliação até o fixpoint
- Já o CSS real para antes disso, porque um navegador é um motor de renderização, não um banco de dados relacional incremental
Possibilidades em outra direção
- Em vez de colocar semântica de Datalog dentro do navegador, também seria possível seguir o caminho oposto: colocar uma sintaxe estilo CSS por cima do Datalog
- Para usuários de linguagens modernas, a sintaxe do Datalog com
:-, ponto final, convenções de maiúsculas/minúsculas e ausência de atribuição pode ser uma barreira de entrada
- Para usuários de linguagens modernas, a sintaxe do Datalog com
- O CSS já possui uma sintaxe que lida diretamente com estruturas em árvore
- Combinadores de descendente, filho e irmão permitem expressar naturalmente relações de pai e filho
- Em Datalog comum, esse tipo de estrutura costuma precisar ser codificado em forma relacional de maneira mais trabalhosa
- O texto destaca que muitos dados reais têm forma de árvore
- JSON
- AST
- sistemas de arquivos
- organogramas
- XML
- Se existisse uma ferramenta que combinasse recursão por fixpoint, sintaxe ao estilo CSS e relações implícitas de pai e filho, seria possível escrever consultas recursivas sobre árvores com uma notação mais familiar
- Segundo o texto, ainda parece não haver uma ferramenta assim bem construída
- Ele termina sugerindo que alguém poderia criar algo como “CSSLog”, só que com um nome melhor
Notas
- Há uma nota sobre a simplificação dos elementos HTML
- O autor antecipa a objeção de que o CSS não lida necessariamente apenas com elementos HTML, mas diz que usa HTML para simplificar a explicação
- A naive evaluation é ineficiente porque recalcula fatos já conhecidos a cada rodada
- A melhoria padrão mencionada é a semi-naive evaluation
- A ideia central é observar, em cada etapa, apenas os fatos recém-derivados
- Também se acrescenta que loops infinitos em si não são novidade em linguagens Turing-complete
- Em JavaScript, por exemplo, é possível escrever
while true {} - Mas o contexto é que não se quer um sistema de renderização do navegador travado para sempre por causa de confusão lógica em um site
- Em JavaScript, por exemplo, é possível escrever
- Uma nota também menciona o atalho com herança de custom properties no CSS
[data-theme="dark"] { --effective-theme: dark; }[data-theme="light"] { --effective-theme: light; }@container style(--effective-theme: dark) { :focus { outline-color: white; } }- Essa abordagem funciona razoavelmente bem nesse caso específico, mas herança não é o mesmo que fecho transitivo real
- Em problemas mais complexos, em que o fecho transitivo precisa seguir cadeias de propriedades além de pai e filho, ela deixa de funcionar
1 comentários
Comentários do Hacker News
Seletores CSS são muito mais fáceis de escrever do que XPath
Recentemente até houve uma apresentação sobre como a nova API DOM do PHP lida com HTML e seletores CSS de forma nativa e muito simples. Antes, era preciso converter CSS para XPath
[1] https://speakerdeck.com/keyvan/parsing-html-with-php-8-dot-4...
Como isso evoluiu com foco em estilização de navegador, é uma pena que não tenha recursos como seleção baseada no conteúdo de texto, como no XPath
Pelo que sei, já houve propostas no passado, mas elas acabaram não entrando na spec por possíveis problemas de desempenho no contexto de renderização do navegador
Ao criar um agente de edição de documentos, exibimos o documento em HTML e deixamos o LLM indicar apenas o CSS selector para puxar os trechos necessários para o contexto, e funcionou quase como mágica
As pessoas podem usar exatamente o padrão com que já estão acostumadas
Seria bom existir um nome para separar a sintaxe do CSS de todo o sistema de regras, funções e unidades definido pelo CSSWG
Há bastante potencial aí, mas para discutir ou pesquisar outros casos de uso, no fim parece que só resta vasculhar código no GitHub com parser de CSS embutido para ver que tipo de coisa estranha as pessoas estão criando
Também estou mexendo com algo parecido com um mecanismo de templates estranho, misturando uma linguagem de marcação leve baseada em nós, seletores CSS para expressar o que entra no template, e uma sintaxe parecida com CSS para controlar como esses pedaços se combinam
https://www.w3.org/TR/selectors-3/
A spec do DOM também faz referência a isso
https://dom.spec.whatwg.org/#selectors
Então o termo genérico CSS selector já está correto, e também dá para chamar só de selector
O nome DOM selector talvez pareça mais limpo, mas se você considerar seletores usados em CSS estático ou em outros motores DOM fora de engines JavaScript, como parser XML, API DOM do PHP etc., isso pode acabar sendo ainda mais confuso
Além disso, existem seletores especiais ligados diretamente à renderização e navegação do navegador, como
:hovere::target-textAinda assim, pode ser útil ter um nome separado para o subconjunto mínimo da sintaxe de consulta menos acoplado ao navegador ou ao CSS
Isso me lembra https://github.com/braposo/graphql-css, que vi numa conferência há algum tempo
Era um projeto de brincadeira, mas gostei porque mostrava bem como transplantar e reutilizar padrões em outros contextos pode tornar possíveis coisas inesperadas
Estou justamente tentando reaproveitar padrões de contextos diferentes desse jeito
Mesmo que a maior parte não vá muito longe, isso tem um apelo bem interessante para o espírito hacker
pyastgrep, como mostrado em https://pyastgrep.readthedocs.io/en/latest/, permite usar seletores CSS para consultar sintaxe Python
O padrão é XPath e, por exemplo, dá para fazer
pyastgrep --css 'Call > func > Name#main'Vai quase exatamente na direção que eu queria apontar
Não entendi bem que tipo de cenário isso resolve
Hoje já dá para alterar condicionalmente um pai com base no filho. Por exemplo,
pretem padding padrão de 16px e, se o filho direto forcode, dá para zerar isso com&:has(> code)A conclusão fica mais próxima de “precisamos corrigir os limites do CSS moderno” do que de imaginar se colocar uma sintaxe parecida com CSS sobre um sistema parecido com Datalog poderia tornar o trabalho com dados em árvore mais familiar para mais engenheiros
Ou seja, a ideia é adicionar novos elementos filhos ou atributos ao DOM
Os LLMs atuais não costumam lidar tão bem com CSS, então isso até dá vontade de testar para ver se, desse jeito, eles conseguiriam raciocinar de forma mais simples
Não consigo pensar em muita utilidade prática, mas ainda assim é legal
Hum... isso não é simplesmente JQ?
Até gosto de CSS até certo ponto, mas odeio como o complexity creep só piora
Entendo a lógica de que linguagens de programação acabam ficando mais poderosas que linguagens não programáveis, mas em vez de continuar aumentando a complexidade de HTML, CSS e JavaScript, eu preferiria que surgisse outra coisa para substituir tudo isso
Também quase nunca uso os novos elementos do HTML5, porque na maioria dos casos nem entendo por que eles são necessários. No fim, muitos containers parecem ser só
divcom IDs únicos e eu até gostaria que existisse algo como um alias para esses IDs para navegaçãohrefinternaCoisas como
[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }demoram demais para eu interpretar mentalmente e já não parecem mais elegantes nem simplesPor outro lado,
h2 { color: red; }ainda é simplesUma expressão como
ancestor(X, Y) :- parent(X, Y).já me faz perder a vontade de pensar. Afinal, o que é:-? Parece uma carinha sorrindoParei de ler em
@container style(--theme: dark) { .card { background: royalblue; color: white; } }É estranho ver padrões que antes funcionavam bem parecerem se deteriorar com o tempo
Por exemplo,
[data-theme="dark"] [data-theme="light"] :focus { outline-color: black; }em pseudocódigo mais próximo do inglês seria algo como: existe um X comdata-theme="dark", seu filho Y temdata-theme="light"e está em foco, entãooutline-colorde Y deve ser blackAssim, em estilo Datalog, isso poderia ser escrito como
outline-color(Y, black) if data-theme(X, "dark") and parent(X, Y) and data-theme(Y, "light") and focused(Y)Ou seja, trocando
:-porife vírgulas porandIndo além, também daria para escrever algo como
Y.outline_color := black if X.data-theme == dark and Y.parent == X and Y.data-theme == dark and Y.focused, fazendoattr(X, val)parecer um açúcar sintático parecido com UFCS na formaX.attr == valSe quisesse algo com mais cara de família ALGOL, também poderia ser
forall Y { Y.outline_color := black if Y.data_theme == "dark" and Y.focused and Y.parent.data_theme == "light" }Aqui Y é introduzido explicitamente e um dos joins fica implícito, então isso parece mais programação geral, mas na prática ainda seria um motor Datalog executando esse tipo de loop com eficiência sempre que as dependências mudassem