DatenLord|Understanding Functor in Rust

本文介绍了数学中Functor的概念,它是一种在范畴之间映射的对象。通过Rust编程语言中的Option类型为例,阐述了Functor如何保持结构的不变性,并解释了如何在Rust中实现Functor的map方法。此外,还讨论了自然变换,它是从一个Functor映射到另一个Functor的映射,保持内部结构的完整性。文章通过实例展示了如何将pop方法视为从Vec到Option的自然变换。
摘要由CSDN通过智能技术生成

Understanding Functor in Rust

In this blog, we introduce the general concept of Functor and Natural Transformations. The blog provides examples of Rust, but we will explain more about its concepts and definitions in the following sections.

In mathematics, a functor is a mapping between categories. It’s an obscure definition and looks horrible if you don’t like math. But don’t worry. Let’s look at an example.

Suppose you have a mapping that plus one. Then if you pass an integer, it will plus it with 1. What if you pass a list of integers? It does not work if we add one to the list directly. However, we could use the map function.

let f = |x| -> i32 { x + 1 };
let y = [1, 2, 3].map(f);
assert_eq!([2, 3, 4], y);

In fact, Functor is just a way to preserve the structure.

What is a category?

Back to the previous question, what is a category?

A category C has objects, morphisms, and binary operation ∘ , which is called the composition of morphisms. In this
context, we could view types as objects and their functions as morphisms. These functions could be composited with∘ . The (f ∘ g)(x) means apply f to g(x) . ie. (f ∘ g)(x) = f(g(x))

Composition is governed by two axioms:

  • Associativity: if f : a → b , g : b → c and h : c → d then h ∘ (g ∘ f) = (h ∘ g) ∘ f

  • Identity: For every object x, there exists an identity morphism 1x : x → x such that for every morrphism f : a → b , 1b ∘ f = f = f ∘ 1a
    在这里插入图片描述

Option Functor

enum Option<T> { Some(T), None }

In Rust, we have Option type, which is either some value of type T or it is no value. As a Functor, it is not only a mapping from T to Option, but also a mapping of functions. Option implements it as a map method.

fn map<U, F>(self, f: F) -> Option<U> {
   match self {
     Some(x) => Some(f(x)),
     None => None,
   }
}

if f is a function that maps from T to U. The method map f from Option to Option. Some(x) is a mapping between objects, which can either be primitive type or higher kinded type, while the map method is the mapping between morphisms.

Definition of Functor

So far, we go through the basic concept of functor and option as an example. Now let’s deep dive into the definition of Functor.

A Functor F : C → D is a mapping that

  • map each type X in C to a type F(X) in D

  • map each function f:X → Y in C to a function F(f) : F(X) → F(Y ) in D s.t

    • F(id_X) = id_F(X) for every X in C

    • F(g ∘ f) = F(g) ∘ F(f) for all f: X → Y and g : Y → Z in C

在这里插入图片描述

The Functor F not only maps objects X, Y, Z in C to F(X), F(Y), F(Z) in D, but also maps f, g, h in C to F(f), F(g), F(h) in D. It preserves the structure including the composition of morphisms. In this context, functions are maps between types inside the category. But Functor maps the whole structure formed by types and functions to another category.

Getting back to the option functor, how can it be proved to be a functor?

By definition of Functor, we need to prove that maps preserve identity and composition.

Condition 1: Identity

None Case:

assert_eq!(None.map(id), None); // By definition of map
assert_eq!(None, id(None)); // By definition of id

Some Case:

assert_eq!(Some(x).map(id), Some(x)); // By definition of map
assert_eq!(Some(x), id(Some(x))); // By definition of id 

Condition 2: Composition

None Case:

assert_eq!(None.map(f).map(g), None.map(g)); // By definition of map
assert_eq!(None.map(g), None); // By definition of map
assert_eq!(None, None.map(|x| g(f(x)))); // By definition of map

Some Case:

assert_eq!(Some(x).map(f).map(g), Some(f(x)).map(g)); // By definition of map
assert_eq!(Some(f(x)).map(g), Some(g(f(x)))); // By definition of composition
assert_eq!(Some(g(f(x))), Some(x).map(|x| g(f(x)))) // By definition of map

Therefore, The Option type constructor and map method form a Functor.

Implement functor in rust

There are various opinions about the implementation of the functor in rust. Due to the limitations of the rust language, some of them are still ideal. Here I would recommend one attempt, which could be implemented by using generic_associated_types(GAT).

trait Functor {
    type Unwrapped;
    type Wrapped<B>: Functor;

    fn map<F, B>(self, f: F) -> Self::Wrapped<B>
    where
        F: FnMut(Self::Unwrapped) -> B;
}

In this trait, there are two associated types. Unwrapped represents the inner type and Wrapped represents the type containing B type inside.

Taking Option as an example, If we have an Unwrapped type A, then the Wrapped type should be Option A. F is a function from A to B. The map method combines the function mapping Ff and sends self as input. Thus, we have Option B directly. If we only consider Ff , we can define fmap

fn fmap<F, A, B>(mut f: F) -> impl FnMut(Option<A>) -> Option<B>
where 
    F: FnMut(A) -> B
{
    move |x| -> Option<B>
    {
        match x {
            Some(x) => Some(f(x)),
            None => None,
        }
    }
}

assert_eq!(Some(6), fmap(|x: i32| x + 1)(Some(5)));

The map method not only contains Ff, it also gives the input, which is one step further.

Imagine implementing a Functor trait for Option. we could have

impl<A> Functor for Option<A> {
    type Unwrapped = A;
    type Wrapped<B> = Option<B>;

    fn map<B, F>(self, f: F) -> Option<B>
    where
        F: FnMut(Self::Unwrapped) -> B,
    {
        self.map(f)
    }
}

However, there is one drawback. If we assign Wrapped to a Result type, the structure-preserve property would be broken. But the compiler won’t check that.

Natural Transformation

We talk about Functors as structure-preserving maps between categories. Derivation from Functors, there is a more abstract concept, Natural transformations. It maps functors to functors.

Suppose we have two categories C and D, and two functors F and G that map a in C to Fa and Ga.
在这里插入图片描述

A natural transformation is a family of morphisms from F to G. The morphism is called the component of at a. In other words, natural transformation is indexed by the object in the domain category C.

If for some x in C, there is no morphism from Fx to Gx, then there can be no natural transformation between F and G.Because natural transformation also needs to preserve the internal structure. It looks very similar to the functor. It
can be considered as a morphism of functors.

Because Functor also maps morphisms, if we have a function maps from a to b. Functor will give you mapping from Fa to Fb. Similarly, we have Gf from Ga to Gb. Through the commutative diagram, we could find there are two ways from Fa to Gb. It gives us an equation.

αb ∘ Ff = Gf ∘ αa

If components satisfy the equation for every f in C, combined with the previous condition, we have a natural transformation α.

Taking pop method of Vec as an example.

let mut vec = vec![1, 2, 3];
assert_eq!(Some(3), vec.pop());

The pop method mapping from a Vec Functor to Option Functor. We could regards it as α. Suppose we have a plus one function.

// αb∘Ff 
let mut vec:Vec<i32> = vec![1, 2, 3].into_iter().map(|x| x+1).collect(); 
assert_eq!(Some(4), vec.pop()); 
// Gf∘αa 
let last = vec![1, 2, 3].pop(); 
assert_eq!(Some(4), last.map(|x| x+1));

Because rust iterators are lazy, we need to use a collect() method to make new elements, which combines the map method as Ff. We use two different ways to send Vec to Option, and both give the same result.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

达坦科技DatenLord

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值