原文:https://divan.dev/posts/visual_programming_go/
本文为 CSDN 翻译,转载请注明来源出处。
在计算中,可视化编程语言(VPL)允许用户通过图形化操作程序元素而不是通过文本指定来创建程序。但一直以来,除了在一些非常有限的领域外,可视化编程都未成功。对此,有人认为文本编程语言混淆了编程的本质,也有网友表示为支持编程而开发的工具并不重要,那么可视化编程的出路到底在哪里?
对此,本文作者 Ivan Daniluk 发表了长文剖析可视化编程语言失败的原因,并探讨新兴的 Go 语言在可视化编程中的应用与解决方案。
作者 | Ivan Daniluk
译者 | 弯月,责编 | 屠敏
出品 | CSDN(ID:CSDNnews)
以下为译文:
我希望能够开门见山,直接讨论主题,但是我觉得还是有必要首先介绍一下背景。
这一切都因为我感觉编写文本代码太让人失望了。
IDE与火把
你知道代码编辑器和火把之间有什么共同之处吗?
历来,我们都习惯了以自上而下、从左到右的方式阅读源文本代码,这种方式与我们阅读西方自然语言的方式相同,而且在水平书写系统中,文本会向下延伸,形成垂直的文本,然后存储在文件中。
为了阅读文本代码并浏览结构,我们需要使用代码编辑器——高亮显示语法并集成了代码分析功能的专用文本编辑器。我使用的是Vim,我很喜欢它舒适的外观以及基于控制台的界面,但是很多人更喜欢高级的代码编辑器,以鼠标点击为重点交互。无论是哪种情况,我们都需要滚动文本,并在文件之间反复跳转。
我们习惯了这种日常工作,很难想象把画面拉远,然后远距离观察。但是如果你真的如此观察,就会发现自己正坐在一个文本墙的前面,并使用这个叫做“编辑器”的滑动窗口在墙上移动,然后将其中的一小部分带到屏幕上以便你近距离查看。
由于除了屏幕上的代码外,你看不到其他代码,因此这意味着整个墙都在黑暗中。
所以,你独自坐在黑暗中,被带有神秘文本的墙壁所包围,然后通过代码编辑器将一束光照射到这面墙上,并阅读其中一小部分。现在,想象一下你眼前的是火把,而不是发光的屏幕,你就会觉得编辑器只是现代版的火把。
当阅读一个新的代码库时,你就好像一个古人在一个黑暗洞穴中,通过火把阅读墙上的字或画。
我不得不说,这是一种多么古老的方法。当然,像古人一样与这个世界互动也没什么错。然而,这里面的问题太多了!我们花费了90%的时间阅读代码,只有10%的时间在写代码,而这90%恰好是我最不喜欢的部分。
阅读代码需要非常好的注意力,接近完美的注意力,超出常人的记忆力,而且非常乏味。我的直觉告诉我,这都是文本形式惹的祸。当我即将阅读新的代码库或有大的拉取请求时,我的脸就像洞居隐士的脸。
甚至有通过眼部追踪系统对阅读代码的研究证实,我们阅读源代码和阅读标准文本的方式完全不同。我们并不会逐行阅读,而是会用扫描的模式,比如紧盯着不明白的代码或感兴趣的区域,来回跳跃,同时跳过某些行或整块代码,就像下面这样:
如果说文本并不是显示代码的最佳形式呢?
可视化编程
显然,我不是第一个思考用文本表示代码的缺点的人。可视化编程语言是一个完整的领域,在过去的60年里,涌现了数百种这类的语言。
你可以从互联网上找到精心准备的材料和文献,其中讲述了可视化编程语言的概述和历史,在这里我给出两个我最喜欢的链接:
- http://blog.interfacevision.com/design/design-visual-progarmming-languages-snapshots/
- https://www.youtube.com/watch?v=mdYfFDJCDHc
简单地概括,这些语言可以分为两大类:
基于块的可视化编程语言
在基于块的可视化编程语言环境中,你不需要输入文本代码,而是可以将预先定义好的块通过拖拽放到脚本区域。你根本不需要打字,还拥有色彩鲜艳的UI,而且几乎不可能犯语法错误。
你可能听说过Scratch、MIT App Inventor、Google Blockly等语言。这些语言主要是为了教儿童和新手程序员学习编程的基础知识,在学校及一些活动中大量应用。多项研究证明,这些语言对学生学习计算机科学课程有积极影响。此外,在许多混合项目中,你也可以将简单的代码示例从文本表示的主流语言转换为块表示,反之亦然,这有助于从基于块的类过渡到“真正的编程”。
但似乎它们的用途也仅限于此了。而且,很多人还说这根本不是可视化编程,只不过是用鼠标输入替换了键盘输入。我基本同意这个观点。
基于流的可视化编程语言
另一类可视化编程语言实际上是所谓基于流的语言的子组,也称为基于节点的编程语言。它们使用流程图来表示状态、逻辑或数据的变化。
有时,这些语言背后的观点是,程序的逻辑应该由熟悉问题领域的人(比如医生或工程师)实现,而将这些逻辑转换成代码的工作应该由其他人或程序完成。这也不无道理。
很多人尝试建立基于流的通用可视化编程语言,例如Raptor、Flowgorithm、Visual Logic或DRAKON,这些语言在3D编程、音乐合成、信号处理或物联网/嵌入式电路设计工具等领域大受欢迎。有些工具你可能很熟悉(甚至每天都在使用),例如虚幻引擎的编辑器、Max / MSP、National Instruments的Labview或Autodesk Dynamo等工具。许多人在实践中积极大量地使用这些工具,但它们仍然是非常狭窄的领域和高度专业化的解决方案。你不太可能使用其中一种工具,编写射线追踪引擎、内核驱动程序或GraphQL服务器。
2.5 其他可视化编程语言
还有一堆奇奇怪怪的产品,比如Prograph或Cables,很难说它们属于哪一类。例如80年代设计的Prograph是第一个真正的视觉对象导向的语言,还引入了一些我非常喜欢的有趣的想法,但当我想使用它来怀旧的时候,却失败了。
另外,70年代的Pygmalion也很有意思。因为它使用了一种被遗忘的编程技术:演示编程。基本上是说,你并不是通过编程来写算法,而是向机器演示算法,酷毙了吧?从某种程度上来说,这个领域还有待探索。
没有主流的可视化编程语言?
然而,目前还没有真正主流的通用可视化编程语言。每项指标都显示,现代编程语言环境100%由文本编程语言主导,没有例外。
为什么会这样?英文有一句谚语说得好:“一画胜千言”,直觉告诉我们文字并不是表示一切的最佳方式。所有那些致力于可视化编程语言的聪明人都清楚地看到了这一点,但似乎我们缺少让视觉编程成为现实所必需的东西。
为什么可视化编程语言失败了?
很久以前这个问题就激起了我的好奇心,简而言之,我的答案是:我们不知道自己在干嘛。就编程是什么这个问题,没有普遍认可的科学解释。我们只是简单粗暴地通过不同的方式设计新语言,无论是视觉还是文本,我们都希望这些语言能够更好地表达我们的思想。
更糟糕的是,我有一种不祥的预感:编程语言理论社区并不关心可视化编程。至少我从未见过正经的研究会邀请心理学家或神经科学家引入另一种新奇的特征到可视化编程语言中。不过,我希望我是错的。
所以,我们都觉得我们需要编程来表示抽象事物,但这究竟是什么意思呢?在阅读、编写甚至思考编程的时候,你在想些什么?我们能够通过哪些框架来验证和测量视觉语言的表现呢?
这类的问题都很少出现,更不用说答案了。
如果你尝试自己回答这些问题的话,最终必将陷入哲学或神经科学领域,或两者兼而有之。由于一直以来,我都对大脑的运作非常感兴趣,阅读大脑相关主题的书籍和论文变成了我的爱好,我认为,这帮助我以新的视角看待这些问题。虽然,神经学家对于大脑运作的的理解尚很肤浅,而我对他们的成果也只是一知半解,但我想分享两个重要而且不是很明显的方面,我相信这些方面是这个主题的基础。至少,对于理解本文有着重要的作用,因为我的依据就建立在这两个方面之上。
人脑的基本知识
人类的大脑由大约90-100亿个神经元组成,这些神经元之间形成了了1千万亿个连接,人脑是太阳系中最复杂的对象,因此无法轻易解释。
我们的意识和思想来自大脑中称为新皮质的部分。大多数人都知道大脑是一种表面布满皱纹的核桃形状的东西,但你可能会惊讶地发现实际上它是一个厚度为5-6毫米的扁平组织。在表面积迅速增加之后,为了适应头骨,它弯曲成了如今这样的形状(头骨的生长进化成本更高,因为它需要顺应骨盆的生长以适应生育过程)。
这种组织在结构上非常均匀,它的整个表面拥有相同的5层神经元,且通过皮质柱组织。一些理论认为,这个皮质柱是新皮层的一个“计算单元”,负责执行一项主要任务——接收大量的输入,从中提取模式并学习如何预测。
意识、思想、高层思维、自我意识只是这些模式提取组织的巨大规模和复杂性的副产品。这里我们谈论的规模相当于可观测宇宙中的星系规模。
空间与时间模式
首先,我希望你注意模式这个概念本身。宇宙中有两种主要的模式:空间和时间。空间就意味着与空间有关,而时间则和时间有关。比如,此时此地。这种差异源于我们宇宙的本质,而进化的大脑学会了相应地处理这两个模式。
例如,视觉主要处理空间信息,视觉皮层可以非常出色地从视网膜提供的视觉输入中提取大量最常见的模式。要想捕获空间关系,你必须“及时地”获取所有输入,看图像的时候你不能一次看一个像素。你必须以正确的方式看到同时排列在一个空间中的所有像素。
另一方面,听力主要与时间模式有关,你无法在同一个时间点上听到所有的音符。它们只能分布在不同的时间上。
当然,我们所有的感官输入都兼顾空间和时间模式,但视觉和听觉这两个例子很好地展示了两种不同模式的核心理念。大脑在最低层次处理它们的方式截然不同,但有意思的是,我们可以使用空间表示来表示时间信息,反之亦然。实际上,我们经常这样做。音符就是一个很好的例子,我们用空间表示编码时间。请注意,为了理解这种表示,你必须训练大脑,通过解码恢复时间,这是一项认知上的艰巨任务——不是每个人都可以阅读乐谱并在头脑中播放一首歌。
我希望你能记住这种空间与时间上的差异。我们很快就会用到。
知识图谱
你需要了解的编程方面的第二个重点是,所有知识都以某种方式存储在你的新皮层中。有无数理论都在设法解释这种存储的方式,但是我认为我们距离真正的答案还很远。但我们知道,对于你所掌握的每一个概念、每一个知识点、每一个抽象或对象,在你看到、听到、想到甚至梦到时,都会有一群神经元兴奋起来。
例如,加州大学伯克利分校的Gallant Lab出色地完成了一项工作:在参与者倾听带语音流标签的AutoStream的过程中,通过fMRI(功能磁共振成像)记录他们大脑的活动,并将大脑活动与标签相匹配,然后创建了大脑的交互式WebGL图谱,你可以在线尝试一下(https://gallantlab.org/huth2016/)。最后发现,语义相近的事物往往会聚集在新皮层表面的同一部位。
但我们已经知道,新皮质是均匀的,细胞中没有特异性。定义神经元的功能时,需要定义它连接的对象以及方式。每个神经元(只是一个可以处理电流的特殊细胞)都有分支的突起,称为神经突,而神经突又分长神经突和短神经突,分别叫做轴突和树突。典型的皮质神经元平均有7000多个神经突,它们与其他神经元形成连接。5层*1千万亿个神经元*7K个连接,你自己慢慢算吧。
这些连接会不断生长、不断改变连接方式,在你的整个生命过程中会不断更新,每当你学到东西时就会形成新的连接,并加强两样东西之间的连接。这些连接被称为连接组,而整个神经科学领域都被称为连接组学。扫描真实的人类大脑,映射所有的连接,并创建一个计算机模型,这是一项非常艰巨的任务,但这正是连接组学努力的方向。一些研究使用来自人脑连接组计划(Human Connectome Project,是美国国立卫生院NIH 2009开始资助的一个5年项目)的数据,并通过分析连接组的图形的某些连接来预测流体智力(或智商IQ)。
请仔细想一想。
你的大脑中有一个物理知识图代表现实的信念。通过物理,准确来说现实世界中的实际物理连接,从分子水平考虑。这就是为什么从头开始学习东西时会很容易,而重新学习时就没有那么容易了。你不能只删除已经长出来的连接。你必须发展一个新的,比以前更强大的一个连接。
需要注意的是,我并没有故意简化上述内容,这些知识本身就很简单。我希望碰巧阅读这篇文章的神经科学家不要认为我故意过度简化,而对我发火,然而,我相信我交待了大脑的本质工作,而我将在这个框架基础之上构建可视化编程。
什么是编程?
我大约从6岁或7岁的时候开始学习编程,编程的本质看起来很简单:我发出指令到计算机。但显然现在已经不是这样了。大多数现代软件甚至不直接与硬件通信。
当人们开始学习一种新的编程语言时,他们经常会问:“我应该做哪些练习吗?”也就是说他们需要通过编程语言解决某个问题。如果没有需要解决的问题,则不需要编程。
这里的所说的“问题”不是指“发生了不好的事情”,而是来自现实世界的任何事物。
编写代码的本质就在于深入问题域,理解其本质,构建其心理模型,并通过编程语言表现其内在本质。
代码与地图
从某种角度来说,代码是现实的二级地图。问题的心理模型是一级,而表现心理模型的地图是二级。
在这里我使用“地图”一词,是因为它表现出了地图的重要属性:实际事物的微缩版。地图不是领土,每个地图都不完美。无限完美的地图不再是地图,那就是黑客帝国里的母体了。
然而,将代码作为地图的难点在于它应该是可逆的。通过阅读代码,你应该能够还原与解决方案完全相同的心理模型。我说的是真的,阅读一个月前写的代码,应该能在你头脑中触发同样的神经元组,而这些神经元组正是当时你写代码的时候形成的!
社交
更为棘手的是,这个过程对于其他大脑也应该是可逆的。这是一个超乎想象的艰巨任务,因为你不能只依靠你的心理模型,而且还要考虑其他大脑的心理模型。你必须建立一个其他程序员的心理模型的心理模型,并通过它验证你的地图,然后决定某些事情是很明显还是会收到广泛的评论。这就是编程的社交方面发挥作用的地方。
编程是一种社交活动。—— Robert C. Martin
编程是纯粹的映射过程,而编程语言是主要的映射工具。优秀的代码结构与问题域的结构非常类似,你是不是也看到了康威定律(Conway law)的影子?
有没有觉得,每当你看到任何新的、没有见过的需求,就会意识到这个需求能很好地适应代码设计,只需要一点微小的改动?虽然有点夸张,但我非常享受这种时刻。
优秀的代码永远是好的地图,是问题域上的心理地图的二级地图。
制图师和收藏者
但并非每个人都是优秀的制图师。
20年前《Programmers‘ Stone》这篇论文(https://www.datapacrat.com/Opinion/Reciprocality/r0/index.html)中,作者解释了为什么一些开发者要比其他开发者优秀好几个数量级,同时引入了“制图”和“收藏”这两个概念。它们并不是科学的概念,但我认为作者说得很正确,当时阅读这篇论文的确给了我很大影响。
“制图师”的头脑中有整个世界的心理地图,它是某种对象模型,各个对象之间有着丰富的连接和关系。他们会不断修葺改进心理地图,任何新的信息都会加到这个地图上正确的位置上。
这个地图造就了结构化的知识,他们能够真正理解世界,并看到事物的原因和影响。
相反,“收藏者”只是把知识收集到大脑中,而不会建立任何联系。他们只是在机械地记忆并学习正确的反映。对于他们而言,理解世界等于记住尽可能多的知识。
我并不会经常提到这篇论文,因为它把人分成了泾渭分明的两种,其中收藏者是错的,而作者认为世界上绝大部分人都是收藏者。成为制图师不会让你变得更好,收藏者也不一定会让你变糟。但最重要的是,我们既是制图师也是收藏者,只不过向这边或那边倾斜一点点而已。但是这个概念依然概括了我对于人们思想的实际观察和经验,所以很值得一提。
怎样解密代码地图
说起对于思想的观察,许多人可能认为自我反省很困难,但我一直都很享受观察自己思想的过程,因此分析自己编写或阅读代码时的思维活动对我来说并不陌生。
从某个角度上,我意识到我解决过的每个问题都是与时空相关的。每个问题都有时间和空间两部分。空间部分反映了事物之间的关系,而时间部分反映了在不同的时间上它们如何交互。
第1步:空间关系
每次我打开不熟悉的代码,第一步就是在头脑中建立一个空间地图,来反映代码表达的含义,并尝试根据该代码应当属于的问题域,将每行代码和文件名连接到我大脑中已有的心理地图上。重要的是,这完全是空间地图——我做的只是找出对象、抽象和它们之间的关系,并用已有的知识来验证这个地图。
比如,我在阅读HTTP库的代码。我有关于HTTP协议(不完美)的心理地图,所以我能够在代码中看到客户端/服务器、请求/响应等。第一次阅读代码时,我会尝试将每一块代码与这些知识连接起来,然后建立起代码的空间地图,再根据已有的心理地图进行验证。
此时,命名非常重要(命名可以说是计算机科学中最难的问题)。为了有效地建立连接,我们需要一些公共的词汇,还有一些广为人知的符号表示法——目前来看,这些都是文本。未来,它们可能是其他形式,比如超级富文本的表情符号语言等任何更方便、更容易使用的东西。但目前显然只有文本。
第2步:时间关系
在头脑中建立起这些连接和关系后,我会扩展到时间维度上,阅读实际函数的代码,尝试找出这些对象在时间上的行为。显然,函数体的文本与其他的代码是不一样的,因此在寻找时间信息时,行的顺序非常重要。第一行肯定会在第二行之前执行,这样我们就能根据函数代码建立起时间行为模式。这就是为什么goto等会引发争论,因为它们会打破时间流。Go语言有非常好用的defer,虽然在时间流方面有点取巧,但确实它起到了好的作用。但基本而言,函数体的规律是正确的,我们会根据它建立心理地图中的时间部分。
视觉还是文本?
无论是视觉表现还是文本表现,都会在这里遗漏一些重点。不论哪种表现形式都在试图用相似的方式表达时间和空间两者,这给认知带来了很大的负担,而且没有任何作用。
我相信这就是为何可视化编程语言不被关注的主要原因。空间部分用可视化的形式表现出来当然非常合理,毕竟这是表现空间的最佳形式,但时间部分还是用文本表示更合适——虽然不是最好的,但文本依然无可匹敌。
甚至,可视化编程语言都没能驾驭视觉表现形式的力量,经常只关注纯粹的符号方面,认为只需要使用图标替换文本就可以带来很大效果。但任何有数据可视化经验的人都知道,无意中将语义上无关的东西画在一起或使用无意义的颜色,很容易就会将可视化搞得乱七八糟。
我见过的大多数可视化编程语言在视觉方面都非常差劲。这方面我不应该指责任何人,毕竟这是个非常艰巨的任务。例如,下面这张维恩图解毫无意义,但它的“外观”非常漂亮,所以看起来非常吸引人。
Go语言
终于可以讨论Go语言了。
Go语言就是可以正确描绘这种地图的语言。
它接受了地图的不完美,并尽可能简化地图。在Go语言中,你无法用两种不同的方法表示同一件事,因此画图是个非常直接的任务——任何需要表达的概念在代码中都有精确的表达方式。可以想见,相反的过程,即阅读地图也非常舒服。在Go语言中,你基本上不需要猜测某段代码是什么意思,也不用猜测作者想做什么。
完美的语言
我不得不提到Gottfried Liebniz这位德国数学家、哲学家,他发明的数学符号我们至今都在使用。
他的一生一直在致力于一件事:寻找完美的语言。完美的意思是:
- 我能精确地表达我的意思。
- 你能理解精确的含义。
- 我可以确定你正确、完整地理解了含义。
他并没有成功,但他发明了二进制系统。我认为第三条规则其实是不可能实现的,但至少,Go语言在第一条和第二条规则方面做出了很大的努力。
我认为,任何编程语言的设计者都应该致力于同样的目标。如果阅读这篇文章的你恰巧在设计任何编程语言,请一定在设计语言时谨记这几条规则。语言是制图的工具,不是用来表达艺术的。
空间制图和时间制图
Go语言甚至考虑到了空间模式和时间模式的区别。Go语言使用具体的类型表达空间,而使用函数或方法表达时间。此外,我们还可以表达一般化的行为,我们稍后再讨论这一点。而那些使用类的语言中,类表示一切,也可以什么都不表示——它可以是数据、行为,也可以兼顾两者,也可以两者都不是,但绝大多数情况下它只是无意义的复杂度而已。Go清晰地区分了两者,代码变得非常容易理解!
我在Twitter上看到有人这样说:“感觉Go中好像只有类型和函数。”不知道这句话是正还是反,但至少,这里只有时间和空间,与整个宇宙是一样的。Go语言并没有对宇宙做出任何限制。
CSP
如果上述还不足以说明问题的话,那么我们来看看Go语言最优异的特性之一——内置的基于CSP理论(Communicating Sequential Processes,通信序列进程)的并发支持。毫无悬念,这个特性非常易于理解。表面看来CSP非常简单,理解它只需要理解两个概念:进程和通道(Go语言中称为“goroutine”(go例程)和“通道”)。这两个概念可以非常容易地与现实世界联系起来,理解起来非常直观!
任何不需要人为干预的东西(即“在后台”的东西)都可以是goroutine。任何与goroutine的交互(通讯)都可以表示成通道。我们可以非常自然地看到并发的模式无处不在:
- 公开演讲——扇出
- 收银员从顾客那里收钱并放到收银机中——扇入
- 发送和接受短信——使用select{}进行的多路复用
- 等等
与其他模型比较,比如async/wait、Future等,你会发现你需要返回一个Future对象,该对象会在未来“解决”——这在现实世界中毫无道理,而且一旦使用过Go后就会发现这种模型是多么不自然,这都是因为它无法很容易地映射到心理模型上。
Go作为制图工具
我在阅读任何Go代码时几乎都可以迅速找到问题的答案——为什么有这段代码?它究竟表示什么?它是否很重要?作者想表达什么?
别误会,当然有很多Go代码写得并不是那么好,但原因主要出在这些代码表示的心理模型上,而不在制图本身。
我全职编写Go代码6年多了,但我依然像刚开始使用时那样热爱该语言。Go语言让编程变得非常有趣。
但是,毕竟占据90%时间的阅读代码文本工作非常令人沮丧……
可视化的想法
终于谈到了正题,我认为:“Go让空间理解过程非常直观,非常适合大脑的思维方式”。也许,它的直观性能让我把理解的过程交给计算机负责?毕竟,计算机很擅长处理大块的文本,而大脑擅长从数据中提取模式,所以也许可以把阅读代码时的沮丧与我钟爱的Go的简单性和强大的可视化结合起来?
所以,我开始思考心理上的空间“伪可视化”表现形式的基本概念。一些基本概念如下:
包
Go语言中的包是抽象的中心逻辑单元。包通常表示大块的抽象,不同包之间有很大的差异。在Go语言中,目录的含义与大多数主流编程语言都不一样。其他语言把目录作为命名空间使用,基本上是处理文件系统的一种方式,而跟问题域毫无关系。在Go语言中,每个目录就是一个包——即一个逻辑单元。我非常喜欢这种形式,因为只需观察目录结构就能很容易地建立高层的空间连接。
带有子包(从Go 1.11开始叫做“Go模块”)的包是一个逻辑单元,我将它看成图中的一组节点,这些节点有通向子包的连接。子包并不意味着它必须被包使用,这一点很容易误解(想像一下一个过时的包,在重构完成后可能依然会在版本控制系统中存活数年,因为许多人会认为它依然有用),而我在阅读代码时必须解决这个差异。
具体类型
具体类型(concrete types)——绝大部分是结构——是Go语言中最重要的工具,用来表示一切事物,不论是真实还是虚无的事物。例如,“颜色”、“行星”、“情绪”、“时间”,任何你能想到的、可以单独抽象的事物都可以在代码中表示成具体类型。就这么简单。
于是我就想,一些人会问“为什么我不能给字符串等基本类型添加方法?”,这种问题完全没有意义。如果不得不修改字符串的行为,那么它就不再是字符串了,而实现这一点的方式就不应该是给字符串添加新方法,而应该是创建新的类型(如type UUID string),该类型应当满足你的需求,并表示UUID,而不是一般的字符串。
所以,我将类型也看作节点,连接到它们所属的包。类型可以拥有其他类型的字段,这些字段的类型来自同一个包,也可以看作互相连接的节点。我并不关心它是类型,还是指向类型的指针,或者是嵌入(Embedding)——我只关心关系。嵌入是一个特别有意思的概念,它特别适合类型结构和方法是隐藏的情况(比如,类型结构和方法属于其他包),因此human.Speak()比human.mouth.Speak()更合理,但如果在同一个包中使用嵌入,我依然会在心理地图中认为Speak()是Mouth类型的方法,而不是Human类型,因为这只是语法问题,而不是认知问题。
函数和方法
简而言之,方法就是第一个参数为类型的函数,因此两者实际上非常相似。但从逻辑角度来看,方法永远属于类型(此处类型即为接受者),我将其看成连接到类型的节点。如果一个方法调用了另一个方法,那么第二个方法很可能更接近其调用者(特别是没有其他调用者的情况,或者它是私有方法的情况)。
函数也一样——如果函数互相调用,则互相连接,而且我认为函数的大小很重要(函数行数越多,节点就越大)。这一点我不想深入,理论上你可以在函数内定义类型(包括匿名类型)和闭包,但我并不考虑这一点,而且我也不想在浏览代码的时候时刻考虑这种情况。
有一点很有意思,似乎它自然地形成了从上至下的顺序——包永远在顶端,然后是类型和一阶函数,然后是方法和所有嵌套或连接的东西。也许是无处不在的重力概念影响了我们的直觉,因此即使在处理心理地图这种完全抽象的事物时也会如此。
接口
接口独立于其他代码块,它们抽象出行为,然而并不指明其实现方式。接口概括了同一个问题中看上去毫无关联的事物之间的共通功能。
由于接口不指定实现方式,因此它成了代码中表达现实的强大工具!同一个类型可以在不同语境中大量使用,而无需事先知晓类型的实现。我无法想象,如果继续使用那些类型必须定义它实现了哪些接口的语言会怎样,而且在使用了Go之后,很难相信竟然还有许多语言这样做。
Go语言中,在你拥有至少两个类型以进行抽象之前,是不需要创建接口的,这样做毫无意义。在我看来,接口像是类型节点周围的云,它让两个节点更接近,形成一个组。接口使空间地图更灵活,因为每个类型都可以实现代码中用到的许多接口,而我最感兴趣的就是表示接口的含义,以及其他类型如何实现它。
名称
前面说过,名称在建立心理地图时非常重要,而且名称传达了许多含义。习惯上,特殊的名字如NewFoo()通常表示构造函数,所以我认为它们离类型节点更近。语义上有关系的一对名称如Start/Stop、Push/Pop的位置更接近,拥有同样前缀或后缀如LightTheme/DarkTheme、ServerTCP/ServerUDP更接近。
以上就是“规则”的几个例子。我并没有概括完整,但你应该大致了解我的想法了。
工具概况
这就是我的努力方向——读取Go代码并解析,然后构建主要代码概念的空间关系三维地图,尽可能地接近大脑的思维方式。如果遇到无法进一步将代码分解成可视化节点的情况,就直接显示函数文本,这样就能阅读文本并构建地图的实践部分。至少,三维可视化能让我立即看到代码的关系、结构,以及它表示的实体和抽象等。不需要再红着眼翻看半个小时的代码,只需要让计算机做这些脏活,从而简化绘制心理地图、建立连接等工作。
可以在浏览器中运行,访问本地服务器来读取文件系统。
目前我只实现了预计功能的20%。在实现过程中我不断深入我的思想,发现了许多不明显的东西,我把它们都记在了待办列表中。例如,我意识到我需要用不同的缩放级别来显示不同的连接。在查看包的概览时,我只关心一种连接,但在阅读函数代码时,我并不关心其他部分,而更关心从该函数中出来的连接的具体情况。
下面是一些早期预览(为了节约流量呢,截图都很小,帧数也很低):
1.首页和container/list stdlib包:
2.浏览更大的Go包——github.com/divan/expvarmon:
3.可视化自己:
4.可视化包的依赖关系。使用文本添加也起来只需要一行import,不管是像leftpad那种很小的包,还是200K行代码的庞然大物。你可以通过可视化,立即“感受到”那行import的分量,从而本能地谨慎思考。
布局算法
我这里采用了受力布局方式,将节点布置在三维空间中,然后反复计算之间的受力关系,直到系统达到稳定的最小能量状态。使用物理方式非常重要,因为这样能符合直觉上的布局。
但值得一提的是,绝大部分受力布局算法都努力产生更漂亮的图形。但我的目的截然相反——我希望只给优秀的代码产生漂亮的布局,而对于糟糕的代码产生难看的布局。
理由很简单:首先,我头脑中的思想就是这样,我只是遵循我的想法——糟糕的代码在思想中就很混乱(这就是为什么过于复杂的代码很难理解);其次,我有意将这种情况可视化。这样就不需要学习类似于“短期记忆无法记住7个以上的对象,所以抽象要尽可能小”之类的规则,只需让可视化给出直观的感受。
说明
下面是一些简单的说明。
- 首先,这不是调用关系图。该图虽然表示了一些函数之间的关系,但与调用关系图有很大区别,它并不是你在使用pprof等工具时看到的结果。
- 其他语言也有代码结构可视化,我也见过一些非常优秀的项目,特别是Java语言。但与我的项目有两个非常重要的区别: 它们的主要目的是帮助在庞大的类结构迷宫和复杂度之中浏览代码。 将类可视化为节点并不能帮助建立空间关系地图,原因如前所述——类可以表示一切,也可以什么都不表示。
- 这种代码可视化的方法只有在Go的简单设计下才有意义。我可以想象,为其他语言构建类似的可视化,但肯定无法像Go这样表示出清晰的心理模型。少即是多。
- 工具也是用Go写的,Web界面方面使用了GopherJS和Vecty框架。它使用了Three.js的GopherJS包,利用WebGL渲染三维场景。
- 我不知道是否应该去掉文件。将代码分解成代码块并命名,有助于逻辑分组,但很容易丢失文件名和分组之间的关系。有时候,文件系统层给心理地图和代码之间的连接过程带来的干扰远大于好处。目前,我并不关心代码在文件系统上的布局方式。
- 在我的思想中,节点并没有颜色,因为我就是这么想的——我并不是感性的人。对于我来说,节点只不过是心理地图构建在同样的神经逻辑“硬件”(皮层)上,两者都处理许多空间信息,所以“感觉上”是相似的。这样做很好,因为正确使用的颜色是非常强大的工具——有了颜色我们可以很容易区分出代码的不同方面: 节点的子节点——给嵌套的类型/方法/函数树着色可以方便浏览,辅助理解 公开/私有API——如果空间中的位置不明确,那么可以用颜色来辅助 给不同类型的节点着色——类型/函数/接口等,这是我目前的做法,用来区分对象 pprof/cover的输出
- 我特别希望能可视化地表示拉取请求中的差异比较,不过以后再做吧。
目前的进展
该项目完全是我在空闲时间进行的实验,现在依然处于早期的alpha阶段,它甚至还没有名字(我内部叫它“codevis”,意为代码可视化)。实验意味着它完全可能因为各种原因而失败,但重要的是我在实验过程中学到的东西。我用了许多主观假设,许多想法我甚至从未跟别人提起过,所以可能别人会有完全不同的观点。不过即使这些想法只适合我自己也没关系。
我现在的目标是将其转换成自我供给的状态,用该工具来分析自己。我会实验两种做法——直接在浏览器中编辑代码,以及通过WebSocket与另一个窗口中的编辑器通信。
我遇到的主要难题之一就是3D编程,这方面我完全没有经验。现在是2019年,但依然很难找到一个有效地在三维空间中绘制文本或几千行代码,而不会大量占用CPU的方法,也没有什么在保持代码可读的前提下进行优化的方法。而且,我在物理上花费了许多时间,如调整节点之间的受力参数等,这简直就是黑科技。让我惊讶的是,我们的大脑非常不善于处理与真实世界不同的物理情况——一个微小的变动就会搞砸一切,再加上要同时考虑大型图和小型图,所以情况变得愈发复杂。
即使在现在的早期阶段,我对结果也非常满意。随便找到一个项目用工具打开就能拿立即看到其结构,还能用wsad键浏览代码,就像玩Quake一样,这非常鼓舞人心。至少,我觉得我走对了方向。
该工具还不能开源,但开源只是时间问题——只要我确信它能稳定处理基本情况,就一定会将其开源。
未来
我还考虑将来完全去掉窗口和屏幕,在AR环境中使用3D代码表示。最近Hololens 2团队的演示给了我希望,我认为干掉屏幕的时机并不遥远了(也就是说,只要有足够高的分辨率,你甚至可以直接在视网膜上显示任何东西,所以也就不需要屏幕了),而NReal最近公布的价格也表明,该市场一直在增长,AR设备的价格也在不断降低。
我很希望能够真正“走进”我感兴趣的代码,或者用手将其拉近。编程将你我绑在椅子上,这一点太不幸了。所以,可视化编程也许能做出些改变。
总结
在上世纪六十年代末,在Garmisch召开的第一届NATO软件工程师大会上第一次提出了“软件危机”这个词。计算能力的迅速增长,导致当时编写软件的方法无法满足需求,但人们还没有发明出新的方法。
我认为,现在我们有了另一次危机,即代码的规模远远超过了处理代码的工具的能力。但是,需要解决的问题的复杂度并没有太多增长——做一个带侧边栏和三个按钮的界面,远远无法与阿波罗任务的代码相比,但语言和工具带阿里的复杂度已经增长了几个数量级,而且还在不断增长。
我认为这个火把问题能提供很多思路。如果人们只能看到编辑器照亮的那一小部分代码,那么很容易过分看重自己的代码对于整体代码结果的影响。如果能立即看到改变,并利用大脑在空间方面的力量来浏览代码,不仅能够更自然地编写更简单、结构更好的代码,还有可能改变理解编程的习惯,从“收藏者”的“使用模式”变成“制图师”的“绘制地图”。
我们塑造工具,工具塑造我们。
【END】