Hands-On Functional Programming in Rust 函数式编程 -对比(第一章)

函数式编程(FP)是仅次于面向对象编程(OOP)的第二种最流行的编程范例。多年来,这两种范式被分为不同的语言,以免混合。多范式语言试图支持这两种方法。 Rust就是这样一种语言。

作为一个广义的定义,函数式编程强调使用可组合和最大可重用函数来定义程序行为。使用这些技术,我们将展示函数式编程如何为许多常见但困难的问题调整巧妙的解决方案。本章将概述本书中介绍的大多数概念。其余章节将致力于帮助您掌握每种技术。

我们希望提供的学习成果如下:

  • 能够使用功能样式来减少代码重量和复杂性
  • 能够利用安全抽象编写健壮的安全代码
  • 能够使用功能原理设计复杂的项目

技术要求

运行提供的示例需要最新版本的Rust,可在此处找到:
https://www.rust-lang.org/en US / install.html

本章的代码也可以在GitHub上找到,这里:
https://github.com/PacktPublishing/Hands-On-Functional-Programming-in-RUST

每章的README.md文件中还包含特定的安装和构建说明。

减少代码重量和复杂性

函数式编程可以大大减少完成任务所需的代码量和复杂性。特别是在Rust中,功能原则的正确应用可以简化通常复杂的设计要求,并使编程成为更高效和有益的体验。

使泛型更通用

使泛型更通用涉及参数化源自函数语言的数据结构和函数的实践。在Rust和其他语言中,这称为泛型。类型和功能都可以参数化。可以对通用类型施加一个或多个约束以指示特征或寿命的要求。

没有泛型,结构定义可能变得多余。以下是定义Point的常见概念的三个结构的定义。但是,结构使用不同的数字类型,因此在intro_generics.rs中将单数概念扩展为三个单独的PointN类型定义:

struct PointU32
{
x: u32,
y: u32
} s
truct PointF32
{
x: f32,
y: f32
} s
truct PointI32
{
x: i32,
y: i32
}

相反,我们可以使用泛型来删除重复的代码并使代码更健壮。通用代码更容易适应新要求,因为可以参数化许多行为(以及需求)。如果需要更改,最好只更改一行而不是一百行。

此代码段定义了参数化的Point结构。现在,单个定义可以捕获intro_generics.rs中Point的所有可能的数值类型:

struct Point
{
x: T,
y: T
}

没有泛型,函数也存在问题。

这是一个简单的函数来对数字进行平方。但是,为了捕获可能的数值类型,我们在intro_generics.rs中定义了三个不同的函数:

fn foo_u32(x: u32) -> u32
{
x*x
} f
n foo_f32(x: f32) -> f32
{
x*x
} f
n foo_i32(x: i32) -> i32
{
x*x
}

函数参数(例如此函数)可能需要特征边界(指定一个或多个特征的约束)以允许在函数体中使用的该类型上的任何行为。

这是foo函数,使用参数化类型重新定义。单个函数可以定义所有数字类型的操作。必须为intro_generics.rs中的基本操作设置显式边界,例如乘法或甚至复制:

fn foo(x: T) -> T
where T: std::ops::Mul + Copy
{
x*x
}

甚至函数也可以作为参数发送。我们称之为高阶函数。

这是一个接受函数和参数的普通函数,然后用参数调用函数,返回结果。注意特征绑定Fn,表明提供的函数是一个闭包。对于可调用的对象,它必须在intro_generics.rs中实现fn,Fn,FnMut或FnOnce特征之一:

fn bar(f: F, x: T) -> T
where F: Fn(T) -> T
{
f(x)
}

作为值的函数

函数名义上是函数式编程的重要特征。具体而言,作为值的函数是整个范式的基石。我们还将在此处介绍术语“闭包”以供将来参考。闭包是一个充当函数的对象,实现fn,Fn,FnMut或FnOnce。

可以使用内置闭包语法定义简单闭包。此语法也是有益的,因为如果允许,将自动实现fn,Fn,FnMut和FnOnce特征。此语法非常适合数据的速记操作。

这是一个0到10范围内的迭代器,映射到平方值。使用发送到迭代器的map函数的内联闭包定义来应用square操作。这个表达式的结果将是一个迭代器。这是intro_functions.rs中的表达式:

