为什么为Penpot选择了Clojure编程语言

1,006 阅读9分钟

"为什么是Clojure?"这可能是我们在Penpot被问及最多的问题。我们在FAQ页面上有一个模糊的解释,所以通过这篇文章,我将解释我们决定背后的动机和精神。

这一切都始于一个PIWEEK。当然了!

在我们2015年的一次个人创新周(PIWEEK)中,一个小团队有了创建一个开源原型工具的想法。他们立即开始工作,经过一周的努力工作(和大量的乐趣),能够发布一个工作原型。这是第一个静态原型,没有后台。

我不是最初团队的成员,但当时有很多理由选择ClojureScript。在短短一周的黑客马拉松中建立一个原型并不容易,ClojureScript当然对此有帮助,但我认为选择它的最重要原因是它很有趣。它提供了一种函数式范式(在团队中,人们对函数式语言有很多兴趣)。

它还提供了一个完全互动的开发环境。我指的不是编译后的浏览器自动刷新。我指的是在运行时刷新代码,而不做页面刷新,也不会丢失状态从技术上讲,你可以在开发一个游戏时,在你还在玩的时候改变游戏的行为,只需在编辑器中触摸几行。使用ClojureScript(和Clojure),你不需要任何花哨的东西。语言结构从一开始就已经考虑到了热重载的问题。

