为什么很少有带有可变类型“运算符”的语言?


47

我的意思是这样的:

<?php
    $number1 = 5;   // (Type 'Int')
    $operator1 = +; // (Type non-existent 'Operator')
    $number2 = 5;   // (Type 'Int')
    $operator2 = *; // (Type non-existent 'Operator')
    $number3 = 8;   // (Type 'Int')

    $test = $number1 $operator1 $number2 $operator2 $number3; //5 + 5 * 8.

    var_dump($test);
?>

而且也可以这样:

<?php
    $number1 = 5;
    $number3 = 9;
    $operator1 = <;

    if ($number1 $operator1 $number3) { //5 < 9 (true)
        echo 'true';
    }
?>

似乎没有任何一种语言具有这种功能-是否有充分的理由为什么没有这种语言?


28
通常,您想要做的事情将被支持带有某种类型的lambda,闭包或匿名函数的某种形式的元编程的所有语言所涵盖,这将是实现此类功能的常用方法。对于方法是一等公民的语言,您将能够或多或少地使用它们与变量相同。尽管这里使用的语法不是那么简单,因为在大多数此类语言中,必须明确指出您实际上是想调用存储在变量中的方法。
thorstenmüller16年

7
@MartinMaat功能语言可以做到这一点。
托尔比约恩Ravn的安徒生

7
在haskell中,运算符的功能与其他任何函数相同。的类型(+)Num a => a -> a -> aIIRC。您还可以定义函数,以便可以将它们写成括号(a + b而不是(+) a b
sara

5
@enderland:您的编辑完全改变了问题的目标。它从询问是否存在任何语言到询问为什么存在那么少的语言。我认为您的编辑会引起很多读者的困惑。
布莱恩·奥克利

5
@enderland:虽然的确如此,但完全改变主题只会使人困惑。如果没有主题,社区将投票关闭它。对于这个问题,答案(在我写这篇文章时)都没有意义。
布莱恩·奥克利

Answers:


104

运算符只是带有有趣名称的函数,周围带有一些特殊的语法。

在许多语言(如C ++和Python)中,您可以通过重写类的特殊方法来重新定义运算符。然后,标准运算符(例如+)根据您提供的逻辑(例如连接字符串或添加矩阵等)进行工作。

由于此类运算符定义的函数只是方法,因此您可以像使用函数一样传递它们:

# python
action = int.__add__
result = action(3, 5)
assert result == 8

其他语言允许您直接将新运算符定义为函数,并以中缀形式使用它们。

-- haskell
plus a b = a + b  -- a normal function
3 `plus` 5 == 8 -- True

(+++) a b = a + b  -- a funny name made of non-letters
3 +++ 5 == 8 -- True

let action = (+)
1 `action` 3 == 4 -- True

不幸的是,我不确定PHP是否支持类似的东西,是否支持它会是一件好事。使用普通函数,它比更具可读性$foo $operator $bar


2
@tac:是的,这很好,甚至可以移植到其他语言:)关于Haskell,我最想念的是该部门是$消除括号(尤其是多个嵌套)的运算符-但这仅适用于非-variadic函数,因此不包括Python和Java。OTOH一元函数的组合可以很好地完成
9000

6
我从来不喜欢运算符重载,因为是的,运算符只是具有特殊语法的函数,但是隐含的契约通常与运算符一起使用,而与运算符不一起使用。例如,“ +”具有一定的期望-运算符优先级,可交换性等-违背这些期望是使人感到困惑和产生bug的可靠途径。尽管我喜欢javascript,但我还是希望它们在加号和串联之间区分+的一个原因。Perl就在那里。
fool4jesus

4
请注意,Python有一个标准operator模块,该模块可以让您编写action = operator.add,并使它可用于定义的任何类型+(不仅限于int)。
dan04 '16

3
在Haskell中,+可以使用Num类型类,因此您可以+为创建的任何新数据类型实现,但方式与您执行其他任何操作(例如为函子使用fmap)相同。Haskell非常自然地允许操作员重载,他们必须努力工作才能不允许!
Martin Capodici

17
不知道为什么人们会沉迷于超载。最初的问题不是关于过载。在我看来,是关于运算符的一等值。为了编写$operator1 = +然后使用它,您根本不需要使用运算符重载
Andres F.

16

有许多语言允许某种元编程。尤其令我惊讶的是,没有任何关于Lisp语言家族的答案。

来自维基百科:

元编程是计算机程序的编写,具有将程序视为其数据的能力。

在下文中:

Lisp可能是具有元编程功能的典型语言,这既因为其历史悠久的历史,又因为其元编程的简单性和强大性。

