我们需要功能较弱的语言

2020-11-14 17:58:41

许多系统都吹嘘自己“强大”,听起来很难辩称这是一件坏事。几乎每个使用这个词的人都认为它总是一件好事。

这篇文章的主题是,在很多情况下,我们需要功能较弱的语言和系统。

在我开始之前,这篇文章几乎没有什么独到的见解。这背后的思路是通过阅读霍夫施塔特的书《哥德尔、埃舍尔、巴赫--永恒的金色辫子》来展开的,这本书帮助我在自己的思考中汇集了各种东西,在这些东西中,我看到了无所作为的原则。菲利普·瓦德勒(Philip Wadler)关于最小功率规则的帖子也很有启发性,最重要的是,我还从Scala会议上这段视频的内容中学到了很多,这段视频讲述了Scala的所有错误之处,它提出了以下相当核心的观点:

表现力的每一次提升都会给所有想要理解这一信息的人带来更大的负担。

我的目的只是用一些例子来说明这一点,这些例子可能比Scala编译器的内部更容易被Python社区访问。

我还需要一句关于定义的话。我们所说的“更强大”或“更不强大”的语言是什么意思?在本文中,我大致指的是:“做任何你想做的事的自由和能力”,这主要是从人类作者向系统中输入数据或代码的角度来看的。这大致符合“表现力”的概念,尽管可能不符合正式的定义。(更正式地说,许多语言都有同等的表现力,因为它们都是图灵完成的,但我们仍然认识到,有些语言更强大,因为它们允许用更少的词语或以多种方式产生某种结果,给作者更大的自由)。

这种自由的问题是,当你用英语写作时,你坚持拥有的每一点权力都与你在这个过程的其他点上必须放弃的权力相对应--当你“消费”你所写的东西时。我将用不同的例子来说明这一点,这些例子的范围可能超出了编程的范围,但本质上是相同的原则。

我们还需要问“这重要吗?”当然,这一点很重要,因为你需要能够“领会”你输入的信息。可能“消费”消息的不同参与者是软件维护员、编译器和其他开发工具,这意味着你几乎总是关心--这既涉及到性能和正确性,也涉及到人的安全问题。

从表现力的最低端开始,你可以称之为数据,而不是语言。但是,“数据”和“语言”都可以被认为是“要被某人接收的信息”,这一原则也适用于两者。

在我多年的软件开发中,我发现客户和用户经常要求“自由文本”域,通常是“备注”。对于最终用户而言,自由文本域是最重要的--他们可以放入他们喜欢的任何内容。从这个意义上说,这是“最有用”的字段--你可以用它做任何事情。

但正因为如此,它也是最没有用处的,因为它是最基本的结构。即使是搜索也不可靠,因为打字错误和表达相同内容的不同方式。我从事涉及数据库的软件开发的时间越长,我就越想尽可能严格地约束一切。当我这样做的时候,我最终得到的数据要有用得多。只有当我严格限制代理将数据输入系统的权力(即自由)时,我才能在消费数据时做一些强有力的事情。

在数据库技术方面,也可以提出同样的观点。“无模式”的数据库在放入数据时为您提供了极大的灵活性和强大的功能,而在取出数据时则极为无用。键值存储是“自由文本”的一个更技术性的版本,有相同的缺点-当你想要提取信息或对数据做任何事情时,它是非常无用的,因为你不能保证那里会有任何特定的键。

网络的成功在一定程度上要归功于一些核心技术,如超文本标记语言(HTML)和样式表(CSS),它们的能力受到了刻意的限制。事实上,你可能不会称它们为编程语言,而是标记语言。然而,这不是偶然的,而是TimBerners Lee刻意制定的设计原则。我只能长篇大论地引用这一页:

20世纪60年代到80年代的计算机科学花费了大量的精力来制造尽可能强大的语言。如今,我们不得不理解选择最强大的解决方案而不是最不强大的解决方案的原因,因为语言越不强大,你就可以更多地利用以该语言存储的数据。如果你用简单的说明性的形式写它,任何人都可以写一个程序来以多种方式分析它。语义网络在很大程度上是一种尝试,将大量现存的数据映射到一种公共语言上,这样数据就可以以其创建者做梦也想不到的方式进行分析。例如,如果一个带有天气数据的网页有描述该数据的RDF,用户可以将其作为表格检索,可能会对其进行保存、绘制,并结合其他信息从中推断事物。天平的另一端是由狡猾的Java小程序描绘的天气信息。虽然这可能会提供一个非常酷的用户界面,但它根本不能被分析。找到该页面的搜索引擎不会知道数据是什么,也不知道它是关于什么的。要了解Java小程序的含义,唯一的方法就是让它在人面前运行。

好的做法:使用最弱的语言来表达万维网上的信息、约束或程序。

请注意,这与保罗·格雷厄姆的建议几乎完全相反(但需要注意的是,“权力”通常是非正式的定义,不能与之相比):

如果你有几种语言可供选择,那么在其他条件相同的情况下,除了最强大的一种语言之外,用任何一种语言编程都是错误的。

在学习“合适的”编程语言时,我看到了这个例子--distutils/setuptools使用的MANIFEST.in文件格式。如果您必须为一个Python库创建一个包,那么您很可能已经使用过它。

文件格式本质上是一种非常小的语言,用于定义哪些文件应该包含在您的Python包中(相对于MANIFEST.in文件,从现在起我们将其称为工作目录)。它可能看起来像这样:

有两种类型的指令:包括类型指令(包括、递归-包含、全局-包括和嫁接)和排除类型指令(排除、递归-排除、全局-排除和修剪)。

出现了一个问题--如何解释这些指令(例如,这些指令的语义是什么)?

如果工作目录(或子目录)中的文件与至少一个包含类型指令匹配,并且与任何排除类型指令都不匹配,则该文件应该包含在包中。

不幸的是,这不是语言的定义。MANIFEST的distutils文档。在关于这一点的专门说明中-指令应理解如下(我的解释如下):

从要包含在包中的空文件列表开始(或者从技术上讲,默认的文件列表)。

对于每个包含类型指令,将所有匹配文件从工作目录复制到包列表中。

正如您所看到的,这种解释定义了一种本质上是命令性的语言--MANIFEST.in的每一行都是一个命令,暗示一个具有副作用的操作。

需要注意的是,这使得该语言比上面的我推测的声明式版本更强大。例如,考虑以下几点:

上述命令的最终结果是包括foo/bar下的.png文件,但不包括foo/bar下的所有其他文件。如果我思维清醒,使用声明性语言复制相同的结果会更困难--你必须做类似以下的事情,这显然不是最优的:

因此,因为命令式语言更强大,所以有一种诱惑要提到它。然而,命令性版本伴随着重大的缺陷:

在解释MANFIEST.in和构建要包含在包中的文件列表时,典型情况下的一个相当有效的解决方案是,首先构建目录及其子目录中所有文件的不可变列表,然后应用规则:加法规则涉及从完整列表到输出列表的复制,减法规则涉及从输出列表中删除。这就是目前的Python实现所做的事情。

这很好用,除非你在完整的列表中有成千上万的文件,其中大多数将被删减或不包括在内,在这种情况下,你可能会花费大量的时间建立完整的列表,结果却忽略了其中的大部分。

一个明显的捷径是不要递归到某些EXCLUDE指令会排除的目录中。然而,只有在排除指令毕竟包括指令的情况下,才能做到这一点。

这不是理论上的问题--例如,我发现执行setup.py sdisp和其他命令可能需要10分钟才能运行,因为如果您使用工具tox,工作目录中会有大量文件。这意味着运行tox本身(使用setup.py)变得非常慢。我目前正在尝试解决这个问题,但看起来真的很难。

添加优化后的用例可能看起来并不难(您可以使用所有包含指令之后的任何EXCLUDE指令来简化文件系统遍历),但它会充分增加补丁不太可能被接受的复杂性-它增加了代码路径的数量和错误的发生率,以至于不值得这么做。

可能唯一实际的解决方案是完全避免MANIFEST.inall,只有在MANIFEST.inall完全空的情况下才进行优化。

首先,在理解语言是如何工作的方面--这方面的文档比我想象的声明性版本要长得多。

