语义导入版本控制不健全

2020-09-15 10:30:01

早些时候有一篇题为围棋模块有一个v2+问题正在流传的文章,它批评了围棋模块设计的一些方面。我认为那里提出的论点实际上只是略过了一个更根本和更严重的问题的表面。关于这个问题,我已经讨论了足够长的时间,并与足够多的其他人讨论,我认为它值得进行更正式的描述。所以,我们开始吧。

GO模块有一个称为语义导入版本控制(SIV)的要求,它规定给定的模块标识符必须在其所有版本之间保持语义兼容。

就Semver而言,这意味着如果您的模块是主要版本1或更高版本,并且您做出了突破性的更改,那么您不仅必须增加主要版本,还必须更改模块本身的名称。(Semver主要版本0没有明确声明兼容性,因此SIV本质上不适用于此。)。

SIV的简化假设确实提供了好处。最值得注意的是,通过强制用名义API兼容性来表示依赖关系,依赖关系解析变得简单得多。如果在给定标识符下可用的每个版本在名义上是等价的,那么在求解依赖图时,工具可以自由地选择它们中的任何一个,向上递增。因此,相同的-嗯,“相同的”-依赖的不同主要版本可以在单个编译单元中很好地共存,因为标识符引用哪个主要版本是没有歧义的。在执行依赖项从一个主要版本到另一个主要版本的大规模迁移时,此功能经常被描述为必不可少的。

但SIV也伴随着成本。成本可能表现为特定的错误、工作流故障或可以单独识别和解决的特定问题,我将尝试指出其中的一些问题。但我认为专注于这些表现本身是错误的,因为我认为它们是实际问题的症状,即目前实施的SIV从根本上是不健全的。

我的主张有很多角度,我会努力把它们连贯地呈现出来。

首先,我想将作为依赖管理工具(如管道)的精确输入的标识符的概念与人类在这些工具(如瓷器)的UX中使用的标识符的概念分开。

软件必须建立并存在于一个域或有界的上下文中,在那里它可以自由地“定义其术语”。当我编写管理用户配置文件的服务时,用户或配置文件的含义就是我选择它的意思-不多也不少。或者,当我编写编程语言时,我可以自由地说类型在声明中跟随标识符,这就是它的方式。这种认知闭包对于构建有用和抽象良好的模型是必要的,程序员理解这种必要性,并期望在编程时付出认知成本不稳定的赌注。

但是,这种自由,就像任何其他自由一样,是有限度的。即使在一个领域内,如果我在没有充分理由的情况下坚持为我的语言创建一个完全虚构的词汇,用户也会理所当然地犹豫不决:认知成本得不到足够的好处。当我们离开单独的软件领域,开始在整个生态系统的背景下工作时,我们失去了更多的这种力量。一个用户类型在两个Repos中可能意味着两个特定和不同的东西,但当我们在跨团队集成会议上或在全体员工中谈论用户时,默认情况下,我们谈论的是一个不同的、更一般的、更抽象的东西。谈话的背景设定了这种期望值。

一般说来,人类,特别是程序员,已经有了一个根深蒂固的身份概念。至关重要的是,身份的概念与版本或时间是正交的。类似地,我的标志包peterbourgon/ff在v3.x.x和v1.x.x在逻辑上仍然是基本相同的东西,尽管它的API已经以非向后兼容的方式发生了变化,但从根本上说,我还是和12岁时一样,我的标志包peterbourgon/ff在v3.x.x和v1.x.x中在逻辑上仍然是一样的,尽管它的API已经以非向后兼容的方式发生了变化。模块断言情况并非如此。

这是一个微妙的问题,但它会导致很多严重的问题,特别是当它与另一个设计决策交互时。在模块中,主要版本0和1是独一无二的,因为它们不是用显式的版本后缀表示的,而是用未版本化的裸露模块名称表示的。因此,当用户编写github.com/user/repo时,模块认为他们显式指定了主版本0或1,但实际上从来不是这样。

因此,用户很容易无意中选择依赖项的旧的或不受支持的主要版本。他们没有很好的方法来发现:因为SIV将主要版本理解为完全不同的版本,模块明确地不理解或暗示模块/v2和模块/v3之间的任何联系。(pkg.go.dev上列出给定模块主要版本的小启示源自附加的非模块元数据。)。更重要的是,模块的作者似乎积极抵制这一祖先确实存在的观念。

这是有原因的-模块不会无意中将其特定领域的身份概念提升到生态系统中。作者认为,应该根据API兼容性而不是作者意图来定义实体,因为他们认为软件生态系统应该始终将消费者的易用性放在首位。在这个世界观中,主版本所代表的远远超过其在语义版本控制下的定义。这是一份与消费者签订的合同,默认情况下,这份合同被认为是无限期支持和维护的。实际上,重大版本升级的“成本”总是非常高的。

这似乎是谷歌内部软件生态系统的产物。在Google,软件包消费者希望他们的依赖关系能够自动更新,例如安全修复,或者更新以适应新的基础架构需求,而无需他们的积极干预。稳定性是如此重要,事实上,包的作者应该主动审计和公关他们的消费者代码,以防止任何可观察到的行为变化。正如我所理解的那样,即使是依赖项升级导致的问题也被认为是依赖项的错误,因为在升级到新版本之前,风险分析不充分,而不是消费者的错误,测试不充分。不出所料,这激发了一种自动升级的文化:即使非常罕见,大版本的升级也可能与自动修复用户代码的工具一起出现。