Lisp语言

接下来是Lisp的快速入门介绍。

查看代码的一种方法是按照一组说明进行操作:先执行此操作,然后再执行此操作,然后再执行此其他操作...这是一个清单!程序要做的事情的清单。当然,您可以在列表中包含列表以表示循环,等等。

如果我们代表含有的元素A,B,C的列表,D是这样的:(ABCD),我们得到的东西,看起来像一个Lisp函数调用,这里a是函数,并且bcd是参数。如果事实是典型的“ Hello World!” 程序可以这样写:(println "Hello World!")

当然bc或者d也可以是对某些结果求值的列表。以下内容:(println "I can add :" (+ 1 3) )然后将打印““我可以添加:4”。

因此,程序是一系列嵌套列表的第一个元素是一个函数。好消息是我们可以操纵列表!这样我们就可以操纵编程语言。

Lisp的优势

Lisps与其说是编程语言,不如说是制作编程语言的工具包。可编程的编程语言。

这不仅在Lisps中更容易创建新的运算符,而且几乎不可能用其他语言编写某些运算符,因为在传递给函数时会评估参数。

例如,使用类似C的语言,您要自己编写一个if运算符,例如:

my-if(condition, if-true, if-false)

my-if(false, print("I should not be printed"), print("I should be printed"))

在这种情况下,两个参数都将按照与参数求值顺序有关的顺序进行评估和打印。

在Lisps中,编写运算符(我们称其为宏)和编写函数几乎是同一件事,并且以相同的方式使用。主要区别在于宏参数在作为参数传递给宏之前不会进行评估。对于像if上面这样写一些运算符,这是必不可少的。

现实世界的语言

在这里显示精确度有点超出范围,但是我建议您尝试使用一个Lisp编程以了解更多信息。例如,您可以看一下:

  • Scheme,一个古老的,相当“纯净的” Lisp,带有一个很小的核心
  • Common Lisp,更大的Lisp,具有良好的集成对象系统以及许多实现(已通过ANSI标准化)
  • 拍打 Lisp型
  • 我最喜欢的Clojure,上面的示例是Clojure代码。在JVM上运行的现代Lisp。在SO上也有一些Clojure宏的示例(但这不是开始的正确位置。首先我会看4clojurebraveclojureclojure koans)。

哦,顺便说一句,Lisp是指LISt Processing。

关于你的例子

我将在下面给出使用Clojure的示例:

如果你能写一个addClojure中的功能(defn add [a b] ...your-implementation-here... ),你可以将其命名+像这样(defn + [a b] ...your-implementation-here... )。实际上,这是在实际实现中所做的事情(该函数的主体涉及更多点,但定义与我上面编写的基本相同)。

那么中缀符号呢?好吧Clojure使用prefix(或波兰语)表示法,因此我们可以创建一个infix-to-prefix将前缀代码转换为Clojure代码的宏。这实际上非常简单(实际上是clojure koans 中的宏练习之一)!也可以在野外看到它,例如,参见Incanter $=macro

这是koans解释的最简单的版本:

(defmacro infix [form]
  (list (second form) (first form) (nth form 2)))

;; takes a form (ie. some code) as parameter
;; and returns a list (ie. some other code)
;; where the first element is the second element from the original form
;; and the second element is the first element from the original form
;; and the third element is the third element from the original form (indexes start at 0)
;; example :
;; (infix (9 + 1))
;; will become (+ 9 1) which is valid Clojure code and will be executed to give 10 as a result

为了进一步说明这一点,一些Lisp引用

“使Lisp与众不同的部分原因在于它旨在发展。您可以使用Lisp定义新的Lisp运算符。随着新抽象的流行(例如,面向对象的编程),在Lisp中实现它们总是很容易。像DNA一样,这种语言也不会过时。”

— Paul Graham,ANSI Common Lisp

“在Lisp中编程就像在与宇宙的原始力量一起玩。感觉就像指尖之间的闪电。没有其他语言可以让您感到亲近。”

— Glenn Ehrlich,通往Lisp的道路


1
请注意,元编程虽然很有趣,但不需要它来支持OP的要求。任何支持一流功能的语言就足够了。
Andres F.

