古物鉴定师的铁锈

2020-10-30 19:30:55

您已经用Clojure编写了软件。它可以支付你的账单。你好好享受吧。你所在的行业从宽松的货币政策中获得了不成比例的好处,导致了一种追逐潮流的文化,即高薪书呆子制作网络应用程序。你对此感到内疚,但你对此无能为力,因为你没有其他能力是理性的人会付钱给你的。

学习“生锈”可能不会对你解决这个问题有多大帮助。它不会帮助你将本体论上的飞跃从疲惫的刻板印象转变为有知觉和真实的东西。你将仍然是一个没有可辨别身份的可替换的剪影。这甚至可能会加剧问题。但是,它将为您提供编写低级软件的有用工具。

铁锈的长处就是Clojure的短处,反之亦然。Ruust没有那么强的表现力和互操作性,它的并发故事也不够完整。也就是说,它可以更好地满足性能或安全方面的关键需求,并且可以嵌入到其他程序中或非常有限的硬件上。

许多人试图将铁锈比作Go,但这是有缺陷的。围棋是一种古老的棋类游戏,强调策略。与国际象棋相比,铁锈更合适,国际象棋是一种专注于低级战术的棋盘游戏。Clojure具有高级权限,是对持久的石头游戏的更好类比。

对于Clojure,我们通常从安装Leiningen开始,它构建项目并将库部署到clojars.org。项目根目录下的project.clj文件指定类似于依赖项的元数据。我们理所当然地认为Clojure的一个优雅方面是它只是一个库,因此项目可以在该文件中指定使用哪个版本的Clojure,就像使用任何其他库一样。

对于Rust,我们从安装Cargo开始,它构建项目并将库部署到crates.io。项目根目录下的Cargo.toml文件指定依赖项等元数据。Rust采用了更传统的方法,即与其构建工具捆绑在一起;它不是一个库,它的编译器不能嵌入到REPL驱动的开发程序中。

使用Clojure,我们使用lein新应用hello-world启动一个应用,该应用创建一个包含以下内容的项目:

(NS hello_world.core(:gen-class))(Defn-main&34;我不会做很多事情...。现在还不行。";[&;args](println";Hello,World!";)

使用Rust,我们启动了一个应用程序,其中包含新的hello_world--bin,它创建了一个包含以下内容的项目:

正如您所看到的,这个过程基本上是相同的,除了语法差异之外,它们都是从相同的主要功能开始的。对于Cargo,如果省略";--bin";,它将创建一个库。无论哪种方式,一定要认真考虑你的项目的名字。像“铁锈”这样的名字确保了许多巧妙的双关语出现的机会,这些双关语肯定不会令人厌烦或陈旧。

虽然Rust项目并不是从任何与Clojure的名称空间声明等价的地方开始的,但是一旦您超越了单个源文件,您将需要使用它。这不是C或C++,您只需像山顶洞人一样包含文件。Ruust将代码分成模块,并且根据文件名自动为每个源文件分配一个模块。我们可以在单独的文件中创建函数,如下所示:

Rust的mod类似于Clojure的ns,因为它创建了一个模块,但它们都放在main.rs的顶部,而不是模块来自的文件中。在此基础上,我们只需在函数名前面加上utils::即可使用它们。请注意,它们是用pub声明的。与Clojure不同,Rust默认将函数设为私有函数。

铁锈的使用类似于Clojure的要求,因为它带来了一个现有的模块。这里稍微修改了一下main.rs,我们显式地引入了符号,这样就不需要给它取别名,就像Clojure的Required对关键字:refer所做的那样:

正如您所知道的,具有自己的包管理器的语言通常有一种特殊格式的库,它们有自己唯一的名称。Python有它的鸡蛋,Ruby有它的宝石。Clojure的缺点是在现有的生态系统上,所以它不能发明自己的格式;它使用与其他JVM语言相同的乏味的JAR。

谢天谢地,Rust没有这个问题,它选择将其格式命名为crates";。这反映了这种语言的工业根源,也反映了它的赞助商--加利福尼亚州山景城--一个简陋的蓝领小镇。要使用板条箱,可以将其添加到Cargo.toml,就像使用project.clj一样。这是我的时间箱加进去后的样子:

要使用它,我们首先需要在Main顶部声明板条箱。rs:

然后,在我们要在其中使用它的文件中,我们将使用以下命令将其引入:

//utils.rs使用time;pub fn say_hello(){println!(";hello,world at{}!";,time::now()。Asctime();}pub fn say_再见(){println!(";再见,世界在{}!";,time::now()。Asctime());}。

到目前为止,我们一直避免看到类型,因为我们的函数都不带参数。铁锈是静态打字的。好处是,您将在编译时而不是在运行时诅咒它。缺点是,探索性编程意味着探索如何说服编译器让您尝试一个想法。让我们修改我们的函数,以便将";Hello&34;或";Goodbai&34;作为参数传递:

//utils.rs use time;pub fn say_thing(word:&;str){let t=time::now();println!(";{},world at{}!";,word,T.asctime());}。

因此,参数的语法类似于ML,首先是名称,然后是冒号,然后是类型。在Rust中,静态分配的字符串的类型为&;str,发音为";string Slice";。堆分配的字符串具有字符串类型。这是您在Clojure或其他高级语言中找不到的区别。阅读字符串以了解更多信息。

请注意,在这个最新版本中,我们还使用let将Time对象移动到本地变量中,这对Clojure用户来说应该很熟悉。在Rust中,您需要指定顶级函数的类型,但几乎从不指定局部变量的类型。Rust具有类型推理功能,因此它可以自己计算出t的类型。碰巧是TM。

正如TM文档指出的那样,asctime函数返回TmFmt。虽然是Printtln!我不知道那是什么,这无关紧要。它实现了一个特征--类似于Clojure协议--称为display,这就是println!需要。这一机制在铁锈中得到了广泛的应用。阅读特征以了解更多信息。

上一节中堆栈和堆分配之间的区别值得关注。在高级语言中,您无法控制使用哪种语言,因此您从不考虑它。在C和C++中,您可以完全控制它,但代价是更容易出错。Rust承诺让您在与高级语言一样安全的同时实现这种控制。

当您直接控制内存分配时,您还可以控制如何将值传递给函数。在高级语言中,通常只需将值传递给函数,该语言将决定是只传递对值的引用,还是传递值的整个副本。在Rust中,您显式传递对值的引用。

这就是&;在&;str中的意思。文字字符串自动表示为引用,但在正常情况下,它们将作为值开始其生命,要将它们作为引用传递,您需要在它们前面加上&;。例如,让';将TM对象传递给Say_Something函数:

//main.rs外部装箱时间;使用utils::say_omething;mod utils;fn main(){let t=time::now();say_omething(";Hello";,&;t);say_omething(";再见&34;,&;t);}。

//utils.rs Use Time;pub FN Say_Something(Word:&;str,t:&;Time::TM){println!(";{},world at{}!";,word,T.asctime());}。

如果我们只说_Something(";Hello";,t);并将参数的类型更改为t:time::tm,会发生什么情况?值t将被";移到函数中,并且在函数之外将不再可用。由于调用Say_Something(";再见";,t);,它将抛出一个错误。阅读参考资料和借阅以了解更多信息。

Clojure程序员会很高兴地发现Rust也相信数据在缺省情况下是不可变的。上一节中的TM对象不能改变--您会得到一个编译错误。例如,因为它实现了Clone特征,所以它有一个名为CLONE_FROM的函数,允许您用一个全新的TM对象替换它。这显然是一个突变,所以如果我们想要使用它,我们必须用let mut声明它:

//main.rs外部装箱时间;使用utils::say_thing;mod utils;fn main(){let mut=time::now();t.clone_from(&;time::now_utc());say_omething(";Hello&34;,&;t);say_omething(";再见&34;,&;t);}。

在该示例中,测试对象被使用UTC时间而不是本地时间新对象完全替换。有趣的是,Say_Something函数仍然不能对其进行变异,因为引用在缺省情况下也是不可变的。如果要在那里运行CLONE_FROM函数,则必须使用可变引用:

//main.rs外部装箱时间;使用utils::say_thing;mod utils;fn main(){let mut=time::now();say_omething(";Hello";,&;mut);say_omething(";再见&34;,&;mut);}。

//utils.rs Use Time;pub FN Say_Something(Word:&;str,t:&;mut time::TM){T.clone_from(&;time::now_utc());println!(";{},world at{}!";,word,T.asctime());}。

这样做的巧妙之处在于,您只需查看函数的类型签名,就可以知道函数何时会改变参数。如果您没有看到&;mut,则它不能这样做(除非它是内部可变的)。它仍然可以执行I/O操作,比如写入磁盘或请求网络资源,因此它在这个意义上不一定是纯粹的,但至少我们知道它相对于它自己的参数来说是纯粹的。

在Clojure中,我们有nil的概念来表示缺少值。这很方便,但是如果我们忘记检查它,就会得到可怕的NullPointerException。Rust追随Haskell等语言的脚步,做了与可变性相同的事情:使其显式地成为类型的一部分。

例如,假设我们希望SAY_SOURCE函数允许您传递TM引用或什么都不传递。如果执行后一种操作,它将只使用time::now_utc()创建自己的TM对象。要表达这一点,我们必须将其设置为可选类型。这意味着将类型更改为Option<;&;Time::TM>;,并更改我们传递给它的值,如下所示:

//main.rs外部装箱时间;使用utils::Say_Something;mod utils;fn main(){let t=time::now();Say_Something(";Hello";,Some(&;t));Say_Something(";再见&34;,无);}。

//utils.rs use time;pub fn say_thing(Word:&;str,t:option<;&;time::TM>;){if T.is_ome(){println!(";{},world at{}!";,word,t.unrapp()。Asctime();}Else{println!(";{},world at{}!";,word,time::now_utc()。Asctime());}}。

因此,如果我们实际上想要传递一个值,我们会用一些(...)将其括起来,如果我们想要传递等价的Clojure‘s nil,我们就不会传入任何值。然后,在SAYSOME中,我们可以使用IS_SOME函数检查t是否包含值,如果包含,则对其调用UnWrap以获得它的值。

与仅使用nil相比,这看起来似乎工作量很大,但好处是NullPointerExceptions是不可能的。编译器强制我们检查它是否包含值。此外,它与&;mut在上一节中具有相同的优势;只需查看它的类型签名,我们就知道哪些参数不允许传递任何值。

在Clojure中,我们可以使用core.match库获得非常强大的模式匹配功能。铁锈在语言中也有类似的机制。这可用于简化使用MATCH关键字的复杂条件语句。阅读Match以了解更多信息。

就我们的目的而言,模式匹配可以帮助我们使if语句更安全。在前一节中,say_omething不是很惯用,因为它手动检查t.is_ome()并调用t.unwire()。使用If let语法要好得多,如下所示:

//utils.rs use time;pub fn say_thing(word:&;str,t:option<;&;time::tm>;){if let ome(T_Ptr)=t{println!(";{},world at{}!";,word,t_ptr;)。Asctime();}Else{println!(";{},world at{}!";,word,time::now_utc()。Asctime());}}。

当然,Clojure有自己的if-let,概念非常相似。唯一的区别是,我们必须使用模式匹配来提取选项类型中的值。这就是一些人(T_Ptr)=t正在做的事情。模式匹配在Rust中被广泛使用,用于从错误处理到析构的所有事情。阅读模式以了解更多信息。

在Clojure中,一切都是表达式,这意味着我们可以不受任何限制地将代码嵌入到代码中。在“锈”一书中,它不是很普遍,但几乎所有的东西都是一种表达方式。您遇到的唯一不能是表达式的东西是声明,如mod、use、fn和let。

如果语句呢?在前一节中,SAYSE_Something故意冗长。编写冗余代码(如调用println)显然没有任何好处!不仅仅是确保自己的工作安全。在Rust中,if语句是表达式,所以我们可以将其嵌入到let语句中,如下所示:

//utils.rs use time;pub fn say_thing(Word:&;str,t:option<;&;time::TM>;){let t_val=if let ome(T_Ptr)=t{*t_ptr}Else{time::now_utc()};println!(";{},world at{}!";,word,t_val。Asctime());}

在这里,我们创建局部变量t_val,它将包含t内部的值,或者如果t为NONE,则包含一个新对象。请注意t_ptr之前的*。这与&;相反,是通过获取引用所引用的值来实现的。我们需要这样做,因为time::now_utc()返回一个值,并且我们需要确保两者返回相同的类型。

还要注意,if语句中的两个表达式都没有以分号结尾。分号用于分隔语句。要返回值,我们只需编写一个不带分号的表达式。这与我们在Clojure中所做的类似。当我们想要返回值时,我们只需将其放在末尾。阅读表达式与语句以了解更多信息。

请注意,在函数末尾返回值时也会执行相同的操作。如果我们想要Say_Something返回我们的TM对象,我们所需要做的就是在类型签名中指明这一点,然后将t_val放在函数的末尾:

//utils.rs use time;pub fn say_thing(Word:&;str,t:option<;&;time::tm>;)->;time::tm{let t_val=if let ome(T_Ptr)=t{*t_ptr}Else{time::now_utc()};println!(";{},world at{}!&34;,word,t_val。Asctime();t_val}。

你可能一直在想,为什么是println!以一声巨响结束。在Clojure中,对副作用较大的函数执行此操作是惯用的做法。在Rust中,它是宏的编译器强制语法。像Clojure这样的Lisp方言的用户肯定喜欢他们的宏,因为他们的同形图标语法提供了一种巨大的威力、简单性和傲慢的个人优越感。

铁锈并不是同性恋,不出所料,宏观系统也没有那么强大。它们的主要用途类似于C宏:通过符号替换减少代码重复。然而,与C宏不同的是,它们是卫生的。阅读宏以了解更多信息。如果您正在寻找在编译时运行任意代码的功能,则可能需要编写编译器插件。

从这里可以学到更多关于铁锈的知识。我们还没有触及生命周期,也就是在没有垃圾收集的情况下实现内存安全的机制。我们还没有看过FFI,它是将段错误和堆栈损坏引入您的程序的机制。我一直链接的“铁锈之书”对读者来说是很棒的下一步。