我在Python中编写了一个简单的基于堆栈的虚拟机,现在我正在尝试用Clojure重写它,由于我没有太多Lisp经验,这证明很难。 This Python snippet处理字节码,字节码表示为元组列表,如下所示:
[("label", "entry"),
("load", 0),
("load", 1),
("add",),
("store", 0)]
或者在Clojure中:
[[:label :entry]
[:load 0]
[:load 1]
[:add]
[:store 0]]
当Function对象加载字节码时,每个“label”元组都会被专门处理以标记该位置,而其他每个元组都保留在最终的字节码中。我认为Clojure相当于这个函数会涉及折叠,但我不知道如何以优雅或惯用的方式做到这一点。有什么想法吗?
答案 0 :(得分:10)
阅读Python片段,看起来您希望最终输出看起来像
{:code [[:load 0]
[:load 1]
[:add]
[:store 0]]
:labels {:entry 0}}
一旦你对目标有了明确的描述,编写代码要容易得多,事实上这是一个非常简单的减少。有许多风格上不同的编写缩减器的方法,但对我来说这种方式似乎最容易阅读。
(defn load [asm]
(reduce (fn [{:keys [code labels]} [op arg1 & args :as instruction]]
(if (= :label op)
{:code code
:labels (assoc labels arg1 (count code))}
{:code (conj code instruction)
:labels labels}))
{:code [], :labels {}},
asm))
此版本支持name
参数,并通过不重复不更改的元素来简化缩小步骤。
(defn load [name asm]
(reduce (fn [program [op arg1 :as instruction]]
(if (= :label op)
(assoc-in program [:labels arg1] (count (:code program)))
(update-in program [:code] conj instruction)))
{:code [], :labels {}, :name name},
asm))
答案 1 :(得分:3)
我不能保证这是惯用的Clojure,但这是你的Python代码的功能版本,至少应该让你非常接近。
(def prog [
[:label :entry]
[:load 0]
[:load 1]
[:add]
[:store 0]])
(defn parse [stats]
(let [
f (fn [[out-stats labels pc] stat]
(if (= :label (first stat))
[out-stats (conj labels [(second stat) pc]) pc]
[(conj out-stats stat) labels (+ 1 pc)]))
init [[] {} 0]
]
(reduce f init stats)))
(println (parse prog))
所以我认为你是正确的,你想要的是折叠。所有功能性折叠都会走集合并将该集合“缩减”为单个值。但是,没有任何内容表明生成的单个值也不能是集合,或者在本例中是集合的集合。
在我们的例子中,我们将使用reduce的三参数版本 - 这允许我们提供初始累加器值。我们需要这样做是因为我们在迭代字节码集合时会跟踪很多状态,而双参数版本几乎要求你的累加器与列表中的项目类似。 (c.f。(reduce + [1 2 3 4])
)
使用功能折叠时,您需要根据您正在积累的内容以及输入集合中的每个元素如何促成该累积进行思考。如果查看Python代码,可以在循环的每个回合中更新三个值:
self.code
)self.labels
)pc
)循环期间没有写入任何其他内容。因此,我们的累加器值将需要存储这三个值。
前一位是最重要的部分。
一旦你有了,其余的应该很容易。我们需要一个初始累加器值,它没有代码,没有标签映射,以及从0开始的PC。在每次迭代时,我们将以两种方式之一更新累加器:
现在,输出:
[[[:load 0] [:load 1] [:add] [:store 0]]
{:entry 0}
4]
这是一个3元素的向量。第一个元素是程序。第二个元素是标签映射。第三个元素是下一个PC值。现在,您可以修改parse以仅生成两个值;这不是一件不合理的事情。您可能不希望这样做,但这更像是API设计的问题。我会把它作为练习留给读者。
我还应该提一下,最初,我省略了let块并简单地内联了命名值。我决定将它们拉出来,希望提高可读性。再说一遍,我不知道哪个更惯用。这可能更像是每个项目的惯例。
最后,我不知道monad是否真的在Clojure社区中起飞,但你也可以创建一个monad用于字节码解析,并将操作“add-statement”和“add-label”定义为值在那个monad。这将大大增加设置的复杂性,但会简化实际的解析代码。事实上,它将允许您的解析代码看起来相当程序化,这可能是也可能不是一件好事。 (别担心,它仍然具有功能和副作用; monads只是让你隐藏管道。)如果你的Python样本非常能代表你需要处理的数据类型,那么monad几乎肯定是不必要的开销。另一方面,如果你实际上必须管理比你的样本所指示的更多的状态,那么monad可能有助于让你保持理智。
答案 2 :(得分:1)
(defn make-function [name code]
(let [[code labels] (reduce (fn [[code labels] inst]
(if (= (first inst) :label)
[code (assoc labels (second inst) (count code))]
[(conj code inst) labels]))
[[] {}] ;; initial state of code and labels
code)]
{:name name, :code code :labels labels}))
我喜欢它有点宽,但太不好。
答案 3 :(得分:0)
我将为这些问题提供一般解决方案。
大多数循环可以毫不费力地使用strait forward map , filter 或 reduce 来完成,如果你的数据结构是递归的,那么循环将是一个递归。
然而,你的循环是一种不同的循环。你的循环累积了一个结果 - 这建议使用 reduce - 但是循环还带有一个局部变量( pc ),所以它不是一个海峡减少。
这是一种相当常见的循环。如果这是Racket,我会使用for/fold1,但由于事实并非如此,我们必须将您的循环塞进 reduce 。
让我们定义一个名为 load 的函数,它返回两个东西,即处理过的代码和处理过的标签。我还将使用一个名为 is-label?的辅助函数。
(defn load [asm]
(defn is-label? [x] (= (first x) :label))
{:code <<< CODE GOES HERE >>>
:labels
<<< CODE GOES HERE >>>
})
现在,你的循环做了两件事,它处理代码,并处理标签。尽可能地,我尝试将循环保持为单个任务。它使它们更容易理解,并且它经常揭示使用更简单的循环结构的机会。
要获取代码,我们只需删除标签即可。这是对过滤器的调用。
{:code (filter (complement is-label?) asm)
:labels
<<< CODE GOES HERE >>>
}
Reduce通常只有一个累加器,但你的循环需要两个:结果和局部变量 pc 。我将这两个打包成一个向量,该向量将立即被循环体解构。向量的两个槽将是我的两个局部变量。
这两个变量的初始值显示为 reduce 的第二个参数。
(first
(reduce
(fn [[result, pc] inst]
<< MORE CODE >>
[{} 0] asm))
(注意变量的初始值是如何远离它们的声明的。如果主体很长,这很难读。这就是Racket的 for / fold1 解决的问题。)
reduce 返回后,我先调用 放弃本地变量 pc 并保留结果。
填充循环体是直截了当的。如果指令是标签,将添加到结果中,否则将 pc 增加1。在任何一种情况下,我都构造了一个包含所有局部变量的新值的向量。
(fn [[result, pc] [_ arg :as inst]]
(if (is-label? inst)
[(assoc result arg pc) pc]
[result (inc pc)]))
此技术可用于将任何accumulator-with-locals循环转换为 reduce 。这是完整的代码。
(defn load [asm]
(defn is-label? [x] (= (first x) :label))
{:code (filter (complement is-label?) asm)
:labels
(first
(reduce
(fn [[result, pc] [_ arg :as inst]]
(if (is-label? inst)
[(assoc result arg pc) pc]
[result (inc pc)]))
[{} 0] asm))})