为复杂性语言辩护:类的意义 - viralinstruction

22-04-07 banq

在2014/15年的冬天,我是一名大学生,我的特点是手上有太多的空闲时间,却没有足够的钱让自己在空闲时间里忙碌。无聊又没钱,编程是一个完美的爱好。如果你已经拥有一台电脑,它是免费的,而且当你与无聊作斗争时,时间的投入并不令人气馁。我是在别人的推荐下选择学习Python的,我可以发自内心地把这个推荐转发给初学者。学习曲线很平缓,当你只需要搞清楚for循环是如何工作的时候,这门语言大多是令人愉快的,没有太多的干扰。我进步得够快了。

然而,有一个概念我在理解上有很大的困难:类。不是在类的实现的深层的黑暗魔法,而是简单的类的概念,因为它出现在它的表面上。

我的学习材料用这样的陈词滥调来介绍类:
类允许你在你的代码中直接为对象建模。假设你写了一些与你的狗Rex有关的代码,它能吠叫。在这种情况下,你可以写

class Dog:
    def __init__(self, name, weight_kg):
        self.name = name
        self.weight_kg = weight_kg

    def bark(self):
        print("WOOF!" if self.weight_kg > 25 else "Woof")

rex = Dog("Rex", 35)
rex.bark()


你能发现为什么上述内容对那些从未接触过“类”概念的人来说是一个糟糕的介绍吗?花点时间反思一下吧。

banq注:“类”是大道至简的语文中修辞手法:比喻。见:数据模型是一种羞耻手法


如果你不了解一个解决方案所解决的问题,你就无法理解它。如果你先提出解决方案,就很难发现一开始就有一个问题。(banq:建模映射是初级程序员缺乏的认知)

这就是我在阅读《小狗雷克斯》的例子时遇到的问题。并不是说我不明白代码是如何工作的,例子中的类确实看起来像一只35公斤重的狗,名叫Rex,会叫。但是你知道什么也像一只35公斤重的狗,叫Rex,会叫吗?两个变量和一个函数。

def bark(weight_kg):
    print("WOOF!" if weight_kg > 25 else "Woof")
    
name = "Rex"
weight_kg = 35
bark(weight_kg)


比较上面的两个代码片断。它们在功能上是相同的。后者要短得多,而且直奔主题。
对于一个已经学过函数和变量的学生来说,它没有引入新的语法或规则。

请暂停一下,反思一下 Python 的类语法是多么的奇怪和不直观。__init__?真的吗?那什么是self呢?为什么一个没有参数的函数被定义为有一个参数的函数?bark(weight) 和 dog.bark() 在功能上有什么区别?

从各方面来看,第二个代码片段都是更好的代码。
所以我认为我可以没有“类”。
我认为,如果 Python 类有存在的理由,它们就必须有一些我看不到的特殊行为!这就是我的想法。

从现在来看,很明显我的假设是错误的。
当然,“类”确实释放了一些新的行为,如果不使用类就无法实现,但这并不是类有用的原因。事实上,我现在写的几乎所有的类都没有做任何用内置类型和函数调用无法实现的事情。

Rex the Dog这个例子的问题在于,类并不是为了能够代表你的狗。
它代表关于完全不同的东西:封装、模块化、抽象。
我们不要对这些术语的确切含义挑三拣四,它们其实都是关于同一件事:管理你自己代码的复杂性

软件不是无限的
作为一种人工制品,软件与其他工艺的实物创作有很大的不同。生产它不需要消耗任何原材料。它不需要专门的工具来制造甚至最高质量的代码。产品没有重量,其物理分布几乎不费吹灰之力。生产数以百万计的拷贝并将它们运往世界各地几乎没有任何成本。

那么,如果没有这些限制,软件是无限制的、无限的吗?不是的,它受到其他约束的限制。有时,软件会受到它所运行的机器的物理能力、磁盘空间、内存使用或计算速度的限制。我并不想贬低这些物理约束。毕竟,我在这个博客上写的很多东西都是关于性能的。但最主要的是,软件受到其创建过程的限制。程序员创建代码的时间有限,而维护代码的时间尤其有限。

你看,维护代码,比如说修复错误,需要程序员详细了解代码的内部运作情况。代码越多,复杂程度越高,理解的时间就越长,程序员由于没有理解代码运行时正在发生或可能发生的事情而增加的错误就越多。

与其他创造性的劳动产品相比,这并不是说软件被诅咒为特别难以理解。正是因为没有其他障碍,程序员们才可以不断地创造,不断地创造,直到我们拥有一个比我们所能建造的任何物理设备都要多的活动部件,并不断地创造,直到我们无法将自己的创造留在脑海中,并在复杂的泥潭中慢下来,耗尽了时间。

