实验 – Projectional Viewer

Hacker News Top 工具

摘要

这个实验利用OhmJS和RWR的管道在Emacs中隐藏代码实现细节,通过专注于结构来提高代码可读性。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/09/11 14:24

# 实验——投影视图 来源:https://programmingsimplicity.substack.com/p/experiment-projectional-viewer 现代编程语言将结构深埋在实现细节之下。信噪比过低——太多*如何实现*的代码掩盖了*做什么*的本质——这使得理解超出简单范畴的问题变得困难。 本实验探究的是:能否在不删除代码的情况下,将实现细节从视图中隐藏?细节仍保留在源代码中,只是在编辑器里被暂时关闭显示。目标在于更清晰地展现解决方案的结构,无需费力梳理结构背后的执行机制。 最初的痛点来自PEG(解析表达式文法)库。我对文法有相当了解,但用过的大多数PEG实现都将文法本身淹没在解析过程中捕获和处理匹配结果的代码层里。文法——即被识别对象的实际结构——就这样消失在了管道代码中。 完整的双向投影视图编辑器(即编辑隐藏视图并反向写回源代码)难以完美实现。因此,本实验退而求其次,实现单向转换:仅为阅读而隐藏细节,不支持编辑隐藏后的形式。关键问题在于:这个较低目标是否容易实现且依然有价值? 其核心思想是将"文法"视为模式匹配的领域专用语言(DSL)——一种嵌套在另一种语言中的小型语言。如果隐藏在此可行,则暗示着一条更广阔的路径:将多个小型专用语言组合进单一代码库,而不是强迫所有需求都通过通用范式来解决。 我从一个真实的小例子开始:用Rebol编写的电话号码文法,包含所有实现细节。 `` parse "(416)555-1234" [ "(" copy area 3 digits ")" copy exch 3 digits "-" copy line 4 digits (print rejoin [area "-" exch "-" line]) ] `` 其中`copy area`、`copy exch`、`copy line`以及末尾的`(print rejoin ...)`代码块都是实现细节——它们关于捕获值并进行后续处理,与有效电话号码的结构无关。剥离这些后,文法本身仅为: `` parse "(416)555-1234" [ "(" 3 digits ")" 3 digits "-" 4 digits ] `` 为了自动完成这一转换,我让Claude Sonnet 5 Code编写了一个Emacs Lisp文件`pelide-view.el`。该文件将缓冲区(或区域)内容发送到基于T2T构建的小型管道处理:先由OhmJS解析器处理,再应用RWR重写语义。管道读取带注解的源代码,返回隐藏后的文法,Elisp代码随后将其显示回编辑器。 管道解析器的文法(`pelide.ohm`)及其重写规则(`pelide.rwr`),连同生成的`pelide-view.el`,均在附录中。其余代码位于GitHub仓库(https://github.com/guitarvydas/rebol)。 受限于时间,我仅针对上述单一示例进行了测试。它能否推广到更复杂的文法仍有待验证。 要复现此实验:需加载并执行`pelide-view.el`,在Emacs中打开`ex.r`,然后按`C-c p t`切换至隐藏视图,再次按键则切回原视图。 管道按预期工作:它隐藏了`copy`子句和末尾的操作块,留下了如上所示的纯文法。在Emacs中来回切换的效果符合预期。 [](https://substackcdn.com/image/fetch/$s_!hhmN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafebce7e-ce9b-4a0a-9245-0ec36a6c9219_486x340.png) [](https://substackcdn.com/image/fetch/$s_!HfrX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b748079-bab5-4602-9d1c-8f722ab2d013_486x340.png) 这背后是我T2T转换技术的运用:OhmJS负责解析,而我称为RWR的自定义领域语言负责过滤、重写和隐藏操作。结果交回Emacs进行只读查看。 即使从这一个示例来看,隐藏后的文法比带注解的源代码更易读——只需一眼就能看清实际匹配的模式。这对整体思路是个好兆头。 这个结果虽小,但在得出有力结论前,我还想用更多文法进行测试。不过它似乎值得继续探索。自然的下一步是为其他更现代的PEG实现(如Lua和Janet的版本)编写隐藏文法。 这也表明将T2T用作过滤器在现代硬件上足够快速可用——切换时的延迟几乎难以察觉。只要我们保持小型语言的"小巧",这种情况应会持续:无需为此编写图灵完备的巨型文法,因为所需处理的代码片段都很小。 该实验还指向一个值得解决的后续问题:编写能够检测大型文件中可隐藏代码存在的文法,而非假设整个文件都是一个可隐藏单元。 本实验是更大问题的一小部分:如何让多个小型DSL在单一项目中共存,而非强制所有需求通过通用语言? 一个已推进的方向是"转换"而非"隐藏":使用小型元语言编写,然后生成标准目标语言。PBP内核就是这样构建的——三个用`.rt`元语言编写的源文件(`0d.rt`、`stock.rt`、`jit.rt`),目前被转换为Python、JavaScript和Common Lisp。转换器本身已可工作(详见内核仓库(https://github.com/guitarvydas/pbp/tree/main/kernel)),但我尚未对其进行投影视图实验。 更前瞻的方向是图示编程:从图示编程语言(DPL)而非文本生成代码片段。已有使用drawio的小型早期示例——决策树仓库(https://github.com/guitarvydas/dtree)和状态机仓库(https://github.com/guitarvydas/sm-pbp)——但同样,隐藏实验尚未应用于它们,甚至还不清楚drawio如何与Emacs集成。 `` ;;; pellide-view.el --- Shell out to PELLIDE for a condensed view -*- ;;; lexical-binding: t; -*- (defgroup pellide-view nil "Shell buffer/region out to the PELLIDE tool for a condensed view." :group 'tools) (defcustom pellide-view-command "pellide" "Shell command to run. Reads source on stdin, writes condensed view on stdout." :type 'string) (defcustom pellide-view-command-args nil "Extra CLI args to pass to `pellide-view-command'." :type '(repeat string)) (defvar-local pellide-view--original nil "Buffer-local stash of the pre-toggle source, used by `pellide-view-toggle-in-place'.") (defun pellide-view--run (text) "Pipe TEXT through `pellide-view-command'; return its stdout as a string." (with-temp-buffer (insert text) (let ((exit-code (apply #'call-process-region (point-min) (point-max) pellide-view-command t t nil pellide-view-command-args))) (unless (zerop exit-code) (error "%s exited %s: %s" pellide-view-command exit-code (buffer-string))) (buffer-string)))) ;;;###autoload (defun pellide-view-region-or-buffer () "Shell the region (or whole buffer) out to PELLIDE; show the result read-only in a `*PELLIDE View*' side window. Non-destructive -- source buffer is untouched." (interactive) (let* ((text (if (use-region-p) (buffer-substring-no-properties (region-beginning) (region-end)) (buffer-string))) (mode major-mode) (result (pellide-view--run text)) (out-buf (get-buffer-create "*PELLIDE View*"))) (with-current-buffer out-buf (let ((inhibit-read-only t)) (erase-buffer) (insert result) (funcall mode) (setq buffer-read-only t))) (display-buffer out-buf '((display-buffer-reuse-window display-buffer-in-side-window) (side . right) (window-width . 0.5))))) ;;;###autoload (defun pellide-view-toggle-in-place () "Toggle the CURRENT buffer's text between source and PELLIDE's condensed view. Destructive: only safe if you don't edit while in the condensed view -- PELLIDE is not assumed invertible, so toggling back just restores the stashed original, discarding any edits made to the condensed text." (interactive) (if pellide-view--original (let ((orig pellide-view--original) (modified (buffer-modified-p))) (setq pellide-view--original nil) (setq buffer-read-only nil) (erase-buffer) (insert orig) (set-buffer-modified-p modified)) (let ((orig (buffer-string)) (modified (buffer-modified-p))) (erase-buffer) (insert (pellide-view--run orig)) (setq buffer-read-only t) (setq pellide-view--original orig) (set-buffer-modified-p modified)))) (global-set-key (kbd "C-c p v") #'pellide-view-region-or-buffer) (global-set-key (kbd "C-c p t") #'pellide-view-toggle-in-place) (setq pellide-view-command "~/projects/rebol/pellide") (provide 'pellide-view) `` `` pelide { main = "parse" spaces item spaces pGrammar spaces item = string | id string = "\"" (~"\"" any)+ "\"" id = letter idtail* idtail = alnum pGrammar = "[" grammarItem+ "]" grammarItem = | space -- space | string -- string | copyItem -- copy | topLevelActionItem -- action | ~"[" ~"]" ~"(" ~")" ~"copy" ~space ~string any -- other topLevelActionItem = "(" spaces actionItem+ spaces ")" copyItem = "copy" spaces id spaces int? spaces id actionItem = | "(" spaces actionItem+ spaces ")" -- recparen | "[" spaces actionItem+ spaces "]" -- recbrack | ~"]" ~"[" ~"(" ~")" ~"copy" any -- other int = digit+ } `` `` %rewrite pelide { main [_parse ws1 item ws2 pGrammar ws3] = ‛parse«ws1»«item»«ws2»«pGrammar»«ws3»’ item [x] = ‛«x»’ string [lq cs+ rq] = ‛«lq»«cs»«rq»’ id [letter idtail*] = ‛«letter»«idtail»’ idtail [alnum] = ‛«alnum»’ pGrammar [lb item+ rb] = ‛«lb»«item»«rb»’ grammarItem_space [s] =‛«s»’ grammarItem_string [s] =‛«s»’ grammarItem_copy [x] =‛«x»’ grammarItem_action [x] =‛«x»’ grammarItem_other [x] = ‛«x»’ topLevelActionItem [lp ws1 A+ ws2 rp] = ‛’ copyItem [_copy ws1 id ws2 int? ws3 id2] = ‛«int»«ws3»«id2»’ actionItem_recparen [lp ws1 a+ ws2 rp] =‛’ actionItem_recbrack [lb ws2 a+ ws3 rb] =‛’ actionItem_other [x] =‛’ int [digit+] = ‛«digit»’ } `` *电子邮件*:[[email protected]](mailto:[email protected]) *Substack*:paultarvydas.substack.com (http://paultarvydas.substack.com/) *视频*:https://www.youtube.com/@programmingsimplicity2980 *Discord*:https://discord.gg/65YZUh6Jpq *Leanpub*:https://leanpub.com/u/paul-tarvydas (https://leanpub.com/u/paul-tarvydas) *Twitter*:@paul\_tarvydas *Bluesky:*@paultarvydas.bsky.social *Mastodon:*@paultarvydas *(早期)博客*:guitarvydas.github.io (http://guitarvydas.github.io/) *参考文献*:https://guitarvydas.github.io/2024/01/06/References.html *笔记与草稿*:https://github.com/guitarvydas/napkin-notes%5D *付费订阅是支持此项工作的自愿方式\。* 分享 (https://programmingsimplicity.substack.com/p/experiment-projectional-viewer?utm_source=substack&utm_medium=email&utm_content=share&action=share) 发表评论 (https://programmingsimplicity.substack.com/p/experiment-projectional-viewer/comments) #### 关于这篇文章的讨论 ### 想了解更多?

相似文章

离线 JS 沙盒

Product Hunt

一个离线的 JavaScript 沙盒,可直接在 Chrome 中编写、运行和测试代码片段,非常适合快速实验和调试。