模块对消费者稳定性的非同寻常的偏爱可能对谷歌内部的软件生态系统来说是理想的,但总体来说,它对软件生态系统是不合适的。

首先,这是因为重大版本升级的成本和收益并不适用于所有项目。对于大量进口的具有较大API表面积的模块,新的主要版本给很多人带来了大量的劳动,因此可能会带来很高的成本。但对于API很小和/或消费者很少的模块,主要版本的提升客观上成本较低。此外,对于用稳定而高效的API对定义明确的领域进行建模的软件来说,突破性的更改可能会带来更多的混乱,而不是创新,因此可能不会带来太多好处。但是对于仍在探索其领域的软件,或者建模具有异常高更改率的东西,能够进行相对频繁的突破性更改对于保持项目的健康可能是必不可少的。

(有时模块的作者建议,更改率高的项目应该简单地坚持V0,直到项目稳定下来。但是,正如主要版本升级的成本和收益对所有项目都不一样,稳定性的定义也不尽相同。一个主要版本表达了语义兼容性,仅此而已-不应该阻止项目使用Semver来表达它们的版本语义,因为生态系统工具已经取代了更严格的定义。)。

此外,偏向于消费者稳定的政策依赖于一系列结构性假设,这些假设可能存在于谷歌这样的封闭系统中,但一般不存在于开放的软件生态系统中。具体地说:我不可能知道是谁导入了我的模块,我不可能承担导入模块所带来的任何风险,而且我不可能永久维护一个主要版本-或者,实际上,维护任何超出我选择维护的内容都是不可行的。当然,要想成为一个社区的好成员,需要对所有这些事情真诚努力,但如果不人为地排除参与,强制工具就不能把它们当作期望。

对消费者的偏见必然意味着对作者的某种偏见。在SIV中,版本的建模使得API兼容性被认为是基本的“东西”,定义身份的权威真理。在该模型中,实际的版本标识符在某种程度上是从该事实中出现的,或者是该事实的一种表达。但是,在P=NP意义上,API兼容性不能也不能被精确定义,甚至不能被发现。主要版本表达的是版本兼容的意图,但不是它的存在。因此,SIV的版本化模型恰恰是落后的。作者所表达的版本是核心真理,API兼容性是(或不是)该真理的紧急属性。SIV剥夺了作者的这种权威。

最后,这种偏见根本不能反映软件开发的整体现实。包的作者根据需要增加主要版本,消费者相应地更新他们的版本钉,每个人都对其含义、风险以及如何管理风险有一个直观的理解。实质性的版本升级应该是微不足道的,甚至是通过工具实现自动化的说法是闻所未闻的。模块和SIV代表了一个规范的论点:至少在某种程度上,我们都做错了,我们应该改变我们的行为。但是,当我们移到更广泛的上下文中时,就像我们失去了一些断言特定领域语言的自由一样,我们也失去了一些进行规范论证的自由。一个以生态系统为目标的工具在传播福音方面的预算必然非常有限--它本质上必须与用户在哪里合作,而不是引导用户到它想让他们去的地方。

SIV为该工具提供了明确标识符的好处,这有助于它解析依赖图。但这是一种内部利益,用户看不见,只有通过它的扩散才能看到。对用户来说,唯一明显的好处是他们的编译单元中可以有“相同”模块的不同版本。当然,情况一直都是这样:不同的是,以前,它是由作者选择加入的,例如,通过创建一个新的repo一个新的主要版本,而现在它对生态系统中的所有工件都是强制性的。

这项授权是合理的吗?在实践中,需要此功能的频率是多少?我知道这在像Google这样的生态系统中相对常见,在那里协调一个主要的版本升级经常需要一个“分阶段”的方法,即在一段时间内同时使用多个版本。但我个人从来没有经历过这种需要,对我的同龄人进行的一项非正式调查也表明,这并不是一件很常见的事情。澄清一下,我不是说它不值钱。但在我看来,强制要求生态系统中的每个人都这样做的好处远远不足以证明所产生的成本是合理的。

当然,这几乎肯定不会发生。即使你相信我的理论基础,模块的设计实际上是冻结的--导致我们走到这一步的过程是完全不同的讨论--那么,我们可以为什么实际的改进而努力呢?

前面,我将内部(管道)使用的标识符与UX(瓷器)中使用的标识符区分开来。如果有一种方法可以去除SIV,并从瓷器中恢复身份和版本的直观概念,同时在管道中保留SIV和更严格的身份和版本定义,我会非常高兴的。这是一种似乎是作者建议的方法,尽管总是有点含蓄的。

可以在Go工具中设想一个新的瓷器子命令,它一致地使用身份和版本的直观概念,并在必要时随时随地转换为SIV语法。Go用户也有可能最终将子命令及其各种编辑器集成理解为使用依赖项主要方式。但我认为最棘手的部分是源文件本身中的import语句,在我看来,它似乎不可避免地是UX的一部分,也不可避免地需要保持其更具限制性的SIV语义以供工具使用。我能看到的一种解决方案是引入从Human-Identityimport语句到SIV-Identity模块名称的映射概念。还有其他的吗?