为什么Lisp允许在let中替换数学运算符?

时间:2019-06-13 16:19:57

标签: clojure scheme lisp

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

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

与此同时,在Clojure中:

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

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

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

8 个答案:

答案 0 :(得分:7)

这不是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)))

答案 1 :(得分:6)

免责声明:这是从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"

这不是特殊形式,与任何其他功能没什么不同。因此,在建立了该名称后,为什么可以被覆盖?

答案 2 :(得分:2)

主流Lisp方言没有为infix操作保留的令牌。 +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是一个保留关键字,而不是标识符,即使它看起来像一个。 Lisps往往没有保留关键字,但是某些方言具有某些符号或符号类别,无法将其绑定为变量。在ANSI Common Lisp中,我们不能使用nilt作为变量。 (具体来说,这些符号nilt来自common-lisp包)。这使某些程序员感到恼火,因为他们想要一个t变量作为“时间”或“类型”。另外,关键字包中的符号(通常以前导冒号出现)不能绑定为变量。原因是所有这些符号都是自我评估的。 nilt和关键字符号会自动求值,因此不能用作表示另一个值的变量。

答案 3 :(得分:1)

之所以允许使用Lisp,是因为所有绑定都是通过词法范围完成的,这是一个来自 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)。 / p>

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

答案 4 :(得分:1)

这并不奇怪,因为在Lisp中,除了可以内置或创建为宏的功能和特殊形式(例如letif)之外,没有其他运算符。因此,这里的+不是运算符,而是分配给符号+的函数,该函数正在添加其参数(在scheme和clojure中,您可以说它只是保持用于加数字的函数的变量),相同的*不是乘法运算符,而是乘以其参数的星号符号,因此这只是方便的表示法,它使用+符号可以是addsum,但+较短,与其他语言相似。

这是您初次发现它时所产生的思想折磨的概念之一,例如作为参数的函数和其他函数的返回值。

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

答案 5 :(得分:1)

在Scheme中,您正在使用let进行本地绑定,以较高的阴影覆盖。由于+*只是对过程求值的变量,因此您只是给旧过程赋予其他变量名。

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

“计划”中没有没有保留字。如果您查看其他语言,则保留字列表非常多。因此,在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是鸭子输入的好语言一样。

答案 6 :(得分:1)

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

允许这样做的原因是,在Scheme中,您可以嵌入其他语言,而在其他语言中,您想使用具有不同语义的*运算符。

例如,如果您实现一种语言来表示正则表达式,则想给*代数 kleene运算符的语义,并编写这样的程序

  

(*(+“ abc”“ def”))

代表一种包含此类单词的语言

empty
abc
abcabc
abcdef
def
defdef
defabc
....

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

答案 7 :(得分:0)

  

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

  • 为了保持一致性和
  • 因为它可能有用。
  

这不会引起混乱吗?

  • 不。

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

LISP用另一种方式倾斜跷跷板,而不是易读性。

一致性

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

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

因此,您当然可以替换它们。

有用性

为什么要覆盖核心算术运算符?例如,the units2 library重新定义了它们,以接受尺寸标注的数量以及纯数字。


Clojure代数很难阅读。

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

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