(0..10).map(|x| x*x);

如果使用块语法,则闭包还可以具有带语句的复杂主体。

这是一个从0到10的迭代器,用复数方程映射。提供给map的闭包包括函数定义和intro_functions.rs中的变量绑定:

(0..10).map(|x| {
fn f(y: u32) -> u32 {
y*y
}l
et z = f(x+1) * f(x+2);
z*z
}

可以定义接受闭包作为参数的函数或方法。要将闭包用作可调用函数,必须指定Fn,FnMut或FnOnce的边界。

这是一个接受函数g和参数x的HoF定义。该定义约束g和x来处理u32类型,并定义了一些涉及对g的调用的数学运算。还使用intro_functions.rs中的简单内联闭包定义提供了对f HoF的调用,如下所示:

fn f(g: T, x: u32) -> u32
where T: Fn(u32) -> u32
{
g(x+1) * g(x+2)
} f
n main()
{
f(|x|{ x*x }, 2);
}

标准库的许多部分,特别是迭代器,鼓励大量使用函数作为参数。

这是一个从0到10的迭代器,后跟许多链式迭代器组合器。 map函数从原始函数返回一个新值。 inspect查看一个值,不会改变它,但允许副作用。 filter省略了所有不满足谓词的值。 filter_map使用单个函数过滤和映射。折叠将所有结果从初始值开始,从左到右工作,将所有结果减少为单个值。这是intro_functions.rs中的表达式:

(0..10).map(|x| x*x)
.inspect(|x|{ println!("value {}", *x) })
.filter(|x| *x<3)
.filter_map(|x| Some(x))
.fold(0, |x,y| x+y);

迭代器

迭代器是OOP语言的一个共同特征,Rust很好地支持这个概念。 Rust迭代器的设计也考虑了函数式编程,允许程序员编写更易读的代码。这里强调的具体概念是可组合性。当迭代器可以被操作,转换和组合时,for循环的混乱可以被单独的函数调用替换。可以在intro_iterators.rs文件中找到这些示例。如下表所示:

带描述的函数名称 例子
链连接两个迭代器:第一个…第二个 (0..10).chain(10..20);
zip函数将两个迭代器组合成元组对,迭代直到最短迭代器的结尾:(a1,b1),(a2,b2),… (0..10).zip(10..20);
枚举函数是zip的一个特例,它创建了编号元组(0,a1),(1,a2),… (0..10).enumerate();
inspect函数在迭代期间将函数应用于迭代器中的所有值 (0..10).inspect(|x| {println!("value {}",*x) });
map函数将函数应用于每个元素,并将结果返回到位 (0..10).map(|x|x*x);
过滤函数将元素限制为满足谓词的元素 (0..10).filter(|x| *x<3);
fold函数将所有值累积到单个结果中 (0..10).fold(0, |x,y| x+y);
如果要应用迭代器,可以使用for循环或调用collect for i in (0..10) {}(0..10).collect::>();

紧凑清晰的表达

在函数式语言中,所有术语都是表达式。函数体中没有语句,只有一个表达式。然后将所有控制流操作符公式化为具有返回值的表达式。在Rust,几乎就是这种情况;唯一的非表达式是let语句和项声明。

这两个语句都可以用块包装,以创建表达式以及任何其他术语。在intro_expressions.rs中有以下示例:

let x = {
fn f(x: u32) -> u32 {
x * x
}l
et y = f(5);
y * 3
};

这种嵌套格式在野外是不常见的,但它说明了Rust语法的宽容性。

回到功能样式表达的概念,重点应该始终是写出易读的文字代码而不会有太多的麻烦或臃肿。当其他人或您以后来阅读您的代码时,应立即理解。理想情况下,代码应该记录自己。如果您发现自己经常编写代码两次,一次在代码中,再次作为注释,那么您应该重新考虑编程实践的有效性。

从函数表达式的一些示例开始,让我们看一下大多数语言中存在的表达式,即三元条件运算符。在正常的if语句中,条件必须占用自己的行,因此不能用作子表达式。

以下是传统的if语句,在intro_expressions.rs中初始化变量:

let x;
if true {
x = 1;
} else {
x = 2;
}

使用三元运算符,可以将此分配移动到单行,如下所示intro_expressions.rs:

let x = if true { 1 } else { 2 };

几乎来自Rust中OOP的每个语句也都是表达式 - if,for,while等等。在Rust中看到的一个在OOP语言中不常见的唯一表达式是直接构造函数表达式。所有Rust类型都可以通过单个表达式实例化。只有在特定情况下才需要构造函数,例如,当内部字段需要复杂的初始化时。以下是intro_expressions.rs中的简单结构和等效元组:

struct MyStruct
{
a: u32,
b: f32,
c: String
} f
n main()
{
MyStruct {
a: 1,
b: 1.0,
c: "".to_string()
};
(1, 1.0, "".to_string());
}

功能语言的另一个独特表达是模式匹配。模式匹配可以被认为是switch语句的更强大的版本。任何表达式都可以发送到模式表达式中,并且可以在执行分支表达式之前将内部信息绑定到局部变量中。模式表达式非常适合使用枚举。两者完美配对。

以下代码段将Term定义为表达式选项的标记并集。在main函数中,构造Term t,然后与模式表达式匹配。请注意标记联合的定义与intro_expressions.rs中模式表达式内部的匹配之间的语法相似性:

enum Term
{
TermVal { value: String },
TermVar { symbol: String },
TermApp { f: Box, x: Box },
TermAbs { arg: String, body: Box }
} f
n main()
{
let mut t = Term::TermVar {
symbol: "".to_string()
};
match t {
Term::TermVal { value: v1 } => v1,
Term::TermVar { symbol: v1 } => v1,
Term::TermApp { f: ref v1, x: ref v2 } =>
"TermApp(?,?)".to_string(),
Term::TermAbs { arg: ref mut v1, body: ref mut v2 } =>
"TermAbs(?,?)".to_string()

};
}

严格的抽象意味着安全的抽象

拥有更严格的类型系统并不意味着代码会有更多要求或更复杂。不要严格打字,而应考虑使用术语表达式打字。表达式输入为编译器提供了更多信息。这些额外信息允许编译器在编程时提供额外的帮助。这些额外信息还允许非常丰富的元编程系统。这是除了更安全,更健壮的代码的明显好处之外的所有内容。

范围数据绑定

Rust中的变量比大多数其他语言更严格。全局变量几乎完全不被允许。密切关注局部变量,以确保在超出范围之前正确解构分配的数据结构,但不会更快。跟踪变量适当范围的这一概念称为所有权和生命周期。

在一个简单的示例中,分配内存的数据结构将在超出范围时自动解构。 intro_binding.rs中不需要手动内存管理:

fn scoped() {
vec![1, 2, 3];
}

在稍微复杂的示例中,分配的数据结构可以作为返回值传递或引用,依此类推。简单范围的这些例外也必须在intro_binding.rs中考虑:

fn scoped2() -> Vec
{
vec![1, 2, 3]
}

这种使用情况跟踪可能变得复杂(并且不可判定),因此Rust有一些限制变量何时可以转义上下文的规则。我们称之为复杂的规则所有权。可以使用以下代码在intro_binding.rs中解释:

fn scoped3()
{
let v1 = vec![1, 2, 3];
let v2 = v1;
//it is now illegal to reference v1
//ownership has been transferred to v2
}

当不可能或不希望转移所有权时,鼓励克隆特征创建所引用的任何数据的副本
intro_binding.rs:

fn scoped4()
{
vec![1, 2, 3].clone();
"".to_string().clone();
}

克隆或复制不是一个完美的解决方案,并带来性能开销。为了使Rust更快,而且速度非常快,我们也有借用的概念。
借用是一种接收对某些数据的直接引用的机制,承诺所有权将由某个特定点返回。参考用&符号表示。在intro_binding.rs中考虑以下示例:

fn scoped5()
{
fn foo(v1: &Vec)
{
for v in v1
{
println!("{}", v);
}
} l
et v1 = vec![1, 2, 3];
foo(&v1);
//v1 is still valid
//ownership has been returned
v1;
}

严格所有权的另一个好处是安全并发。每个绑定都由特定线程拥有,并且可以使用move关键字将所有权转移到新线程。这已在intro_binding.rs中使用以下代码进行了解释:

use std::thread;
fn thread1()
{
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().ok();
}

要在线程之间共享信息,程序员有两个主要选项。首先,程序员可以使用传统的锁和原子引用组合。在intro_binding.rs中使用以下代码对此进行了解释:

use std::sync::{Mutex, Arc};
use std::thread;
fn thread2()
{
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
} p
rintln!("Result: {}", *counter.lock().unwrap());
}

