如何实现原始程序“应用”

时间:2018-03-30 20:27:35

标签: clojure scheme lisp sicp

我一直在阅读SICP并越来越多地进入lisps / clojure,我发现自己想知道如何实施申请。当然有一些愚蠢的方式,如(defn apply [f xs] (eval (cons f xs))),但我找不到一个例子来涵盖真正的实现。我想,一旦我在SICP中达到4.1,它就会被覆盖,但很遗憾地发现他们根据现有的基础方案实施来定义应用。

如何从头开始实施这一目标?

编辑:

我认为我问这个问题的方式有点不清楚。我知道如何在SICP中提到的eval / apply交互方面实现apply。我所指的是计划中的基本适用,它们在应用的metacircular版本的定义中落后。基本上......如果你没有用某种基本语言实现的应用,如何使用args列表调用一个函数,每个函数都单独传递。

4 个答案:

答案 0 :(得分:2)

由于Clojure托管在JVM平台上(并且设计为具有出色的Java互操作性),底层平台的特性可以通过。

您可以在源代码中看到在JVM上应用:https://github.com/clojure/clojure/blob/clojure-1.9.0/src/clj/clojure/core.clj#L652

出于效率原因,请注意最多4个arities的特定代码。 5级及以上的Arities以较低效的方式处理。

(defn apply
  "Applies fn f to the argument list formed by prepending intervening arguments to args."
  {:added "1.0"
   :static true}
  ([^clojure.lang.IFn f args]
     (. f (applyTo (seq args))))
  ([^clojure.lang.IFn f x args]
     (. f (applyTo (list* x args))))
  ([^clojure.lang.IFn f x y args]
     (. f (applyTo (list* x y args))))
  ([^clojure.lang.IFn f x y z args]
     (. f (applyTo (list* x y z args))))
  ([^clojure.lang.IFn f a b c d & args]
     (. f (applyTo (cons a (cons b (cons c (cons d (spread args)))))))))

ClojureScript实现也是如此,但与上面的JVM实现完全不同:

(defn apply
  "Applies fn f to the argument list formed by prepending intervening arguments to args."
  ([f args]
   (if (.-cljs$lang$applyTo f)
     (let [fixed-arity (.-cljs$lang$maxFixedArity f)
           bc (bounded-count (inc fixed-arity) args)]
       (if (<= bc fixed-arity)
         (apply-to f bc args)
         (.cljs$lang$applyTo f args)))
     (apply-to-simple f (seq args))))
  ([f x args]
   (if (.-cljs$lang$applyTo f)
     (let [arglist (list* x args)
           fixed-arity (.-cljs$lang$maxFixedArity f)
           bc (inc (bounded-count fixed-arity args))]
       (if (<= bc fixed-arity)
         (apply-to f bc arglist)
         (.cljs$lang$applyTo f arglist)))
     (apply-to-simple f x (seq args))))
  ([f x y args]
   (if (.-cljs$lang$applyTo f)
     (let [arglist (list* x y args)
           fixed-arity (.-cljs$lang$maxFixedArity f)
           bc (+ 2 (bounded-count (dec fixed-arity) args))]
       (if (<= bc fixed-arity)
         (apply-to f bc arglist)
         (.cljs$lang$applyTo f arglist)))
     (apply-to-simple f x y (seq args))))
  ([f x y z args]
   (if (.-cljs$lang$applyTo f)
     (let [arglist (list* x y z args)
           fixed-arity (.-cljs$lang$maxFixedArity f)
           bc (+ 3 (bounded-count (- fixed-arity 2) args))]
       (if (<= bc fixed-arity)
         (apply-to f bc arglist)
         (.cljs$lang$applyTo f arglist)))
     (apply-to-simple f x y z (seq args))))
  ([f a b c d & args]
   (if (.-cljs$lang$applyTo f)
     (let [spread-args (spread args)
           arglist (cons a (cons b (cons c (cons d spread-args))))
           fixed-arity (.-cljs$lang$maxFixedArity f)
           bc (+ 4 (bounded-count (- fixed-arity 3) spread-args))]
       (if (<= bc fixed-arity)
         (apply-to f bc arglist)
         (.cljs$lang$applyTo f arglist)))
     (apply-to-simple f a b c d (spread args)))))

答案 1 :(得分:1)

我前一段时间制作了dynamic lisp language而我没有曝光apply。我确实提供了休息参数,因此我用eval和语言中的宏做了几次尝试。我很快发现宏没用,所以eval是唯一的解决方案。你的例子有一个缺陷:

(defn mapply [f xs] (eval (cons f xs)))
(mapply cons '(1 (3)))
; ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn  

原因是由eval评估的结果表达式变为:

(cons 1 (3))

而不是

(cons '1 '(3))

因此,为了模仿它,你需要确保已经评估的值不会第二次得到评估。我们可以通过引用值来解决这个问题:

(defn m2apply [f xs] (eval (cons f (map #(list 'quote %) xs))))
(m2apply cons '(1 (3)))
; ==> (1 3)

是的..但是你真的在做比你需要的更多的计算。对于具有apply的词法解释器,您只需要将其作为原语泄漏到环境中。是的,它是不起眼的apply,它的唯一目的是调用内部(基元)并在扩展环境中评估用户函数体。在一种不是lisp的语言中,apply和一整套原语和数据结构将在实现语言中实现,而它只是暴露出来。

答案 2 :(得分:1)

实现apply的方式与实现函数调用的方式直接相关。如果编译代码,则在运行时有一个协议,您可以知道函数调用之间如何交换值,apply可以发出满足此协议的代码。我们可以在一个快速而又脏的翻译中做同样的事情。我们来定义一个包:

(defpackage :interpreter (:use :cl))
(in-package :interpreter)

我们定义一个函数对象,它有一个可选的名称,一个参数列表,代码以及一组被封闭的绑定:

(defstruct fn name parameters code closed)

我们还定义了一个框架,它有一组绑定和一个可选的父框架:

(defstruct frame bindings parent)

这里我们有一个简单的解释器,我们将当前帧放在评估环境中:

(defstruct env frame)

绑定可以是FN类型的对象,也可以是cons对。我们编写通用函数来使用统一的API来操作它们。函数和变量共享相同的命名空间:

(defgeneric name (object)
  (:method ((fn fn)) (fn-name fn))
  (:method ((pair cons)) (car pair)))

(defgeneric value (object)
  (:method ((c cons)) (cdr c))
  (:method ((fn fn)) fn))

我们定义了两个函数my-applymy-eval

(declaim (ftype function my-apply my-eval))

有一个全球环境,简单地说:

(defparameter *global-frame*
  (make-frame
   :bindings (list (make-fn :name '+
                            :parameters '(x y)
                            ;; built-in
                            :code (lambda (x y) (+ x y)))
                   (make-fn :name 'addition
                            :parameters '(x y)
                            :code '(+ x y)))
   :parent nil))

空的环境隐含地保留在全局框架中:

(defgeneric frame (env)
  (:method ((empty null)) *global-frame*)
  (:method ((env env)) (env-frame env)))

解析绑定涉及访问父框架:

(defun resolve (name frame &optional (on-error :error))
  (labels ((recurse (frame)
             (cond
               (frame (or (find name (frame-bindings frame) :key #'name)
                          (recurse (frame-parent frame))))
               ((eql :error on-error) (error "Unknown: ~a" name)))))
    (recurse frame)))

评估功能如下:

(defun my-eval (code env &aux (frame (frame env)))
  (flet ((ev (exp) (my-eval exp env)))
    (typecase code
      (symbol (value (resolve code frame)))
      (atom code)
      (cons
       (destructuring-bind (head . tail) code
         (case head
           (list (mapcar #'ev tail))
           (let (destructuring-bind ((var val) expr) tail
                  (my-eval expr
                           (make-env :frame (make-frame :bindings `((,var . ,(ev val)))
                                                        :parent frame)))))
           (thunk (make-fn :name nil
                           :parameters nil
                           :code (first tail)
                           :closed (frame-bindings frame)))
           (apply (my-apply (ev (first tail))
                            (ev (second tail))
                            env))
           (t (my-apply (resolve head (frame env))
                        (mapcar #'ev tail)
                        env))))))))

评估函数接受以下术语:

  • (list <...>)构建一个包含其参数评估结果的列表
  • (apply <fn-expr> <arg-expr>),评估所有参数并调用my-apply原语。
  • (let (<var> <val>) <expr>),本地绑定
  • (thunk <expr>)关闭当前环境并生成一个没有参数的匿名闭包,返回值<expr>
  • (<f> . <args>)函数调用
  • 符号将针对值进行解析,其他值将按原样返回。

内置my-apply知道如何动态地将参数绑定到值:

(defun my-apply (fn arguments env)
  (assert (= (length arguments)
             (length (fn-parameters fn)))
          ()
          "Length mismatch when calling ~S with argsuments ~S"
          fn
          arguments)
  (let ((code (fn-code fn)))
    (typecase code
      (function (apply code arguments))
      (t (my-eval code
                  (make-env :frame
                            (make-frame :bindings (append (fn-closed fn)
                                                          (mapcar #'cons
                                                                  (fn-parameters fn)
                                                                  arguments))
                                        :parent (frame env))))))))

例如:

(my-eval '(let (f (let (x 10) (thunk (addition x 5))))
           (let (x 20) (apply f (list)))) nil)
=> 15

在上面的示例中,f是一个关闭x到10的绑定并调用addition的函数。闭包不会看到后来发生的绑定。对apply的调用会解析f并构建一个空列表。对addition的调用解析为(+ 10 5),它本身最终调用CL函数+。您可以(trace my-eval)查看评估方式。上面的代码有点乱。

答案 3 :(得分:0)

我认为你不能在语言中从头开始定义它:在某些时候你的语言需要一种在一堆参数上实际调用函数的机制,apply几乎就是这一点

这就是为什么它是一个原始的:询问你如何实现apply就像问你如何实现cons+:迟早需要触底来你调用一个函数它没有在语言中定义,或者只是部分地用语言定义:+例如可以部分地通过检查类型和从中提取实际机器号来实现,但是迟早你会去要求机器为您添加一些机器编号(或者,如果您的机器不直接支持添加,则可以执行某些等效操作)。