这就是为什么类在 Python 中很重要:它们可以帮助我们在程序成长的过程中,使我们的程序更容易受到控制。
作为一个初学者,我不知道,因为我还没有经历过我的一个程序的复杂性像滚雪球一样失控的情况。我从来没有见过一扇锁着的门,所以我不明白为什么有人会想要一把钥匙。

此后,我有幸向其他初学者教授 Python。在这些课程中,我优先考虑在课程结束时给学生布置一个大型的个人项目,即使这意味着必须在课程的其他方面进行削减。在指导学生时,这提供了一个独特的机会,让他们知道,而不是告诉他们,良好的编码实践可以帮助他们解开面条状的代码,重新控制他们正在下沉的项目。

一个没有类的 Python会怎么样?
那么,如果我们禁止 Python 中的类,会发生什么呢?

哦,这将使语言变得非常简单 就像我举的 Rex 的例子一样--剩下的将是纯粹的领域逻辑、商业逻辑,真正的交易!几乎没有代码浪费在模板或仪式上。几乎没有代码被浪费在模板或仪式上! 没有那么多奇怪的语法要教,没有像我这样的新手在学习时被卡住的路障。最重要的是,几乎不会有任何功能的损失,因为类大多不会给你带来任何新的行为。

可能还会发生的是,用户会发现变量的数量会变得无法控制。直到有一天,一些程序员想到了一个很好的主意,他们可以减少保存在头脑中的变量的数量,只要他们将变量分组在一个dict中。

def bark(dog_dict):
    print("WOOF!" if dog_dict["weight_kg"] > 25 else "Woof")

rex = {"name": "Rex", "weight_kg": 35}


因此,他们会意外地重新引入类,只是这一次,类的存在在代码中是隐含的,它们的行为是临时定义的,它们的不变量散布在源文件中,而且没有语言级的工具或内省来帮助程序员。

类仍然存在,但是作为隐含的模式。

这类结构会自发地、不断地出现在代码中。

以前的编程语言不支持函数,但后来人们发现,指令往往是按功能分组的,而且将其概念化为一个函数可以使代码更容易推理。函数的引入并没有使编程变得更复杂,相反,它在重要的方面变得更简单。

以前的语言没有结构,但后来程序员们发现了将数据集归为一种抽象的、更高阶的数据的有用性,这就是结构。而且,这一特性并没有使程序变得更加复杂,而是使其更加简单。

Julia和Rust
与那个时代的语言相比,现代的编程语言被塞得满满的。我个人碰巧喜欢Rust和Julia,这两种语言都是(在)有名的复杂和有特色的。

Julia有一个复杂的类型系统。像AbstractSet{Union{Nothing, <:Integer}}这样的类型并不容易学会解析,也不容易在现有代码中进行推理。但是这种类型的结构,以及它的复杂性,只是程序员对它所代表的数据的意图的实例化。在一个更简单的类型系统中,这个类型将不存在,但同样的意图还是存在的。

Python曾经没有一个足够丰富的类型系统来表达这样的概念,因此程序员在阅读Python时,不得不自己去解决一个特定的变量隐含地符合该类型的约束--如果读者幸运的话,可以从代码注释中读到这些信息,但大多数时候,这些知识只能通过在你的头脑中保留周围代码对变量的所有隐含假设来获得。

这真是太糟糕了。

并非巧合的是,最近的Python版本引入了由复杂的类型系统支持的类型提示,这样,程序员现在就可以用collections.abc.Set[typing.Option[numbers.Integral]]来表达同一个想法。尽管这个新的类型系统很复杂,但 Python 也因此而变得更好。事实上,自从学习 Julia 以来,我对 Python 最好的体验就是打开我的一个旧的 Python 代码库,用类型提示来注释所有的东西,然后在上面运行一个类型检查器。

著名的是,Rust的编译器执行了 "所有权 "的概念--一块数据可以对另一块数据负责。但Rust并没有发明这个概念。它是支撑面向对象的核心思想之一,比Rust的出现还要早几十年。Julia语言没有所有权的概念,而FASTA.Record的文件串却说。

看到了吗?Julia确实有一个 "所有权 "的概念,只是没有,你知道,在实际的语言中。但是使用FASTA.Record的程序员必须跟踪谁拥有它的数据,而这种心理记账的方式使FASTA.Record更难使用,因为关于它的代码更难遵循。Rust的所有权模型的复杂性并没有增加到一个简单的程序中,它只是编译器对你的代码遵守它无论如何都要遵守的规则非常迂腐而已。

