内联求值 (Inline Evaluation) 探险之旅

2025年3月12日

内联求值已经存在很长时间了,但并非每个程序员都有机会使用它。如果你还没用过,这是一个尝试的机会。

为此,我编写了一个简单的编辑器,其中包含一些代码示例。 以下示例都是可编辑和可执行的,但是没有 run 按钮来执行它们。 你可以使用 Control-rRUN/EVALUATE 从下面的编辑器窗格中选择的代码片段。

要查看它的实际效果,请将光标直接放在要评估的表达式之后。 然后,按 Control-r (按住 Control 键,同时按 r 键)。 例如,如果要评估 (+ 1 2 3),请将光标放在右括号 ) 之后,然后按 Control-r。 为了简洁起见,从现在开始,我们将使用 ^r 作为 Control-r 的简写。

1;`-- Place cursor after 1 then hit ^r
"Gremlins are cuties!"; `-- Place cursor after " then hit ^r
(+ 1 2 3); `-- Place cursor after 1 then hit ^r
(+ 1 2 3); `-- Place cursor after the ')' then hit ^r
(+ 1 (/ 8 2) (* 2 3) ); `-------`-` Place cursor after each ')' and then hit ^r
(+ 1 (/ 8 2) (* 2 3)); `-- Place cursor after ')' and then hit ^r
;`-- Place cursor above this line and hit ^r

当你按下 ^r 时,编辑器会找到最接近的前一个完整表达式,并将其发送到运行时会话 (REPL) 进行评估。 然后,结果将显示在代码旁边,正好在你正在看的位置。 无需切换上下文即可查看代码运行结果。

你可能已经注意到,你可以评估表达式的较小部分,例如 (/ 8 2),就像评估完整表达式一样容易。 你还可以评估跨越多行的表达式。

Lisp 语言使编写可以执行此操作的编辑器工具变得简单。 编写一个编辑器插件来检测由括号分隔的表达式是很容易的。

有时,当我不确定函数名称或行为时,我不会搜索它们,而是直接尝试内联评估它们,以查看它们是否存在以及它们的行为是否符合我的预期。

例如,我知道我创建了一个函数,该函数将 HTML 渲染到编辑器上方的 div 中,但我记不清它叫 show-html 还是 display-html。 让我们评估以下两个表达式,看看哪个有效:

;; was it show-html or display-html??
(show-html "<h1>It's in a DIV!</h1>");; `-- ^r eval here
(display-html "<h1>It's in a DIV!</h1>");; `-- ^r eval here

通过内联求值,我可以快速发现哪个函数已定义 以及 它是否符合我的预期,而无需离开编辑器或中断我的流程。

在下一个示例中,让我们应用相同的方法来发现 lookmovereset 函数的功能。

在以下所有示例中,尝试使用 ^r 评估每个表达式以查看结果。

(look)
(move :east)
(reset)

请注意,look 函数如何返回描述你在文本冒险游戏中位置的数据,而 move 函数使你可以在这个虚拟世界中导航。

这提供了一个机会来演示内联求值如何帮助我们逐步构建和完善代码。

评估 (look)(move :east) 可以作为游戏的一个简陋界面,但使用我们的 display-html 函数显示这些数据肯定会更好。 对吧? 对吧??

现在,如果我们检查 look 函数返回的数据,我们可以看到它返回某种 hash map,其中包含键 :desc:seen:exit 等。 这些键都映射到游戏状态的 string 描述。

让我们开始将 look 返回的数据格式化为可以显示的 HTML。

;; First lets see what data is returned
(look);; I see a :desc key that holds a description
(get (look) :desc);; OK we have a description let's put that into an HTML string
(str "<p>" (get (look) :desc) "</p>");; Now let's display that HTML
(display-html (str "<p>" (get (look) :desc) "</p>"))

让我们通过将该段落标签分解成它自己的函数来改进我们的代码:

;; Evaluate this to define the paragraph function
(defn p [content] (str "<p>" content "</p>"));; now let's see how it works
(p "Hello")
(display-html (p (get (look) :desc)))

好的,现在我们有点进展了。 但是我们还必须格式化房间里 :seen 的东西:

(look);; Let's extract the :seen items
(get (look) :seen);; Add some context to the raw data
(str "You see: " (get (look) :seen));; put it in a paragraph
(p (str "You see: " (get (look) :seen)));; let's add the :desc and the :seen together
(str (p (get (look) :desc)) (p (str "You see: " (get (look) :seen)))) ;; then display it
(display-html (str (p (get (look) :desc)) (p (str "You see: " (get (look) :seen)))))

因此,我们正在构建一些代码,以将 look 函数返回的数据格式化为 HTML。

所以让我们把这段代码放在一个函数中,开始处理这个函数,而不是仅仅组合表达式。

;; so here's our initial format function
(defn look-html [data] (str (p (get data :desc)) (p (str "You see: " (get data :seen)))));; let's see if it's working
(look-html (look));; and finally
(display-html (look-html (look)))

好的,现在我们有了一个可以重复使用的 look-html 函数,但肯定还有改进的空间。 如果我们查看 look 函数返回的数据,你可以看到还有一个 :img-path。 让我们用它来为我们的游戏显示添加更多的视觉趣味。

;; let's build up an expression to format the :img-path as 
;; an img tag
(get (look) :img-path)
(str "<img src='" (get (look) :img-path) "'/>");; let's insert the image tag into our look-html function
(defn look-html [data] (str ;; v-- added img here --v (str "<img src='" (get data :img-path) "'/>") (p (get data :desc)) (p (str "You see: " (get data :seen)))));; let's see if it's working
(look-html (look));; and let's take a look!
(display-html (look-html (look)))

一点也不差。 好的,现在我们只有最后一条信息要添加到我们的 look-html 函数中。 当你评估 (look) 函数时,你会注意到一个 :exits 条目,它为玩家提供了有关他们可以从当前位置移动到哪些方向的线索。

我认为我们已经到了你可以将 :exits 信息添加到 look-html 函数的地步了。

;; here's the :exits data
(get (look) :exits);; Add the exits data to the function below:
(defn look-html [data] (str (str "<img src='" (get data :img-path) "'/>") (p (get data :desc)) (p (str "You see: " (get data :seen))) (p (str "Exits: " ))));; test it out here to see if it's working
(look-html (look));; and finally take a look!
(display-html (look-html (look)))

你想玩个游戏吗?

现在我们已经构建了 look-html 函数,让我们使用内联求值来探索文本冒险。

以下是几个游戏交互函数。 使用内联求值,评估每个函数以发现其用途以及对游戏状态的影响。 这演示了内联求值如何同时用作开发 探索工具。

(display-html (look-html (look)))
(move :east)
(stack)
(push :picture)
(peek)
(pop)
(unlock-function :_something?_)
(reset)

不要害怕增强用户界面。 这里有一些建议:

结束语?

感谢你花时间体验内联求值。

作为程序员,我们经常陷入认知惯性,限制了我们对编程过程的期望。 这些心理惯性反过来又限制了我们创造的潜力。 我观察到语言设计者、编程架构倡导者和工具构建者的决策直接忽略或阻碍了这种类型的交互性——我发现这种情况真的很不幸。

很难让人们尝试他们经验之外的事物。 抵制不熟悉的想法并将其驳回是很自然的。 许多阅读此书的人可能会辩称,这种交互性不是必需的或有用的。 但是,恕我直言,事实并非如此。

是的,这些可能只是玩具编辑器中的玩具示例,但这种级别的交互性非常真实。 Clojure 程序员——以及其他使用类似评估功能的人——每天都在专业环境中使用这种技术,从为主要零售商提供支持的数据管道到你可能自己使用的视频流服务。

我的论点不是 ClojureLisp 本质上优于其他语言,而是内联求值非常有价值,并且应该在编程环境中得到更广泛的应用。

通过亲身体验,你可能会受到启发,对你的编程语言和工具提出更高的期望——甚至可以创建你自己的编程语言和工具。

Bruce Hauman 程序员 bhauman@gmail.com 关于 github.com/bhauman linkedin Mastodon blog feed