其次,通道为线程之间的消息传递和作业排队提供了一个很好的机制。发送特征也会自动为大多数对象实现。在intro_binding.rs中考虑以下代码:

use std::thread;
use std::sync::mpsc::channel;
fn thread3() {
let (sender, receiver) = channel();
let handle = thread::spawn(move ||{
//do work
let v = vec![1, 2, 3];
sender.send(v).unwrap();
});
handle.join().ok();
receiver.recv().unwrap();
}

所有这些并发都是类型安全的并且是编译器强制执行的。尽可能多地使用线程,如果您不小心尝试创建竞争条件或简单的死锁,那么编译器将阻止您。我们称之为无所畏惧的并发。

代数数据类型

除了结构/对象和函数/方法之外,Rust函数式编程还包括对可定义类型和结构的一些丰富的补充。

元组提供了定义简单匿名结构的简写。枚举为复杂数据结构的联合提供了一种类型安全的方法,并增加了构造函数标记以帮助进行模式匹配。标准库广泛支持从基类型到集合的通用编程。甚至对象系统特征也是类的OOP概念和类型类的FP概念之间的混合交叉。功能风格潜伏在每个角落,即使你不在Rust中寻找它们,你可能会发现自己在不知不觉中使用这些功能。

类型别名有助于为复杂类型创建简写名称。

