10 个版本
0.1.9 | 2019 年 10 月 2 日 |
---|---|
0.1.8 | 2019 年 8 月 10 日 |
#766 在 开发工具
539 每月下载量
用于 4 个 crates(3 直接使用)
10KB
312 代码行
该项目是一个 Rust 中函数式编程的库。
fp-core.rs
Rust 中函数式编程的库。
它包含纯函数式数据结构,以补充 Rust 标准库的函数式编程需求。
安装
将以下行添加到您的 Cargo.toml 中
fp-core = "0.1.8"
如果您有 Cargo Edit,您可以直接
$ cargo add fp-core
Rust 中函数式编程术语
函数式编程(FP)提供了许多优势,因此它的流行度一直在增加。然而,每种编程范式都有自己的独特术语,FP 也不例外。通过提供术语表,我们希望使学习 FP 更容易。
在适用的情况下,本文档使用 Fantasy Land 规范 和 Rust 编程语言中定义的术语来给出代码示例。
请注意,该项目有时会使用未完成的 FP 相关 crates,但它们包含特定类型类或数据类型,适合用于解释。其中许多是实验性的,不适合生产使用。
目录
- 参数
- 高阶函数(HOF)
- 闭包
- 部分应用
- 柯里化
- 自动柯里化
- 引用透明度
- Lambda
- Lambda 演算
- 纯度
- 副作用
- 幂等性
- 函数组合
- 延续
- 无点风格
- 谓词
- 合同
- 范畴
- 值
- 常量
- 变异性
- 高阶类型
- 函子
- 指向函子
- 提升
- 等式推理
- 幺半群
- 单子
- 可半群
- 应用函子
- 同态
- 集合等价类
- 序号
- 半群
- 可折叠
- 透镜
- 类型签名
- 代数数据类型
- 选项
- 函数式编程参考
- Rust语言中的函数式编程开发
- 我对这个项目的看法
参数
函数接受的参数数量。从单元、二元、三元等词可以看出,这个术语由两个后缀组成,“-ary”和“-ity”。例如,加法函数接受两个参数,因此定义为二元函数或二参数函数。这种函数有时也被称为“二元的”,而二元函数必须给出两个且仅两个参数,currying和部分应用除外(见下文)。
let sum = |a: i32, b: i32| { a + b }; // The arity of sum is 2
高阶函数(HOF)
接受函数作为参数和/或返回函数的函数。
let filter = | predicate: fn(&i32) -> bool, xs: Vec<i32> | {
xs.into_iter().filter(predicate).collect::<Vec<i32>>()
};
let is_even = |x: &i32| { x % 2 == 0 };
filter(is_even, vec![1, 2, 3, 4, 5, 6]);
闭包
闭包是一个作用域,它在创建函数时保留函数可用的变量。这对于部分应用工作很重要。
let add_to = |x: i32| move |y: i32| x + y;
我们可以用数字调用add_to
,并返回一个内嵌x
的函数。注意,我们还需要将x的所有权移动到内部的lambda。
let add_to_five = add_to(5);
在这种情况下,x
被保留在add_to_five
的闭包中,值为5
。然后我们可以用y
调用add_to_five
,得到所需的结果。
add_to_five(3); // => 8
闭包通常用于事件处理器,以便在最终调用时它们仍然可以访问父级中定义的变量。
进一步阅读
部分应用
部分应用函数意味着通过预先填充原始函数的一些参数来创建一个新函数。
为了轻松实现这一点,我们将使用部分应用crate
#[macro_use]
extern crate partial_application;
fn foo(a: i32, b: i32, c: i32, d: i32, mul: i32, off: i32) -> i32 {
(a + b*b + c.pow(3) + d.pow(4)) * mul - off
}
let bar = partial!( foo(_, _, 10, 42, 10, 10) );
assert_eq!(
foo(15, 15, 10, 42, 10, 10),
bar(15, 15)
); // passes
部分应用通过在数据可用时嵌入数据,帮助从更复杂的函数创建更简单的函数。curried函数会自动部分应用。
进一步阅读
柯里化
将接受多个参数的函数转换为接受它们一个一个的函数的过程。
每次函数调用时,它只接受一个参数,并返回一个接受一个参数的函数,直到所有参数都传递完毕。
fn add(x: i32) -> impl Fn(i32)-> i32 {
move |y| x + y
}
let add5 = add(5);
add5(10); // 15
进一步阅读
自动柯里化
将接受多个参数的函数转换为如果给定少于其正确数量的参数,则返回接受剩余参数的函数的过程。当函数接收到正确数量的参数时,它就会被评估。
尽管目前Rust中无法实现自动currying,但在Rust论坛上对此问题进行了辩论:https://internals.rust-lang.org/t/auto-currying-in-rust/149/22
引用透明度
一个可以替换为其值而不改变程序行为的表达式被称为引用透明。
假设我们有一个名为greet的函数
let greet = || "Hello World!";
任何对greet()
的调用都可以替换为Hello World!
,因此greet是引用透明的。
Lambda
可以像值一样处理的匿名函数。
fn increment(i: i32) -> i32 { i + 1 }
let closure_annotated = |i: i32| { i + 1 };
let closure_inferred = |i| i + 1;
Lambdas通常作为参数传递给高阶函数。你可以像上面那样将lambda分配给变量。
Lambda 演算
使用函数创建一个通用计算模型的数学分支。
这与图灵机,一个等效模型形成对比。
λ演算有三个关键组成部分:变量、抽象和应用。变量只是某些符号,比如x
。抽象类似于函数:它将变量绑定到“公式”中。应用是函数调用。没有例子,这是没有意义的。
恒等函数(在Rust中为|x| x
)在大多数文献中看起来像\ x. x
(在LaTeX或Unicode中,\
是Lambda)。它是一个抽象。如果1
是一个我们可以使用的值,那么(\ x. x) 1
就是一个应用(计算它给出1
)。
但还有更多...
纯λ演算中的计算
让我们发明布尔值。\ x y. x
可以是真,而\ x y. y
可以是假。如果是这样,\ b1 b2. b1(b2,(\x y. y))
就是and
。让我们评估它来展示如何
b1 |
b2 |
它们的and |
---|---|---|
\ x y.x |
\x y.x |
\x y.x |
\ x y.x |
\x y.y |
\x y.y |
\ x y.y |
\x y.y |
\x y.y |
\ x y.y |
\x y.x |
\x y.y |
我将把or
作为一个练习留给你们。此外,现在可以实施if
:\c t e. c(t, e)
,其中c
是条件,t
是结果(then
)和e
是否则子句。
SICP将数字作为练习。它们定义0为\f . \x. x
,将一个加到它上作为\n. \f. \x. f(n(f)(x))
。这甚至不是ASCII艺术,所以我们加上:0 + 1
(\n. \f. \x. f(n(f)(x)))(\f. \x. x) = \f. \x. f((\x'. x')(x)) = \f. \x. f(x)
基本上,表达式中的 f
的数量就是数字。我将把找出较大数字的任务留给读者作为练习。有耐心的话,你可以证明 \f. \x. f(f(x))
等于 2。这将有助于加法:\n m. \f. \x. n(m(f)(x))
应该加两个数字。让我们来做 4
(\n m. \f. \x. n(f)(m(f)(x)))(\f. x. f(f(x)), \f. \x. f(f(x)))
= \f. \x. (\f'. \x'. f'(f'(x')))(f)((\f'. \x'. f'(f'(x')))(f)(x))
= \f. \x. (\x'. f(f(x')))(f(f(x')))
= \f. \x. f(f(f(f(x))))
乘法更难,维基百科上有更好的解释。另一个好的参考资料是Stack Overflow。
纯度
一个函数是纯的,如果它的返回值只由其输入值决定,并且不会产生副作用。
let greet = |name: &str| { format!("Hi! {}", name) };
greet("Jason"); // Hi! Jason
与以下每个相反
let name = "Jason";
let greet = || -> String {
format!("Hi! {}", name)
};
greet(); // String = "Hi! Jason"
上述示例的输出基于函数外部存储的数据...
let mut greeting: String = "".to_string();
let mut greet = |name: &str| {
greeting = format!("Hi! {}", name);
};
greet("Jason");
assert_eq!("Hi! Jason", greeting); // Passes
...而这一例修改了函数外的状态。
副作用
如果函数或表达式除了返回值外,还与(从或写入)外部可变状态交互,则称它具有副作用。
use std::time::SystemTime;
let now = SystemTime::now();
println!("IO is a side effect!");
// IO is a side effect!
幂等性
如果一个函数重新应用于其结果不会产生不同的结果,则称该函数是幂等的。
// Custom immutable sort method
let sort = |x: Vec<i32>| -> Vec<i32> {
let mut x = x;
x.sort();
x
};
然后我们可以像这样使用排序方法
let x = vec![2 ,1];
let sorted_x = sort(sort(x.clone()));
let expected = vec![1, 2];
assert_eq!(sorted_x, expected); // passes
let abs = | x: i32 | -> i32 {
x.abs()
};
let x: i32 = 10;
let result = abs(abs(x));
assert_eq!(result, x); // passes
函数组合
将两个函数组合起来形成一个第三函数,其中一个函数的输出是另一个函数的输入。以下是一个 Rust 中的 compose 函数示例。
macro_rules! compose {
( $last:expr ) => { $last };
( $head:expr, $($tail:expr), +) => {
compose_two($head, compose!($($tail),+))
};
}
fn compose_two<A, B, C, G, F>(f: F, g: G) -> impl Fn(A) -> C
where
F: Fn(A) -> B,
G: Fn(B) -> C,
{
move |x| g(f(x))
}
然后我们可以这样使用它
let add = | x: i32 | x + 2;
let multiply = | x: i32 | x * 2;
let divide = | x: i32 | x / 2;
let intermediate = compose!(add, multiply, divide);
let subtract = | x: i32 | x - 1;
let finally = compose!(intermediate, subtract);
let expected = 11;
let result = finally(10);
assert_eq!(result, expected); // passes
延续
在任何给定的时间点,程序中尚未执行的部分称为继续。
let print_as_string = |num: i32| println!("Given {}", num);
let add_one_and_continue = |num: i32, cc: fn(i32)| {
let result = num + 1;
cc(result)
};
add_one_and_continue(1, print_as_string); // Given 2
当程序需要等待接收数据才能继续时,通常会看到连续性,一旦收到响应,它就会被传递给程序的其余部分,即连续性。
无点风格
编写函数,其中定义不明确标识使用的参数。这种风格通常需要柯里化或其他高阶函数。也称为隐式编程。
谓词
谓词是返回 true 或 false 的函数。谓词的常见用途是作为数组过滤的回调。
let predicate = | a: &i32 | a.clone() > 2;
let result = (vec![1, 2, 3, 4]).into_iter().filter(predicate).collect::<Vec<i32>>();
assert_eq!(result, vec![3, 4]); // passes
合同
合约指定了函数或表达式在运行时的行为义务和保证。这充当一组期望从函数或表达式的输入和输出中遵循的规则,并且当合约被违反时,通常报告错误。
let contract = | x: &i32 | -> bool {
x > &10
};
let add_one = | x: &i32 | -> Result<i32, String> {
if contract(x) {
return Ok(x + 1);
}
Err("Cannot add one".to_string())
};
然后你可以像这样使用 add_one
let expected = 12;
match add_one(&11) {
Ok(x) => assert_eq!(x, expected),
_ => panic!("Failed!")
}
范畴
范畴论中的范畴是一组对象及其之间的形态。在编程中,通常类型充当对象,函数充当形态。
要成为有效的范畴,必须满足 3 个规则
- 必须有一个恒等形态,它将对象映射到自身。其中
a
是某个范畴中的对象,必须有一个从a -> a
的函数。 - 态射必须可复合。其中
a
、b
和c
是某个范畴中的对象,而f
是从a -> b
的态射,g
是从b -> c
的态射;g(f(x))
必须与(g • f)(x)
等价。 - 复合必须是结合的
f • (g • h)
等同于(f • g) • h
由于这些规则在非常抽象的层面上控制着复合,范畴论在揭示新的事物组合方式方面非常出色。特别是,许多人认为这是数学的另一个基础(因此,从这个数学观点来看,一切都将是一个范畴)。本指南中的许多定义都与范畴论相关,因为这种方法优雅地适用于函数式编程。
范畴的例子
当指定一个范畴时,对象和态射是至关重要的。此外,展示这些规则是否得到满足也很不错,但通常留给读者作为练习。
- 任何类型及其上的纯函数。(注意,我们要求纯度,因为副作用会影响结合性(第三条规则)。)
这些例子出现在数学中
- 1:只有一个对象及其恒等态射的范畴。
- 张量范畴:幺半群将在后面定义,但任何幺半群都是一个只有一个对象且从这个对象到自身有许多态射的范畴。(是的,也存在一个幺半群的范畴——这不是那个范畴——这个例子是任何幺半群都是其自身的范畴。)
进一步阅读
值
可以分配给变量的任何东西。
let a = 5;
let b = vec![1, 2, 3];
let c = "test";
常量
一旦定义就不能重新赋值的变量。
let a = 5;
a = 3; // error!
常量是 引用透明 的。也就是说,可以将其替换为它们所代表的值而不影响结果。
变异性
函数式编程中的变异性指的是与它们的组件之间的子类型之间的子类型之间的关系。
与 像 TypeScript 或 C# 这样的面向对象编程 或 像 Scala 或 Haskell 这样的函数式编程语言 中的变异性用法不同
Rust 中的变异性用于类型检查期间对类型和生命周期参数的类型检查。以下是一些例子
- 默认情况下,所有生命周期都是协变的,除了
'static
,因为它比其他所有生命周期都要长 'static
总是相对于其他所有生命周期是反变的,无论它出现在哪里或如何使用- 如果您在
PhantomData
中使用Cell<T>
或UnsafeCell<T>
,则它是不变
进一步阅读
- https://github.com/rust-lang/rustc-guide/blob/master/src/variance.md
- https://nearprotocol.com/blog/understanding-rust-lifetimes/
高阶类型 (HKT)
Rust 还不支持高阶类型 yet。首先,HKT 是一个具有“空洞”的类型,因此您可以声明一个类型签名,如 trait Functor<F<A>>
。
虽然Rust原生不支持HKT(高阶类型),但我们总有一套解决方案,称为轻量级高阶类型
以下是在Rust中实现上述理论的示例:
pub trait HKT<A, B> {
type URI;
type Target;
}
// Lifted Option
impl<A, B> HKT<A, B> for Option<A> {
type URI = Self;
type Target = Option<B>;
}
高阶类型对于泛函编程至关重要。
进一步阅读
- https://gist.github.com/CMCDragonkai/a5638f50c87d49f815b8
- https://www.youtube.com/watch?v=ERM0mBPNLHc
函子
一个实现了映射函数的对象,在遍历对象中的每个值以生成相同类型的新函子时,遵循以下两条规则:
保留身份
object.map(x => x) ≍ object
可组合
object.map(compose(f, g)) ≍ object.map(g).map(f)
(f
和g
是任意函数)
例如,以下可以视为类似函子的操作:
let v: Vec<i32> = vec![1, 2, 3].into_iter().map(| x | x + 1).collect();
assert_eq!(v, vec![2, 3, 4]); // passes while mapping the original vector and returns a new vector
利用HKT实现,您可以为Functor定义以下特质:
pub trait Functor<A, B>: HKT<A, B> {
fn fmap<F>(self, f: F) -> <Self as HKT<A, B>>::Target
where F: FnOnce(A) -> B;
}
然后将其应用于如Option之类的类型:
impl<A, B> Functor<A, B> for Option<A> {
fn fmap<F>(self, f: F) -> Self::Target
where
F: FnOnce(A) -> B
{
self.map(f)
}
}
// This conflicts with the above, so isn't tested
// below and is commented out. TODO: add a careful
// implementation that makes sure Optional and this
// don't conflict.
impl<A, B, T> HKT<A, B> for T
where
T: Sized + Iterator<Item = A>,
U: Sized + Iterator<Item = B>,
{
type URI = Self;
type Target = U;
}
impl<A, B, T> Functor<A, B> for T
where
T: Iterator<Item = A>,
{
fn fmap<F>(self, f: F) -> Self::Target
where
F: FnOnce(A) -> B,
A: Sized,
B: Sized,
{
self.map(f)
}
}
#[test]
fn test_functor() {
let z = Option::fmap(Some(1), |x| x + 1).fmap(|x| x + 1); // Return Option<B>
assert_eq!(z, Some(3)); // passes
}
底层数学
一个令人困惑的事实是,函子是范畴论中的态射。实际上,这意味着从范畴C
到D
的函子保留范畴的性质,因此数据在某种程度上得到了保留。
技术上,每个范畴都有一个到最简单(非空)范畴(1)的函子:因为范畴1
只有一个对象和一个函数,所以将起始范畴中的所有对象和函数映射到1
中的相应元素。因此,数据在“良好”的意义上并没有得到保留。这种函子有时被称为遗忘函子,因为它们丢弃了结构。
然而,不那么遗忘的例子提供了更多的见解,并赋予了关于类型的有效陈述。不幸的是,这些在数学上相当粗糙。
指向函子
一个具有将任何单个值放入其中的of函数的对象。
#[derive(Debug, PartialEq, Eq)]
enum Maybe<T> {
Nothing,
Just(T),
}
impl<T> Maybe<T> {
fn of(x: T) -> Self {
Maybe::Just(x)
}
}
然后可以这样使用它:
let pointed_functor = Maybe::of(1);
assert_eq!(pointed_functor, Maybe::Just(1));
提升
在泛函编程中,提升通常意味着将函数提升到上下文(函子或单子)中。例如,给一个函数a -> b
并将其提升到List
中,那么签名将如下所示:List[a] -> List[b]
。
进一步阅读
- https://wiki.haskell.org/Lifting
- https://stackoverflow.com/questions/43482772/difference-between-lifting-and-higher-order-functions
- https://stackoverflow.com/questions/2395697/what-is-lifting-in-haskell/2395956
等式推理
当应用程序由表达式组成且没有副作用时,可以从部分推导出系统的真值。
幺半群
一个具有将集合中的配对“组合”成另一个集合元素的二元函数的集合。
一个简单的单子是数字的加法
1 + 1
// i32: 2
在这种情况下,数字是集合,+
是函数。
还必须存在一个“恒等”值,当与值组合时不会改变它。
加法的恒等值是0
。
1 + 0
// i32: 1
还要求操作分组不会影响结果(结合律)
1 + (2 + 3) == (1 + 2) + 3
// bool: true
数组连接也形成一个单子
[vec![1, 2, 3], vec![4, 5, 6]].concat();
// Vec<i32>: vec![1, 2, 3, 4, 5, 6]
恒等值是空数组[]
[vec![1, 2], vec![]].concat();
// Vec<i32>: vec![1, 2]
如果提供了恒等和组合函数,函数本身形成单子
fn identity<A>(a: A) -> A {
a
}
foo
是任何接受一个参数的函数。
compose(foo, identity) ≍ compose(identity, foo) ≍ foo
我们可以将单子表达为Rust特质,其类型签名如下:
use crate::applicative_example::Applicative;
trait Empty<A> {
fn empty() -> A;
}
trait Monoid<A, F, B>: Empty<A> + Applicative<A, F, B>
where
F: FnOnce(A) -> B,
{
}
根据Fantasy Land规范,单子应该实现Empty
和Applicative
。
单子
一个Monad是一个实现Applicative
和Chain
规范的特质。chain
类似于map
,但它会展开结果中的嵌套对象。
首先,Chain
类型可以像下面这样实现
pub trait Chain<A, B>: HKT<A, B> {
fn chain<F>(self, f: F) -> <Self as HKT<A, B>>::Target
where F: FnOnce(A) -> <Self as HKT<A, B>>::Target;
}
impl<A, B> Chain<A, B> for Option<A> {
fn chain<F>(self, f: F) -> Self::Target
where F: FnOnce(A) -> <Self as HKT<A, B>>::Target {
self.and_then(f)
}
}
然后,Monad
本身可以简单地推导出Chain
和Applicative
pub trait Monad<A, F, B>: Chain<A, B> + Applicative<A, F, B>
where F: FnOnce(A) -> B {}
impl<A, F, B> Monad<A, F, B> for Option<A>
where F: FnOnce(A) -> B {}
#[test]
fn monad_example() {
let x = Option::of(Some(1)).chain(|x| Some(x + 1));
assert_eq!(x, Some(2)); // passes
}
pure
在其他函数式语言中也被称为return
。flat_map
在其他语言中也被称为bind
。
值得注意的是,monads是范畴论中相当高级的一个主题。事实上,有人称之为三元组,因为它们涉及到伴随函子和它们的单位——这两者在函数式编程中都是很少见的。一个常见的比喻是将monad想象成一个Burrito,其中"pure"是取一个玉米饼(空的Burrito)并使用"chain"添加配料的行为。
monads的纯数学表示形式看起来并不像这样,但它们是等价的。
可半群
具有extract
和extend
函数的对象。
trait Extend<A, B>: Functor<A, B> + Sized {
fn extend<W>(self, f: W) -> <Self as HKT<A, B>>::Target
where
W: FnOnce(Self) -> B;
}
trait Extract<A> {
fn extract(self) -> A;
}
trait Comonad<A, B>: Extend<A, B> + Extract<A> {}
然后我们可以为Option实现这些类型
impl<A, B> Extend<A, B> for Option<A> {
fn extend<W>(self, f: W) -> Self::Target
where
W: FnOnce(Self) -> B,
{
self.map(|x| f(Some(x)))
}
}
impl<A> Extract<A> for Option<A> {
fn extract(self) -> A {
self.unwrap() // is there a better way to achieve this?
}
}
extract
从comonad中提取一个值。
Some(1).extract(); // 1
extend
在Comonad上运行一个函数。
Some(1).extend(|co| co.extract() + 1); // Some(2)
这可以被视为monad的逆。事实上,在范畴论中这被称为“对偶”。(基本上,如果你知道什么是A
,那么一个coA
就是A
定义中所有箭头反转的东西。)
应用函子
一个applicative函子是一个具有ap
函数的对象。ap
将一个对象中的函数应用于另一个相同类型的对象中的值。给定一个纯程序g: (b: A) -> B
,我们必须将其提升到g: (fb: F<A>) -> F<B>
。为了实现这一点,我们将引入另一个高阶类型,称为HKT3
,它能够完成这项工作。
在这个例子中,我们将使用Option数据类型。
trait HKT3<A, B, C> {
type Target2;
}
impl<A, B, C> HKT3<A, B, C> for Option<A> {
type Target2 = Option<B>;
}
由于Applicative实现了Fantasy Land规范中的ap
和Pure
的of
,我们必须像下面这样实现类型
// Apply
trait Apply<A, F, B> : Functor<A, B> + HKT3<A, F, B>
where F: FnOnce(A) -> B,
{
fn ap(self, f: <Self as HKT3<A, F, B>>::Target2) -> <Self as HKT<A, B>>::Target;
}
impl<A, F, B> Apply<A, F, B> for Option<A>
where F: FnOnce(A) -> B,
{
fn ap(self, f: Self::Target2) -> Self::Target {
self.and_then(|v| f.map(|z| z(v)))
}
}
// Pure
trait Pure<A>: HKT<A, A> {
fn of(self) -> <Self as HKT<A, A>>::Target;
}
impl<A> Pure<A> for Option<A> {
fn of(self) -> Self::Target {
self
}
}
// Applicative
trait Applicative<A, F, B> : Apply<A, F, B> + Pure<A>
where F: FnOnce(A) -> B,
{
} // Simply derives Apply and Pure
impl<A, F, B> Applicative<A, F, B> for Option<A>
where F: FnOnce(A) -> B,
{
}
然后我们可以像这样使用Option Applicative
let x = Option::of(Some(1)).ap(Some(|x| x + 1));
assert_eq!(x, Some(2));
同态
一个保持其定义域结构不变的功能。请参阅范畴定义以获取更多信息。
前几个(内映射、同态和同构)比其他部分更容易理解。其余部分需要F-代数的概念。更简单的Haskell声明列在参数形态的维基百科上,但这些概念还没有扩展到更一般的范畴论。简而言之,F-代数的观点是将代数对象的集合论定义重新定义为纯粹的范畴论基础:将思想从包含元素的集合转移到具有形态的对象集合。然而,这里的一些思想尚未被推广到这种运动。
自同态
输入类型与输出类型相同的函数。
// uppercase :: &str -> String
let uppercase = |x: &str| x.to_uppercase();
// decrement :: i32 -> i32
let decrement = |x: i32| x - 1;
同构
两种对象之间可逆的变换对。
例如,二维坐标可以存储为i32向量[2,3]或结构体{x: 2, y: 3}。
#[derive(PartialEq, Debug)]
struct Coords {
x: i32,
y: i32,
}
let pair_to_coords = | pair: (i32, i32) | Coords { x: pair.0, y: pair.1 };
let coords_to_pair = | coords: Coords | (coords.x, coords.y);
assert_eq!(
pair_to_coords((1, 2)),
Coords { x: 1, y: 2 },
); // passes
assert_eq!(
coords_to_pair(Coords { x: 1, y: 2 }),
(1, 2),
); // passes
同构在使结构相同方面至关重要。由于我们知道上面的结构体与一对相同,所有存在于对上的函数都可以存在于结构体上。如果f是同构,g是陪域上的内映射:f-1g f将扩展g以应用于定义域。
同态
同态只是一个保持结构的映射。它是形态的旧术语。实际上,函子只是范畴之间的同态,因为它在映射下保持原始范畴的结构。
assert_eq!(A::of(f).ap(A::of(x)), A::of(f(x))); // passes
assert_eq!(
Either::of(|x: &str| x.to_uppercase(x)).ap(Either::of("oreos")),
Either::of("oreos".to_uppercase),
); // passes
累积
一个reduceRight
函数,它将一个函数应用于累加器以及数组中的每个值(从右到左),以将其缩减为单个值。
let sum = |xs: Vec<i32>| xs.iter().fold(0, |mut sum, &val| { sum += val; sum });
assert_eq!(sum(vec![1, 2, 3, 4, 5]), 15);
反子
一个unfold
函数。 unfold是fold的反义词。它从一个单值生成一个列表。
let count_down = unfold((8_u32, 1_u32), |state| {
let (ref mut x1, ref mut x2) = *state;
if *x1 == 0 {
return None;
}
let next = *x1 - *x2;
let ret = *x1;
*x1 = next;
Some(ret)
});
assert_eq!(
count_down.collect::<Vec<u32>>(),
vec![8, 7, 6, 5, 4, 3, 2, 1],
);
合子
anamorphism和catamorphism的组合。
Apomorphism
它是paramorphism的反义词,就像anamorphism是catamorphism的反义词一样。与paramorphism一样,您结合访问累加器和已累积的内容,apomorphism让您有提前返回的潜力。
集合等价类
这是一个具有等价关系的集合。
一个具有equals
函数的对象,该函数可以用于比较同一类型的其他对象。
它必须遵守以下规则才能成为Setoid
a.equals(a)
(自反性)a.equals(b)
和b.equals(c)
,那么a.equals(c)
(对称性)a.equals(b)
和b.equals(c)
,那么a.equals(c)
(传递性)
将Vector转换为Setoid
请注意,我将Self
/ self
当作a
来处理。
trait Setoid {
fn equals(&self, other: &Self) -> bool;
}
impl Setoid for Vec<i32> {
fn equals(&self, other: &Self) -> bool {
self.len() == other.len()
}
}
assert_eq!(vec![1, 2].equals(&vec![1, 2]), true); // passes
在Rust标准库中,已经提供了Eq,它与本节讨论过的Setoid类似。此外,Eq还有equals
实现,它涵盖了Rust中已经存在的一系列数据结构。
序号
实现Ord规定的对象或值,也实现了Setoid规定。
Ord对象或值必须满足以下规则,对所有a
、b
或c
都适用。
- 完备性:
a <= b
或b <= a
- 反对称性:
a <= b
和b <= a
,则a == b
- 传递性:
a <= b
和b <= c
,则a <= c
可以在以下链接找到Rust文档中关于Ord的说明:Ord
半群
具有combine
函数的对象,可以将它与相同类型的另一个对象组合。
它必须遵守以下规则才能成为Semigroup
a.add(b).add(c)
等价于a.add(b.add(c))
(结合性)
use std::ops::Add;
pub trait Semigroup<M>: Add<M> {
}
assert_eq!(
vec![1, 2].add(&vec![3, 4]),
vec![1, 2, 3, 4],
); // passes
assert_eq!(
a.add(&b).add(&c),
a.add(&b.add(&c)),
); // passes
可折叠
具有可以将该对象转换成其他类型的foldr/l
函数的对象。我们使用rats
作为例子,但该包只实现了fold_right
。
fold_right
等价于Fantasy Land Foldable的reduce
,其过程如下
fantasy-land/reduce::Foldable f=>f a~> ((b,a) ->b,b) ->b
use rats::foldable::Foldable;
use rats::kind::IntoKind;
use rats::kinds::VecKind;
let k = vec![1, 2, 3].into_kind();
let result = VecKind::fold_right(k, 0, | (i, acc) | i + acc);
assert_eq!(result, 6);
如果您要手动实现Foldable
,它的特质如下
use crate::hkt::HKT;
use crate::monoid::Monoid;
pub trait Foldable<A, B>: HKT<A, B> {
fn reduce<F>(b: B, ba: F) -> <Self as HKT<A, B>>::Target
where
F: FnOnce(B, A) -> (B, B);
fn fold_map<M, N, F>(m: M, fa: F) -> M
where
M: Monoid<N>,
F: FnOnce(<Self as HKT<A, B>>::URI) -> M;
fn reduce_right<F>(b: B, f: F) -> <Self as HKT<A, B>>::Target
where
F: FnOnce(A, B) -> (B, B);
}
透镜
一个lens是一个类型,它将某些其他数据结构的getter和不可变setter配对。
trait Lens<S, A> {
fn over(s: &S, f: &Fn(Option<&A>) -> A) -> S {
let result: A = f(Self::get(s));
Self::set(result, &s)
}
fn get(s: &S) -> Option<&A>;
fn set(a: A, s: &S) -> S;
}
#[derive(Debug, PartialEq, Clone)]
struct Person {
name: String,
}
#[derive(Debug)]
struct PersonNameLens;
impl Lens<Person, String> for PersonNameLens {
fn get(s: &Person) -> Option<&String> {
Some(&s.name)
}
fn set(a: String, s: &Person) -> Person {
Person {
name: a,
}
}
}
对于给定的数据结构具有get和set对,可以启用一些关键功能。
let e1 = Person {
name: "Jason".to_string(),
};
let name = PersonNameLens::get(&e1);
let e2 = PersonNameLens::set("John".to_string(), &e1);
let expected = Person {
name: "John".to_string()
};
let e3 = PersonNameLens::over(&e1, &|x: Option<&String>| {
match x {
Some(y) => y.to_uppercase(),
None => panic!("T_T") // lol...
}
});
assert_eq!(*name.unwrap(), e1.name); // passes
assert_eq!(e2, expected); // passes
assert_eq!(e3, Person { name: "JASON".to_string() }); // passes
lenses也是可组合的。这允许轻松地进行深层次数据的不可变更新。
struct FirstLens;
impl<A> Lens<Vec<A>, A> for FirstLens {
fn get(s: &Vec<A>) -> Option<&A> {
s.first()
}
fn set(a: A, s: &Vec<A>) -> Vec<A> {
unimplemented!() // Nothing to set in FirstLens
}
}
let people = vec![Person { name: "Jason" }, Person { name: "John" }];
Lens::over(composeL!(FirstLens, NameLens), &|x: Option<&String>| {
match x {
Some(y) => y.to_uppercase(),
None => panic!("T_T")
}
}, people); // vec![Person { name: "JASON" }, Person { name: "John" }];
进一步阅读
类型签名
Rust中的每个函数都会指明它们的参数类型和返回值类型。
// add :: i32 -> i32 -> i32
fn add(x: i32) -> impl Fn(i32)-> i32 {
move |y| x + y
}
// increment :: i32 -> i32
fn increment(x: i32) -> i32 {
x + 1
}
如果函数接受另一个函数作为参数,它会被括号包裹。
// call :: (a -> b) -> a -> b
fn call<A, B>(f: &Fn(A) -> B) -> impl Fn(A) -> B + '_ {
move |x| f(x)
}
字母a
、b
、c
、d
用于表示参数可以是任何类型。以下版本的map接受一个将类型为a
的值转换成类型b
的函数a
,一个类型为a
的值的数组,并返回一个类型为b
的值的数组。
// map :: (a -> b) -> [a] -> [b]
fn map<A, B>(f: &Fn(A) -> B) -> impl Fn(A) -> B + '_ {
move |x| f(x)
}
进一步阅读
代数数据类型
由将其他类型组合在一起制成的复合类型。两种常见的代数类型类别是sum和product。
求和类型
类型和是两种类型组合成另一种类型。之所以称为和,是因为结果类型的可能值数量是输入类型的总和。
Rust 有 enum
,在 ADT 中字面上表示 sum
。
enum WeakLogicValues {
True(bool),
False(bool),
HalfTrue(bool),
}
// WeakLogicValues = bool + otherbool + anotherbool
积类型
积类型以您可能更熟悉的方式组合类型。
struct Point {
x: i32,
y: i32,
}
// Point = i32 x i32
之所以称为积,是因为数据结构可能的总值是不同值的乘积。许多语言都有元组类型,这是积类型的简单表达。
另请参阅 集合论
进一步阅读
选项
Option 是一个 和类型,有两种情况通常称为 Some 和 None。
Option 对于组合可能不返回值的函数很有用。
let mut cart = HashMap::new();
let mut item = HashMap::new();
item.insert(
"price".to_string(),
12
);
cart.insert(
"item".to_string(),
item,
);
fn get_item(cart: &HashMap<String, HashMap<String, i32>>) -> Option<&HashMap<String, i32>> {
cart.get("item")
}
fn get_price(item: &HashMap<String, i32>) -> Option<&i32> {
item.get("price")
}
使用 and_then 或 map 来顺序执行返回 Option 的函数
fn get_nested_price(cart: &HashMap<String, HashMap<String, i32>>) -> Option<&i32> {
return get_item(cart).and_then(get_price);
}
let price = get_nested_price(&cart);
match price {
Some(v) => assert_eq!(v, &12),
None => panic!("T_T"),
}
Option
也称为 Maybe
。 Some
有时称为 Just
。 None
有时称为 Nothing
。
函数式编程参考
Rust 语言中的函数式编程开发
我对这个项目的看法
请注意,该项目并没有涵盖每个定义及其代码示例,因为它不值得在这个词汇表中投入太多时间。我已额外努力添加了补充的外部参考,您可以查阅以进行进一步学习。
依赖项
~460KB