【问题标题】:Why does Lisp allow replacement of math operators in let?为什么 Lisp 允许替换 let 中的数学运算符?
【发布时间】:2019-06-13 16:19:57
【问题描述】:

我知道在Scheme中我可以这样写:

(let ((+ *)) (+ 2 3)) => 6

除此之外,在 Clojure 中:

(let [+ *] (+ 2 3)) => 6

我知道这可行,但感觉很奇怪。我认为在任何语言中,数学运算符都是预定义的。 C++ 和 Scala 可以进行运算符重载,但这似乎不是。

这不会引起混乱吗?为什么 Lisp 允许这样做?

【问题讨论】:

  • 还要注意,在 Lisp-2 中,绑定变量不会改变函数命名空间中的绑定。
  • 注意,在Scheme中,正确的语法是:(let ((+ *)) (+ 2 3)) => 6
  • 在 Lisp et ses derivedes 中,+- 等不是运算符——它们是函数,并且错误地引用了某只猫:“一个函数做我们所说的任何事情,也不是更多或更少”。欢迎来到镜子。拥有权利的同时也被赋予了重大的责任。注意你的头......

标签: clojure scheme lisp


【解决方案1】:

这不是一般的 Lisp 功能。

在 Common Lisp 中,绑定核心语言函数的效果是 undefined。这意味着开发人员不应期望它可以在可移植代码中工作。实现也可能发出警告或错误信号。

例如SBCL 编译器将发出此错误信号:

; caught ERROR:
;   Lock on package COMMON-LISP violated when
;   binding + as a local function while
;   in package COMMON-LISP-USER.
;   See also:
;     The SBCL Manual, Node "Package Locks"
;     The ANSI Standard, Section 11.1.2.1.2

;     (DEFUN FOO (X Y)
;       (FLET ((+ (X Y)
;                (* X Y)))
;         (+ X Y)))

我们可以在 Common Lisp 中拥有自己的 +,但它必须位于不同的(=符号命名空间)中:

(defpackage "MYLISP"
  (:use "CL")
  (:shadow CL:+))

(in-package "MYLISP")

(defun foo (a b)
  (flet ((+ (x y)
           (* x y)))
    (+ a b)))