或者,newtype结构模式可用于创建具有不同非等效类型的别名。在intro_datatypes.rs中考虑以下示例:

//alias
type Name = String;
//newtype
struct NewName(String);

即使在参数化时,结构仅在用于将多个值存储到单个对象中时可以是重复的。这可以在intro_datatypes.rs中看到:

struct Data1
{
a: i32,
b: f64,
c: String
} s
truct Data2
{
a: u32,
b: String,
c: f64
}

元组有助于消除冗余的结构定义。使用元组不需要先前的类型定义。在intro_datatypes.rs中考虑以下示例:

//alias to tuples
type Tuple1 = (i32, f64, String);
type Tuple2 = (u32, String, f64);
//named tuples
struct New1(i32, f64, String);
struct New2(u32, String, f64);

通过实现正确的特征,可以为任何类型实现标准运算符。在intro_datatypes.rs中考虑以下示例:

use std::ops::Mul;
struct Point
{
		x: i32,
		y: i32
}
 impl Mul for Point{
	type Output = Point;
	fn mul(self, other: Point) -> Point{
		Point{
			x: self.x * other.x,
			y: self.y * other.y
		}
	}
}

标准库集合和许多其他内置类型是通用的,例如intro_datatypes.rs中的HashMap:

use std::collections::HashMap;
type CustomHashMap = HashMap;

枚举是多种类型的类型安全联合。请注意,递归枚举定义必须将内部值包装在容器(如Box)中,否则大小将是无限的。这在intro_datatypes.rs中描述如下:

enum BTree
{
Branch { val:T, left:Box>, right:Box> },
Leaf { val: T }
}

标记的联合也用于更复杂的数据结构。在intro_datatypes.rs中考虑以下代码:

enum Term
{
TermVal { value: String },
TermVar { symbol: String },
TermApp { f: Box, x: Box },
TermAbs { arg: String, body: Box }
}

Traits有点像对象类(OOP),如下面的代码示例所示,在intro_datatypes.rs中:

trait Data1Trait
{
//constructors
fn new(a: i32, b: f64, c: String) -> Self;
//methods

fn get_a(&self) -> i32;
fn get_b(&self) -> f64;
fn get_c(&self) -> String;
}

特征也类似于类型类(FP),在下面的代码片段中显示在intro_datatypes.rs中:

trait BehaviorOfShow
{
fn show(&self) -> String;
}

