Common Lisp 中的本地状态

Posted

技术标签:

【中文标题】Common Lisp 中的本地状态【英文标题】:Local State in Common Lisp 【发布时间】:2020-03-12 05:40:32 【问题描述】:

Common Lisp 中的新手问题:

如何让我的过程在每次调用时返回具有自己本地绑定的不同过程对象?目前,我使用 let 创建本地状态,但是两个函数调用共享相同的本地状态。这是代码,

(defun make-acc ()
  (let ((balance 100))
    (defun withdraw (amount)
      (setf balance (- balance amount))
      (print balance))
    (defun deposit (amount)
      (setf balance (+ balance amount))
      (print balance))
    (lambda (m)
      (cond ((equal m 'withdraw)
              (lambda (x) (withdraw x)))
            ((equal m 'deposit)
              (lambda (x) (deposit x)))))))

;; test

(setf peter-acc (make-acc))

(setf paul-acc (make-acc))

(funcall (funcall peter-acc 'withdraw) 10)
;; Give 90

(funcall (funcall paul-acc 'withdraw) 10)
;; Expect 90 but give 80

我应该用其他方式吗?我的写作方式有问题吗?有人可以帮我解决这个疑问吗?提前致谢。

【问题讨论】:

请注意,Common Lisp 有一个对象系统,因此通常不需要通过 lambda 对状态进行建模。 【参考方案1】:

也许你想要面向对象?

(defclass account ()
  ((balance :initarg :balance
            :initform 100
            :accessor balance)))

(defmethod withdraw ((acc account) amount)
  (decf (balance acc) amount))

(defmethod deposit ((acc account) amount)
  (incf (balance acc) amount))

用法:

(defparameter alice-account (make-instance 'account))
(withdraw alice-account 25) ;; => 75
(balance alice-account) ;; => 75

我们可以创建另一个余额的帐户:

(defparameter bob-account (make-instance 'account :balance 90))

更多信息,我建议使用食谱:https://lispcookbook.github.io/cl-cookbook/clos.html

【讨论】:

【参考方案2】:

请注意,即使在处理了defun-is-global 问题之后,您需要的机器也远远少于执行此类操作所需的机器。例如:

(defun make-account (balance)
  (lambda (op amount)
    (ecase op
      ((withdraw)
       (decf balance amount))
      ((deposit)
       (incf balance amount)))))

(defun account-operation (account op &rest args-to-op)
  (apply account op args-to-op))

然后

> (setf joe-acct (make-account 10))
#<Closure 1 subfunction of make-account 4060010B54>

> (setf mary-acct (make-account 100))
#<Closure 1 subfunction of make-account 4060010C94>

> (account-operation joe-acct 'withdraw 10)
0

> (account-operation mary-acct 'deposit 10)
110

显然account-operation 只是为了方便。

【讨论】:

【参考方案3】:

这里唯一严重的问题是defun,通常 lisp 不用于定义本地函数。

例如,您可以将 lambdas 用于这些操作,尤其是当您想要返回 lambdas 时...

(defun make-acc ()
  (let* ((balance 100)
         (withdraw (lambda (amount)
                     (setf balance (- balance amount))
                     (print balance)))
         (deposit (lambda (amount)
                    (setf balance (+ balance amount))
                    (print balance))))
    (lambda (m)
      (cond
        ((equal m 'withdraw) withdraw)
        ((equal m 'deposit) deposit)))))

请注意,我使用了let* 而不是let,因为您需要能够在以下两个绑定中看到balance

【讨论】:

【参考方案4】:

一般规则是 defun 应该只在定义顶层函数时使用。要定义局部函数,可以使用两个特殊运算符 fletlabels (manual)。

例如:

(defun make-acc ()
  (let ((balance 100))
    (flet ((withdraw (amount)
             (setf balance (- balance amount))
             (print balance))
           (deposit (amount)
             (setf balance (+ balance amount))
             (print balance)))
      (lambda (m)
        (cond ((equal m 'withdraw)
               (lambda (x) (withdraw x)))
              ((equal m 'deposit)
               (lambda (x) (deposit x))))))))

labelsflet 类似,但在有递归定义时使用。

那么在make-acc返回的函数里面就不需要返回函数了,但是在里面你可以简单的执行需要的操作:

(defun make-acc ()
  (let ((balance 100))
    (flet ((withdraw (amount)
             (setf balance (- balance amount))
             (print balance))
           (deposit (amount)
             (setf balance (+ balance amount))
           (print balance)))
      (lambda (m x)
        (cond ((equal m 'withdraw)
               (withdraw x))
              ((equal m 'deposit)
               (deposit x)))))))

调用会更简单,会返回预期值:

CL-USER> (setf paul-acc (make-acc))
#<CCL:COMPILED-LEXICAL-CLOSURE (:INTERNAL MAKE-ACC) #x3020021640AF>
CL-USER> (funcall paul-acc 'withdraw 10)

90 
90
CL-USER> (funcall paul-acc 'withdraw 10)

80 
80

最后,如果你愿意,你还可以返回两个不同的函数来对账户进行存款和取款:

(defun make-acc (initial-amount)
  (let ((balance initial-amount))
    (flet ((withdraw (amount)
             (setf balance (- balance amount))
             (print balance))
           (deposit (amount)
             (setf balance (+ balance amount))
             (print balance)))
          (values #'withdraw #'deposit))))

例如将其用作:

(multiple-value-bind (paul-withdraw paul-deposit)
    (make-acc 100)
  (funcall paul-withdraw 10)
  (funcall paul-withdraw 10))

【讨论】:

以上是关于Common Lisp 中的本地状态的主要内容,如果未能解决你的问题,请参考以下文章

Common Lisp 中的原子和符号有啥区别?

解析 Common Lisp 列表中的符号

替换 Common Lisp 列表中的项目?

Common Lisp宏中的词法绑定

将 Common Lisp 中的宏参数视为(区分大小写的)字符串

如何使用依赖于包装它的较短列表的 map 循环较长的列表以将某些函数应用于 Common Lisp 中的较长列表?