其次,在分析一个特定的MANIFEST.in文件时,你必须执行你头脑中的命令,以便计算出结果,而不是能够单独取每一行,或者以任何对你有意义的顺序。

这实际上会导致包装错误。例如,很容易相信一个指令是这样的:

在MANIFEST.in文件的顶部,任何以~结尾的文件名(由某些编辑创建的临时文件)都会被排除在包中。实际上,它什么都不做,如果其他命令包含这些文件,那么这些文件将被错误地包含在其中。

我发现的这个错误的例子(排除不能按预期工作或无用的指令)包括:

另一个结果是,为了清楚起见,您不能以任何您喜欢的方式对MANIFEST.in文件中的行进行分组,因为重新排序会改变该文件的含义。

此外,实际上没有人会真正使用额外的能量。我敢打赌99.99%的MANIFEST.in文件没有使用命令式语言的附加功能(我下载了250个,但没有找到任何可以使用的)。因此,在这里,使用声明性语言而不是命令式语言会更好地服务于我们。但向后兼容迫使我们坚持这一点。这突出了另一点--通常可以为一种语言添加特性以使其功能更强大,但兼容性问题通常不会让您降低它的功能,例如通过删除特性或添加更多约束。

Django网络框架的一个核心部分是URL路由。这是解析URL并将其分派给该URL的处理程序的组件,可能会传递从该URL提取的一些组件。

在Django中,这是使用正则表达式完成的。对于一个显示小猫信息的应用程序,你可能会有一个kitten/urls.py,其中包含以下内容:

Urls从小猫导入url导入视图urlPatterns=[url(r';^kitten/$';,view。List_kitten,name=";kitten_list_kitten&34;),url(r';^kitten/(?p<;id>;\d+)/$';,查看。Show_kitten,name=";kitten_show_kitten";),]。

正则表达式有一个内置的捕获工具,用于捕获传递给视图函数的参数。因此,例如,如果这个应用程序运行在cuteness.com上,那么像http://www.cuteness.com/kittens/23/results这样的网址调用Python代码show_kitten(Requestid=";23";)。

现在,除了能够将URL发送到特定功能之外,Web应用程序几乎总是需要生成URL。例如,小猫列表页面需要包含指向单个小猫页面的链接,即show_kitten。显然,我们希望以一种干爽的方式来完成这项工作,重新使用URL路由和配置。

但是,我们将在相反的方向上使用URL路由配置。在做URL路由的时候,我们还在做:

在URL生成中,我们知道希望用户到达的处理程序函数和参数,并希望在完成URL路由之后生成一个将用户带到那里的URL:

为了做到这一点,我们基本上必须预测URLRouting机制的行为。我们问的是“给定一定的产出,最终的投入是多少?”

在很早的时候,Django并不包括这个功能,但是人们发现,对于大多数的URL来说,有可能“颠倒”URL模式。可以解析regex以查找静态元素和捕获元素。

请注意,首先,这是完全可能的,因为用于定义URL路由的语言是一种有限的正则表达式。我们可以很容易地使用更强大的语言来定义URL路由。例如,我们可以使用以下函数定义它们:

从django.conf.urls导入url,NoMatch def Match_kitten(Path):kitten=';kitten/';if path。以(小猫)开始:返回路径[len(小猫):],{}Raise NoMatch()def Capture_id(Path):part=Path。Split(';/';)[0]try:id=int(Part),ValueError:Raise NoMatch()返回路径[len(Part)+1:],{';id';:id}urlPatterns=[url([Match_kitten],Views)。List_kitten,name=';kitten_list_kitten';),url([Match_kitten,Capture_id],Views。Show_kitten,name=";kitten_show_kitten";),]。

当然,我们可以提供一些帮助器,让Match_kitten和Capture_id这样的东西变得更加简洁:

Urls从django.conf.urls导入url,m,c urlPatterns=[url([m(';kitten/';),Views([m(';kitten/';),View.。List_kitten,name=';kitten_list_kitten';),url([m(';kitten/';),c(id=int)],Views。Show_kitten,name=";kitten_show_kitten";),]。

现在,这种用于URL路由的语言实际上比基于正则表达式的语言强大得多,假设m和c返回上述函数。用于匹配和捕获的接口并不局限于正则表达式的功能-例如,我们可以在数据库中查找ID,或者其他许多类似的东西。(=

然而,缺点是,URL颠倒是完全不可能的。一般而言,对于图灵完整语言,你不能问“给出这个输出,输入是什么?”我们可能会检查该函数的源代码并寻找已知的模式,但这很快就变得完全不切实际了。

然而,对于正则表达式,语言的有限性质给了我们更多的选择。一般来说,基于正则表达式的URL配置是不可逆的-一个简单的正则表达式。不能唯一反转。(因为我们希望正常生成规范的URL,所以唯一的解决方案很重要。碰巧的是,对于这个通配符,Django当前选择了一个任意字符,但不支持其他通配符)。但是,只要任何类型的通配符只在捕获组中找到(可能还有其他一些限制),正则表达式就可以反转。

因此,如果我们希望能够可靠地反向URL路由,我们实际上需要一种比正则表达式更弱的语言。选择正则表达式大概是因为它们足够强大,而没有意识到它们太强大了。

此外,在Python中为这类事情定义迷你语言是非常困难的,而且在实现和使用时都需要相当多的样板和冗长-比使用基于字符串的类语言正则表达式要多得多。在像Haskell这样的语言中,代数数据类型的轻松定义和模式匹配等相对简单的功能使这些事情变得容易得多。

提到Django的URL路由中使用的regex,我想到了另一个棘手的问题:

Regex的许多用法都相对简单,但无论何时调用regex,无论您是否需要,都可以获得全部功能。结果之一是,对于某些正则表达式,需要进行回溯以查找所有可能的匹配,这意味着有可能构建恶意输入,而正则表达式的实现需要花费大量时间来处理这些输入。

这是许多网站和服务中一大类拒绝服务漏洞的原因,包括Django中的一个漏洞,是由于URL验证器CVE-2015-5145中意外的“邪恶”正则表达式造成的(还有一个关闭了整个Stack Exchange-2016-07-22更新)。

JJJA模板引擎的灵感来自于Django模板语言,但在理念和语法上有所不同。

与Django相比,JJIA2的一个主要优势是性能。JJIA2有一个实现策略,即编译成Python代码,而不是运行用Python编写的解释器,这就是Django的工作方式,这将导致性能大幅提升-通常是5到20倍。(YMMV等)。

Armin Ronacher,JJJA的作者,试图使用同样的策略来加速Django模板渲染。然而,也存在一些问题。

当他提出这个项目时,他知道的第一点是,Django中的扩展API使得在JJJA中采用的方法非常困难。Django允许自定义模板标记,几乎完全控制编译和呈现步骤。这允许一些强大的自定义模板标记,比如django-sekizai中的addtoblock,这乍一看似乎是不可能的。然而,如果为这些不太常见的情况提供较慢的后备,快速实现可能仍然很有用。

然而,还有一个影响很多模板的关键区别,那就是传入的上下文对象(保存模板所需的数据)在Django的模板呈现过程中是可写的。模板标记能够分配给上下文,实际上一些内置的模板标记(如url)正好可以做到这一点。

请注意,在这两种情况下,Django模板引擎的强大功能才是问题所在--它允许代码作者做一些在JJIA2中不可能做的事情。然而,其结果是,在尝试编译为快速代码的过程中设置了非常大的障碍。

这不是理论上的考虑。在某种程度上,模板制作的性能成为许多项目的一个问题,因此许多项目被迫改用金佳。这远远谈不上是最理想的经济形势!

通常情况下,让优化变得困难的问题只有在事后才能弄清楚,并不是说简单地给一种语言添加限制就一定会让它更容易优化。有一些语言不知何故成功地击中了一个“酸点”,给作者或消费者提供的力量微乎其微!

您可能还会说,对于Django模板设计人员来说,允许上下文对象是可写的是显而易见的选择,因为默认情况下,Python数据结构通常是可变的。这就把我们带到了巨蟒…

我们可以用很多方式来思考Python语言的强大,以及它如何使每个人和程序的生活变得艰难。

.