Image of the first version of UXBOX (Penpot's original concept) in 2016

我知道,今天(2022年),你也可以用React在普通的JavaScript上实现类似的功能,也可能用其他我不熟悉的框架。不过,由于语言的内在限制、密封模块、缺乏适当的REPL,这种能力支持也很可能是有限的和脆弱的。

关于 REPL

在其他动态语言中,如JavaScript、Python、Groovy(等等),REPL功能是事后添加的。因此,它们经常有热重载的问题。这些语言模式在实际代码中没有问题,但在 REPL 中并不适合(例如,JavaScript 中的const 在 REPL 中评估了两次相同的代码)。

这些REPL通常被用来测试为REPL制作的代码片段。相比之下,Clojure中的REPL使用很少涉及直接输入或复制到REPL中,更常见的是评估实际源文件中的小代码片断。这些片段经常作为注释块留在代码库中,所以当你将来修改代码时,你可以在 REPL 中再次使用这些片段。

在Clojure REPL中,你可以不受任何限制地开发整个应用程序。Clojure REPL的行为与编译器本身没有什么不同。你能够在已经运行的应用程序中对任何命名空间的特定函数进行各种运行时反省和热替换。事实上,在生产环境中发现后端应用程序在本地套接字上暴露REPL的情况并不少见,以便能够检查运行时,并在必要时修补特定的功能,甚至不必重新启动服务。

从原型到可用的应用程序

2015年PIWEEK之后,Juan de la Cruz(Penpot的设计师,也是项目创意的原作者)和我开始在业余时间研究这个项目。我们利用从第一个原型中获得的所有经验教训重写了整个项目。在2017年初,我们在内部发布了可以称为第二个功能原型的东西,这次是有后台的。问题是,我们仍然在使用Clojure和ClojureScript!

最初的原因仍然是有效和相关的,但如此重要的时间投资的动机揭示了其他原因。这是一个非常长的清单,但我认为其中最重要的特点是:稳定性、向后兼容性和语法抽象(以宏的形式)。

Image of the current Penpot interface

Image by:

(Andrey Antukh, CC BY-SA 4.0)

稳定性和向后兼容性

稳定性和向后兼容性是Clojure语言的最重要目标之一。通常情况下,在没有测试其真正的用处之前,并不急于将所有时髦的东西纳入语言。在Clojure编译器的alpha版本上运行生产的人并不少见,因为即使在alpha版本上出现不稳定的问题也很罕见。

在Clojure或ClojureScript中,如果一个库在一段时间内没有提交,那么它很可能就很好。它不需要进一步开发。它可以完美地工作,而且没有必要去改变那些功能正常的东西。相反,在 JavaScript 的世界里,当你看到一个库已经有几个月没有提交了,你就会觉得这个库已经被放弃了,或者没有人维护。

有很多次,我下载了一个6个月没有人碰过的JavaScript项目,却发现其中一半以上的代码已经被废弃,没有人维护了。在其他情况下,它甚至无法编译,因为一些依赖项没有尊重语义版本。

这就是为什么Penpot的每一个依赖都是精心挑选的,考虑到连续性、稳定性和向后兼容性。他们中的许多人都是内部开发的。只有当第三方库被证明具有相同的属性时,或者当内部开发的努力和时间比例不值得时,我们才会委托给第三方库。

我认为一个很好的总结是,我们试图拥有最小的必要的外部依赖性。React可能是一个大的外部依赖性的好例子。随着时间的推移,它已经显示出他们对向后兼容性的真正关注。每个主要的版本都会逐步纳入变化,并有明确的迁移路径,允许新旧代码共存。

句法抽象

我喜欢Clojure的另一个原因是它清晰的句法抽象(宏)。这是其中的一个特点,作为一般规则,它可能是一把双刃剑。你必须小心使用它,不要滥用它。但是,随着Penpot这个项目的复杂性,有能力提取某些常见的或冗长的结构,帮助我们简化了代码。这些语句不能一概而论,它们所提供的可能价值必须视具体情况而定。这里有几个重要的例子,它们给Penpot带来了很大的变化。

  • 当我们开始构建Penpot时,React只有组件这个类。但这些组件被建模为rumext 库中的函数和装饰器。当React发布了带有钩子的版本,大大增强了组件的功能,我们只需要改变宏的实现,Penpot的90%的组件可以保持不被修改。随后,我们逐渐从装饰器完全转移到钩子,而不需要费力的迁移。这加强了前面几段的相同想法:稳定性和向后兼容性。
  • 第二个最重要的情况是,使用本地语言结构(向量和地图)来定义虚拟DOM的结构,而不是使用类似JJSX的自定义DSL,这很容易。使用这些本地语言构造将使一个宏在编译时最终产生对React.createElement 的相应调用,仍然为额外的优化留下空间。显然,语言是面向表达式的这一事实使这一切变得更加习以为常。

下面是一个简单的JavaScript例子,基于React文档中的例子。

function MyComponent({isAuth, options}) {
    let button;
    if (isAuth) {
        button = <LogoutButton />;
    } else {
        button = <LoginButton />;
    }

    return (
        <div>
          {button}
          <ul>
            {Array(props.total).fill(1).map((el, i) =>
              <li key={i}>{{item + i}}</li>
            )}
          </ul>
        </div>
    );
}

这里是ClojureScript中的等价物。

(defn my-component [{:keys [auth? options]}]
  [:div
   (if auth?
     [:& logout-button {}]
     [:& login-button {}])
   [:ul
    (for [[i item] (map-indexed vector options)]
      [:li {:key i} item])]])

所有这些用于表示虚拟DOM的数据结构在编译时都被转换为适当的React.createElement

事实上,Clojure是如此的面向数据,这使得使用相同的语言的本地数据结构来表示虚拟DOM是一个自然和逻辑的过程。Clojure是LISP的一种方言,语言的语法和AST使用相同的数据结构,可以用相同的机制来处理。

对我来说,通过ClojureScript与React合作感觉比用JavaScript工作更自然。所有添加到React的额外工具,如JSX、不可变的数据结构,或处理数据转换的工具,以及状态处理,都只是ClojureScript语言的一部分。

客座语言

最后,Clojure和ClojureScript的基本原理之一是,它们是作为客体语言建立的。也就是说,它们是在现有的平台或运行时间之上工作的。在这种情况下,Clojure建立在JVM之上,ClojureScript建立在JavaScript之上,这意味着语言和运行时之间的互操作性非常有效。这使我们能够利用整个生态系统,既Clojure加上在Java中完成的一切(ClojureScript和JavaScript也是如此)。

还有一些代码片断,当它们用命令式语言(如Java或JavaScript)编写时,会更容易编写。Clojure可以与它们在同一个代码库中共存,而没有任何问题。

前台和后台之间的代码共享也很容易,即使每个人都可以在一个完全不同的运行时(JavaScript和JVM)中运行。对于Penpot,几乎所有最重要的管理文件数据的逻辑都写在代码中,并在前端和后端执行。

也许你可以说我们选择了一些人所说的 "无聊 "的技术,但实际上它一点也不无聊。

权衡利弊

很明显,每个决定都有权衡。选择使用Clojure和ClojureScript也不例外。从商业角度来看,选择Clojure可以被看作是有风险的,因为它不是一种主流语言,与Java或JavaScript相比,它的社区相对较小,而且寻找开发人员本身就比较复杂。

但在我看来,学习曲线要比乍看之下低得多。一旦你摆脱了对的恐惧(或者我开玩笑地称之为:对括号的恐惧),你就会很快开始熟练使用这门语言。有大量的学习资源,包括书籍和培训课程。

我注意到真正的障碍是范式的转变,而不是语言本身。对于Penpot,项目的必要和固有的复杂性使得编程语言成为我们在面临开发时的最小问题:建立一个设计平台是不小的成就。