1
OP问题的示例:(let((opp#'+))(print(apply opp'(1 2))))
Kasper van den Berg 2016年

1
没有提到Common Lisp吗?
coredump

3
我记得在一次关于语言的小组讨论中,Ken谈到了APL中的优先级,并得出结论:“我几乎根本不使用括号!” 观众大喊:“那是因为迪克用光了他们!”
JDługosz

2
在C ++风格的语言中,您可以重新实现if,但是需要用lambdas 包装thenelse参数。PHP和JavaScript具有function(),C ++具有lambda,并且Apple通过lambdas 对C进行了扩展
达米安·耶里克

9

$test = $number1 $operator1 $number2 $operator2 $number3;

大多数语言实现都有一个步骤,其中解析器分析您的代码并从中构建树。因此,例如,表达式5 + 5 * 8将被解析为

  +
 / \
5   *
   / \
  8   8

感谢编译器对优先级的了解。如果使用变量代替运算符来输入变量,则在运行代码之前它将不知道正确的运算顺序。对于大多数实现而言,这将是一个严重的问题,因此大多数语言都不允许这样做。

您当然可以构想一种语言,其中解析器只是将上述内容解析为一系列表达式和运算符,以便在运行时进行排序和评估。大概对此没有太多的应用。

许多脚本语言都允许在运行时评估任意表达式(或至少在情况下为任意算术表达式expr)。在那里,您可以将数字和运算符组合成一个表达式,然后由语言进行评估。在PHP(以及其他许多语言)中,该函数称为eval

$test = eval("$number1 $operator1 $number2 $operator2 $number3");

还有一些语言允许在编译时生成代码。我想到了D中mixin表达式,我相信您可以在其中编写类似

test = mixin("number1 " + operator1 + " number2 " + operator2 + "number3");

在这里operator1operator2并且必须是在编译时已知的字符串常量,例如模板参数。number1number2number3保留为正常的运行时变量。

其他答案已经讨论了不同的方式,取决于语言,运算符和函数如何或多或少是同一件事。但是通常,内置的infix运算符符号like +和命名的callable like 之间在语法上存在差异operator1。我将把细节留给其他答案。


+1您应该从“在PHP中使用eval()语言构造可能的...”开始。从技术上讲,它提供了问题中要求的准确结果。
Armfoot '16

@Armfoot:很难说出问题的重点在哪里。标题强调“类型运算符的变量”方面,而eval没有回答该方面,因为对于eval运算符来说只是字符串。因此,在开始讨论替代方案之前,我先解释了为什么运算符类型的变量会引起问题。
MvG

我理解您的观点,但是认为通过将所有内容填充到字符串中,您基本上是在暗示变量类型不再相关(因为使用PHP作为示例,这似乎是问题的重点),最后,您可以以与某些变量属于“类型运算符”的相同方式放置它们,同时获得相同的结果。这就是为什么我相信您的建议可以为问题提供最准确的答案。
Armfoot '16

2

Algol 68具有该功能。您在Algol 68中的示例如下所示:

int number1 = 5;                              ¢(类型'Int')¢
op运算 符1 = intint aba + b ; ¢(类型不存在的“运算符”)¢
prio 运算符1 = 4;
int number2 = 5;                              ¢(类型'Int')¢
op运算 符2 = intint aba * b ;  ¢(类型不存在的“运算符”)¢
prio 运算符2 = 5;
int number3 = 8;                              ¢(类型'Int')¢

int 测试 = number1 运算符1 number2 运算符2 number3 ; ¢ 5 + 5 * 8 ¢

var_dumptest);

您的第二个示例如下所示:

int number4 = 9;
op运算 符3 = boolint aba < b ;
prio operator3 = 3;
如果 number1 $ operator3 number4 然后 ¢ 5 <9(true)¢
print(true
fi

您将注意到,定义了运算符并分配了包含所需操作的方法主体。运算符及其操作数都被键入,并且可以为运算符分配优先级,以便评估将以正确的顺序进行。您可能还会注意到,操作符和可变符号之间的字体略有不同。

实际上,尽管该语言是使用字体编写的,但当今的机器无法处理字体(纸带和打孔卡),因此使用了变幅字体。该程序可能输入为:

'INT' NUMBER4 = 9;
'OP' 'OPERATOR3' = 'BOOL' ('INT' A,B) A < B;
'PRIO' 'OPERATOR3' = 3;
'IF' NUMBER1 'OPERATOR3' NUMBER4 'THEN' 'C' 5 < 9 'C'
PRINT('TRUE')
'FI'

当您为运营商定义自己的符号时,您还可以使用该语言玩有趣的游戏,我很多年前就利用过该符号... [2]。


参考文献:

[1] CHLindsey和SG van der Meulen于1971年对Algol 68进行的非正式介绍

[2] Algol 68短语,一种帮助编译器编写的工具,在不列颠哥伦比亚省汤普塞特市的Algol 68中,1976年在英国诺里奇东英吉利大学举行的Algol 68应用国际会议上

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.