Clojure 函数的方案(subst)

Scheme to Clojure Function (subst)

我正在阅读 Paul Graham 的 The Roots of Lisp

我已经尝试转换第 5 页上的函数 subst,其定义如下:

(defun subst (x y z)
  (cond ((atom z)
         (cond ((eq z y) x)
               ('t z)))
        ('t (cons (subst x y (car z))
                  (subst x y (cdr z))))))

对应的Clojure实现。我没有任何一种语言的生产经验(我一直在阅读 Clojure),所以任何帮助将不胜感激,因为我正在阅读这篇文章以了解 LISP 的根源。我最接近的是这个(但这是非常错误的):

(defn subst
 [x y z]
 (if (not (nil? z)) z x)
     (if (= z y) x z)
    (cons (subst x y (first z))
          (subst (x y (rest z)))))

"Traduttore, traditore"

(这里可以译为"translator, traitor",但是这样就废了双关语,本身就很好玩)

很难在您的 Clojure 代码中暗示可能的修复,因为规范不明确: 如果你严格按照 Lisp 的根源,你将在 Clojure 之上实现一个 Lisp,subst 可能与书中的相似。 但是如果你想实现 Lisp 中常用的 subst,这里显示的代码将无法实现。

尽管 Clojure 具有 consnil? 函数,但它们的含义与 Common Lisp 中的不同(分别为 consnull):参见 了解详情。 在翻译 subst 之前,您必须确定在 Clojure 中惯用的做法是什么。

通常subst用于转换由cons单元组成的;请注意,例如 subst 不会递归到向量、字符串等。在这些树中,树的特定子集是那些 Lisp 形式的树。事实上,subst 的一个重要用例是在代码生成期间搜索和替换表单。

据我所知,如果您将自己限制在 Clojure Cons 类型,您将不支持代码作为数据。 由于 Clojure 代码还使用向量和映射,您可能需要递归到此类对象中。所以,如何翻译subst并不是一个容易指定的问题。

一个可能的起点是阅读 LispReader.java 以确定构成 AST 的对象集,并查看您想执行哪种代码遍历。

我的建议是先独立学习这些语言。有了一点经验,您将有更好的方法来了解它们之间的相似之处和不同之处。

这就是我要做的,包括一个单元测试来验证:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require [clojure.walk :as walk]))

(defn subst
  [replacement target listy]
  (walk/postwalk (fn [elem]
                   (if (= elem target)
                     replacement
                     elem))
    listy))

(dotest
  (is= (subst :m :b [:a :b [:a :b :c] :d])
    [:a :m [:a :m :c] :d]))

然而,我不会花很多时间阅读关于 Common Lisp 的 40 年前的文章,尽管我认为 Paul Graham 的书 Hackers & Painters 非常令人兴奋。

Clojure 已经将最先进的 lisp 发展了至少一个数量级(我想更像是 2 个数量级)。主要改进包括 JVM 的使用、持久数据结构、并发性、语法和数据文字,仅举几例。

请参阅此 Clojure learning resources 列表,并可能从 Getting Clojure 或类似内容开始。


更新

更多来自 Paul Graham 关于 Clojure

方案版本的翻译可能是这样的:

(defn my-subst [new old data]
  (when-not (nil? data)
    (cond (sequential? data) (cons (my-subst new old (first data))
                                   (my-subst new old (next data)))
          (= data old) new
          :else data)))

user> (my-subst 1 :x '(1 2 :x (:x 10 :x [:x :z :x])))
;;=> (1 2 1 (1 10 1 (1 :z 1)))

这非常接近(尽管不完全相同,因为有不止一种原生集合类型,这让您面临选择:哪些应该被视为替换目标)。此示例处理 'listy'(顺序)结构,同时省略哈希映射和集合。 另一个问题是保留原始序列的类型和形式,这并不像听起来那么简单(例如 (into (empty (list 1 2 3)) (list 1 2 3)) => (3 2 1)

所以你要做的,就是先决定替换的语义,而在scheme中只是一个自然的列表处理。

从已经提到的 clojure.walk 开始,使用它进行替换的最简单方法可能是

(defn subst [new old data]
  (clojure.walk/prewalk-replace {old new} data))

user> (subst :z :x [1 :x 3 '(:x {:a :x}) #{:x 1}])
;;=> [1 :z 3 (:z {:a :z}) #{1 :z}]