"一种简单的语言?Go语言简单是一个谎言"
这就是Zig语言网页顶部附近用大字写的。Zig是最近进行的一项实验,目的是为了消除困扰现代编程语言的所有被嘲笑的复杂性。封闭、函数特性、运算符重载--编程已经够难的了,为什么我们不能至少用一种简单的语言编程,而不需要所有这些垃圾呢?

说实话,Zig看起来是一种很酷的语言,我想有一天能学会它,但我不能说它的简单性是它最有吸引力的品质。在标题的下面,网站上写着。

专注于调试你的应用程序而不是调试你的编程语言知识。
我为什么要这样呢?现代语言复杂性的全部意义在于减少你的应用程序所需的调试量,因为它的复杂性是由语言适当管理的。

Zig使用手动内存管理,并且没有像Rust那样在编译时保证内存安全。当然,这意味着,与编写Rust代码相比,编写Zig代码可能更简单,它可以编译,但在运行时崩溃,之后你就可以专注于 "调试你的应用程序"。

对我来说,这是个文字游戏。在Zig中找出如何满足例如所有权规则的困难,仅仅是惯例,是对 "应用程序 "的调试,而在Rust中,同样的困难是对 "编程语言 "的调试。

当然,Zig并不是第一种明确追求简单性的语言。Go在Zig之前就这样做了,其动机基本相同。摆脱语言的束缚。在某些方面,他们是成功的。Go被誉为一种容易学习的语言。但另一方面......好吧,让我用别人的话来结束。

Go语言的每一份文档都反复强调它是 "简单 "的,这是个谎言。或者说,这是一个半真半假的谎言,它方便地掩盖了这样一个事实:当你把某样东西变得简单时,你就把复杂性转移到其他地方。

总结
以 "为......辩护 "为格式的帖子没有太多的细微差别,当然,本帖的问题也不是一目了然的。"更多的语言特点 "并不等于 "更多的好",现代语言复杂性的诋毁者确实有值得考虑的观点,至少是孤立的。

上述例子中的所有语言功能--类、高级类型和借贷检查器--都有一个重要的共同特征:它们都感觉是自发地从现有的代码中产生的,与语言设计者是否考虑过它们无关。从这个意义上说,它们是最好的一种特性;它们没有增加新的东西来担心,而只是提供一个词汇和工具来处理已经存在的问题。

不是所有的语言特性都是这样的。例如,Julia 有四种不同的方法来定义一个函数,而for 循环的外观也有同样多的变化。人们可以将一个类型定义为结构体、可变结构体、抽象类型和原始类型(所有前者都可能是参数化的)。类型可以作为具体类型、抽象类型、联合类型或联合所有类型放置在类型层次中。类型的表示(即类型的类型)可以是DataType、Union、UnionAll或Bottom。

这样的复杂性并不完全是多余的,但肯定是要学习的,而且我不清楚这样的设计是最干净的。当然,感觉上,这并不需要如此复杂。

最糟糕的一种功能是重复的API,通常是因为一个旧的、设计不良的API一直存在,只是为了满足向后的兼容性,也许还有一小部分用户拒绝停止使用它。我并不喜欢语言中的这种复杂性,人们避开它是正确的。

然而,在一个更基本的层面上,反对者是对的,即使语言中合理的复杂性也会给用户带来成本。我喜欢Rust,但我写它超过两个小时就会希望我写的是Julia,因为编译器让我试图写一些该死的代码来工作的努力受挫。即使一个严格的编译器只执行你要手动执行的不变性,编译器也是愚钝的,而且极难说服你,不,这种反模式在这种情况下实际上是合适的。当一个程序的结构由人类自由控制时,人类可以选择捷径和简单的解决方案。Rex the Dog可以只保留两个变量,而且真的会减少模板,即使也会有一些地雷。

这篇文章的开始是我向一个编程初学者推荐Python,这并不是偶然的。语言的学习已经需要大量的时间投入,而一种语言中塞进更多的东西,即使是设计良好的东西,也需要更大的投入。大型Python项目因其自身难以管理的重量而僵化而臭名昭著,但从另一个角度看,学习Python所需的时间相对较少。我当然不会推荐Rust作为任何人的第一种语言。

我们很容易理解负责绿地项目的程序员团队的困境,他们面临着这样的选择:是花几个月的工资来招收新员工,让他们学习一门难学的语言,然后有一半的人离开,还是选择一门容易上手的语言。

那么,在现代编程语言的复杂性方面既有缺点也有优点,我们应该得出什么结论?恐怕这篇博文不可能有令人满意的结论。虽然我不相信答案只是一个意见问题,但也不完全是一个事实问题。唯一的解决方法是我们作为专业人士要运用我们的判断力。

1