混合面向对象的编程和函数编程

如前所述,Rust支持面向对象和函数式编程风格。数据类型和函数对这两种范例都是中性的。特征特别支持两种风格的混合混合。首先,在面向对象的样式中,使用struct,trait和impl可以使用构造函数和一些方法定义一个简单的类。在intro_mixoopfp.rs中使用以下代码片段对此进行了解释:

struct MyObject
{
a: u32,
b: f32,
c: String
} t
rait MyObjectTrait
{
fn new(a: u32, b: f32, c: String) -> Self
fn get_a(&self) -> u32;
fn get_b(&self) -> f32;
fn get_c(&self) -> String;
} i
mpl MyObjectTrait for MyObject
{
fn new(a: u32, b: f32, c: String) -> Self
{
MyObject { a:a, b:b, c:c }
} f
n get_a(&self) -> u32
{
self.a
} f
n get_b(&self) -> f32
{
self.b
} f
n get_c(&self) -> String
{
self.c.clone()
}
}

将函数式编程的支持添加到对象上就像定义使用函数式语言特性的特征和方法一样简单。例如,在适当使用时,接受闭包可以成为一个很好的抽象。在intro_mixoopfp.rs中考虑以下示例:

trait MyObjectApply
{
fn apply(&self, f:F) -> R
where F: Fn(u32,f32,String) -> R;
} i
mpl MyObjectApply for MyObject
{
fn apply(&self, f:F) -> R
where F: Fn(u32,f32,String) -> R
{
f(self.a, self.b, self.c.clone())
}
}

改善项目架构

功能程序鼓励良好的项目架构和原则设计模式。使用函数式编程的构建块通常可以减少设计选择的数量,使得良好的选项变得明显。
“There should be one - and preferably only one - obvious way to do it.” – PEP 20

文件层次结构,模块和命名空间设计

Rust程序主要以两种方式之一编译。第一种是使用rustc来编译单个文件。第二个是描述使用货物进行编译的整个包。我们在此假设项目是使用cargo建造的,如下:

1.要启动程序包,首先要在目录中创建Cargo.toml文件。从现在开始,该目录将成为您的包目录。这是一个配置文件,它将告诉编译器应该在包中包含哪些代码,资产和额外信息:

[package]
name = "fp_rust"
version = "0.0.1"

2.在此基本配置之后,您现在可以使用货物构建来编译整个项目。您决定放置代码文件以及命名它们的位置取决于您希望如何在模块命名空间中引用它们。每个文件都将被赋予自己的模块mod。您还可以在文件中嵌套模块:

mod inner_module
{
fn f1()
{
println!("inner module function");
}
}

3.在这些步骤之后,可以将项目添加为货物依赖项,并且可以在模块内部使用名称空间来公开公共符号。请考虑以下代码段:

extern crate package;
use package::inner_module::f1;

这些是Rust模块的基本构建块,但这与函数式编程有什么关系呢?以功能样式构建项目是一个过程,并适用于某些例程。通常,项目架构师将首先设计核心数据结构,并在复杂情况下也开始设计物理结构(代码/服务将在运行中运行)。一旦详细描述了数据布局,就可以规划核心功能/例程(例如程序的行为方式)。到目前为止,如果在架构阶段发生编码,则可能存在未实现的代码。最后阶段涉及用正确的行为替换这个模拟代码。

按照这个逐步开发过程,我们还可以看到一个原型文件布局形成。通常看到这些阶段在实际程序中从上到下书写。尽管作者不太可能在这些明确的阶段进行规划,但由于简单起见,它仍然是一种常见的模式。请考虑以下示例:

//trait definitions
//data structure and trait implementations
//functions
//main

对此类分组进行分组可能有助于标准化文件布局并提高可读性。在符号定义的长文件中来回搜索是编程中常见但令人不愉快的部分。这也是一个可以预防的问题。

函数设计模式

除文件布局外,还有许多功能设计模式可帮助减少代码重量和冗余。如果使用得当,这些原则有助于阐明设计决策并实现强大的架构。大多数设计模式都是单一责任原则的变体。根据具体情况,这可以采取多种形式,但目的是相同的;编写能很好地完成一件事的代码,然后根据需要重用该代码。我已解释如下:

