มอง CSS ให้เป็นภาษาสืบค้น
(evdc.me)- โครงสร้างของ CSS ที่ใช้ selector และกฎ เพื่อเลือกชุดเป้าหมายและนำคุณสมบัติไปใช้ มีความคล้ายคลึงในเชิงรูปแบบกับ Datalog ที่ทำงานด้วยเซตและกฎ
- การผสาน selector อย่าง
div.awesomeสร้าง จุดตัด และใน Datalog ก็เกิด join ที่คล้ายกันผ่านการใช้ตัวแปรเดิมซ้ำ - CSS ปัจจุบันไม่สามารถนำผลลัพธ์ของ computed style กลับมาใช้เป็นเงื่อนไขการเลือกได้ จึงยากที่จะเขียน การสืบค้นแบบส่งผ่านเชิงเวียนเกิด หรือการแพร่กระจายซ้ำของสถานะที่ได้มาโดยตรง
- Datalog ใช้ กฎแบบเวียนเกิด และ การประเมินแบบจุดตรึง เพื่อขยายความสัมพันธ์ไปเรื่อย ๆ จนไม่มีข้อเท็จจริงใหม่เกิดขึ้น และด้วยคุณสมบัติความเป็นโมโนโทนิกจึงสามารถจบการคำนวณได้ภายในขอบเขตที่มีจำกัด
- CSS จริงมีฟีเจอร์อย่าง Container Queries ที่อ่านข้อมูลจากบรรพบุรุษได้ แต่เลือกแนวทางที่ป้องกัน feedback loop และวงจร และยังคงเปิดช่องให้ไวยากรณ์แบบ CSS ไปประยุกต์กับภาษาสืบค้นแบบเวียนเกิดได้
โครงสร้างที่คล้ายกันของ CSS และ Datalog
- CSS มีโครงสร้างแบบ เลือกชุดเป้าหมาย และ ใช้กฎกับเป้าหมายที่ถูกเลือก
- "Things" เช่นองค์ประกอบ HTML มีอยู่ก่อน แล้ว selector จะชี้ไปยังเซตที่มีคุณสมบัติร่วมกัน
- สามารถอธิบายเซตด้วย selector อย่าง
div,#child,.awesome,[data-custom-attribute="foo"] - สามารถรวม selector แบบ
div.awesomeเพื่อสร้าง จุดตัด ได้
- กฎของ CSS ผูก selector เข้ากับ declaration เพื่อกำหนดคุณสมบัติอย่าง
colorหรือfont-sizeให้กับองค์ประกอบที่เลือก- แต่คุณสมบัติเหล่านี้ส่วนใหญ่เป็นการเปลี่ยน สถานะภายนอกภาษา และไม่สามารถนำผลลัพธ์นั้นกลับมาเป็นเงื่อนไขของ selector ได้
- รูปแบบอย่าง
div[color=red]ที่สืบค้นผลลัพธ์ของ style ย้อนกลับ เบราว์เซอร์จะไม่ยอมรับ
- Datalog ก็ทำงานในลักษณะคล้ายกันด้วย ชุดข้อเท็จจริง และ การอนุมานตามกฎ
- อะตอมและความสัมพันธ์อย่าง
parent(alice, bob)เป็นหน่วยพื้นฐาน - ใช้ตัวแปร
X,Yเพื่อเลือกชุดรายการที่ตรงเงื่อนไข - เมื่อนำตัวแปรเดียวกันมาใช้ซ้ำเพื่อเชื่อมเงื่อนไข ก็จะเกิด join ที่คล้ายกับการรวม selector ของ CSS
- อะตอมและความสัมพันธ์อย่าง
- โครงสร้าง
head(X, Y) :- body1(X, Z), body2(Z, Y)มีรูปแบบคล้ายกฎของ CSS เพียงแต่ทิศทางกลับกัน- selector ของ CSS ใกล้เคียงกับ body ของ Datalog ส่วน 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 ก็จะเป็น effectively-dark ด้วย ถ้าไม่มี
data-theme="light"คั่นอยู่ - จากนั้นจึงต้องใช้สไตล์กับ
.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 จริง
- Container Queries ใน CSS จริงสามารถใช้กฎโดยอิงจาก style ของบรรพบุรุษหรือคอนเทนเนอร์ได้
- รองรับรูปแบบอย่าง
@container style(--theme: dark) { .card { background: royalblue; color: white; } }
- รองรับรูปแบบอย่าง
- แต่ตัวอย่าง transitive dark mode ต้องการเงื่อนไขที่แรงกว่าการดูบรรพบุรุษแบบธรรมดา
- แต่ละองค์ประกอบต้องรู้ว่าตัวเองเป็น effectively-dark หรือไม่
- สถานะนั้นต้อง แพร่กระจายแบบส่งผ่าน ไปยังลูกหลานทั้งหมด
- การแพร่กระจายต้องหยุดที่ขอบเขต
data-theme="light"
- Container Queries จัดการเงื่อนไขข้อที่สองไม่ได้
- แม้อ่าน custom property ของบรรพบุรุษได้ แต่ไม่สามารถสืบค้น สถานะที่อนุมานได้ ซึ่งคำนวณโดยกฎอื่นไปแล้วซ้ำอีก
- มองเห็นข้อมูลที่มีอยู่เดิมใน DOM ได้ แต่ไม่สามารถใช้ผลการคำนวณแบบเวียนเกิดมาเป็นเงื่อนไขของ selector ได้
- บทความที่เกี่ยวข้อง ในปี 2015 ก็ชี้ว่า element queries ชนกับปัญหาเดียวกัน
- ถ้าอนุญาตให้สืบค้นคุณสมบัติที่ตั้งค่าด้วย query ซ้ำได้ ความเสี่ยงของลูปและการวนไม่สิ้นสุดจะเพิ่มขึ้นมาก
- CSS Working Group หลีกเลี่ยงปัญหานี้ด้วย การจำกัดทิศทางของการไหลของข้อมูล
- อนุญาตให้ลูกหลานสืบค้นข้อมูลของบรรพบุรุษได้
- แต่กัน feedback ทิศทางกลับหรือวงจรที่ย้อนมาสู่ style ของตัวเอง
- จึงคงการคำนวณให้มีที่สิ้นสุดได้โดยไม่ต้องพึ่งความหมายเชิงจุดตรึง
ความเป็นไปได้ในการกลับด้านไวยากรณ์ CSS ให้เป็นภาษาสืบค้นแบบเวียนเกิด
- แทนที่จะนำความหมายแบบ Datalog มาใส่ใน CSS ผู้เขียนเสนอว่าแนวทางใหม่ที่เป็นจริงได้มากกว่าคือ นำไวยากรณ์ CSS ไปวางทับบน Datalog
- ไวยากรณ์อย่าง
:-, จุดปิดท้าย, และอะตอมที่ไม่มี declaration ของ Datalog เป็นอุปสรรคต่อผู้ใช้ภาษาสมัยใหม่พอสมควร - CSS เองก็มีไวยากรณ์ selector ที่หลากหลายอยู่แล้วสำหรับจัดการโครงสร้างแบบต้นไม้
- ไวยากรณ์อย่าง
- ผู้เขียนชี้ว่าข้อมูลจริงจำนวนมากมีลักษณะเป็น โครงสร้างต้นไม้
- JSON
- AST
- ระบบไฟล์
- ผังองค์กร
- XML
- ในโดเมนเหล่านี้ การผสานไวยากรณ์แบบ CSS ที่จัดการความสัมพันธ์พ่อแม่/ลูกโดยนัย เข้ากับ การเวียนเกิดแบบจุดตรึง อาจมีประโยชน์มาก
- Datalog ทั่วไปมักต้องแปลงโครงสร้างต้นไม้มาเขียนใหม่ในรูปเชิงสัมพันธ์ ซึ่งยุ่งยาก
- ถ้านำความรู้สึกแบบ selector ของ CSS มาใช้กับการสืบค้นแบบเวียนเกิดได้ตรง ๆ ก็อาจทำให้นักพัฒนาจำนวนมากเข้าถึงได้ง่ายขึ้น
- เครื่องมือในรูปแบบนี้ยังไม่ปรากฏชัดนัก
- ชื่อ "CSSLog" เป็นเพียงชื่อชั่วคราว และอาจมีภาษาอื่นที่ได้ชื่อดีกว่านี้
- ยังมีพื้นที่ให้จัดการการสืบค้นต้นไม้แบบเวียนเกิดด้วยสัญกรณ์ที่คุ้นเคยกว่าเดิม
ประเด็นเสริมและลิงก์อ้างอิง
- Datalog ถือกำเนิดขึ้นตั้งแต่ทศวรรษ 1970 ในบริบทของฐานข้อมูลเชิงสัมพันธ์และงานวิจัย AI ยุคนั้น ก่อนจะกลับมาได้รับความสนใจซ้ำในหลายรูปแบบ
- รูปแบบการคำนวณจุดตรึงที่ง่ายที่สุดมักถูกแนะนำในชื่อ naive evaluation แต่สามารถไม่มีประสิทธิภาพเพราะคำนวณข้อเท็จจริงเดิมซ้ำทุกครั้ง
- การปรับปรุงที่พบได้บ่อยคือ semi-naive evaluation ซึ่งใช้เฉพาะข้อเท็จจริงใหม่ที่เกิดขึ้นในแต่ละรอบ
- ความเป็นโมโนโทนิกยังนำไปสู่คุณสมบัติที่เป็นประโยชน์ในระบบกระจายอีกด้วย
- ยังมีวิธีเลียนแบบ transitive dark mode บางส่วนด้วยการสืบทอด custom property
[data-theme="dark"] { --effective-theme: dark; }[data-theme="light"] { --effective-theme: light; }@container style(--effective-theme: dark) { :focus { outline-color: white; } }- วิธีนี้ใช้แทนได้ค่อนข้างดีในกรณีเฉพาะนี้ แต่ไม่ได้ให้ transitive closure ที่แท้จริง ในภาพรวม
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
CSS selector เขียนง่ายกว่า XPath มาก
ไม่นานมานี้ยังมีการพูดถึงด้วยว่า DOM API ใหม่ของ PHP ทำให้จัดการ HTML และ CSS selector ได้แบบเนทีฟอย่างง่ายมาก แต่ก่อนต้องแปลง CSS ไปเป็น XPath
[1] https://speakerdeck.com/keyvan/parsing-html-with-php-8-dot-4...
แต่น่าเสียดายที่มันพัฒนามาโดยเน้นการจัดสไตล์บนเบราว์เซอร์ เลยไม่มีความสามารถอย่างการ เลือกตามเนื้อหาข้อความ แบบ XPath
เท่าที่รู้เคยมีข้อเสนอมาก่อน แต่เข้า spec ไม่ได้เพราะอาจมีปัญหาด้านประสิทธิภาพในบริบทการเรนเดอร์ของเบราว์เซอร์
ตอนทำเอเจนต์สำหรับแก้ไขเอกสาร ผมแสดงเอกสารเป็น HTML แล้วให้ LLM ระบุแค่ CSS selector เพื่อดึงชิ้นส่วนที่ต้องการเข้ามาเป็นคอนเท็กซ์ ซึ่งได้ผลดีแทบจะเหมือนเวทมนตร์
คนสามารถใช้วิธีที่คุ้นเคยได้เหมือนเดิม
น่าจะดีถ้ามีชื่อที่ใช้เรียกแยกระหว่าง ไวยากรณ์ CSS กับระบบทั้งหมดของกฎ ฟังก์ชัน หน่วย ฯลฯ ที่ CSSWG นิยามไว้
ฝั่งนี้ดูมีศักยภาพพอตัว แต่ถ้าจะคุยหรือค้นคว้าเคสการใช้งานอื่น ๆ สุดท้ายก็ดูเหมือนต้องไปคุ้ยโค้ดบน GitHub ที่มี CSS parser อยู่ เพื่อดูว่าคนกำลังสร้างของประหลาดอะไรบ้าง
ผมเองก็กำลังลองจับของประหลาดคล้าย template engine ที่ผสมภาษามาร์กอัปแบบเบา ๆ ที่อิง node, CSS selector สำหรับบอกว่าอะไรจะเข้าไปในเทมเพลต, และไวยากรณ์คล้าย CSS สำหรับควบคุมว่าชิ้นส่วนเหล่านี้จะประกอบกันอย่างไร
https://www.w3.org/TR/selectors-3/
และสเปก DOM ก็อ้างอิงสิ่งนี้
https://dom.spec.whatwg.org/#selectors
ดังนั้นคำเรียกรวมว่า CSS selector ก็ถูกต้องอยู่แล้ว หรือจะเรียกแค่ selector ก็ได้
ชื่อ DOM selector อาจดูสะอาดตากว่า แต่ถ้าคิดถึง selector ที่ใช้ใน CSS แบบสแตติกหรือใน DOM engine อื่นนอก JS engine เช่น XML parser หรือ PHP DOM API ก็อาจยิ่งชวนสับสน
อีกอย่างยังมี selector พิเศษอย่าง
:hoverหรือ::target-textที่ผูกตรงกับการเรนเดอร์และการนำทางของเบราว์เซอร์แต่ถ้าจะตั้งชื่อแยกให้กับ ส่วนย่อยขั้นต่ำของไวยากรณ์คิวรี ที่ผูกกับเบราว์เซอร์หรือ CSS น้อยกว่า ก็น่าจะมีประโยชน์
นึกถึง https://github.com/braposo/graphql-css ที่เคยเห็นในงานคอนเฟอเรนซ์เมื่อก่อน
มันเป็นโปรเจ็กต์เล่น ๆ แต่ผมชอบที่มันแสดงให้เห็นได้ดีว่า การย้ายรูปแบบหนึ่งไปปลูกในอีกบริบทแล้วนำกลับมาใช้ใหม่ สามารถเปิดทางให้เกิดอะไรที่คาดไม่ถึงได้
ผมก็กำลังลองหยิบแพตเทิร์นจากต่างบริบทมาใช้ในแนวนี้พอดี
ส่วนใหญ่คงไปไม่ไกลมาก แต่ในแง่ความเป็นแฮ็กเกอร์มันน่าสนใจไม่น้อย
pyastgrep อย่างที่เห็นใน https://pyastgrep.readthedocs.io/en/latest/ สามารถใช้ CSS selector เพื่อคิวรีไวยากรณ์ของ Python ได้
ค่าเริ่มต้นคือ XPath เช่น
pyastgrep --css 'Call > func > Name#main'มันเกือบตรงกับทิศทางที่ผมอยากชี้เลย
ผมยังไม่ค่อยเข้าใจว่ามันแก้สถานการณ์แบบไหน
ตอนนี้ก็เปลี่ยนพาเรนต์แบบมีเงื่อนไขตามลูกได้อยู่แล้ว เช่น
preมี padding ปกติ 16px แต่ถ้ามีลูกโดยตรงเป็นcodeก็ทำให้เป็น 0 ได้ด้วย&:has(> code)ข้อสรุปเลยไม่ใช่แนวว่า "ต้องแก้ข้อจำกัดของ CSS สมัยใหม่" แต่ใกล้เคียงกับว่า ถ้าเอา ไวยากรณ์คล้าย CSS ไปวางบน ระบบคล้าย Datalog มันอาจทำให้งานกับข้อมูลแบบต้นไม้ดูคุ้นมือสำหรับวิศวกรมากขึ้นไหม
พูดอีกแบบคือเป็นเรื่องของการเพิ่มองค์ประกอบลูกหรือแอตทริบิวต์ใหม่เข้าไปใน DOM
LLM ตอนนี้จริง ๆ แล้วไม่ได้เก่งเรื่อง CSS มากนัก เลยยิ่งอยากลองดูว่าถ้าทำแบบนี้แล้ว LLM จะให้เหตุผลได้ง่ายขึ้นไหม
นึกการใช้งานจริงไม่ค่อยออก แต่ก็ยังเท่อยู่ดี
อืม... ฟังดูเหมือนนี่คือ JQ เฉย ๆ หรือเปล่า
ผมชอบ CSS ในระดับหนึ่ง แต่ไม่ชอบที่มันมี ความซับซ้อนค่อย ๆ เพิ่มพอกพูน มากขึ้นเรื่อย ๆ
ผมเข้าใจเหตุผลที่ว่าภาษาโปรแกรมย่อมทรงพลังกว่าภาษาที่ไม่ใช่ภาษาโปรแกรม แต่แทนที่จะทำให้ HTML·CSS·JavaScript ซับซ้อนขึ้นไปเรื่อย ๆ ผมกลับรู้สึกว่าน่าจะดีกว่าถ้ามีอะไรสักอย่างมาแทนทั้งก้อนนั้นไปเลย
องค์ประกอบใหม่ ๆ ใน HTML5 ส่วนใหญ่ผมก็ยังไม่ค่อยเข้าใจว่าจำเป็นไปเพื่ออะไร เลยแทบไม่ใช้ สุดท้ายก็เริ่มคิดว่าคอนเทนเนอร์จำนวนมากก็เป็นแค่
divที่มี ID เฉพาะเท่านั้น และถึงขั้นเคยอยากให้มี alias สำหรับ ID แบบนั้นไว้ใช้กับการนำทางhrefภายในหน้าอะไรอย่าง
[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; }ถ้าคลี่เป็น pseudocode ภาษาอังกฤษ ก็จะใกล้เคียงกับว่า มี X ที่data-theme="dark"และมีลูก Y ของมันที่data-theme="light"และอยู่ในสถานะโฟกัส ก็ให้outline-colorของ Y เป็น 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)ดูเป็น syntactic sugar คล้าย UFCS แบบX.attr == valได้ด้วยถ้าอยากให้ดูเป็นสาย ALGOL มากขึ้น ก็อาจเป็น
forall Y { Y.outline_color := black if Y.data_theme == "dark" and Y.focused and Y.parent.data_theme == "light" }ตรงนี้มีการประกาศ Y อย่างชัดเจนและทำให้ join หนึ่งตัวเป็นนัย เพื่อให้ดูเหมือนการเขียนโปรแกรมทั่วไปมากขึ้น แต่ในความเป็นจริงก็คือ Datalog engine จะคอยรันลูปแบบนี้อย่างมีประสิทธิภาพทุกครั้งที่ dependency เปลี่ยน