这是在GopherCon Europe 2019(加那利群岛版)上发表的演讲的博客版,分享了可视化编程语言为什么失败的一些想法,并首次展示了 Go 在进行代码可视化方面的实践。
直接深入到项目中之前,首先需要解释一下实现背后的思路。最初的工作方式就像写文本一样写代码,这种方式带来了几乎是生死攸关的挫败和沮丧。
IDE vs 火炬
知道代码编辑器和火炬之间有什么共同之处吗?
在过去,我们用自上而下、从左到右的方式阅读源代码文本,这和阅读自然语言的方式是一样的。由于它是个横向的书写系统,会逐渐向下增长,最后形成又长又直的文本段,存储在文件中。
为了阅读这种代码文本并浏览它的结构,我们会使用代码编辑器 — 集成了语法突出显示和代码分析功能的专用文本编辑器。我使用 Vim 享受那种基于控制台的界面的舒适外观和感觉,但许多人喜欢更高级的代码编辑器,以鼠标为重点提供丰富的交互。任何情况下,我们都会滚动文本并在文件之间进行很多跳转。
我们习惯了这种日常生活方式,以至于很难拉远距离去观察它。但如果这样做,会发现事实上就像坐在一面文本墙的前面,使用这个叫做“编辑器”的滑动窗口在墙上移动,并将其中的一小部分发送到你的屏幕上,这样就能看得更近。
因为永远看不到代码的其余部分,意味着除了自己屏幕上的那部分,整面墙都在黑暗中。
所以你是独自坐在黑暗中,被神秘的文字包围着,并使用代码编辑器把一束光照射到这面墙上来阅读其中的一小部分。现在,想象一下这是一个火炬而不是发光的屏幕,会发现编辑器只是一个现代版的火炬。
阅读一个新的代码库时,就像在黑暗洞穴中的一个古代人,使用火炬阅读墙上的文字或绘画。
必须得说,这是一种非常古老的方法。当然,用在古代与世界如何互动做这种类比并没有什么错。但我由此发现很多问题:我们花费 90%的时间阅读代码,只有 10%用来编写代码,而这 90%恰好是我最不喜欢的部分。
我们花费 90%的时间阅读代码,只有 10%用来编写代码,而这 90%恰好是我最不喜欢的部分。
阅读代码需要非常好的注意力,近乎完美的专注力,超过寻常的记忆能力,这是非常愚蠢而乏味的。直觉告诉我,文本的形式是导致它的主要原因。在将要阅读新的代码库时,我的脸就像在洞穴中那位隐士的脸。
更重要的是,使用眼动追踪系统进行代码理解的研究证实,我们对源代码的文本阅读和标准的方式完全不同。我们不是逐行地连续阅读它,而是表现出一种清晰的扫描模式,例如会固定注视于认知上有挑战性的代码部分或感兴趣的区域,并来回跳跃,完全跳过某些行或整个块。
如果文本不是表示代码的最佳形式,那该怎么办?
可视化编程
显然,我不是第一个反思代码的这种文本表示方式缺点的人。可视化编程语言(VPLs)是一个完整的领域,在过去的 60 年里,已经出现了数百种的可视化语言。
在互联网上你可以找到一些精心准备的材料和文献,包括 VPL 的概述和历史,这里只链接两个我最喜欢的:巨大的策划页面,带有一堆VPL的链接和快照,以及 Emily Nakashima 关于可视化编程语言历史的精彩演讲。
下面是一个非常简短的概述,分为“两个半”不同的类型:
1 基于块(Block-based)的 VPL
基于块的可视化语言是这样的情景:不以文本形式输入代码,而是将预定义的代码块拖放到脚本区域中。使用者完全不需要打字技巧,它拥有丰富多彩的 UIs,几乎不可能出现语法错误。
您可能听说过Scratch、MIT App Inventor、Google Blockly等语言,它们主要专注于为儿童、初学者教授编程的基础知识,并在世界各地的学校和活动(例如Hour of Code)中大量使用。多项研究证实它们对学生的计算机科学课程起到了积极的影响。此外,许多混合项目将简单的代码样本从主流语言的文本式表示转换为块式表示,反之亦然,这似乎有助于把这些基于块的课程转换为“真正的编程”。
但这似乎是它们能做到的最大极限。此外,很多人说这根本不是可视化编程,只是用鼠标输入替换键盘输入,我个人倒是同意这种说法。
2 基于流(Flow-based)的 VPL
另一大类可视化编程语言实际上是所谓的基于流的语言的一个子类,也称为基于节点的编程语言。它们使用流程图来表示状态、逻辑或数据的变化。
有时,在这些语言背后的推理是,程序的逻辑应该由熟悉问题领域的人(如医生或工程师)来实施,而将逻辑实际转换为代码则是其他人或程序的工作。
有很多人尝试制作通用的基于流的 VPL,如Raptor、Flowgorithm、Visual Logic或DRAKON,这种方法在 3D 编程、音乐合成、信号处理或物联网/嵌入式电路设计工具等利基领域得到了最大的普及。有些我们可能很熟悉,甚至自己每天都在使用,像Unreal Blueprints、Max/MSP、National Instruments 的Labview或Autodesk Dynamo这样的工具。许多人在实践中积极大量地使用这些工具,但它们仍然是相当狭窄的领域和高度专业化的解决方案。我们不太可能使用其中一个来编写光线跟踪引擎、内核驱动程序或 GraphQL 服务器。
2.5 其它奇特的 VPL
还有一些更为奇特并难以分类的东西,像Prograph或Cables。80 年代设计的 Prograph 是第一个真正的可视化面向对象语言,它引入了一些有趣的想法,不过我尝试使用它的modern reincarnation却失败了。
此外,70 年代的Pygmalion特别有趣,因为它是一个被遗忘的编程技术的好例子,被称为演示编程。基本上它不是通过编码,而是通过向机器展示来编写算法,这真的很酷,在某种程度上属于还未被充分探索过的领域。
没有主流的 VPL?
但目前还没有真正主流的通用可视化编程语言。透过任何一个角度来看,现代编程语言的环境 100%都是由文本式编程语言主导,没有例外。
为什么会这样?一句古老的格言告诉我们,“ 一幅画胜过千言万语 ”。凭直觉我们知道,文字显然不是代表一切的最佳方式。所有那些致力于可视化编程语言的聪明人都清楚地发现了某些东西,但似乎我们缺少一点使可视化编程成为现实必需品的东西。
为什么 VPL 失败了?
这个问题很久以前激起了我的好奇心,简而言之,我的回答就是我们不知道我们在做什么。对于编程是什么没有公认的科学解释,我们只是粗暴、强制地用不同方法来设计新的语言、视觉和文本,并希望它们能更好地与我们的大脑协同工作。
更糟糕的是,我越来越感觉到 PLT(编程语言理论)社区实际上并不关心它。至少在将一个新的奇特的特性引入编程语言时,我从未见过心理学家或神经科学家进行过适当的研究。不过,我很希望是自己犯了一个大错。
所以,尽管我们同意需要代码来表示抽象,但是它究竟意味着什么?当阅读、编写甚至思考代码时,脑海里会发生什么?通过哪个框架,可以验证和测量可视化语言的表现是更好还是更差?
我很少看到有人问这些问题,更不用说如何去满足这些答案了。
如果我们试图自己回答这些问题,不可避免地会以哲学或神经科学领域,或两者兼而有之而告终。由于一直对大脑的工作方式很感兴趣,阅读有关大脑相关主题的书籍和论文就成了我的一种爱好,我认为这有助于从新的角度看待这些问题。虽然神经科学家几乎没有触及大脑如何工作的表面,而我只是在摸索他们所触及的部分的表面,但我想分享两个重要而不太明显的观点,相信它们是这个主题的基础。至少在本文中它们很重要,会进一步地跟着我来建立我的观点。
人脑
人类的大脑由大约900-1000亿个神经元组成,形成超过1000万亿个连接,这使得它成为太阳系中最复杂的物体,所以我们无法轻易地解释它。
我们的意识和思想是大脑中称为新皮质的部分,大多数人都知道它是一种带皱纹的胡桃木形状的东西,但会惊讶地发现它实际上是一个厚度为 5-6 毫米的比萨饼大小的扁平组织。它这样折叠,是要适应颅骨后表面面积的快速增长(颅骨的生长是昂贵的进化,因为这意味着还需要盆骨骨骼的生长以适应出生的过程)。
这种组织的结构非常均匀,整个表面都有相同的 5 层神经元,它们组织成皮质柱。一些理论认为,这个皮质柱是新皮质层的单一“计算单元”,它执行一项主要的任务:接受大量的输入,从中提取模式,并学习如何预测。
意识、思想、高层次思维、自我意识,是这些模式提取组织的大规模和复杂性的“简单”副产品。我们谈论的是可观测的宇宙中许多星系的规模,即使有帮助的话,也只是一个大致的范围。
空间模式 vs 时间模式
首先,想让大家注意的是模式本身的概念。宇宙中有两种主要的模式:空间上的和时间上的。空间上的仅仅意味着与空间相关,时间上的则是与时间相关。空间和时间,此时此地,这种差异源于宇宙的本质,并且进化的大脑学会了相应地处理它们。
视觉,主要处理空间信息,视觉皮层在从视网膜提供的感觉输入中提取大量常见的模式方面做得非常出色。为了捕获空间关系,必须“一次”获取所有的输入,一个一个像素地看无法看到完整的图像,必须同时看到以正确的方式排列在一个空间中的所有像素。
听力,主要与时间模式有关,一个人不能通过同时听所有音符来听旋律,它们必须在时间上实时地进行传播。
当然,所有的感官输入都与空间和时间模式有关,而视觉和听觉是展示它们有着根本区别的核心理念的很好的例子。大脑在最低层处理它们的方式截然不同,但有趣的是我们可以使用空间来表示时间信息,反之亦然,实际上,我们经常这样做。音乐符号就是一个很好的例子,我们用空间表示法对时间分量进行编码。这里需要注意的是,为了理解这些符号,必须训练你的大脑把它再解码回到时间上,这是认知上的一项艰巨任务,不是每个人都能在头脑中阅读音乐符号然后演奏一首歌。
记住这种空间与时间上的差异,接下来就可以利用它了。
知识图
编程方面要知道的第二个最重要的事情是,所有知识都以某种方式存储在大脑的新皮质中。有无数的理论想尝试解释它是如何存储的,我认为还远远没有回答这些问题。但是我们确实知道,对于所知的每一个概念、每一个知识片段、每一个抽象概念或意识到的每一个物体,都有一大群神经元在看到、听到、思考甚至做梦时会兴奋起来。
例如,加州大学伯克利分校的 Gallant Lab 所进行的一项出色工作,使用 FMRI(功能性磁共振成像)记录了参与者进行的大脑活动,接收标记过的自动语音流,并将大脑活动映射到标签上,创建了大脑的交互式WebGL图谱,大家可以在线尝试。我们会发现,语义上接近的事物往往聚集在新皮质表面的同一部分。
我们已经知道,新皮质是均匀的,细胞中没有特异性。神经元功能作用的定义是它与谁以及如何连接。每一个神经元,它只是一个能处理电流的特殊细胞,拥有称为神经突的分支突起,有长有短,相应地称为轴突和树突。典型的皮质神经元平均有多达 7000 个神经突,它们与其他神经元形成连接。5 层共数十亿个神经元,平均每个有 7000 个连接,可以在脑海中想象一下。
这些连接在一生中不知疲倦地增长、重组和更新,每当学到什么就会形成新的连接,并加强两两之间的连接。这些连接被称为神经连接体,神经科学有一个领域致力于此,称为连接经济学(Connectonomics)。扫描真实的人脑并绘制所有连接的图,创建它们的计算机模型,这是一项非常困难的任务,但这正是连接经济学领域正在尝试做的事情。有些研究使用来自人类连接体的项目中的数据,通过分析图中的网络就可以预测人类的流动智力(fluid intelligence)或智商,这也是神经连接体的本质所在。
仔细思考一下,头脑中有一个物理的知识图,它代表了人类对现实的看法。这里所说的物理,确切地说,是指现实世界中的实际物理连接,就像在分子层面上一样。这就是为什么从零开始学习比重新学习更容易的原因。不能只删除已经增长的连接,必须培养一个新的、比以前更强大的。
需要注意的是,我不想让这看起来简单,虽然一切都很简单。希望碰巧阅读这篇文章的神经科学家不要因为故意过度简化而生气。相信这抓住了我们大脑所做的事情的本质,下面将在这个框架之上进行构建。
什么是编程?
当我在 6、7 岁刚开始学习如何编码时,编程的本质看起来很简单:就是给计算机下指令。但很明显这不再是真的,大多数现代软件甚至不直接与硬件通信。
当人们开始学习一种新的编程语言时,他们经常会问“有什么建议来进行实践吗?”,这意味着“需要有一个问题来交给编程语言去解决”。如果没有要解决的问题,就不需要编写代码。
“问题”并不是指“发生了什么不好的事情”或“DDoS机会”,而是来自现实世界的任何事情 ,即问题领域。
代码编写的本质是将问题域内化(internalize),理解其本质,构建其思维模型,并用编程语言捕获洞察力。
代码编写的本质是内化问题域,理解其本质,用编程语言捕获洞察力。
代码即图
在某种程度上,代码是现实的二度图,问题域的思维图是第一级的,代码作为思维图的图是第二级的。
我喜欢在这里使用“图”这个词,因为它抓住了图的重要属性 — 减少了所代表的实际事物。图不是某个版图,每一个图的设计都不会完善,无限完美的图就不再是图,它是一个 Matrix(译者注:意指电影《黑客帝国》中的虚拟程序)。
然而,代码作为图的挑战在于它是可逆的。应该通过阅读代码,能够完全恢复解决方案的同一个思维模型。事实上,意思是通过阅读一个月前写的代码,同样的神经元组应该会在头脑中变得兴奋起来,当你写下它们的时候,就被激活了!
社会方面
为了产生指数级的效果,这个过程对于其他大脑也应该是可逆的。这是比想象中更具挑战性的一个任务,因为现在不能只依靠个人的思维模型,而且还要考虑其他大脑的思维模型。必须建立一个其他程序员的思维模型的思维模型,并通过它验证你自己的图,并决定一些东西是否很明显或应该被广泛评论。这就是编程的社会性方面发挥作用的地方。
编程是一种社会活动。
(Robert C. Martin)
于是,编程就是一个纯粹的制图过程,编程语言是主要的制图工具。好代码的构建方式几乎总是类似于问题域的结构(建议您可以去了解一下康威定律)。
当一个新的、完全出乎意料的需求出现在程序中的时候,它能很好地适应代码原来的设计,只需要一个小小的改变吗?这有点夸张,但我以此为奋斗目标。
好的代码始终是问题域思维图的好的二度图。
制图者 vs 包装者 (Mappers vs Packers)
但并非所有人都是优秀的制图者。
20 年前撰写的文章“程序员之石”中,作者解释了为什么有些开发人员比其他开发人员更有用,并引入了“制图者”和“包装者”这样两种思维模式。它不是一个科学的概念或任何东西,我认为作者正在研究一些东西,阅读这些模式对我的影响很大。
制图者拥有某个世界的内部思维图,这个世界包含丰富多样的连接和关联的对象模型。他们不断地填充和开发这个世界的思维图,试图把每一个新的信息这个内部图的正确位置上。
它为知识提供了一种结构,使人们能够真正理解和了解因果关系。
另一方面,包装者在头脑中包装知识,不用正确地建立连接。他们对正确的响应进行记忆和学习,他们要做的则是如何最大化记忆中的知识包。
我并不经常提到这篇文章,因为它有点划清了人们之间的界限 — 包装者被放在错误的一边,而作者又声称世界上到处都是包装者。当一个制图者并不能成为一个好的人,而当一个包装者也不会成为一个坏的人。但是,最重要的是,我们既是制图者又是包装者,只是对前者或后者都有偏见。但是,这个概念与我所有的实践经验和对人们如何思考的观察产生了共鸣,所以我认为在这里值得一提。
如何解码代码图
对许多人来说,谈到对思考的观察,要求自我反思是很艰难的。但我总是喜欢观察我头脑中发生的事情,所以分析在编程或阅读代码时发生的事情对我来说并不是一项完全陌生的任务。
某种程度上,我意识到遇到过的每一个问题都是带有时空性质的。它既有空间成分,也有时间成分。空间捕捉事物之间的关系,时间捕捉事物如何随着时间的推移而互动。
第 1 步:空间关系
当打开不熟悉的代码库时,要经历的第一步是建立一个临时的内部图来展示这个代码代表了什么,并尝试将代码行或文件名连接到现有的与这个代码相关的问题域的思维图上。这里需要注意的是,这是一个纯粹的空间关系图 — 尝试捕捉对象、抽象及其关系,并通过现有的知识来验证这个图。
想象一下,我正在读一个 HTTP 库的代码,用一些(不完善的)HTTP 思维图作为协议,并希望在代码中看到诸如客户端/服务器、请求/响应等内容。当我第一次阅读代码时,会尝试将代码片段与知识联系起来,并在代码中构建它所代表的时间图,并通过现有的思维图来验证它。
这就是命名 — 这个计算机科学中最难的问题 — 变得重要的地方。为了有效地建立这些连接,我们需要使用一些共享的词汇表和众所周知的符号表示,目前主要是文本方式。在将来,它可能是其他东西,比如超级丰富的表情符号等一切用起来更传统、更便宜的编程语言。现在,还显然只是口头说说而已。
第 2 步:时间成分
当在脑海中建立了这些连接和关系时,扩展到时间维度然后阅读函数代码,实际上可以看到随时间推移这些对象背后编码的行为。与其他类型的源代码不同,如果函数体文本写得不够明显,行顺序就很重要了,因为它编码了时间信息。第一行会在第二行之前执行,我们在函数的代码中对时间行为模式做了编码。这就是为什么人们争论 goto 操作符的原因,因为它打破了时间流,而在 Go 中,我们有个很酷的关键字 defer,为了更大的好处而欺骗了普遍的时间流规则。但是函数体的一般规则是正确的,它把思维图的时间成分进行了编码。
视觉还是文字?
这正是代码的视觉和文本表示经常忽略的地方,人们尝试用类似的方式表达空间和时间两个成分,但这只是增加了认知负荷,没有任何帮助。
我认为这就是为什么 VPL 仍然不是每个人都在使用和兴奋的主要原因。视觉上对空间成分进行编码绝对是完全合理的,而且这是对其进行编码最自然的方式,但时间成分仍然可以更好地用文本来表达,虽然不完美,但仍然是一项无与伦比的人类发明。
更重要的是,VPL 并没有利用视觉表现的力量,通常只关注纯粹的符号方面,而假设图标与文本之间有很大的区别。但是,任何从事数据可视化工作的人都知道,不经意间把语义上无关的东西画得很接近,或者使用不具有任何意义的颜色,是多么容易把可视化搞得一团糟。
从视觉的角度来看,我见过的大多数 VPL 都很糟糕,这是一项非常困难的任务。例如,Venn 图完全没有意义,但它是“视觉的”并且看起来很酷,既有误导性又有吸引力。
Go
在开始谈论 Go 前,再最后强调一点。
Go 是能正确实现这种图的语言。
它接纳了图的不完美性质,并尝试让绘制的过程尽可能简单。它不允许以两种不同的方式绘制相同的东西,这使得编码成为一项简单的任务,我们都准确地知道程序员是如何用 Go 来表达代码中的概念。这不足为奇,它使反向过程 — 解码 — 也更加舒适。在 Go 中,通常不需要去猜测这段代码的实际含义,以及作者试图在这里抓住的内容。
完美的语言
这里不能不提到Gottfried Liebniz(戈特弗里德·莱布尼茨),德国数学家和哲学家,我们今天都在用他的数学符号。
除此之外,他还痴迷于追求完美的语言,对我们来说这意味着:
我可以准确地说出我的意思;
你可以准确理解我的意思;
我可以肯定你已经正确而完整地理解了我的意思。
但他没有成功,不知何故最终发明了二进制系统。我不认为其中第三条规则是切实可行的,而对我来说,Go 是一种努力优化规则 1 和规则 2 的语言。
我认为任何编程语言设计师都应该沉迷于这种追求。如果您正在阅读这篇文章,并且恰好是未来任何语言的编程语言设计师,请记住用这些设计语言的规则。要设计制图的工具,而不是艺术的自我表达。
空间与时间制图
Go 捕捉到了空间和时间模式之间的差异,在 Go 中我们使用具体的类型来表示空间,而使用函数/方法来表示时间。另外,我们会有归纳行为的接口,很快就可以实现。将 Go 与基于类的语言进行比较,类可以同时表示全部,或不表示任何东西。它可以是数据、行为,两者兼而有之或都不是,但大多数时候它表示了一种偶然的复杂性。Go 明确了这一区别,它使代码更容易理解!
我记得在推特上看到一条评论,有人说:“感觉就像在 Go 中只有类型和函数”。不确定这是不是只是在大声抱怨,但是在宇宙中,虽然“只有”空间和时间,不过这并不会使宇宙受到任何形式的限制。
CSP(通信顺序进程)
如果这还不够的话,最突出的 Go 特性之一 — 基于 CSP(Communicating Sequential Processes)理论的内置并发 — 同样,毫无疑问,它在我们的大脑中发挥得非常好。从表面上看,CSP 有点简单,它提供了两个主要学习的概念:流程和渠道(Go 的“goroutines”和“channels”)。这很容易与物质世界联系起来,并直观地理解!
任何没有参与的行为,“背后”可以是一个流程。任何互动的方式 — 交流 — 都可以表示为一种渠道。很自然,可以在任何地方看到并发模式:
正在做公开演讲:扇出(fan out);
收银员从客户那里收钱并放入抽屉里:扇入(fan in);
正在发送和接收文本消息:通过 select{}进行多路复用;
等等。
可以和 async/await 和 Futures 等模型做个比较:可以返回 Future 类型的对象,并在将来的某个时间点进行“解析”。它在现实世界中没有意义,而且非常难于被用在 Go 的并发性工作,因为不容易把它绘制到我们的思维模型上。
Go 作为制图工具
当随机地阅读 Go 的代码时,我几乎总能快速回答有关任何代码部分的问题 — 它为什么存在?它究竟代表什么?它是必然的还是偶然变得复杂?作者试图用这部分说明什么?
不要误会我的意思,有很多 Go 的代码写得不好,问题主要在于它编码的思维图,而不是编码本身。
在糟糕的 Go 代码中,问题主要在于它编码的思维图,而不是编码本身。
我全职写了 Go 近 6 年,仍然像头几个月一样享受它,它让编程再次成为我的乐趣。
但是,90%令人沮丧的是代码的文本阅读部分。
可视化的想法
这就是把单个点连接起来的地方,我想“是的,Go 能让空间的解码过程对大脑来说非常直接和自然”。也许是因为如此直接,我可以抓住它们,将解码这个任务正则化并转移到计算机上?毕竟,计算机很好,能处理大块的文本,大脑也很擅长从数据中提取模式,所以也许我可以将对代码文本阅读的失望,与对简洁和可视化能力的热爱结合起来?
所以首先开始来抓住代码思维空间的伪视觉表示的一些基本原理,如下:
包
Go 中的包是抽象的中心逻辑单元,它们通常代表大块的抽象,彼此之间有很大差异。Go 与大多数主流的编程语言在目录(Directory)上有所不同,其他的语言倾向于只将目录用于命名空间,基本上是处理文件系统的一种变通,这与问题域的结构没有什么关系。而在 Go 中,每个目录都是一个包(一个逻辑单元),我真的很喜欢这个,因为通过查看目录树可以更简单地构建高层的空间连接。
带有子包的包(从 Go1.11 开始被称为“Go 模块”)是一个逻辑单元,我将其视为一组与子包连接的图形节点。作为一个子包并不意味着实际上会在这个包中使用它,所以很容易做出错误的假设(某个旧的子包在重构中存活下来并在版本控制中保留了很多年,因为每个人都假设它在使用中)。在阅读代码时,必须在头脑中解决这种认识上的差距。
实体类型
实体类型(主要是结构)是 Go 提供的最重要的工具,可以表示任何物理的或短暂的东西。“颜色”、“行星”、“心情”、“时间”,任何可能被认为是独立抽象的概念都可以作为代码中实体类型的候选者。
那些问“为什么我不能将方法添加到字符串之类的基本类型中?”的人都忽略了这一点。如果必须修改字符串的行为,它可能不再是一个字符串。抓住这种差异的方式,不是通过添加新方法,而是通过创建一个新类型(例如 type UUID string),它可以满足所有需要并代表您的 UUID,但不是一个常规的字符串。
我还将类型视为连接到它们所属包的节点。具有来自同一个包的其他类型字段的类型,我也将其视为相互连接的节点。有趣的是,但在这一点上,我不在乎它是一个类型,还是指向一个类型的指针,甚至是嵌入,只是抓住它们之间的关系。嵌入是一个特别奇怪的概念,当类型结构和方法被隐藏起来(例如,在另一个包中)时,它能很好地工作。所以 human.Speak()比 human.mouth.Speak()更有意义,但是如果在同一个软件包中使用嵌入,我仍然在思维图中认为,Speak()是 Mouth 类型的方法,而不是 Human 的方法。因此只有语法上的胜利,而不是认知上的胜利。
函数/方法
简而言之,方法就是首参是一种类型的函数。但从逻辑上讲,该方法始终与类型(接收器)相关联,我将其视为具有类型节点的连接节点。如果一个方法调用另一个方法,则第二个方法可能更接近调用方(尤其是如果没有其他调用方和/或它是私有的)。
对于函数也是一样的,如果它们相互调用它们就会被连接,它们的大小很重要(代码行越多,节点就越大)。从技术上讲,可以在函数中定义类型(包括匿名类型)和闭包,但这并不是我想的那样,而且在浏览代码时,我绝对不会保留这种思维形象。
这很有趣,但自上而下的顺序似乎很重要,包总是在顶部,然后是类型和一阶函数,然后是方法和所有嵌套/连接的东西。这样可能是因为万有引力的概念确实影响了我们的直觉,即使是像思维图这样的抽象事物。
接口
接口从剩余的代码块中凸显出来,对行为进行归纳并隐式地执行。在某种程度上,它们抓到了在这个特定问题的背景下看似无关的事物的共同功能。
隐式使得接口成为在代码中表达现实的强大工具!相同的类型可以在无数不同的场景中使用,而无需事先了解它们。我无法想象返回类型被强制声明要实现哪些接口的语言,并且在体验了 Go 之后,很难相信许多语言仍然这样做。
在 Go 中,如果没有对至少两种类型进行归纳,不会创建接口,因为它是没有意义的。在我的脑海中,我看到接口有点类似于实现它的类型节点周围的云,这使得它们更接近,在某种程度上形成了一个群组。它为内部的空间图提供了灵活性,因为每种类型都可以实现在这个特定代码中使用的许多接口,而我最感兴趣的是抓住这些接口的含义以及实现它的内容的其他类型。
名称
正如我之前所说,名称对于建立思维图的连接非常重要,它们传达了很多意义。像 NewFoo()这样的传统特殊名称通常代表构造函数,所以我看到它们连接并更接近类型节点。像 Start/Stop 或 Push/Pop 这样语义相关的名称对在我的脑海里肯定是彼此接近的,同样的前缀/后缀(比如 LightTheme/DarkTheme 或 ServerTCP/ServerUDP)在我脑海中也倾向于聚集在一起。
所以这只是一些“规则”的例子。我不认为我把它们都弄明白了,但这应该能让你大致了解我在做什么。
工具预览
这是我正在尝试做的事情,阅读 Go 的代码,解析它,并构建主要代码概念的空间关系的一个 3D 地图,尽可能接近我在脑海中的方式。每当我不能将代码进一步分解成要可视化的节点时,只需要显示函数的文本,这样我就可以阅读并填入代码的时间部分。而 3D 可视化可以让我立即看到这段代码是关于什么的,它的结构如何,它代表了什么实体和抽象,等等。滚动半小时而不会像我一样眼睛红了,所以我温柔地要求计算机为我干脏活累活,这样极大地促进了这种思维图的解码和连接过程。
它可以在浏览器上工作,并通过与本地服务器通信来访问文件系统。
我目前已经实施了目标计划的 20%,在努力实现的过程中,更深入地发现了一些不明显的的事情,而这些事情堆在积压的工作中。例如,我意识到,在不同的连接地点,应该有不同的缩放度,如果正在浏览一个包,对一种连接感兴趣,当读取函数代码时,不会太关心其余的部分,只会希望出现更多函数连接的详细说明。
下面是一些早期效果的预览(为了节省流量,采用了较小的窗口和低的 fps):
介绍屏幕和 container/list 的 stdlib 包:
浏览更大的 Go 包 github.com/divan/expvarmon:
可视化自身:
包依赖关系的可视化。对于文本,添加新的依赖只是 import 一行,无论它是像 leftpad 这样较小的包还是 200K LoC 像大象一样。通过可视化方法,可以立即“感受”到导入行的重量,并且几乎本能地强迫我们在使用该依赖项之前三思而后行。
布局算法
在这里使用力导向的布局方法,将节点放置到 3D 空间中,然后在它们之间多次施加物理力,直到系统达到稳定的最小能量状态。物理在这里非常重要,因为它有助于直观地展示最终布局。
但值得注意的是,大多数力导向的算法试图为任意类型的数据生成一个美观的图形。然而,在我的例子中,我想要的却是相反的 — 我希望它只为良好的代码生成漂亮的布局,并为糟糕的代码生产糟糕的图片。
这里的理由很简单:首先,不管怎样,它在我的头脑中是这样工作的,所以我坚持它。糟糕的代码在我的头脑中看起来很凌乱(这就是为什么对于过于复杂的代码很难工作的原因)。其次,我希望这个方面有意地是可见的,而用不着去学习好的代码规则,例如“我们不能在短期内存中保留超过 7 个对象,所以要保持较小的抽象规模”,而是让可视化本身和直觉来完成这项工作。
备注
对于这种方法,有一些随机的备注:
首先,它不是一个调用图。这个图抓住了函数间的一些关系,但它与调用图非常不同。例如,当打开 Pprof 输出时,看到的会不一样。
代码结构的可视化也存在于其他语言中,并且我已经看到了一些非常酷的项目,特别是 Java。但它们在两个方面有着根本的不同:
主要目的是帮助在错综复杂的类层次结构和偶然的复杂性中导航;
把类可视化成节点,对于构建空间的关系图并没有真正的帮助,因为我前面解释过的原因,类同时代表了一切,又代表什么也不是的东西。
所以这种代码可视化方法才有意义,因为 Go 的设计很简单。可以想象为其他语言构建类似的可视化,但我不知道他们如何能像 Go 一样表示思维图。少即是多。
编写的工具是用 Go 编写的,使用GopherJS和Vecty框架来实现 Go 中基于 Web 的 UI。不过,它使用Three.js的 GopherJS 包装器,并通过 WebGL 渲染了 3D 场景。
我不确定是否需要抛弃文件。将代码文本分块并命名可以帮助逻辑分组,但是很容易让文件名和它们应该分组的实际内容之间的同步变得太松散。某种程度上,文件系统层会对思维图到代码的连接过程造成很大的损害。目前,我不关心如何在文件系统上对代码进行布局。
在我的脑海中,我没有使用颜色来表示“节点”。因为,它在脑海中不是实际的视觉图像,因为我不是那种审美好的人。这只是我们的思维图建立在与视觉皮层相同的神经“硬件”上,两者都处理大量的空间信息,所以它们“感觉”相似。这个想法很好,因为如果使用得当,颜色是一个非常强大的工具,通过颜色,我们可以快速地可视化代码的各个不同的方面:
节点的子节点:为类型/方法/函数的嵌套树着色可以提高浏览和理解能力;
公共/私人 API:如果空间中的位置没有清楚地显示这一点,颜色可能会有所帮助;
不同种类的节点(类型/功能/接口等)着色:我现在所做的,有助于区分对象;
Pprof/cover 输出。
特别的,我希望在 pull 请求中有不同的可视化表示,这是将来要做的事情。
状态
这个项目是一个完整的实验,我在空闲时间就在做它,它目前处于早期的 alpha 阶段,甚至没有名字(内部我只使用“codevis”— 代码可视化)。实验意味着它绝对会因为很多原因而失败,但重要的是从这个实验中吸取的教训。我在这里使用了如此多的主观假设,这是我第一次与任何人分享所有这些想法,所以也许它对其他人来说完全不同。不过,如果它适合我,我就很高兴。
我现在的目标转移到一个 dogfooding 阶段,让这个工具为我自己工作。我会尝试两个选项:在浏览器中编辑代码,并通过 WebSocket 在另一个窗口中与编辑器交谈。
我遇到的主要挑战之一是 3D 编程本身,我对它几乎没有经验。在 2019 年,很不幸的是,在不消耗 CPU 的情况下高效地绘制 3D 文本或绘制上千行代码,或者让代码进行一目了然的优化,仍然是很困难的。另外,我花了很多时间来研究物理,因为节点之间力的参数调整几乎是一种黑暗魔法。令人惊讶的是,我们的大脑在物理方面的工作有多么糟糕,因为物理常数与现实世界中的常数是不同的,一个微小的变化,整个世界都会疯狂,加上它对小图表和大图表都能很好地工作,这使得它更加复杂。
即使在早期阶段,我也很享受目前的结果。非常令人鼓舞的事情是获得一个随机的项目,使用这个工具打开它,立即就能看到它的结构,并且能够使用“wasd”键来浏览代码库,就像漂亮的老版的雷神之锤 I(一款世界著名的射击类游戏)一样。至少,我觉得我现在正走在正确的轨道上。
它还没有为开源做好准备,但这只是一个时间问题。只要我确定它在基本情况下能可靠地工作,我就一定会公开它。
未来
此外,我希望在某个时候能完全摆脱窗口和屏幕,并在 AR 环境中使用这种 3D 代码表示。Hololens 2 团队的最新演示给了我希望,我们离干掉屏幕的时刻不远(也就是说,如果有足够高的分辨率,你可以在视网膜上呈现想要的任何屏幕,所以它们会变得过时),而且 NReal 的最新价格公告显示市场正在增长,AR 设备的价格也越来越低。
我非常希望能够亲自走到我感兴趣的代码部分旁边,或者用手把它拉近。不幸的是,编程几乎把你锁定在坐姿。向可视化编程的过渡可以稍微改变这里的游戏。
结论
60 年代末,在加米施召开的第一次北约软件工程会议上,“软件危机”一词被创造出来。随着计算能力的迅速提高,现有的软件编写方法已经不充分,但新的编写方法还没有发明出来。
我认为现在我们面临着另一个危机,代码库的规模与我们必须处理的工具不匹配。我们正在解决的问题的复杂性并没有增加太多,这使得带有侧边栏和三个按钮的用户界面不太可能与 Apollo 任务代码竞争,但是我们使用的语言和工具的复杂性已经变得越来越大,而且还在不断增加。
我觉得这个火炬问题造成了很大的影响,当只看到代码的一小部分被编辑器点亮时,很容易忽略代码对整个代码结构的影响。立即看到这些变化,并能够利用我们大脑的空间力量来导航代码,不仅可以自然地激励我们编写更简单、结构更好的代码,而且可能会改变对编程的传统理解,从包装者“应用模式“的观念转到制图者“构建图“的观念。
先塑造我们的工具,然后工具塑造我们。
原文链接:https://divan.dev/posts/visual_programming_go/
评论 3 条评论