# rust-fsharp **Repository Path**: ydyr/rust-fsharp ## Basic Information - **Project Name**: rust-fsharp - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-05-11 - **Last Updated**: 2024-05-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 给FSharper的Rust, 给Rustacean的F# # 这个仓库是干什么的? 这个仓库是给Rust和F#程序员用以互相了解的入门手册. 下面我来解释为什么为把这两个语言放在一起: [Rust是我的第一语言](https://github.com/Dhghomon/programming_at_40/blob/master/README.md). 我最近在研究与Rust接近的语言, 其中之一是F#(因为Rust编译器原先是用OCaml写的, 并且它吸收了后者的很多语法). 几乎每份F#资料都假定读者是C#开发者. (在C#中你这么做, 但在F#中你这么做) 这个方法没什么问题, 但很多人包括我其实不了解C#. 而当我看到F#代码时我发现Rust要素到处都是: Option, Result, 匹配语句, 迭代器, 等等, 作为Rustacean它们的好处无需赘言. 让一个Rustacean读一本到处都提到C#的F#教材就好比让一个德国人用葡萄牙教材学荷兰语. 而对于FShaper来说, 默认读者不知道Option, 模式匹配等的Rust教程也令人不快. 既然两者如此相似, 给Rustacean的F#教程必然也可以作为F#程序员的Rust教程. 所以我写了这篇文档. 我随时可能在文中加入新想到的点子, 但考虑到我还不怎么熟悉F#, 欢迎在评论和PR中指出我的错误或措辞不当之处. 介绍到此为止. 开始正文吧! # 免安装体验语言 两个语言都提供了免安装的在线运行环境, Rust有[Rust Playground](https://play.rust-lang.org/), 而F#的对应网站叫[Try F#](https://try.fsharp.org/). # 基于表达式的语言 在两个语言中, 几乎所有东西都是表达式, 代码块中最后一个表达式会作为其返回值. Rust用花括号标明作用域, 而F#用空白符. 注意两者的相似性: Rust: ```rust fn main() { let x = { let x = 5; let y = 10; x + y // 最后一个表达式是返回值 // 不需要return关键字! }; } ``` F#: ```fsharp let x = let x = 5 let y = 10 x + y ``` # 原始类型 Rust和F#都有类型推理. 它们都使用`let`绑定. 所以如果你这么写: ```rust let x = 9; ``` 在Rust中`x`会成为`i32`, 在F#中是`int`...其实也是一个32位有符号整型. 顺便, F#不要求在结尾放置分号, 但如果你放了, 编译器也不会抱怨. 没必要在上面的例子中添加逗号, 但如果你这么做了, 效果也一样: ```fsharp let x = let x = 5; let y = 10; x + y ``` (这是因为在两个情况下它都返回unit类型.) *译注: 此处有误. F#中`let`不是单独的表达式, 而是更大表达式的一部分, 绑定与表达式主体以`in`或缩进连接. 比如* ```fsharp let x = 1 x + 1 ``` *与* ```fsharp let x = 1 in x + 1 ``` *是等价的. `;`在F#中用于连接两个表达式, 如* ```fsharp printf "hello, " printf "world" ``` *相当于* ```fsharp printf "hello, "; printf "world" ``` *如果左边表达式的返回值不是`unit`, 编译器会发出警告* 让我们来看一些原始类型: * Rust`u8`在F#中是`uint8`, 别名`byte`(.NET: `System.Byte`). * Rust`u16`在F#中是`uint16`(.NET: `System.UInt16`) * Rust`u32`在F#中是`uint32`(.NET: `System.UInt32`), 别名`uint`. * Rust`u64`在F#中是`uint64`(.NET: `System.UInt64`) * Rust`i8`在F#中是`int8` which is also an `sbyte`(.NET: `System.SByte`). * Rust`i16`在F#中是`int16`(.NET: `System.Int16`) * Rust`i32`在F#中是`int32`(.NET: `System.Int32`), 别名`int`. * Rust`i64`在F#中是`int64`(.NET: `System.Int64`) * Rust`usize`在F#中是`unativeint`(.NET: `System.UIntPtr`) * Rust`isize`在F#中是`nativeint`(.NET: `System.IntPtr`) * Rust`f32`在F#中是`float32`, 别名`single`(.NET: `System.Single`). * Rust`f64`在F#中是`float`, 别名`double`(.NET: `System.Double`). 对于有小数部分的数字, Rust和F#都以此为默认浮点类型. Rust和F#都用后缀标识32位整型和64位浮点以外的数字类型. 在Rust中你还可以用类型声明, 而在F#你只能用后缀. * Rust`let x: u8 = 5`或`let x = 5u8`在F#中是`let x = 5uy`. (**u**nsigned b**y**te) * Rust`let x: u16 = 5`或`let x = 5u16`在F#中是`let x = 5us`. (**u**nsigned **s**hort) * Rust`let x: u32 = 5`或`let x = 5u32`在F#中是`let x = 5u`. (**u**nsigned) * Rust`let x: u64 = 5`或`let x = 5u64`在F#中是`let x = 5UL`. (**U**nsigned **L**ong) * Rust`let x: i8 = 5`或`let x = 5i8`在F#中是`let x = 5y`. (b**y**te) * Rust`let x: i16 = 5`或`let x = 5i16`在F#中是`let x = 5s`. (**s**hort) * Rust`let x: i32 = 5`或`let x = 5i32`在F#中是`let x = 5`. (默认情况, 没有后缀) * Rust`let x: i64 = 5`或`let x = 5i64`在F#中是`let x = 5L`. (**L**ong) * Rust`let x: f32 = 5.0`或`let x = 5.0f32`在F#中是`let x = 5.0f`或`let x = 5f`. * Rust`let x: f64 = 5.0`或`let x = 5.0f64`在F#中是`let x = 5.0`. (默认情况, 没有后缀) 在Rust和F#中你都可以在数字中加入`_`以提升可读性. 下面的代码对两者都适用. ``` let num = 8000000; let readableNum = 8_000_000; let maybeReadableNum = 8___________000________000; ``` 至于F#的`decimal`类型(.NET的`Decimal`), Rust是没有的. 准确地说, [它有](https://crates.io/crates/rust_decimal), 但不在标准库里. Rust标准库很小(甚至连随机数函数都没有), 十进制数由外部crate(大体上create = NuGet包)提供. 给Rust项目加入外部crate很简单, 只需要在`cargo.toml`里写一行: ```toml [dependencies] rust_decimal = "1.14" <-- 就这样 ``` 输入`cargo run`(类比`dotnet run`), 然后它就会下载外部包, 编译, 接着运行. (显然`cargo.toml`的F#等价物是`paket.dependencies`.) *译注: 更接近`xxx.fsproj`. paket是可选的.* * Rust`char`在F#中是`char`(.NET`Char`). Rust`char`是UTF-32的, 永远有四个字节, 而F#的是UTF-16. * Rust`unit`在F#中也是`unit`. 它们都用`()`作为其字面量. 这是令Rustacean十分开心的类型. (几乎)任何东西都是表达式! * Rust`str`在F#中是`string`. F#文档叫它"字符串是用于表示文本的字符的顺序集合。 对象 String 是表示字符串的对象的顺序集合 System.Char". * Rust`String`在F#中是[`StringBuilder`](https://docs.microsoft.com/zh-cn/dotnet/api/system.text.stringbuilder). F#文档: "表示可变字符字符串". 它用于高效构造不可变的`string`对象, 很像如下定义的`String`. *译注: 上文所说"F#文档"实为.NET API参考和.NET基础知识文档* ```rust pub struct String { vec: Vec, } ``` (给F#读者: `Vec`类似于[ResizeArray](https://twitter.com/ReedCopsey/status/1399492773900914691)) 那么F#`string`的Rust等价物是: ```rust pub struct String { vec: Vec, } ``` 然而, .NET字符串是[不可变的](https://docs.microsoft.com/zh-cn/dotnet/api/system.string#Immutability): ``` 对象 String 称为不可变(只读),因为创建对象后无法修改其值。 似乎修改 String 对象的方法实际上返回包含修改的新 String 对象。 由于字符串是不可变的,因此对单个字符串执行重复添加或删除操作的字符串 操作例程可能会造成重大性能损失。例如,以下代码使用随机数生成器创建 一个字符串,该字符串的范围为 1000 个字符,0x0001 0x052F。 尽管代码似乎使用字符串串联将新字符追加到命名 str的现有字符串, 但它实际上为每个串联操作创建一个新 String 对象。 ``` 这点上与`Vec`颇为不同, 后者在尾部追加`char`时仅需在超出容量时重新分配内存(这是自动完成的). ## 类型转换 在Rust中, 你可以用`as`把一个原始类型值转换到另一个类型. ```rust fn print_isize(number: isize) { // 只接受isize println!("{}", number); } fn main() { let x: i8 = 9; print_isize(x as isize); // 所以用as转换到isize } ``` 在F#中, 你可以在'变量'之前写上要转换的类型名. 譬如有一个函数只接受`byte`类型: *译注: 这不是F#的特殊语法构造, 只是对`byte`函数(与`byte`类型重名)的简单函数调用. 准确地说, 这是一个内联函数, 没有额外的运行时开销.* ```fsharp let printByte (number: byte) = printfn "%i" number ``` (给Rust读者: `%i`代表打印一个整型. 如果你把它改成`%s`, 编译会失败) 然后给他一个写作`9`的数字, F#会认为它是一个`int`(Rust的`i32`), 你得把它转换成`byte`. ```fsharp let printByte (number: byte) = printfn "%i" number let number = 9 printByte (byte number) ``` 注意两者的区别: F#中我们得特地注明函数接受`byte`类型, 这样它才会编译失败; 否则它会用函数类型适配输入类型. Rust更加严格, 要求用泛型处理可变的类型. (F#也有泛型 - 二者区别见对应章节) 然而, F#在函数输入上更加聪明. 比如: ```fsharp let printNumber number = printfn "%i" number let number = 9 printNumber number ``` 编译器会根据`%i`和`number`上的let绑定推断出函数接受int, 然后将函数签名设为`number: int -> unit`. 但如果你加上一行: ```fsharp let printNumber number = printfn "%i" number let number = 9 printNumber number // 给它一个int printNumber (byte number) // 再给一个byte ``` 这一回它就不会编译了, 因为它认为这个函数接受`int`, 我们却又让它接受`byte`类型. 前一个输入确定了输入类型, 所以它正确地拒绝了后者: ``` This expression was expected to have type 'byte' but here has type 'int' ``` 说到这点, 让我们来比较一下两个编译器对同一个问题发出的信息. 在F#写一个接受`int`的函数, 在Rust中写一个接受`i32`的, 分别用`byte`(Rust中的`u8`)调用它们, 看看编译器怎么说: F#: ```fsharp let printNumber (number: int) = printfn "%i" number let number = 9 printNumber (byte number) ``` Rust: ```rust fn print_i32(number: i32) { println!("{}", number); } fn main() { let x: u8 = 9; print_i32(x); } ``` 报错分别为: F# ``` This expression was expected to have type 'byte' but here has type 'int' ``` Rust ``` 7 | print_i32(x); | ^ | | | expected `i32`, found `u8` | help: you can convert a `u8` to an `i32`: `x.into()` ``` 注意两则信息对同一事物的不同说法. Rust: expected `i32`, found `u8` = **函数需要i32, 但你给了我u8** F#: This expression was expected to have type 'byte' but here has type 'int' = **你的输入说明这是一个byte, 但函数是int类型的** (Rust信息中提到的`.into()`是另一个类型转换的方法 - 后文详述) 在Rust中, `as`关键字常用于在存在重名或过长名字的作用域中重命名类型和枚举体等. ```rust // 外部crate的奇怪枚举 enum ErrorTypes { BigErrorExplanationThatIsTooLongToType, WhyDidTheyWriteTheseEnumVariantsLikeThisSeriously, ButThisIsPartOfExternalCodeINeedToCompile, } // 缩短它们 use ErrorTypes::{ BigErrorExplanationThatIsTooLongToType as ErrorOne, ButThisIsPartOfExternalCodeINeedToCompile as ErrorTwo, WhyDidTheyWriteTheseEnumVariantsLikeThisSeriously as ErrorThree, }; fn main() { let x = ErrorOne; match x { ErrorOne => {} ErrorTwo => {} ErrorThree => {} } } ``` ## F#的`inline`关键字 (感谢[isaacabraham](https://github.com/Dhghomon/rust-fsharp/issues/1)的提供) 如果我们想让`printNumber`函数接受多种类型, 有一种让它变得泛型的方法: `inline`关键字. Rust也有`inine`, 但它是偶尔[用于优化](https://nnethercote.github.io/perf-book/inlining.html)的特性. 顺带一提, 两个语言的特性语法格式类似: - Rust特性: `#[attribute_name]` - F#特性: `[]` 回到`inline`: 在F#中它用于构造泛型代码. 看看[文档](https://docs.microsoft.com/zh-cn/dotnet/fsharp/language-reference/functions/inline-functions), 看着很眼熟: ``` 内联函数是直接集成到调用代码中的函数。 使用内联函数 使用静态类型参数时,由类型参数参数化的任何函数都必须是内联函数。 这可保证编译 器可以解析这些类型参数。 使用普通泛型类型参数时,没有此类限制。 inline 的存在会影响类型推理。 这是因为内联函数可以具有静态解析的类型参数,而非 内联函数不能。 ``` 啊! 很像Rust的静态分派/单态化(单态化 = 让某物取单独一种表现形式): 也就是说, 代码中的某个位置提示编译器这个泛型函数取某个特定的类型参数时, 它会在编译时将其静态分派到这个具体类型. 让我们先来看看Rust泛型是怎么工作的. ```rust use std::fmt::Display; fn print_thing(input: T) { println!("{}", input); } fn main() { print_thing(8); print_thing('a'); print_thing("Hiya there."); } ``` [std::fmt::Display](https://doc.rust-lang.org/std/fmt/trait.Display.html)特型允许你用`{}`打印对应类型的某个值, 它的打印结果是人类可读的. 自己实现时, 你得用函数指导它该怎么打印. (另一方面, `Debug`添加个特性就可以实现, 它的输出更详尽但不好看) *译注: F#也有两种显示方式: 一是与C#等兼容的`ToString`重写, 二是更详尽的`StructuredFormatDisplayAttribute`. F#自动为联合/记录等类型定义实现后者* 然后我们就有了一个`fn print_thing`, 它接受一个泛型参数`T`, 我们告诉编译器这个参数必须实现`Display`特型. Rust的所有原始类型都实现了`Display`. 既然我们告诉了编译器这一点, 它就会允许我们在`println!`里写`{}`, 因为它知道有`Display`的类型可以被显示. 接下来我们用三个类型分别调用: `i32`, `char`和`&str`. 程序成功编译, 发生了静态分派(单态化). 编译器分别生成了三个有具体类型的函数, 没有运行时开销. 编译结果类似这样: ```rust fn print_thing_i32(input: i32) { println!("{}", input); } fn print_thing_char(input: char) { println!("{}", input); } fn print_thing_str(input: &str) { println!("{}", input); } ``` F#的`inline`关键字做的是差不多的事. 在使用`inline`之前, 我们的定义是这样的: ```fsharp printNumber : number: int -> unit let printNumber number = printfn "%i" number let number = 9 printNumber number printNumber (byte number) ``` 把鼠标光标悬停在`printNumber`上面, 编辑器告诉我们它是`printNumber: number: int -> unit`. 但如果加上`inline`: ```fsharp let inline printNumber number = printfn "%i" number let number = 9 printNumber number printNumber (byte number) ``` 现在它变成了: ``` val printNumber : number:'a -> unit (requires 'a : (byte|int16|int32|int64|sbyte|uint16|uint32|uint64|nativeint|unativeint)) ``` 看起来这个泛型函数能处理任何整型输入, `inline`关键字让编译器给`printNumber number`单态化为一个接受`int16`的函数, 给`printNumber (byte number)`单态化一个接受`byte`的函数. # 模式匹配 Rust和F#都使用关键字`match`. `match`的用法很相似, 但有一些不同. 下面是第一个: 如果你不匹配所有情况, Rust会编译失败. F#能编译, 但会给你一个警告: Rust: ```rust enum Mood { Good, Okay, Bad } fn check_mood(mood: Mood) { match mood { Mood::Good => println!("Feeling good"), Mood::Bad => println!("Not so good") } } ``` Rust产生如下报错: ``` error[E0004]: non-exhaustive patterns: `Okay` not covered ``` 在F#类似的代码为: ```fsharp type Mood = | Good | Okay | Bad let myMood = Good let matchMood = match myMood with | Good -> printfn "Feeling good" | Bad -> printfn "Not so good" ``` 编译成功, 但我写这段文字的时候能看到: `Incomplete pattern matches on this expression. For example, the value 'Okay' may indicate a case not covered by the pattern(s).` 另外注意, F#代码的格式可以根据你的偏好而改变. 下面的代码是等价的: ```fsharp type Mood = | Good | Okay | Bad let myMood: Mood = Good let matchMood = match myMood with | Good -> printfn "Feeling good" | Bad -> printfn "Not so good" ``` *译注: F#有一个社区格式化工具fantomas* 注意这里我们指定了类型`let myMood: Mood = Good`. F#编译器通过类型推理判断`Good`属于哪个类型; Rust不这么做. 所以如果你定义了一个字段相同的联合: ```fsharp type Mood = | Good | Okay | Bad type Mood2 = | Good | Okay | Bad let myMood = Good let matchMood = match myMood with | Good -> printfn "Feeling good" | Bad -> printfn "Not so good" ``` 它会使`myMood`变成`Mood2`(它重影了`Mood`). 当心! 不管字段顺序如何, 这都会发生: ```fsharp type Mood = | Good | Okay | Bad type Mood2 = | Bad | Good let myMood = Good let matchMood = match myMood with | Good -> printfn "Feeling good" | Bad -> printfn "Not so good" ``` 这里`myMood`也是`Mood2`的实例. Rustacean更倾向于写`let myMood: Mood`, 但记住, 如果你定义了字段相同的类型, 并且不在let绑定中声明类型, F#编译器不会自动为你补上. *译注: 你可以给联合加上`[]`特性, 这样就必须使用类似Rust的限定访问: `Mood.Good`.* 有趣的是, F#还有一个`function`关键字, 它是`match (name) with`的缩写. ```fsharp type Options = | Sunny | Rainy | Other let message = function | Sunny -> printfn "It's sunny today" | Rainy -> printfn "It's rainy" | Other -> printfn "Not sure what the weather is." message Sunny ``` 这段代码打印"It's sunny today". 在[F#文档](https://docs.microsoft.com/zh-cn/dotnet/fsharp/language-reference/match-expressions)中, 你可以看到两者的例子: ```fsharp // Pattern matching with multiple alternatives on the same line. let filter123 x = match x with | 1 | 2 | 3 -> printfn "Found 1, 2, or 3!" | a -> printfn "%d" a // The same function written with the pattern matching // function syntax. let filterNumbers = function | 1 | 2 | 3 -> printfn "Found 1, 2, or 3!" | a -> printfn "%d" a ``` # 柯里化 Rust没有柯里化, 而在F#中它到处都是. 柯里化的意思是, 对于一个接受多个参数的函数, 可以一次只给它一个或部分参数. 对Rustacean来说, 它像是... ```rust fn add_three(num_one: i32, num_two: i32, num_three: i32) { num_one + num_two + num_three } ``` 但它的返回值不是`i32`, 而是函数运行所需的剩下参数. 给它一个`i32`(比如我们输入`add_three(8)`)然后它就知道了`num_one`的类型然后返回这个: ```rust fn two_left(num_two: i32, num_three: i32) { let num_one = 8; num_one + num_two + num_three } ``` (`two_left`这个名字只是个例子, 用来表示现在它长什么样) 现在如果你输入`two_left(9, 10)`, 它会完成函数调用, 输出`i32`, 返回最终结果27. 但如果只输入`two_left(9)`, 它会返回: ```rust fn one_left(num_three: i32) -> i32 { let num_one = 8; let num_two = 9; num_one + num_two + num_three } ``` 在F#中大概是这样: ```fsharp let addThree a b c = a + b + c let twoLeft = addThree 8 let oneLeft = twoLeft 9 printfn "%i" (oneLeft 10) ``` 有趣的事实: 所有F#函数都是柯里化的, 所以我们会看到这样的奇怪签名: ```fsharp val addThree : a: int -> b: int -> c: int -> int ``` 当你向其中传入三个数字, 它会依次用一个参数柯里化这个函数, 直到最后三个参数都有了对应的值, 最后返回`int`(我的讲解可能有点不对劲 - 如果是的话告诉我) 在Rust中, 函数签名则是这样的: ```rust fn add_three(a: i32, b: i32, c: i32) -> i32 ``` 注意F#不是为了耍酷才在参数间使用空格的: 它是柯里化语法. 如果你不想要函数被柯里化, 将参数放在元组里: ```fsharp let addThree (a, b, c) = a + b + c printfn "%i" (addThree (8, 9, 10)) ``` FSharper不喜欢把东西写在括号里(尤其是嵌套括号), 喜欢管道运算符. 下面的代码更好看: ```fsharp let addThree (a, b, c) = a + b + c let finalNumber = (8, 9, 10) |> addThree printfn "%i" finalNumber ``` 那么让我们来看看管道运算符. Rust也有对应的东西. # 管道运算符 Rust没有管道这个术语, 也没有`|>`运算符. 然而, 有些语法能模拟管道运算符的效果. 先来看看管道运算符是干什么的. F#里(几乎)所有东西都是表达式, 这些东西都会有返回值, 即使是`()`类型. `|>`帮你方便地把返回值传递给别的函数. 下面是一个例子: ```fsharp let addOne x = x + 1 let timesTwo x = x * 2 let printIt x = printfn "%A" x 8 |> addOne |> timesTwo |> printIt ``` 上面有三个函数, 每一个都分别执行某个操作: 一个加上1, 下一个乘以2, 最后一个把它打印出来. 顺便: `%A`美化打印元组, 记录和联合. 这类似于Rust的`Debug`特型, 后者使用`{:?}`(`Debug`打印)而非`{}`(`Display`打印)格式化说明符. 使用`%A`可以避免选择`%i`(int), `%s`(string)等. 另一个说明符`%O`可以打印所有对象. (自动调用了`ToString()`) Rust里对应(呃, 差不多对应)的东西是这样的: ```rust fn add_one(x: i32) -> i32 { x + 1 } fn times_two(x: i32) -> i32 { x * 2 } fn print_it(x: T) { println!("{:?}", x) } fn main() { print_it(times_two(add_one(8))); } ``` 这里我们得输入一排括号进行调用. `T: std::fmt::Debug`是泛型的, 意思是告诉编译器"我保证你得到的是可以用`Debug`打印的". 如果你想用F#式从左到右的语法, 你可以创建一个结构体给它定义方法. 但对于这种简单情况这样做不怎么值得. 然而, 这种语法经常同迭代器组合出现, 后者被从左到右传递. 迭代器操作大概是Rust最'管道式'的语法了. 比如, 下面的代码取0到10的数字, 乘以二, 保留偶数, 然后保存到`Vec`里. 就像这样: ```rust fn main() { let times_two_then_even: Vec = (0..=10) .map(|number| number * 2) .filter(|number| number % 2 == 0) .collect(); println!("{:?}", times_two_then_even); } ``` 打印出`[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]`. *译注: 相比F#, 这其实更是C#风格的代码. 上面的例子在C#里这么写:* ```csharp Enumerable.Range(0, 11) .Select(x => x * 2) .Where(x => x % 2 == 0) .ToList(); ``` *当然, F#也可以用C#的API:* ```fsharp open System.Linq (seq { 0..10 }) .Select(fun x -> x * 2) .Where(fun x -> x % 2 = 0) .ToList() ``` *另外, 还有一个社区的[FSharp.Core.Fluent](https://fsprojects.github.io/FSharp.Core.Fluent/)为F#内建类型扩展了这种语法.* F#版本很像: ```fsharp let timesTwoThenEven = [0..10] |> List.map (fun number -> number * 2) |> List.filter (fun number -> number % 2 = 0) printfn "%A" timesTwoThenEven ``` 输出:`[0; 2; 4; 6; 8; 10; 12; 14; 16; 18; 20]` 注意区别: 两者都用了闭包/匿名函数, 但写法分别为: Rust: `|variable_name_here| variable_name_here * 2` F#: `(fun variable_name_here -> variable_name_here * 2)` 在Rust中, `||`用于定义闭包, `()`用于定义一般函数, 而在F#中, 一般函数没有特别的语法, 闭包使用`fun`关键字. 在两个例子中, 你都要给输入命名, 以进行下一步操作. (顺便, Rust和F#都区分闭包(捕获了变量的函数)和匿名函数(不过是没有名字的函数), 但Rustacean倾向于把`||`包围的的东西都叫做闭包, 即使从技术角度来讲它是一个匿名函数. 甚至[书](https://doc.rust-lang.org/book/ch13-01-closures.html)上都说"**可以**捕获环境的匿名函数", 而非捕获**了**环境的匿名函数. 关于术语定义的讨论, 参见[这个issue](https://github.com/Dhghomon/easy_rust/issues/44)) Rust和F#还有一个重大区别: Rust有所谓"零开销抽象"的机制, 意味着优美的代码相比`for`循环等原始机制没有性能开销. 让我们再次考察Rust的`times_two_then_even`: ```rust let times_two_then_even: Vec = (0..=10) .map(|number| number * 2) .filter(|number| number % 2 == 0) .collect(); ``` 它返回一个`Vec`, 因为最后有`.collect()`方法. 如果我们不加上这个方法, 并且不保存到一个变量里的话: ```rust fn main() { (0..=10) .map(|number| number * 2) .filter(|number| number % 2 == 0); } ``` 编译成功, 但编译器说我们其实什么都没做: ``` warning: unused `Filter` that must be used --> src/main.rs:2:5 | 2 | / (0..=10) 3 | | .map(|number| number * 2) 4 | | .filter(|number| number % 2 == 0); | |__________________________________________^ | = note: `#[warn(unused_must_use)]` on by default = note: iterators are lazy and do nothing unless consumed ``` 这是因为调用迭代器方法而不`collect`它们或保存到变量只能造成一个复杂的复合类型; 我们还没有`map`或`filter`任何东西. 让我们先试着激怒编译器: ```rust fn main() { let times_two_then_even: i32 = (0..=10) // 告诉编译器这是i32 .map(|number| number * 2) .filter(|number| number % 2 == 0); } ``` 编译器抱怨道: ``` expected type `i32` found struct `Filter, [closure@src/main.rs:3:14: 3:33]>, [closure@src/main.rs:4:17: 4:41]>` ``` 所以我的得到的类型是`Filter>>`. 如果我们调用一大堆`.map`我们会得到一个很大的结构: ``` found struct `Map, [closur e@src/main.rs:3:14: 3:33]>, [closure@src/main.rs:4:14: 4:33]>, [closure@src/main .rs:5:14: 5:33]>, [closure@src/main.rs:6:14: 6:33]>, [closure@src/main.rs:7:14: 7:33]>, [closure@src/main.rs:8:14: 8:33]>, [closure@src/main.rs:9:14: 9:33]>, [c losure@src/main.rs:10:14: 10:33]>` ``` 这个结构体做好了求值的准备, 我们决定好要对它做什么了(比如收集到`Vec`里)它*才*会运行. 然而, F#的管道运算符在你使用的那一刻就会运行. 如果你这么写: ```fsharp let addOne x = x + 1; let number = addOne 9 |> addOne |> addOne |> addOne |> addOne |> addOne |> addOne ``` 它每次都会调用`addOne`. F#的迭代器函数也一样. 显然管道运算符的过度使用会导致较差的性能, 尽管f#已经很快了. (比C#慢一点, 甩了Python之类的几条大街) Rust的性能与C和C++比肩. *译注: 此处有误. Rust的例子展示的不是零开销抽象, 而且惰性迭代器机制在C#, Java等语言里也有出现. 至于F#, `List`和`Array`模块的函数是急性求值的, 但`Seq`模块的不是:* ```fsharp let timesTwoThenEven = seq { 0..10 } |> Seq.map (fun number -> number * 2) |> Seq.filter (fun number -> number % 2 = 0) |> Seq.toList ``` *问题不在于管道运算符, 而在于管道运算符调用的函数. `|>`在标准库中的全部定义如下:* ```fsharp let inline (|>) arg func = func arg ``` F#还有一个所谓"前向组合运算符", 写作`>>`, 是一个将多个函数组合到一起的优雅方式. 它与管道运算符类似, 但是是将多个组合转化为单个函数. 下面是一个简单例子: ```fsharp let addOne x = x + 1 let timesTwo x = x * 2 let printOut x = printfn "%i" x let addMultiplyPrint = addOne >> timesTwo >> printOut addMultiplyPrint 9 ``` 这段代码打印`20`. 偶尔这个写法很有用, 但一般管道运算符就足够清晰了, 而且允许你进行一些改动(比如你可以把`let addOne x = x + 1`改成`let addNum x y = x + y`) F#还有一个从右到左的管道运算符`<|`. F#程序员不喜欢过多使用它, 说它只有在的确能够提升可读性的罕见情况下才能使用. 组合两种管道运算符能产生一个奇怪的语法: ```fsharp let printTwo x y = printfn "%i and %i" x y 8 |> printTwo <| 9 ``` 编译器接受这段代码, 打印出`8 and 9`. 但为什么不直接写`printTwo 8 9`呢? 当然这种写法偶尔很有意思. 一个例子: ```fsharp type Diplomat = { name: string message: string } let diplomat1 = {name = "Quintus Aurelius"; message = "We demand concessions!"} let diplomat2 = {name = "Argentyx"; message = "We would rather die!"} let meeting person1 person2 = printfn "%s says: %s" person1.name person1.message printfn "%s responds: %s" person2.name person2.message diplomat1 |> meeting <| diplomat2 ``` 两个使者在中间会面时, `diplomat1 |> meeting <| diplomat2`的写法比无聊的`meeting diplomat1 diplomat2`好看些. 结果当然是一样的: ``` Quintus Aurelius says: We demand concessions! Argentyx responds: We would rather die! ``` 等等, `type Diplomat`是干什么的? 让我们来看一下: # Rust结构体, F#记录和结构体 Rust中的主要自定义类型是结构体, 语法看起来和我们刚才用F#定义的很像. 让我们考察Rust的`Diplomat`定义, 并回顾F#版本: ```rust struct Diplomat { name: String, message: String } ``` ```fsharp type Diplomat = { name: string message: string } ``` 注意Rust中的字段是用逗号分隔的, 而F#的是换行. 如果你想在一行内定义类型, 可以使用分号分隔: ```fsharp type Diplomat = { name: string; message: string } ``` Rust程序员会注意到我们在创建`Diplomat`时根本没有给出类型名. 我们不过写了: ```fsharp let diplomat1 = {name = "Quintus Aurelius"; message = "We demand concessions!"} ``` *译注: 你可以在初始化字段前显式写上类型, 比如`{Diplomat.name = "Quintus Aurelius"; message = "We demand concessions!"}`* 这又是F#编译器根据字段名推断类型的一个例子. 推理规则并*不够*深. 比如, 如果我们定义一个`Diplomat2`类型: ```fsharp type Diplomat = { name: string; message: string } type Diplomat2 = { name: string; message: string } ``` `meeting`函数会认为我们想使用`Diplomat2`, 因为后者重影了前者: ```fsharp let meeting person1 person2 = printfn "%s says: %s" person1.name person1.message printfn "%s responds: %s" person2.name person2.message ``` 但这么定义类型会导致错误! ```fsharp type Diplomat = { name: string; message: string } type MessagelessDiplomat = { name: string } ``` 这里编译器认为这个函数使用`MessgelessDiplomat`, 而它没有`message`字段, 所以报错了. Rustacean会更喜欢这么定义函数: ```fsharp let meeting (person1: Diplomat) (person2: Diplomat) = printfn "%s says: %s" person1.name person1.message printfn "%s responds: %s" person2.name person2.message ``` 你还可以在F#中用`||`定义匿名记录: ```fsharp let diplomat1 = {| name = "Marcus Aurelius" message = "The Emperor demands tribute." |} printfn "%s says: %s" diplomat1.name diplomat1.message ``` 如果你想给记录加上方法, 使用`member`关键字, 然后是`this`和方法体. ```fsharp type Diplomat = { name: string message: string } member this.Talk = printfn "%s says: %s" this.name this.message ``` *译注: `member`后的`this`可以为任意变量名或`_`, 比如* ```fsharp member self.Talk = printfn "%s says: %s" self.name self.message ``` *如果使用`_`, 就不能在方法体中引用`this`参数.* *另外, 没有参数列表的成员准确地说不是方法, 而是属性. 在上面的例子中, 编译器会生成一个`get_Talk()`方法, 对`Talk`属性的访问调用相当于调用了`get_Talk`方法. 一般来说, 属性应是无副作用的简单操作, 如访问自己的某个字段或该字段上的某个属性. 上面的例子最好改成:* ```fsharp member this.Talk() = printfn "%s says: %s" this.name this.message ``` 现在我们的外交会议变成了这样: ```fsharp type Diplomat = { name: string message: string } member this.Talk = printfn "%s says: %s" this.name this.message let meeting (person1: Diplomat) (person2: Diplomat) = person1.Talk person2.Talk let diplomat1 = {name = "Quintus Aurelius"; message = "We demand concessions!"} let diplomat2 = {name = "Argentyx"; message = "We would rather die!"} diplomat1 |> meeting <| diplomat2 ``` 作为对比, 我们再来看一下Rust的结构体. 回顾上面的`Diplomat`定义: ```rust struct Diplomat { name: String, message: String } ``` 实例化结构体时, 你先指明你再创建的是`Diplomat` - Rust编译器不会根据字段来为你判断类型. 另外不要忘了分号. 这么写是没用的: ```rust struct Diplomat { name: String, message: String } fn main() { let diplomat1 = Diplomat{ name: "Quintus Aurelius".to_string(), message: "We demand concessions!".to_string() } } ``` 幸运的是, Rust编译器会猜出你要做上面. 下面是编译器信息: ``` error: expected `;`, found `}` --> src/main.rs:10:6 | 10 | } | ^ help: add `;` here 11 | } | - unexpected token ``` 挺不错的. 另外上面代码的格式有点难看. 输入`cargo fmt`(或在Playground点击Rustfmt按钮)让它好看一点. ```rust struct Diplomat { name: String, message: String, } fn main() { let diplomat1 = Diplomat { name: "Quintus Aurelius".to_string(), message: "We demand concessions!".to_string(), }; } ``` 好多了! 另外注意: 分号对于Rust是必需的, 因为它是一个基于表达式的语言. Rust会将表达式的最后一部分视为返回值, 但我们没有把`Diplomat`返回, 所以加上分号返回一个`unit`. F#程序员可能会好奇`.to_string()`是干什么的, 以及为什么Rust需要它. 这是因为Rust有很多个`String`类型, 这里用到的是最简单的: 它和F#的`string`类似, 是一个自我所有的集合, 此处它是一个`u8`字节的集合. (我们在上文看到过它的定义了) 没有`.to_string()`方法, 我们处理的则是"&str", 它是一个借用引用 - 没有所有权的类型. 它本质上是字符串的不可变视图. 因为它没有所有权, 编译器会拒绝下面的定义: ```rust struct Diplomat { name: &str, message: &str, } fn main() { let diplomat1 = Diplomat { name: "Quintus Aurelius", message: "We demand concessions!", }; } ``` 因为`Diplomat`没有其数据的所有权, 你可能会传入为另一个对象所有的数据, 后者可能在某一时刻被析构回收, 然后你的引用就会指向一块无效内存. 编译器提示你注明数据会存在多久: ``` error[E0106]: missing lifetime specifier --> src/main.rs:2:11 | 2 | name: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 | struct Diplomat<'a> { 2 | name: &'a str, | ``` 这条信息指出了你要做什么才能让代码通过编译. 它说"有个生命周期, 叫<'a>, Diplomat实例能存活这么久, 你得传入也能存活这么久的`&str`参数."" 顺带一提, 两个语言都用尖括号`<>`来表示泛型, 但有些微区别: `<'a>`: 在Rust中, 这是一个生命周期标记: "这个对象的生命周期至少为'a." `<'a>`: 在F#中, 这是一个泛型参数. ``: 在Rust中, 这是一个泛型参数. 简而言之, 初学Rust时始终用`String`, 直到你学到了生命周期. `&str`性能比`String`更好, 但不灵活. 回到结构体. Rust如何添加方法呢? 它使用所谓`impl`块. `impl`块与结构体定义是分离的, 你可以定义任意多个. 在`impl`中添加方法. 你可以定义四种方法: - 接受`Self`的方法(对应F#`this`): 它们有结构体的所有权! 结构体会存活到函数结束. 这些方法常用于builder模式, 引入一个对象, 进行一些改动, 然后把`Self`返回回去. 这有点F#风格, 它能将方法互相串联起来. 让我们考察这种写法. 下面是一个简单的结构体`City`: ```rust struct City { name: String, population: i32 } ``` 给它一些方法. 开始一个`impl`块: ```rust impl City { } ``` 加上一些方法. 先是一个`new`方法. 注意: `new`不是Rust的关键字 - 我们也可以叫它`neu`或`nouveau`什么的. ```rust fn new() -> Self { // 也可以写City Self { name: "".to_string(), population: 0 } } ``` 因为这个方法不接受`self`(或`&self`或`&mut self`)参数, 它被称作关联方法, 要这么调用: `City::new()`. 这是因为不能用点号操作符在某个`City`实例上调用它. 这样就创建了一个没有名字, 人口为0的城市. 现在加上两个接受self参数并将其修改并返回的方法: ```rust fn population(mut self, population: i32) -> Self { self.population = population; self } fn name(mut self, name: &str) -> Self { self.name = name.to_string(); self } ``` (注意, 也可以用`&str`, 因为我们不关心函数结束了以后要用它干什么. 此处无需关心生命周期, 它是一个为结构体所有的`String`) 有了这些定义后, 就可以用F#式的语法建造一座城市了: ```rust fn main() { let city = City::new().population(100).name("My city"); } ``` 当然, 一般`new`方法会更接近这样的: ```rust fn new(population: i32, name: &str) -> Self { Self { population, name: name.to_string() } } ``` 为什么不写`population: population`呢? 我们可以这么做, 但如果字段名和变量名正好一样的话, 写`population`就够了. 这里不使用F#式的用法了, 所以传入方法的是`&self`和`&mut self`. `self`是一个指向自身的引用(可读但不可写)而`&mut self`是指向自身的可写引用. 显然, 引用必须遵循一些规则以避免异常行为. 1. 你可以有任意多个不可变引用. 2. 你最多有一个可变引用. 3. 如果你有一个可变引用, 其他的引用都被冻结了: 只要这个可变引用还在, 没有办法使用其他可变或不可变的引用. 下面是不能编译的代码的例子: ```rust fn main() { let mut my_string = "I am a string".to_string(); let x = &my_string; let y = &mut my_string; println!("{}", x); } ``` 一旦将`x`声明为`my_string`的引用, 它就期望一个`String`, 说"我是字符串". 然而, 在下一行`y`出现了, 它能修改`x`引用的要在两行后打印的数据. 这是不对的, 编译器会这么告诉我们: ``` error[E0502]: cannot borrow `my_string` as mutable because it is also borrowed as immutable --> src/main.rs:4:13 | 3 | let x = &my_string; | ---------- immutable borrow occurs here 4 | let y = &mut my_string; | ^^^^^^^^^^^^^^ mutable borrow occurs here 5 | 6 | println!("{}", x); | - immutable borrow later used here ``` 注意上面说"immutable borrow later used here", 因为编译器很聪明, 不仅能追踪引用的创建, 还能追踪它们是否被使用. 如果我们把第六行注释了它就能允许, 因为尽管我们创建了能存活到作用域结束的引用, 编译器能看出它们从未使用, 所以不会抱怨. (几年前编译器还做不到这个, 总是会对此类引用的*存在*报错, 而不是追踪它们的实际使用) # 可变性 Rust和F#都用默认不可变的let绑定绑定变量. 在Rust中, `mut`关键字用以将某项声明为可变的. F#的关键字更长: `mutable`. 这符合F#的"尽可能避免可变性"的设计哲学. 在Rust中它不过是一种常识性的默认行为 - 如果你不必修改它的话, 干嘛声明为可变呢? 在Rust中修改可变变量使用`=`和新值. 注意着里没有`let`关键字! 再次使用`let`会绑定一个新的变量重影原来的. `=`不允许你改变变量原有的类型. ```rust let mut x = 7; x = 8; ``` 在F#中, 用`<-`操作符修改可变变量的值. ```fsharp let mutable x = 7 x <- 8 ``` # 代码顺序 在Rust中, 代码顺序只在内部作用域中有用, 外部的代码不关心顺序. 一个典型的程序会包含这样的代码: ```rust struct SomeStruct { field: i32 } enum SomeChoices { Good, Bad } fn do_thing() { // 做这件事 } fn main() { // 做那件事 } ``` 但你也可以轻易地将结构体, 枚举和函数定义放在`main`下面, 没有问题. 唯一的例外是宏定义. (宏是类似函数的构造, 但可以在编译开始之前扩展为源代码, 用一个`!`调用 - 后文详述) 下面的代码能编译: ```rust macro_rules! my_macro { () => { println!("You didn't give me anything"); }; ($a:expr) => { { println!("Here's your expression back"); $a } }; } fn main() { let x = my_macro!(); let z = my_macro!(9); } ``` 但一旦把宏定义移到`main`下面, `my_macro()!`就不能被识别了. 至于F#, 顺序永远是必要的. 如果你有一个这样的`Customer`记录: ```fsharp type Customer = { name: string accountBalance: float } let billy = { name = "Billy Brown" accountBalance = 100.00 } ``` 然后把定义放到`let billy`下面的话, 编译器会报错. 不仅如此: 构成F#项目的源代码文件的顺序也很重要! 如果你有多个文件, 必须要把它们依序声明. 这是出于F#的"data in data out"原则, 每件事都必须按顺序完成. 除了命名结构体, Rust还有元组结构体. 它们主要有两个用途: - 不需要字段名的简单结构体. ```rust struct RGB(u8, u8, u8); fn main() { let my_colour = RGB(80, 65, 0); } ``` - 新类型. 类型别名可以用`type`关键字声明: ```rust type VecOfInts = Vec; fn main() { let some_vec: VecOfInts = vec![8, 9, 10]; } ``` 这个类型100%等价于`Vec`. 但包含了另一个类型的结构体则是一个新类型. 尽管你可以访问其底层类型的方法(通过`.0`), 它事实上是不同的类型, 可以在上面实现其他特型. 下面的代码就不行了: ```rust struct VecOfInts(Vec); fn main() { let some_vec: VecOfInts = vec![8, 9, 10]; } ``` 但这个可以: ```rust struct VecOfInts(Vec); fn main() { let some_vec = VecOfInts(vec![8, 9, 10]); } ``` 既然`VecOfInts`有自己的类型, 这个不行: ```rust println!("{:?}", some_vec); ``` 而这个可以: ```rust println!("{:?}", some_vec.0); ``` FSharper听到这里可能已经启动了, 因为F#程序员*大量*使用这样的新类型, 随时定义它们, 大大增强了类型安全性和可读性. 下面是一个例子: ```fsharp type Temperature = | Celsius of float | Fahrenheit of float | Kelvin of float | Réaumur of float let getTemperature temperature = match temperature with | Celsius number -> printfn "%.1f Celsius" number | Fahrenheit number -> printfn "%.1f Fahrenheit" number | Kelvin number -> printfn "%.1f Kelvin" number | Réaumur number -> printfn "%.1f Réaumur" number getTemperature (Réaumur 9.0) let temperature = Réaumur 9.0 ``` 可以看到`Temperature`就像一个枚举, 你可以对它模式匹配. `Temperature`的用例还可以包含值, 尽管写法和Rust一些不一样. 下面是Rust中功能相同的代码. ```rust enum Temperature { Celsius(f64), Fahrenheit(f64), Kelvin(f64), Reaumur(f64) } impl Temperature { fn get_temperature(&self) { use Temperature::*; match self { Celsius(number) => println!("{} Celsius", number), Fahrenheit(number) => println!("{} Fahrenheit", number), Kelvin(number) => println!("{} Kelvin", number), Reaumur(number) => println!("{} Réaumur", number), } } } fn main() { let temperature = Temperature::Reaumur(9.0); temperature.get_temperature(); } ``` 让我们来比较一下两者. 先是Temperature type/enum: ```fsharp type Temperature = | Celsius of float | Fahrenheit of float | Kelvin of float | Réaumur of float ``` ```rust enum Temperature { Celsius(f64), Fahrenheit(f64), Kelvin(f64), Reaumur(f64) } ``` 模式匹配几乎是一样的, 尽管在F#中它是独立的函数, 而Rust中是方法. 在两个语言中你都可以做相对的事(在F#中写方法, 在Rust中写独立函数), 但Rust中方法更常用(一个原因是[方便处理引用](https://doc.rust-lang.org/nomicon/dot-operator.html)), 而F#中的函数能自由传递调用, 不受到关联类型的束缚. ```fsharp let getTemperature temperature = match temperature with | Celsius number -> printfn "%.1f Celsius" number | Fahrenheit number -> printfn "%.1f Fahrenheit" number | Kelvin number -> printfn "%.1f Kelvin" number | Réaumur number -> printfn "%.1f Réaumur" number ``` ```rust impl Temperature { fn get_temperature(&self) { use Temperature::*; match self { Celsius(number) => println!("{} Celsius", number), Fahrenheit(number) => println!("{} Fahrenheit", number), Kelvin(number) => println!("{} Kelvin", number), Reaumur(number) => println!("{} Réaumur", number), } } } ``` 注意: - `get_temperature`接受`self`引用, 因为我们只需要读取数据, 不需要析构它. - 我们用`use Temperature::*;`引入枚举的所有分支(类似F#的`open` *译注: 应该是`open type`*)来避免`Temperature::Celsius(number), Temperature::Fahreinheit(number)`这样长的代码. 这不是必要的, 但可以减少打字的负担. - 标识符Réaumur被写作Reaumur. Rust还在推进[非ASCII标识符](https://doc.rust-lang.org/stable/unstable-book/language-features/non-ascii-idents.html)功能, 目前它还是一项不稳定的特性. 迟早它会成为稳定特性的. *译注: 目前已稳定.* # 集合类型 下面是两个语言集合类型的快速概览: **主要类型** Rust: Vec, array, tuples F#: list, array, sequence, tuples ## `Vec` 前已述及, .NET的`string`是*Unicode字符序列*, 而Rust的`Vec`比较符合序列的定义, `Vec`, `Vec`, `Vec>`等等. 而Rust的`String`实际上就是[Vec](https://doc.rust-lang.org/src/alloc/string.rs.html#279-281)(而非chars). 这就是为什么下面的F#代码会对Rustacean很熟悉: ```fsharp let printChar (str : string) (index : int) = printfn "First character: %c" (str.Chars(index)) ``` 它把字符串分解成`char`序列, 然后打印出字符串的第一个字符. *译注: 原文如此* 在Rust中, 你会看到这样的代码: ```rust fn print_char(input: &str, index: usize) { let input = input.to_string(); println!("First character: {}", input.chars().nth(index).unwrap_or(' ')); } fn main() { print_char("Hey there", 8); } ``` (`unwrap_or()`方法的意思是, 如果参数是`Some`的话, 返回`Some`的值, 否则返回默认值, 另一个更简单的的`.unwrap()`方法在`None`的情况下会panic) *译注: `unwrap_or()`同`Option.defaultValue`.* 然后是`Vec`本身: 它们通过`Vec::new()`(空`Vec`)或`Vec::from()`(如果你有可以转化为`Vec`的东西的话, 比如数组), 或最简单的`vec![]`宏来创建. 你得注明类型才能让下面的代码通过编译: ```rust fn main() { let mut x = vec![]; } ``` 但只要向`Vec`中推入元素, 它就能推断出类型了: ```rust fn main() { let mut x = vec![]; x.push(9); } ``` 参见[文档](https://doc.rust-lang.org/std/vec/struct.Vec.html)中`Vec`的所有方法, 注意左边栏. 最常见的包括: `push`: 如上, 将单个元素推入`Vec`. `with_capacity`: 每个`Vec`初创时容量都为0, 然后是4, 然后每次需要扩容时都会倍增. 你可以用下面的代码观察这个机制, 它追踪了`Vec`的容量, 当其倍增时打印. ```rust fn main() { let mut new_vec = vec![]; let mut current_capacity = new_vec.capacity(); for _ in 0..100_000 { new_vec.push('a'); if new_vec.capacity() != current_capacity { println!("Capacity is now {}", new_vec.capacity()); current_capacity = new_vec.capacity(); } } } ``` 看看这些内存重新分配! 在这种情况下你还不至于担心性能, 但如果你没理由重新分配的话, 就不要这么做: ``` Capacity is now 4 Capacity is now 8 Capacity is now 16 Capacity is now 32 Capacity is now 64 Capacity is now 128 Capacity is now 256 Capacity is now 512 Capacity is now 1024 Capacity is now 2048 Capacity is now 4096 Capacity is now 8192 Capacity is now 16384 Capacity is now 32768 Capacity is now 65536 Capacity is now 131072 ``` 把`let mut new_vec = vec![];`改成`let mut new_vec = Vec::with_capacity(100_000);`, 你就得到了不需要重新分配内存的`Vec`. (顺便, 如果你在此之后再推入一个'a', 它的容量就会变成200000.) `pop` - 返回一个`Option`(`T`是`Vec`储存的类型). 如果你确定是有值的话就`unwrap`它, 否则用模式匹配或`unwrap_or()`等能安全处理它的方法. `chunks` - 将你的`Vec`分段. 如果你有一个25元素的`Vec`, 然后调用`chunks(6)`, 你会得到四个6元素的块和一个1元素的块. `windows` - 类似`chunks`, 但每次只移动一个元素. 如果你有一个25元素的`Vec`, 然后调用`windows(6)`, 你会得到一个对应索引0 - 6的块, 然后是1 - 7的, 然后2 - 8的, 以此类推. `contains` - 返回`bool`, 表示它是否包含了某元素. `first`和`last` - 返回第一个或最后一个元素, 类型为`Option<&T>`(因为可能为空). &T代表指向元素的引用: 我们没有把它弹出来. `sort` - 自动给`Vec`排序. 用`sort_by`指明排序方法. 这就是Rust中最常用的集合类型了. 让我们转向F#中用的最多的: 列表. ## `List`(F#) F#的列表与Rust中的类似类型大有不同, 因为Rust多数时候避免链表的使用, 而这里的"列表"指的就是链表. Rust有一个[链表类型](https://doc.rust-lang.org/std/collections/struct.LinkedList.html), 但文档中明确建议不要使用它: *注意: 多数时候`Vec`或`VecDeque`更好, 因为基于数组的集合一般更快速, 更省内存, 能更好地利用CPU缓存*. 我从没用过, 也没见过用了的代码. 如果你好奇的话, 这里有个关于在Rust中实现链表的[警示故事](https://rust-unofficial.github.io/too-many-lists/): 语言设计本身就不适合实现链表, 并且它多数时候都没有什么好处. 我特别中意这篇文章的开头: ``` Just so we're totally 100% clear: I hate linked lists. With a passion. Linked lists are terrible data structures. Now of course there's several great use cases for a linked list: You want to do a lot of splitting or merging of big lists. A lot. You're doing some awesome lock-free concurrent thing. You're writing a kernel/embedded thing and want to use an intrusive list. You're using a pure functional language and the limited semantics and absence of mutation makes linked lists easier to work with. ... and more! But all of these cases are super rare for anyone writing a Rust program. 99% of the time you should just use a Vec (array stack), and 99% of the other 1% of the time you should be using a VecDeque (array deque). These are blatantly superior data structures for most workloads due to less frequent allocation, lower memory overhead, true random access, and cache locality. ``` 注意到`You're using a pure functional language and the limited semantics and absence of mutation makes linked lists easier to work with.`这句话了吗? 这就是为什么它在F#中很常见. 下面是它的用法: 列表用`[]`定义, 其中元素用分号分隔(因为它们是表达式). 或者用空白符: ```fsharp let myList = [8; 9; 10] let myList2 = [ 8 9 10 ] ``` 作为链表, 它们被分为头部和尾部. 头部是第一个元素, 尾部是剩下的. ```fsharp let myList = [8; 9; 10] printfn "Head: %i Tail: %A" myList.Head myList.Tail ``` 你也可以用范围来定义`[0..10]`. 注意这包含了范围的结尾10. 在Rust中`0..10`不包含结尾, 但`0..=10`包含. F#还有步长操作符, 放在中间. 比如`[0 .. 2 .. 10]`是一个从0开始, 步长为2, 一直到10的链表. 因为它们是由头部和尾部构成的, FSharper喜欢用递归函数来操作它们. 先操作头部, 然后把尾部放到函数里重新调用, 直到整个列表处理完毕. (遇到`[]`, 也就是空列表的时候处理完毕) 模式匹配的语法是`head::tail`(或者其他你喜欢的记法, `h::t`, `StartOfList::RestOfList`等, `::`才是关键). 对于Rustacean, 我认为下面的类型最接近F#的列表: ```rust #[derive(Debug)] struct FSharpList { head: Vec, // 一个保证拥有不超过一个元素的Vec tail: Vec } impl FSharpList { // 用它作为范围 fn new>(input: I) -> Self { let mut list = Self { head: vec![], tail: vec![] }; for i in input { if list.head.is_empty() { list.head.push(i); } else { list.tail.push(i); } } list } } fn main() { let fsharp_list = FSharpList::new(0..=10); println!("{:?}", fsharp_list); } ``` 打印: ```rust FSharpList { head: [0], tail: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } ``` `::`运算符就是解构`fsharp_list.head`和`fsharp_list.tail`到两个变量的简写. ```fsharp let list = [0..10] let head::tail = list printfn "%i and %A" head tail ``` 这会打印出: `0 and [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]`. (注意: 头部和尾部的类型在F#中分别为`'a`和`'a list`(比如`int`和`int list`), 但`Vec`相比`Option`实现头部更方便一些.) 让我们写一个函数, 打印列表的头部, 然后把剩下的再放回函数里. 下面的代码看起来是正确的: ```fsharp let myList = [0..5..25] let printList list = match list with | h::t -> printfn "Got a %i" h printList t | [] -> printfn "All done" printList myList ``` 但编译器不这么认为, 而且也没有提供什么有用的帮助信息: ``` The value or constructor 'printList' is not defined. Maybe you want one of the following: printf Printf printfn PrintfModule ``` 不, 你只需要加上一个`rec`关键字. 这样编译器就会在函数作用域内部看见函数本身的名字了. 加上`rec`关键字后, 它就能令人满意地工作: ```fsharp let myList = [0..5..25] let rec printList list = match list with | h::t -> printfn "Got a %i" h printList t | [] -> printfn "All done" printList myList ``` 它打印出: ``` Got a 0 Got a 5 Got a 10 Got a 15 Got a 20 Got a 25 All done ``` F#的列表是不可变的, 但你可以用`::`返回加入了新元素的新列表, 或者用`@`连接两个列表. ```fsharp let listOne = [0..3] // [0; 1; 2; 3] let listTwo = 8 :: listOne // [8; 0; 1; 2; 3] let twoListsTogether = listOne @ listTwo // [0; 1; 2; 3; 8; 0; 1; 2; 3] ``` `::`操作符又名 `cons operator`. 读过Rust之书的人可能会认出这是什么, 第15章[提到了它](https://doc.rust-lang.org/book/ch15-01-box.html#more-information-about-the-cons-list). 这个数据结构在Rust中很汉奸, 但引入了递归的概念以及Rust处理它的方法. Rust也有递归, 所以下面的函数能运行: ```rust fn print_vec(mut input: Vec) { if !input.is_empty() { // 注意! println!("{}", input.pop().unwrap()); print_vec(input); // 调用同一个函数 } else { println!("All done!"); } } fn main() { let my_vec: Vec = (1..10).rev().collect(); print_vec(my_vec); } ``` (顺便, `VecDeque`在头部和尾部都能高效地推入弹出, 但我们在例子中只调用`.rev()`来反转范围, 然后就创建了`Vec`) 但Rust不会被递归类型愚弄: ```rust struct Book { next_book: Option } ``` 它会抱怨, 因为这个类型需要无限的内存. ``` error[E0072]: recursive type `Book` has infinite size --> src/main.rs:1:1 | 1 | struct Book { | ^^^^^^^^^^^ recursive type has infinite size 2 | next_book: Option | ------------ recursive without indirection | help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `Book` representable | 2 | next_book: Box> | ^^^^ ^ ``` 所以它说的`Box`是什么? 它是指向堆数据的智能指针, 有了这个类型以后Rust就可以处理递归类型了: ``` At compile time, Rust needs to know how much space a type takes up. One type whose size can’t be known at compile time is a recursive type, where a value can have as part of itself another value of the same type. Because this nesting of values could theoretically continue infinitely, Rust doesn’t know how much space a value of a recursive type needs. However, boxes have a known size, so by inserting a box in a recursive type definition, you can have recursive types. ``` 加上`Box`后它就会这样, 然后编译器会乐意工作: ```rust struct Book { next_book: Box>, } fn main() { let my_book = Book { next_book: Box::new(Some(Book { next_book: Box::new(None) })), }; } ``` `Book`结构体没有什么实际意义, 不过是用来演示让编译器允许Rust中递归链表的例子. 如你所见, 编译器会确保你的每个数据类型的大小在编译时都是已知的, 并且每一个引用都不会破坏规则. (想知道如何绕过规则的话, 参考[此处](https://doc.rust-lang.org/book/ch15-04-rc.html). Rust比它给你的第一印象要灵活, 只要你知道如何确保不修改你不该碰的数据的话). # 特型(Rust)和操作符重载(F#) Rust的特型允许你在自定义类型上实现常用功能. 特型本质上是一组命名的方法, 你可以用这个名字来向编译器保证泛型函数的参数能够执行你想要的操作. 比如, 可能你有类型`struct City`, 想要将多个`City`加起来. 没有其他办法让编译器知道这一点, 你得给`City`实现`Add`特型. 接下来, 参考[`Add`特型的定义文档](https://doc.rust-lang.org/std/ops/trait.Add.html), 看看你要做什么. 如果你在右边看到了"Requied Method", 这就意味着你要自己实现这个函数. `Add`需要`fn add`, 文档中有一个示例实现: ```rust impl Add for Point { type Output = Self; fn add(self, other: Self) -> Self { Self { x: self.x + other.x, y: self.y + other.y, } } } ``` 这个特型还有一个*关联类型*: 这个类型与特型相伴. 你可以看到这一行: ```rust type Output = Self; ``` `Output`不是特殊的关键字: 它只是给这个关联类型挑选的名字. 返回类型可以为任意, 取决于你想要加什么. 你要返回同一类型的(`Self`)还是别的? 可以是一个`i32`或其他任何你想要的类型. 那么我们就来实现一个返回其他类型的`Add`吧. 我们会让`City`加起来返回`Metropolis`. ```rust struct City { name: String, population: u32 } #[derive(Debug)] // 可以打印它 struct Metropolis { cities: Vec, population: u32 } impl std::ops::Add for City { type Output = Metropolis; // 看, 输出类型不必是自身 fn add(self, other: Self) -> Metropolis { // 创建一个Vec把city放进去 let mut other_cities = Vec::new(); other_cities.push(self.name); other_cities.push(other.name); // +会返回这个 Metropolis { cities: other_cities, population: self.population + other.population } } } fn main() { // 创建塔林和赫尔辛基 let tallinn = City { name: "Tallinn".to_string(), population: 426_538 }; let helsinki = City { name: "Helsinki".to_string(), population: 631_695 }; // 合并为metropolis let talsinki = tallinn + helsinki; println!("{:?}", talsinki); } ``` 下面是输出: `Metropolis { cities: ["Tallinn", "Helsinki"], population: 1058233 }`. 现在再来看看F#的对应机制: [操作符重载](https://docs.microsoft.com/zh-cn/dotnet/fsharp/language-reference/operator-overloading). 看看如何在下面的例子中模拟上面的city; 下面是文档中的例子(重载了一系列操作符): ```fsharp type Vector(x: float, y : float) = member this.x = x member this.y = y static member (~-) (v : Vector) = Vector(-1.0 * v.x, -1.0 * v.y) static member (*) (v : Vector, a) = Vector(a * v.x, a * v.y) static member (*) (a, v: Vector) = Vector(a * v.x, a * v.y) override this.ToString() = this.x.ToString() + " " + this.y.ToString() let v1 = Vector(1.0, 2.0) let v2 = v1 * 2.0 let v3 = 2.0 * v1 let v4 = - v2 printfn "%s" (v1.ToString()) printfn "%s" (v2.ToString()) printfn "%s" (v3.ToString()) printfn "%s" (v4.ToString()) ``` 然后实现我们自己的`City`和`Metropolis`记录: ```fsharp // Declaring here first because // City will need to reference it type Metropolis = { cities: List population: int } type City = { name: string population: int } static member (+) (a, b: City) = let metropolis = { cities = [a.name; b.name] population = a.population + b.population } metropolis let tallinn = { name = "Tallinn"; population = 426_538 } let helsinki = { name = "Helsinki"; population = 631_695 } let metropolis = tallinn + helsinki printfn "%A" metropolis ``` 将鼠标悬停在`metropolis`上方, 显示`val metropolis : Metropolis`, 而`printfn`打印出了我们想要的结果: ``` {cities = [Tallinn; Helsinki]; population = 1058233} ``` *译注: F#的操作符重载原本只是为了和.NET兼容并用于少数场景, 但结合SRTP它可以有出人意料的强大功能, 一个名为[FSharpPlus](https://fsprojects.github.io/FSharpPlus/)的库用它实现了typeclass等机制.* 尽管在这个例子中很相似, Rust的特型的完全不同的东西. 下面是它们作为特型范围的例子: ```rust trait Duckish {} // 像鸭的 trait Doggish {} // 像狗的 ``` 只有像鸭子的生物才实现`Duckish`特型, 而像狗的实现`Doggish`. 这是因为我们要实现`fn bark`和`fn quack`, 不想让错误的类型被传入. 注意这些特型没有任何方法, 但没有关系, 因为它们只是范围. 全部代码如下: ```rust trait Duckish {} trait Doggish {} fn bark(_: &T) { // 传入一个引用 println!("RRRRRR bow woww!!!"); } fn quack(_: &T) { println!("KVAK!"); } struct Goose; // 上面都没有 impl Duckish for Goose {} // Goose有这个特型了 struct Coyote; impl Doggish for Coyote {} fn main() { let my_goose = Goose; let my_coyote = Coyote; quack(&my_goose); bark(&my_coyote); } ``` 输出是 ``` KVAK! RRRRRR bow woww!!! ``` # `map`, 迭代器, 等等 Rustacean和FSharper都很熟悉迭代器, `map`/`filter`/`fold`等, 通过不断应用这些操作来得到最终结果. 基于这一点, 本节的大部分内容将只是两个语言的简单例子, 展示它们分别是这么做到相同的事情的. ## 基本使用 在Rust中, 你要先选择何种迭代器: `iter()`是引用迭代器, `iter_mut()`是可变引用迭代器, `into_iter()`是可消耗的迭代器. 在F#中, 语法是先写类型名再写方法名. 比如, 用`List.map`映射一个`List`. *译注: `List.map`中的`List`不是类型名, 而是模块名. 前者是``FSharpList`1``, 后者是`ListModule`. 你也可以对`List`实例调用`Seq`模块的函数, 因为`'a list`实现了`'a seq`.* 如果你好奇的话, 可以参考[Rust的迭代器方法](https://doc.rust-lang.org/std/iter/trait.Iterator.html)(屏幕左边的**Provided Methods**)和[F#的列表函数](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-listmodule.html). 此外还有数组的方法等, 也在核心库参考中. 你会注意到有很多方法名字是一样的, 但有些方法一个语言有而一个没有, 所以如果你在进行大量迭代任务的话, 最好只简单浏览整个页面, 然后记下对你有用的少数方法. 先是两个语言都有的简单映射操作. 先创建有三个数字的`Vec`/`List`用于倍增. ```rust fn main() { let num_vec = vec![7, 8, 9]; let doubled_vec: Vec<_> = num_vec .into_iter() .map(|number| number * 2) .collect(); println!("{:?}", doubled_vec); } ``` 不出意外地打印了`[14, 16, 18]`. 注意以下几点: - 我们用`into_iter()`小号了`num_vec`. 所以以后就不能访问它了. - 我们只写了`Vec<_>`, 因为Rust能判断出它是`Vec`, 你也可以显式写`Vec`. - 我们在最后用了`collect`方法, 否则它就只是一个迭代器(它是惰性的, 除非被消耗, 否则不进行计算). F#代码更简单: ```fsharp let listOne = [7..9] let listTwo = listOne |> List.map (fun number -> number * 2) printfn "%A" listTwo ``` Rustacean要注意的只有`fun number -> number * 2`, 它相当于`|number| number * 2`. 看到`||`就要想到`fun`, 看到`fun`就要想到`||`. 最大的区别在于, Rust迭代器方法适用于任何实现了对应特型的类型, 而F#的方法只适用于内建类型. 实现特型需要定义`next()`方法. 它返回`Option`类型(`Item`是调用`.next()`返回的). 让我们给`Metropolis`实现`Iterator`. 每个`Metropolis`都是由多个城市组合而成的, 我们的Metropolis迭代器会返回它们的名字(如果有的话). 它类似于这样: *译注: 原文此处无代码.* 实现了`Iterator`后, 我们可以在实现`IntoIterator`, 因为`cities` `Vec`是可迭代对象而非迭代器. Rust文档指出, "通过给一个类型实现`IntoIterator`, 你定义它将如何被转化为一个迭代器. 这对于集合类型很常见." 所以调用`into_iter`后, 就创建了一个`Metropolis`的`Iterator`. 这个例子比第一个长, 但它不过是遵循实现`IntoIterator`的常见格式罢了. 你可以[在文档](https://doc.rust-lang.org/std/iter/trait.IntoIterator.html)看到另一个例子作为对比 - 它们几乎是一一对应的. ```rust struct Metropolis { cities: Vec, population: u32 } impl IntoIterator for Metropolis { type Item = String; type IntoIter = IntoIter; fn into_iter(self) -> Self::IntoIter { IntoIter(self) } } pub struct IntoIter(Metropolis); impl Iterator for IntoIter { type Item = String; fn next(&mut self) -> Option { if !self.0.cities.is_empty() { Some(self.0.cities.remove(0)) } else { None } } } ``` 好消息: 实现`IntoIterator`允许你使用`for`循环! 让我们在`main`里尝试一下: ```rust fn main() { let my_city = Metropolis { cities: vec!["Tallinn".to_string(), "Helsinki".to_string()], population: 1_000_000 }; for city in my_city { println!("{}", city); } } ``` 打印出: ``` Tallinn Helsinki ``` 在F#中, 每个集合都实现接口`seq<'T>`. `seq<'T>`相当于`IntoInterator`, 而`Iterator`相当于`System.Collections.Generic.IEnumerator<'T>`. 实现这些接口时, 你还得同时实现过时了的非泛型版本, 后者在2005年的.NET Framework 2.0中被泛型版本取代了. 在实践中, 你几乎从不用手动实现这些接口, 而是访问实际的集合并遍历它们. 另外, 这些重复代码也不多 - 不过四行罢了(`interface ... with / member GetEnumerator / interface ... with / member Current`). F#的`for`循环接受任意实现了`seq<'T>`接口的类型, 所以这个构造是库函数的一个替代手段. 与Rust`for`不同的是, F#`for`循环只接受可以按需创建迭代器的可迭代对象, 而非迭代器本身. *译注: `Seq`模块里的库函数和序列表达式`seq`也接受任意实现了`seq<'T>`接口的类型.* ```fsharp type Metropolis = { cities: string ResizeArray population: uint } with interface seq with member this.GetEnumerator() = new IntoIter(this) :> _ // :>运算符将IntoIter向下转型为接口要求的System.Collections.Generic.IEnumerator interface System.Collections.IEnumerable with member this.GetEnumerator() = new IntoIter(this) :> _ // :>运算符将IntoIter向下转型为接口要求的System.Collections.Generic.IEnumerator let mutable current = Unchecked.defaultof<_> interface System.Collections.Generic.IEnumerator with member _.Current = current member _.Dispose() = () // 在Rust中没有对应, 在遍历结束时调用 interface System.Collections.IEnumerator with member this.MoveNext() = // 与Rust的"nest"接近, 但返回的是结果是否为Some, 而结果被储存到一个私有可变字段中, 用Current属性读取 if metropolis.cities.Count > 0 then current <- metropolis.cities.[0] metropolis.cities.RemoveAt 0 true else false member _.Reset() = failwith "不支持复位, Rust和F#版本只允许遍历迭代器一次, 因为cities集合中的元素在遍历时被移除. F#版本可以使用ResizeArray的IEnumerator来支持多次遍历, 但这样就与Rust版本不一致了, 后者由编译器强迫单次遍历." member _.Current = current :> _ ``` *译注: 原文注释为"downcasts", 但`:>`实际是向上转型. 向下转型运算符是`downcast`或`:?>`.* ```fsharp let my_city = { cities = ResizeArray ["Tallinn"; "Helsinki"] population = 1_000_000u } for city in my_city do printfn "%s" city ``` ``` Tallinn Helsinki ``` Fold更有意思. 它的作用不只是把数字加在一起. 我们先使用较简单的例子. 下面是Rust中`fold`的简单应用: ```rust fn main() { let sum = (1..=10) .fold(0, |first_number, next_number| first_number + next_number); println!("{}", sum); } ``` FSharper会注意到参数顺序是反的: 先是初始值(这里是0), 然后是变量名, 最后是要执行的操作. ```fsharp let sum = [1..10] |> List.fold (fun firstNumber secondNumber -> firstNumber + secondNumber) 0 printfn "%i" sum ``` 而对于Rustacean来说, 你要这么做: 先是`List.fold`, 然后是`fun`, 给'变量'命名, 然后告诉它要做什么. 写完这些后, 再给函数初始值. 但有一个有趣的地方. 你可以这么写: ```fsharp let sum = [1..10] |> List.fold (+) 0 printfn "%i" sum ``` 哇! `(+)`做的就是将`+`运算符作为一个函数使用, 而既然它接受两个参数(并且fold*给出*两个参数), 它就能放在这里. 你可以看作是使用Rust的`add(x, y)`函数而非`x + y`, 因为`+`操作符在Rust里就是这么调用的. Rust: `(1..=10).fold(0, |a, b| Add::add(a, b))` F#: `[1..10] |> List.fold (+) 0` 区别在于, 函数参数是匿名传入的, `(+)`函数自会处理它们.