Sula:用Scryer Prolog编写的Gemini协议服务器深度解析

Sula:一个特立独行的开源项目
在主流Web开发框架充斥着JavaScript、Go和Rust的今天,一个名为 Sula 的开源项目却选择了一条截然不同的技术路线——它是一个用 Scryer Prolog 编写的 Gemini 协议服务器。这个项目虽然在 Hacker News 上的热度并不算高(14 个 points),但它所代表的技术理念和工程实践,却值得每一位关注小众协议与逻辑式编程的开发者细细品味。
Sula 的独特之处在于它同时押注了两个「非主流」技术:一个是作为传输协议的 Gemini,另一个是作为实现语言的 Scryer Prolog。这两者的结合,构成了一次关于「简单性」与「声明式编程」的有趣实验。

Gemini协议是什么:对臃肿Web的反思
Gemini 是一个介于 Gopher 与 HTTP 之间的轻量级网络协议,诞生于对现代 Web 日益臃肿的反思。它由一位化名 Solderpunk 的开发者在 2019 年发起,最初的讨论在一个 Gopher 邮件列表上展开。Gopher 协议诞生于 1991 年的明尼苏达大学,比万维网(WWW)的普及还要早,它以纯文本菜单的形式组织信息,在90年代初期曾是互联网信息检索的主要方式之一。Gemini 的设计者认为,Gopher 过于简陋(不支持加密,格式表达力有限),而现代 HTTP 又走向了另一个极端——过度复杂化。Gemini 试图在两者之间找到一个「刚刚好」的平衡点。
如今的 HTTP/HTTPS 生态背负着庞大的历史包袱:复杂的 Cookie 追踪、无处不在的 JavaScript、层层嵌套的 CSS,以及各种广告与隐私侵犯机制。HTTP/2 和 HTTP/3 的规范文档动辄数百页,一个完整实现需要处理数十种头部字段、多种认证机制、内容协商、缓存控制等复杂逻辑。仅以HTTP/2为例,其规范(RFC 7540)定义了二进制分帧层、多路复用、服务器推送、流优先级、HPACK头部压缩等机制,一个符合标准的完整实现通常需要数万行代码。而HTTP/3更进一步,将传输层从TCP切换到基于UDP的QUIC协议,引入了0-RTT连接建立、改进的拥塞控制和连接迁移等特性,复杂度再上一个台阶。
Gemini 则反其道而行之,主张极致的简洁:
- 强制 TLS 加密:所有连接默认使用 TLS(运行在默认端口 1965,这个数字取自人类首次太空行走的年份——1965年3月18日苏联宇航员阿列克谢·列昂诺夫完成了人类历史上首次舱外活动),保证传输安全。TLS(Transport Layer Security)是互联网上最广泛使用的加密传输协议,其前身是SSL(Secure Sockets Layer)。TLS通过非对称加密进行密钥交换,再用对称加密保护实际数据传输,确保通信的机密性和完整性。传统的TLS依赖PKI(公钥基础设施)体系,即由证书颁发机构(CA)签发数字证书来验证服务器身份。Gemini 还支持 TOFU(Trust On First Use)信任模型,类似于 SSH 的密钥认证方式,用户可以用客户端证书代替传统的用户名密码认证。TOFU模型绕过了复杂的CA体系——客户端在首次连接时记住服务器的公钥指纹,此后如果指纹发生变化则发出警告。这种模型大幅降低了部署门槛,个人站长无需购买或申请CA证书即可运行加密服务,但代价是无法防范首次连接时的中间人攻击。
- 极简的请求响应模型:客户端发送一个 URL(不超过 1024 字节),服务器返回一个两位数的状态码、一个 META 字段和内容体,没有繁复的头部字段。整个协议规范仅有不到 4000 字,一个下午就能读完并开始实现。状态码的设计也极为精简:10-19表示需要输入,20-29表示成功,30-39表示重定向,40-49表示临时错误,50-59表示永久错误,60-69表示需要客户端证书。这与HTTP的数百种状态码和复杂的内容协商形成了鲜明对比。
- Gemtext 标记语言:一种比 Markdown 更加克制的文本格式,只支持三级标题(
#、##、###)、链接(=>开头,每行一个)、无序列表(*开头)、引用(>开头)和预格式化文本块(用三个反引号包裹)。它刻意不支持内联格式(如加粗、斜体)和内联链接,确保文档的逻辑结构完全由行来决定,这使得解析器的实现极为简单。这种「面向行」的设计哲学意味着Gemtext解析器只需逐行读取,根据每行的前几个字符即可确定其类型,无需维护复杂的解析状态——一个功能完整的Gemtext解析器通常只需要50-100行代码即可实现。
这种设计哲学吸引了一批崇尚「小而美」的开发者与内容创作者,他们希望回归到一个以文本为中心、无追踪、无干扰的网络空间。目前 Gemini 生态中已有数十种服务器和客户端实现:服务器端有 Go 语言编写的 Molly Brown、Rust 编写的 Gemserv 和 Agate、Python 编写的 Jetforce 等;客户端则有跨平台图形界面的 Lagrange(C语言实现,界面精美)、终端下的 Amfora(Go语言)和 Bombadillo 等。据社区统计,Gemini 空间(Geminispace)中已有数千个活跃的「胶囊」(capsule,类似于网站),涵盖个人博客、技术文档、文学作品等内容。为 Gemini 编写服务器,本身就是对这种理念的认同。
为什么选择Scryer Prolog构建服务器
如果说 Gemini 是「非主流协议」,那么 Scryer Prolog 就是「非主流语言」中的代表。Prolog 作为逻辑式编程(Logic Programming)的经典语言,其核心思想与命令式编程完全不同——你不是告诉计算机「怎么做」,而是描述「事实」与「规则」,让推理引擎自动求解。
Prolog 的历史可以追溯到 1972 年,由法国马赛大学的 Alain Colmerauer 和逻辑学家 Philippe Roussel 共同创造,其理论基础来自英国爱丁堡大学 Robert Kowalski 的研究——将一阶谓词逻辑用作程序设计语言。Prolog 的名称来自法语「PROgrammation en LOGique」(逻辑编程)。在人工智能研究的早期黄金时代(1970-1980年代),Prolog 曾是 AI 领域最重要的编程语言之一,被广泛用于专家系统、自然语言处理和定理证明。1982年,日本通产省启动了雄心勃勃的「第五代计算机」计划,选择 Prolog 作为核心编程语言,目标是构建能进行知识推理的智能计算机,虽然该计划最终未能完全实现目标,但极大推动了逻辑编程的研究。值得注意的是,逻辑编程的影响远超Prolog语言本身——它催生了约束逻辑编程(CLP)、归纳逻辑编程(ILP)、答案集编程(ASP)等多个重要分支,而Datalog(Prolog的受限子集)至今仍在数据库查询优化、程序分析和网络安全策略配置等领域发挥着重要作用。
Prolog 程序由三个核心要素构成:事实(Facts,如 parent(tom, bob). 表示 tom 是 bob 的父母)、规则(Rules,如 grandparent(X, Z) :- parent(X, Y), parent(Y, Z). 定义祖父母关系)和查询(Queries,如 ?- grandparent(tom, Who). 询问 tom 的孙辈是谁)。Prolog 的执行引擎通过两个核心机制工作:统一化(Unification)——将变量与具体值进行模式匹配,更准确地说是找到使两个项(term)相等的最一般替换(Most General Unifier, MGU);回溯(Backtracking)——当某条推理路径失败时,自动退回尝试其他可能性。这种机制实质上是对搜索树的深度优先遍历,使得 Prolog 天然擅长处理搜索、约束满足和结构化数据的解析。
Scryer Prolog的现代化特性
Scryer Prolog 是一个用 Rust 编写的现代 Prolog 实现,由 Mark Thom 主导开发,它力图严格遵循 ISO Prolog 标准(ISO/IEC 13211-1:1995),同时提供良好的性能和现代化的工具链。ISO标准的制定历时近十年,旨在解决早期Prolog方言之间严重的不兼容问题——Edinburgh Prolog、DEC-10 Prolog、C-Prolog等实现在语法细节、内置谓词行为和模块系统上各行其是,导致代码难以移植。该标准规定了Prolog的核心语法(包括运算符优先级、引号约定)、内置谓词的精确语义(包括异常条件和错误类型)、算术求值规则以及输入输出流的行为。
Scryer 的底层实现基于 WAM(Warren Abstract Machine),这是 David H.D. Warren 在 1983 年提出的 Prolog 执行模型,几乎所有高效的 Prolog 实现都以 WAM 为基础。WAM 定义了一套精密的内存架构,包括堆(Heap,存储复合项和变量绑定)、栈(Stack,分为环境帧和选择点帧)、踪迹栈(Trail,记录需要在回溯时撤销的变量绑定)和PDL(Push-Down List,用于统一化过程中的临时数据)。WAM的指令集分为几大类:put/get指令处理参数传递,unify指令处理结构体内部的统一化,call/execute指令管理控制流,try/retry/trust指令实现回溯机制中的选择点管理。这种设计使得Prolog程序可以被编译为高效的字节码而非纯粹解释执行,运行效率比朴素的解释器高出数个数量级。Scryer 用 Rust 重新实现了这套抽象机,既获得了 Rust 的内存安全保证和优秀的运行时性能,又保持了对 ISO 标准的严格遵守。
相比于历史悠久的 SWI-Prolog(由 Jan Wielemaker 自 1987 年开始开发,至今仍是最流行的 Prolog 实现),Scryer 的设计哲学有明显不同。SWI-Prolog 采取务实路线,提供了大量非标准扩展、丰富的库生态(包括 HTTP 服务器、语义Web工具、ODBC数据库连接、图形界面框架XPCE等)和成熟的开发环境,但其代码库庞大(主要用 C 编写,核心代码超过30万行)且某些行为偏离 ISO 标准。Scryer 则选择了「正确性优先」的路线:它拒绝非标准的快捷方式,坚持 ISO 标准中对算术、异常处理和模块系统的严格定义。这种选择意味着 Scryer 的库生态远不如 SWI-Prolog 丰富,但它为那些关心语言语义精确性的开发者提供了一个可靠的基础。近年来 Scryer 社区也在积极扩展其标准库,已经增加了 HTTP/TLS 支持、加密原语和文件系统操作等实用功能。
用 Prolog 编写网络服务器听起来有些反直觉,因为处理 socket、TLS 握手、字节流解析这类任务,通常被认为是命令式语言的强项。然而,Prolog 的模式匹配(pattern matching)和声明式规则,在处理协议解析这类「结构化输入」时,恰恰能展现出独特的表达力。协议的语法规则可以被优雅地描述为一组 Prolog 子句。
特别值得一提的是 Prolog 中的 DCG(Definite Clause Grammars,确定子句文法) 机制。DCG 是 Prolog 内置的语法分析工具,它允许开发者用接近 BNF(巴科斯-诺尔范式)的声明式语法来描述数据格式,Prolog 编译器会自动将其转换为高效的解析代码。DCG在Prolog中通过术语扩展(term_expansion)机制实现:编译器将DCG规则自动转换为普通的Prolog子句,每个非终结符被翻译为一个带有两个额外参数的谓词——分别代表输入列表(差列表的前半部分)和剩余列表(差列表的后半部分)。例如,greeting --> [hello], name.会被展开为greeting(S0, S) :- S0 = [hello|S1], name(S1, S).。差列表(Difference Lists)是这一机制的关键数据结构,它允许在O(1)时间内完成列表拼接,避免了传统列表append操作的O(n)开销。
在Sula的场景中,Gemini 请求的格式是 gemini://host/path\r\n,在 DCG 中可以非常自然地表述为一系列语法规则,每条规则对应协议格式的一个组成部分。相比之下,命令式语言中通常需要手动编写状态机或使用正则表达式来完成同样的解析任务,代码往往更长且更难验证其正确性。DCG 的声明式特性使得协议规范与实现代码之间的对应关系一目了然,大大降低了引入解析错误的风险。这使得DCG不仅适合语法分析,也被广泛用于字符串生成、序列化任务,甚至在某些情况下用于生成测试用例——同一套语法规则既能解析输入也能生成符合格式的输出。
一次工程可行性的验证
Sula 这样的项目,其价值不完全在于「实用」,更在于「证明可行」。它向社区展示了 Scryer Prolog 已经成熟到足以处理真实的网络 I/O、TLS 加密和协议实现。这在逻辑编程社区中具有里程碑意义——长期以来,Prolog 被认为只适合原型设计、学术研究和特定领域(如编译器构造和约束求解),而不适合构建需要与操作系统底层交互的系统级应用。Sula 的存在直接反驳了这种刻板印象。
对于逻辑式编程的爱好者而言,这是一个宝贵的参考实现,展示了如何在 Prolog 中组织一个完整的服务端应用——如何管理并发连接、如何处理文件系统访问、如何集成 TLS 库,以及如何将这些「不纯」的副作用操作与 Prolog 纯逻辑推理的核心优雅地结合。这里涉及到逻辑编程中一个长期存在的张力:纯Prolog是声明式的、可逆的、具有引用透明性的,但现实世界的I/O操作本质上是有副作用的、有序的、不可逆的。不同的Prolog系统采取了不同的策略来调和这一矛盾——SWI-Prolog通过库谓词直接暴露命令式I/O接口,Mercury语言通过类似Haskell的唯一性类型系统来跟踪I/O状态,而Scryer则倾向于使用ISO标准定义的流式I/O接口配合模块化的副作用隔离。
小众协议与小众语言结合的技术价值
有人可能会问:一个几乎没人用的协议,配上一个几乎没人用的语言,究竟有什么意义?
事实上,这类项目恰恰是开源生态健康的体现。它们不追逐商业热点,而是纯粹出于技术兴趣与探索精神。这种「非功利性」的实践往往能带来几方面的价值:
- 拓展语言的应用边界:证明 Scryer Prolog 不只能做学术演示,也能构建实际运行的网络服务。每一个这样的项目都在为 Prolog 社区积累实战经验,发现并修复运行时系统的不足,推动语言实现的成熟化。历史上,编程语言的实用化往往依赖于这类「杀手级应用」的推动——Ruby的Rails框架、JavaScript的Node.js运行时、Elixir的Phoenix框架,都是通过一个标志性项目证明了语言在特定领域的可行性,从而吸引更多开发者加入生态。
- 丰富 Gemini 生态:为这个小众协议增加了一种全新的服务器实现选择,增强了生态多样性。多样化的实现有助于发现协议规范中的歧义与不足——当不同语言、不同编程范式的实现者独立解读同一份规范时,他们往往会在边界情况上暴露出规范描述不清的问题,这反过来推动了协议的完善。这种现象在互联网标准的发展中屡见不鲜:HTTP的RFC文档经历了多次修订,很大程度上就是因为不同浏览器和服务器对同一规范的理解存在分歧,只有通过多样化的实现才能将这些隐含的歧义暴露出来。
- 教育与启发价值:为想学习 Prolog 或理解声明式编程的人,提供了一个真实、完整的代码范本。与教科书上通常使用的家族关系推理或八皇后问题不同,一个网络服务器涉及 I/O、错误处理、配置管理等现实世界的复杂性,是一个远更有说服力的教学案例。对于计算机科学教育而言,逻辑编程常常被局限在「编程语言」课程的一两个章节中,学生缺乏将其应用于实际问题的机会和动力。像Sula这样的项目提供了一座桥梁,让抽象的逻辑编程概念与可触摸的、可运行的系统级应用产生联系。
- 交叉授粉效应:不同技术社区的交汇往往能产生意想不到的创新。Gemini 社区中的开发者可能因此接触到逻辑编程思维,而 Prolog 社区的开发者也可能因此了解到简约协议设计的魅力。这种跨社区的知识流动,正是开源运动最珍贵的产物之一。计算机科学史上充满了这样的例子:函数式编程中的模式匹配影响了Rust和Swift的设计,Smalltalk的消息传递启发了Objective-C和Erlang,数据库领域的关系代数与逻辑编程的交汇产生了Datalog。创新往往不是在单一领域的纵深推进中产生,而是在不同思想传统的碰撞中迸发。
结语:技术多样性为何重要
Sula 或许永远不会成为主流,它的用户群体可能始终局限于一小撮 Gemini 爱好者与 Prolog 极客。但正是这样一批「非主流」的探索者,让软件世界保持着思想的多样性与创新的活力。
从生物学中我们知道,物种的多样性是生态系统韧性的基础。软件技术的演化也遵循类似的规律——今天的小众实验,可能为明天的主流范式提供灵感。Erlang 曾经是电信行业的小众语言,如今它的 Actor 模型影响了无数并发编程框架。Actor模型由Carl Hewitt在1973年提出,将并发计算建模为大量独立的「演员」之间通过异步消息传递进行通信,每个Actor拥有自己的私有状态,不与其他Actor共享内存,从根本上消除了锁竞争和死锁等传统并发编程的顽疾。Erlang在1986年由爱立信的Joe Armstrong等人开发,其「let it crash」哲学和热代码升级能力使其在电信交换机中实现了极高的可用性,这些理念后来深刻影响了Akka、Go的goroutine和channel,以及Elixir语言的设计。Lisp 的 S-表达式和宏系统在诞生数十年后,仍在影响着新语言的设计——Clojure将Lisp的理念带入了JVM生态,Julia的元编程系统借鉴了Lisp的宏,甚至Rust的声明宏和过程宏也能看到Lisp影响的影子。我们无法预知哪个「非主流」的想法会在未来的某个时刻变得至关重要。
在一个被少数几种技术栈主导的时代,Sula 提醒我们:编程语言与网络协议的选择远比想象中丰富,而「简单」与「声明式」这两种被主流忽视的价值,依然有着旺盛的生命力。对于愿意跳出舒适区、探索技术边界的开发者来说,这样的项目值得去阅读、去研究,甚至去贡献。
核心要点
相关推荐

从Vibe Coding到Spec Coding:AI全栈开发工程化方法论实战
深入解析AI编程从Vibe Coding到Spec Coding的演进路径,涵盖全栈架构选型、分层实现策略与团队级AI工程方法论,帮助开发者跳出聊天式编程局限,掌握规格驱动的工程化开发能力。

Meta推出Pocket:像刷TikTok一样玩AI生成游戏
Meta推出AI社交应用Pocket,用户通过自然语言描述即可生成可玩的互动游戏,支持触摸、动作、声音等多模态交互,并像TikTok一样分享和混编创作,开辟AI原生社交内容新形态。

VeloFiler:Rust打造的键盘优先双栏文件管理器
VeloFiler是一款基于Rust和GPUI框架开发的macOS双栏文件管理器,支持Vim式键盘导航、多格式文件预览、SSH/SFTP远程管理,为开发者提供零鼠标的高效文件操作体验。