【讨论】:

  • Common Lisp 竭尽全力确保无法重新定义语言。我想知道实现这一点需要多少代码。 (CLISP 确实抱怨全局变量,但不是 flet
  • @Sylvester:不,语言标准说,后果是不确定的。
  • 那么实现不需要像大多数实现那样发出错误信号吗?令人惊讶的是,其中有多少人在发生这种情况时表现相同。
  • @Sylwester:CLISP 有“包锁”,所以我认为在 CLISP 中不需要那么多代码。
【解决方案2】:

免责声明:这是从 Clojure 的角度来看的。

+ 只是另一个函数。你可以传递它并用它写sum,有部分应用程序,阅读有关它的文档,...:

user=> (apply + [1 2 3])
6
user=> (reduce + [1 2 3])
6
user=> (map (partial + 10) [1 2 3])
(11 12 13)
user=> `+
clojure.core/+
user=> (doc +)
-------------------------
clojure.core/+
([] [x] [x y] [x y & more])
  Returns the sum of nums. (+) returns 0. Does not auto-promote
  longs, will throw on overflow. See also: +'

所以你可以在不同的命名空间中有很多+。默认情况下,核心为您“使用”-ed,但您可以简单地编写自己的。您可以编写自己的 DSL:

user=> (defn + [s] (re-pattern (str s "+")))
WARNING: + already refers to: #'clojure.core/+ in namespace: user, being replaced by: #'user/+
#'user/+
user=> (+ "\\d")
#"\d+"
user=> (re-find (+ "\\d") "666")
"666"

它不是特殊形式,它与任何其他功能没有什么不同。那么既然已经确定了,为什么不允许被覆盖?

【讨论】:

    【解决方案3】:

    在Scheme 中,您正在使用let 进行本地绑定,隐藏更高级别的内容。由于+* 只是碰巧对过程求值的变量,因此您只是给旧过程提供了其他变量名称。

    (let ((+ *))
      +)
    ; ==> #<procedure:*> (non standard visualization of a procedure)
    

    Scheme 中没有保留字。如果您查看其他语言,保留字的列表非常高。因此在 Scheme 中你可以这样做:

    (define (test v)
      (define let 10)           ; from here you cannot use let in this scope
      (define define (+ let v)) ; from here you cannot use define to define stuff
      define) ; this is the variable, not the special form
    ;; here let and define goes out of scope and the special forms are OK again
    (define define +) ; from here you cannot use top level define
    (define 5 6) 
    ; ==> 11
    

    这样做的真正好处是,如果您选择了一个名称,而标准的下一个版本恰好使用相同的名称来表示类似但不兼容的东西,您的代码不会中断。在我使用过的其他语言中,新版本可能会引入冲突。

    R6RS 让一切变得更简单

    从 R6RS 我们有库。这意味着我们可以完全控制我们从标准中获得哪些顶级表格到我们的程序中。你有几种方法可以做到:

    #!r6rs
    (import (rename (except (rnrs base) +) (* +)))
    
    (+ 10 20) 
    ; ==> 200 
    

    这也可以。

    #!r6rs
    (import (except (rnrs base) +))    
    (define + *)
    
    (+ 10 20) 
    ; ==> 200 guaranteed
    

    最后:

    #!r6rs
    (import (rnrs base)) ; imports both * and +   
    (define + *)         ; defines + as an alias to *
    
    (+ 10 20) 
    ; ==> 200 guaranteed
    

    其他语言也这样做:

    JavaScript 可能是最明显的:

    parseFloat = parseInt;
    parseFloat("4.5") 
    // ==> 4
    

    但你不能碰他们的运营商。它们被保留是因为语言需要为运算符优先级做很多事情。就像 Scheme JS 是鸭子打字的好语言一样。

    【讨论】:

      【解决方案4】:

      主流 Lisp 方言没有为中缀操作保留的标记。 +exptformatopen-file 之间没有明确的区别:它们都只是符号。

      执行(let ((+ 3)) ...) 的Lisp proram 在精神上与执行{ int sqrt = 42; ... } 之类的C 程序非常相似。标准 C 库中有一个 sqrt 函数,由于 C 有一个命名空间(它是 Lisp-1),所以 sqrt 现在被隐藏了。

      我们在 C 中不能做的是{ int + = 42; ...},这是因为+ 是一个操作符标记。调用标识符,因此存在语法错误。我们也不能使用{ struct interface *if = get_interface(...); },因为if 是保留关键字而不是标识符,即使它看起来像一个。 Lisp 往往没有保留关键字,但某些方言具有某些符号或符号类别,不能绑定为变量。在 ANSI Common Lisp 中,我们不能使用 nilt 作为变量。 (具体来说,那些符号 nilt 来自 common-lisp 包)。这让一些程序员很恼火,因为他们想要一个t 变量来表示“时间”或“类型”。此外,关键字包中的符号(通常以冒号开头)不能绑定为变量。原因是所有这些符号都是自我评估的。 nilt 和关键字符号对自身求值,因此不能作为变量来表示另一个值。

      【讨论】:

        【解决方案5】:

        我们在 lisp 中允许这样做的原因是所有绑定都使用 词法范围 完成,这是一个来自 lambda 演算 的概念。

        lambda 演算是用于管理变量绑定的简化系统。在 lambda 演算中,诸如

        之类的规则
        (lambda (x) (lambda (y) y))
        

        (lambda (x) (lambda (y) x))
        

        甚至

        (lambda (x) (lambda (x) x))
        

        经过仔细指定。

        在 lisp 中,可以将 LET 视为 lambda 表达式的语法糖,例如,您的表达式 (let ([+ x]) (+ 2 3)) 等价于 ((lambda (+) (+ 2 3)) x),根据 lambda 演算可简化为 (x 2 3)

        总之,lisp 是基于统一应用一个非常简单明了的模型(称为 lambda 演算)。如果一开始看起来很奇怪,那是因为大多数其他编程语言没有这种一致性,或者它们的变量绑定基于数学模型。

        【讨论】:

        • (当我在这篇文章中说 lisp 时,我实际上是指“方案”)
        【解决方案6】:

        Scheme 的理念是施加最小的限制,从而为程序员提供最大的权力。

        允许此类事情的一个原因是,在 Scheme 中您可以嵌入其他语言,而在其他语言中,您希望使用具有不同语义的 * 运算符。

        例如,如果您实现一种语言来表示正则表达式,您希望为* 赋予代数kleene 运算符 的语义并编写类似这样的程序

        (* (+ "abc" "def") )

        表示包含类似这样的词的语言

        empty
        abc
        abcabc
        abcdef
        def
        defdef
        defabc
        ....
        

        从主要语言,无类型 lambda 演算开始,可以创建一种语言,您可以在其中重新定义除了 lambda 符号之外的所有内容。这是计算方案的模型。

        【讨论】:

          【解决方案7】:

          这并不奇怪,因为在 lisp 中,除了函数和特殊形式(如 letif,可以内置或创建为宏)之外,没有其他运算符。所以这里+ 不是一个运算符,而是一个分配给符号+ 的函数,它正在添加它的参数(在scheme 和clojure 中,你可以说它只是保持添加数字的函数的变量),同样的@987654326 @ 不是乘法运算符,而是将其参数相乘的星号符号,因此这只是一种方便的表示法,它使用 + 符号它可以是 addsum+ 更短且与其他语言相似。

          当您第一次发现它时,这是一个令人费解的概念,例如作为参数的函数和其他函数的返回值。

          如果您使用非常基本的 Lisp 和 lambda 演算,您甚至不需要基础语言中的数字和 + 运算符。您可以使用相同的技巧从函数和加减函数创建数字,并将它们分配给符号 +-(参见 Church encoding

          【讨论】:

          • 我不明白为什么我投了反对票,+ 不是 lisp 中的运算符,它只是函数,这个答案是它的扩展。应该是评论。
          • 嗯,这种语言叫做 Lisp,而不是 LIPS。实际上,来自 McCarthy 的原始 Lisp 并没有真正支持 lambda 演算语义,它甚至不支持词法闭包。这花了相当多的时间才弄清楚。 McCarthy 的原始 Lisp 直接支持数字运算,用函数替换它们是不切实际的。 Lisp 1 中(浮点)数的原始加法运算被命名为 SUM。在许多 Lisp 系统中,用您自己的实现全局重新定义 + 并不是一个好主意...
          • @RainerJoswig 感谢您的评论,在我创建了名为 Lips 的 lisp 解释器后,我在编写 lisp 时不断输入错误。
          • @RainerJoswig 我不想说用您自己的实现替换+ 是可行的,只有它是可能的(仅使用scheme 或Clojure)并且您不需要+ at全部创建一个,您可以从空中获取它(参考原始 SICP 视频和 conscarcdr 实现)。
          • 'only it's possible' -> 一般情况下,因为 + then 可能会以有趣的方式使 Lisp 或您的程序崩溃,因为可能会从各种地方调用全局函数(-> 后期绑定)
          【解决方案8】:

          为什么 Lisp 允许重新绑定数学运算符?

          • 为了一致性和
          • 因为它很有用。

          这不会引起混乱吗?

          • 没有。

          大多数编程语言,遵循传统的代数符号,对加法和减法等基本算术函数都有特殊的语法。优先级和关联规则使某些函数调用隐含。这种语法以一致性为代价使表达式更易于阅读。

          LISP 以另一种方式倾斜跷跷板,他们更喜欢一致性而不是易读性。

          一致性

          在 Clojure(我知道的 Lisp)中,数学运算符(+-*、...)没有什么特别之处。

          • 他们的名字只是普通的符号。
          • 它们是 core 函数,与其他函数一样。

          所以你当然可以替换它们。

          有用性

          为什么要覆盖核心算术运算符?例如,the units2 library 重新定义它们以接受标注数量和普通数字。


          Clojure 代数更难阅读。

          • 所有运算符都是前缀。
          • 所有操作员应用程序都是明确的 - 没有优先级。

          如果您确定让中缀运算符具有优先级,您可以这样做。 Incanter 这样做:here 是一些示例,here 是源代码。

          【讨论】:

            猜你喜欢
            • 2014-12-03
            • 2018-05-17
            • 1970-01-01
            • 2012-10-30
            • 1970-01-01
            • 2010-09-10
            • 2011-07-17
            • 1970-01-01
            相关资源
            最近更新 更多