纯函数:除了函数参数之外,这些函数没有副作用或逻辑依赖性。副作用是状态的改变,除了返回值之外,还会影响函数之外的任何事物。纯函数是有用的,因为它们可以被抛掷和组合,并且通常不经意地使用而没有意外影响的风险。

纯函数可能出错的最糟糕的事情是错误的返回值,或者在极端情况下,堆栈溢出。

即使在鲁莽使用时,使用纯函数也很难导致错误。在intro_patterns.rs中考虑以下纯函数示例:

fn pure_function1(x: u32) -> u32
{
x * x
} f
n impure_function(x: u32) -> u32
{
println!("x = {}", x);
x * x
}

不变性:不变性是一种有助于鼓励纯粹功能的模式。默认情况下,Rust变量绑定是不可变的。这是Rust不鼓励你避免可变状态的不那么微妙的方式。不要这样做。如果绝对必须,可以使用mut关键字标记变量以允许重新分配。这在以下示例中显示在intro_patterns.rs中:

let immutable_v1 = 1;
//immutable_v1 = 2; //invalid
let mut mutable_v2 = 1;
mutable_v2 = 2;

函数组合:功能组合是一种模式,其中一个功能的输出连接到另一个功能的输入。以这种方式,功能可以链接在一起,从简单的步骤创建复杂的效果。这与intro_patterns.rs中的以下代码段一起显示:

let fsin = |x: f64| x.sin();
let fabs = |x: f64| x.abs();
//feed output of one into the other
let transform = |x: f64| fabs(fsin(x));

高阶函数:之前已经提到过,但我们还没有使用过这个术语。 HoF是接受函数作为参数的函数。许多迭代器方法都是HoF。在intro_patterns.rs中考虑以下示例:

fn filter

(self, predicate: P) -> Filter where P: FnMut(&Self::Item) -> bool { ... }

函数:如果你能超越这个名字,这些都是一个简单而有效的设计模式。它们也非常多才多艺。这个概念有点难以完全捕捉,但你可能会认为算子是函数的逆。函数定义转换,接受数据,并返回转换结果。仿函数定义数据,接受函数,并返回转换结果。仿函数的一个常见示例是经常出现在容器上的绑定映射方法,例如Vec。以下是intro_patterns.rs中的示例:

let mut c = 0;
for _ in vec!['a', 'b', 'c'].into_iter()
.map(|letter| {
c += 1; (letter, c)
}){};

A monad is a monoid in the category of endofunctors, what’s the problem?
– Philip Wadler

Monads:Monads是人们学习FP的常见障碍。 Monad和functor可能是您在深入理论数学的旅程中可能遇到的第一个词。我们不会去那里。出于我们的目的,monad只是一种具有两种方法的特征。这在以下代码中显示在intro_patterns.rs中:

trait Monad {
fn return_(t: A) -> Self;
//:: A -> Monad
fn bind(m: Self, f: Fn(A) -> MB) -> MB
where MB: Monad;
//:: Monad -> (A -> Monad) -> Monad
}

如果这无助于澄清事物(并且可能没有),monad有两种方法。第一种方法是构造函数。第二种方法允许您绑定操作以创建另一个monad。许多常见的特征隐藏了半单子,但是通过使概念明确,概念变成了强大的设计模式而不是凌乱的反模式。不要试图重新发明你不需要的东西。

函数currying:函数currying是一种技术,对于那些来自面向对象或命令式语言的背景的人来说可能看起来很奇怪。造成这种混淆的原因在于,在许多函数式语言中,函数默认为curry,而其他语言则不然。默认情况下,Rust函数不会被curry。

curried和noncurried函数之间的区别在于curried函数逐个发送参数,而非curried函数一次性发送参数。查看正常的Rust函数定义,我们可以看到它不是curry。在intro_patterns.rs中考虑以下代码:

fn not_curried(p1: u32, p2: u32) -> u32
{
p1 + p2
} f
n main()
{
//and calling it
not_curried(1, 2);
}

curried函数在intro_patterns.rs中逐个获取每个参数,如下所示:

fn curried(p1: u32) -> Box u32>
{
Box::new(move |p2: u32| {
p1 + p2
})
} f
n main()
{
//and calling it
curried(1)(2);
}

Curried函数可用作函数工厂。前几个参数配置最终函数的行为方式。结果是一种允许复杂操作符的简写配置的模式。 Currying通过转换补充所有其他设计模式
将各个功能分成多个组件。

懒惰评估:懒惰评估是一种在其他语言中技术上可行的模式。然而,由于语言障碍,在FP之外看到它是不常见的。正常表达式和惰性表达式之间的区别在于,在访问之前不会评估惰性表达式。这是一个简单的懒惰实现,在intro_patterns.rs中的函数调用后面实现:

let x = { println!("side effect"); 1 + 2 };
let y = ||{ println!("side effect"); 1 + 2 };

在调用函数之前,不会计算第二个表达式,此时代码会解析。对于惰性表达式,副作用发生在解析时而不是初始化时。这是一种糟糕的懒惰实现,因此我们将在后面的章节中进一步详细介绍。这种模式相当普遍,一些运营商和数据结构需要懒惰才能工作。一个必要的懒惰的简单例子是一个懒惰的列表,否则可能无法创建。内置的Rust数字迭代器(惰性列表)很好地使用了这个:(0 …)。

记忆是我们将在这里介绍的最后一种模式。它可能被认为是一种优化而不是设计模式,但由于它有多常见,我们应该在这里提到它。 memoized函数只计算一次唯一结果。一个简单的实现将是一个由哈希表保护的函数。如果参数和结果已经在哈希表中,则跳过函数调用并直接从哈希表中返回结果。否则,计算结果,将其放入哈希表中,然后返回。此过程可以使用任何语言手动实现,但Rust宏允许我们编写一次memoization代码,并通过应用此宏重用该代码。使用以下代码片段在intro_patterns.rs中显示:

#[macro_use] extern crate cached;
#[macro_use] extern crate lazy_static;
cached! {
FIB;
fn fib(n: u64) -> u64 = {
if n==0 || n==1 { return n }
fib(n-1) + fib(n-2)
}
} f
n main()
{
fib(30);
}

此示例使用两个包和许多宏。在本书的最后,我们不会完全解释这里发生的一切。宏和元编程有很多可能。缓存功能结果只是一个开始。

元编程

Rust中的元编程术语通常与术语宏重叠。 Rust中有两种主要的宏类型:

  • Recursive
  • Procedural

两种类型的宏都将抽象语法树(AST)作为输入,并生成一个或多个AST。常用的宏是println。通过使用宏来生成格式化输出,可变数量的参数和类型与格式字符串连接。要像这样调用递归宏,就像添加一个函数的函数一样调用宏!在争论之前。宏应用程序也可以被[]或{}包围:

vec!["this is a macro", 1, 2];

递归宏由macro_rules定义!声明。 macro_rules定义的内部与模式匹配表达式的内部非常相似。唯一的区别是macro_rules!匹配语法而不是数据。我们可以使用这种格式来定义vec宏的简化版本。这在以下代码片段中显示在intro_metaprogramming.rs中:

macro_rules! my_vec_macro
{
( $( $x:expr ),* ) =>
{
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
}
}

此定义仅接受并匹配一种模式。它期望以逗号分隔的表达式列表。语法模式( ( ( x:expr),*)与逗号分隔的表达式列表匹配,并将结果存储在复数变量$ x中。在表达式的主体中,有一个块。该块定义了一个新的vec,然后迭代$ x *将每个$ x推入vec,最后,该块返回vec作为结果。在intro_metaprogramming.rs中,宏及其扩展如下:

//this
my_vec_macro!(1, 2, 3);
//is the same as this
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}

重要的是要注意表达式作为代码而不是值移动,因此副作用将被移动到评估上下文,而不是定义上下文。

递归宏模式与令牌字符串匹配。可以根据匹配的令牌执行单独的分支。在intro_metaprogramming.rs中,一个简单的大小写匹配如下所示:

macro_rules! my_macro_branch
{
(1 $e:expr) => (println!("mode 1: {}", $e));
(2 $e:expr) => (println!("mode 2: {}", $e));
} f
n main()
{
my_macro_branch!(1 "abc");
my_macro_branch!(2 "def");
}

名称递归宏来自宏中的递归,所以我们当然可以调用我们定义的宏。递归宏可以是定义特定于域的语言的快速方法。请考虑intro_metaprogramming.rs中的以下代码段:

enum DSLTerm {
TVar { symbol: String },
TAbs { param: String, body: Box },
TApp { f: Box, x: Box }
} m
acro_rules! dsl
{
( ( $($e:tt)* ) ) => (dsl!( $($e)* ));
( $e:ident ) => (DSLTerm::TVar {
symbol: stringify!($e).to_string()
});
( fn $p:ident . $b:tt ) => (DSLTerm::TAbs {
param: stringify!($p).to_string(),
body: Box::new(dsl!($b))
});
( $f:tt $x:tt ) => (DSLTerm::TApp {
f: Box::new(dsl!($f)),
x: Box::new(dsl!($x))
});
}

宏定义的第二种形式是过程宏。递归宏可以被认为是一种很好的语法来帮助定义过程宏。另一方面,过程宏是最常见的形式。程序宏可以做很多事情,递归形式根本不可能。

在这里,我们可以获取结构的TypeName并使用它来自动生成特征实现。这是宏定义,在intro_metaprogramming.rs中:

#![crate_type = "proc-macro"]
extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;
use proc_macro::TokenStream;
#[proc_macro_derive(TypeName)]
pub fn type_name(input: TokenStream) -> TokenStream
{
// Parse token stream into input AST
let ast = syn::parse(input).unwrap();
// Generate output AST
impl_typename(&ast).into()
} f
n impl_typename(ast: &syn::DeriveInput) -> quote::Tokens
{
let name = &ast.ident;
quote!
{
impl TypeName for #name
{
fn typename() -> String
{
stringify!(#name).to_string()
}
}
}
}

在intro_metaprogramming.rs中,相应的宏调用如下所示:

#[macro_use]
extern crate metaderive;
pub trait TypeName
{
fn typename() -> String;
} #
[derive(TypeName)]
struct MyStructA
{
a: u32,
b: f32
}

如您所见,过程宏设置起来有点复杂。但是,好处是所有处理都直接使用普通的Rust代码完成。这些宏允许以非结构化格式使用任何语法信息,以在编译之前生成更多代码结构。过程宏作为单独的模块处理,以便在正常的编译器执行期间进行预编译和执行。提供给每个宏的信息是本地化的,因此不可能考虑整个程序。但是,可用的本地信息足以实现一些相当复杂的效果。

摘要

在本章中,我们简要概述了本书中将出现的主要概念。从代码示例中,您现在应该能够直观地识别功能样式。我们还提到了这些概念有用的一些原因。在其余章节中,我们将提供每种技术何时以及为何适合的完整背景。在这种情况下,我们还将提供掌握技术和开始使用功能实践所需的知识。

从本章开始,我们学会了尽可能多地参数化,并且可以将函数用作参数,通过组合简单行为来定义复杂行为,并且只要编译就可以安全地使用Rust中的线程。

本书的结构是为了首先介绍更简单的概念,然后,随着本书的继续,一些概念可能变得更抽象或技术性。此外,所有技术都将在正在进行的项目中引入。该项目将控制电梯系统,随着图书的进展,要求将逐渐变得更加苛刻。

问题

1.什么是功能?
2.什么是仿函数?
3.什么是元组?
4.设计用于标记联合的控制流表达式是什么?
5.具有函数作为参数的函数的名称是什么?
6.在memoized fib(20)中会调用多少次?
7.可以通过通道发送哪些数据类型?
8.为什么函数返回时需要装箱功能?
9. move关键字有什么作用?
10.两个变量如何共享单个变量的所有权?

进一步阅读

Packt有很多其他很好的资源来学习Rust:

  • https://www.packtpub.com/application-development/rust-programming-example
  • https://www.packtpub.com/application-development/learning-rust

有关基本文档和教程,请参阅此处:

  • Tutorial: https://doc.rust-lang.org/book/first-edition/
  • Documentation: https://doc.rust-lang.org/stable/reference/

你可能感兴趣的:(Hands-On,Functional,Programming,in)