0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Rust中GAT和高阶类型

jf_wN0SrCdH 来源:Rust语言中文社区 作者:Rust语言中文社区 2022-11-07 10:21 次阅读

05ad3ac8-5de9-11ed-a3b6-dac502259ad0.jpg

Rust在类型系统级别上与Haskell,Scala有许多相似之处。

两者都有类型(type),泛型类型(generic types),关联类型(associated types)和特质/类型类(traits/type classes)(基本上是等效的)。

但是,Haskell有Rust缺乏的一个特性:高阶类型(Higher Kinded Types), 也就是通常说的HKTs。

这不是Rust故意不添加,也不是Rust应该填补的一些差距。其实这是Rust的一个有意的设计。

结果就是,到没有GATs出现时,某些代码无法真正在Rust中实现。

以Haskell中的Functor为例。Functor其实是对类型上实现的map的一个更加抽象的实现,不关心具体类型的一个接口

比如,在Scala中,要实现一个map的Functor是这样的:

importscala.language.higherKinds

traitFunctor[F[_]]{
defmap[A,B](fa:F[A])(f:A=>B):F[B]
}

如果你使用过Scala的集合api,这看起来应该非常相似。所有集合都可以使用map。

List(1,2,3).map(_+1)
//List(2,3,4)

你可能要问,什么是高阶类型啊.你可以先理解为: 高阶类型是类型的类型。这是什么意思呢? 我以Scala里面的代码来说明一下。

scala>traitMyList[A]{
defhead:A
deftail:MyList[A]
}

然后用:k命令看下是什么类型:

scala>:k-vMyList
MyList'skindisF[A]
*->*
Thisisatypeconstructor:a1st-order-kindedtype.

MyList[String]类型是一个一阶类型(1st-order-kinded type);任何MyList的类型都是由A参数化的,可以将MyList看作是一个类型函数,如A => MyList[A],因此给定一个类型A,我们可以创建一个新的类型MyList[A]。如果MyList是一阶类型,那么什么是类型化类型呢?其实想想,什么是高阶函数啊? 它不就是一个接受函数然后返回另外另一个函数的函数。同理可得,什么是高阶类型呢? 它是由另一个类型参数化的类型。我再写一个简单的例子。

scala>traitFoo[F[_]]:k-vFoo
Foo'skindisX[F[A]]
(*->*)->*
Thisisatypeconstructorthattakestypeconstructor(s):ahigher-kindedtype.

这里的Foo就是一个高阶类型。它是一个类型构造函数,参数也是一个类型构造函数。为了说清楚这点,我再写一个例子.我写一个trait,里面有一个leftFold的方法。

scala>traitFoldable[F[_]]{
|defleftFold[A,B](ob:F[A])(zero:B)(fn:(B,A)=>B):B
|}

然后呢,我再实现一个listFoldable, 这是最常见的吧。

scala>implicitvallistFoldable:Foldable[List]=newFoldable[List]{
|defleftFold[A,B](ob:List[A])(zero:B)(fn:(B,A)=>B):B={
|ob.foldLeft(zero)(fn)
|}
|}

上面我定义了一个适用于任何List类型的Foldable对象。leftFold方法将取A并使用A构造List[A]。

一种更好的写法:

scala>importscala.language.implicitConversions
importscala.language.implicitConversions

scala>implicitclassListFoldableOpt[A](list:List[A])(implicitfold:Foldable[List]){
|defleftFold[B](zero:B)(fn:(B,A)=>B):B=
|fold.leftFold(list)(zero)(fn)
|}

scala>List(1,2,3).leftFold(0)(_+_)//6

这里先收一下,回到Rust中。

Rust中的很多不同结构都实现了map函数,比如: Option, Result, Iterator, and Future.

但是, 在Rust里面,还不能编写可以给多种类型实现的通用Functortrait, 就像我刚才上面写的trait Functor[F[_]]。通常呢 在Rust里面是单个类型单独去实现map。例如,我这里自己写了一个MyOption和MyResult枚举类, 并实现了map方法.

#[derive(Debug,PartialEq)]
enumMyOption{
Some(A),
None,
}

implMyOption{
fnmapB,B>(self,f:F)->MyOption{
matchself{
MyOption::Some(a)=>MyOption::Some(f(a)),
MyOption::None=>MyOption::None,
}
}
}

#[test]
fntest_option_map(){
assert_eq!(MyOption::Some(5).map(|x|x+1),MyOption::Some(6));
assert_eq!(MyOption::None.map(|x:i32|x+1),MyOption::None);
}

#[derive(Debug,PartialEq)]
enumMyResult{
Ok(A),
Err(E),
}

implMyResult{
fnmapB,B>(self,f:F)->MyResult{
matchself{
MyResult::Ok(a)=>MyResult::Ok(f(a)),
MyResult::Err(e)=>MyResult::Err(e),
}
}
}

#[test]
fntest_result_map(){
assert_eq!(MyResult::Ok(5).map(|x|x+1),MyResult::(6));
assert_eq!(MyResult::Err("hello").map(|x:i32|x+1),MyResult::Err("hello"));
}

上面的几个例子都是直接在自己的结构中定义的map方法。如果没有GATs, 就不可能将map定义为一个trait的方法。这是为什么呢?下面是一个在Rust里面简单的Functortrait实现,以及Option的实现.

traitNaiveFunctor{
typeT;
fnmap(self,f:F)->Self
where
F:FnMut(Self::T)->Self::T;
}

implNaiveFunctorforOption{
typeT=A;
fnmapA>(self,mutf:F)->Option{
matchself{
Some(a)=>Some(f(a)),
None=>None,
}
}
}

在上面的trait定义中,先给NaiveFunctor内部的值定义了一个关联类型T。给Optionimpl这个trait,T=A。这就是问题所在。Rust将T硬编码为一种类型A,因为通常在map函数中,希望返回的类型是B,但在之前版本稳定的Rust中,没有办法说"我想要一个既能与NaiveFunctor关联的类型,又要求这个类型和关联类型有一点不一样。

这就是泛型关联类型发挥作用的地方了。

多态Functor的实现

为了得到一个多态Functor.我想要的是:"我的类型最终应该是我在其中包裹的类型".例如,对于Option,我想说的是"如果我有一个Option,那么它肯定包含一个A类型,但如果它包含一个B类型,它将是Option"

这里就需要使用泛型关联类型.

traitFunctor{
typeUnwrapped;
typeWrapped:Functor;
fnmap(self,f:F)->Self::Wrapped
where
F:FnMut(Self::Unwrapped)->B;
}

说明下上面的写法:

每个Functor都有一个关联的Unwrapped类型.

还有另一个关联类型Wrapped,它与Self类似,但有一个不同的包装值

和前面例子一样, map接受两个参数的一个是:self和一个函数

函数形参将从当前关联的Unwrapped值映射到一个新的类型B

map的输出是一个Wrapped

有点抽象。具体点现在看下Option类型是什么样子的

implFunctorforOption{
typeUnwrapped=A;
typeWrapped=Option;

fnmapB,B>(self,mutf:F)->OptionwhereF:FnMut(A)->B{
matchself{
Some(x)=>Some(f(x)),
None=>None,
}
}
}

#[test]
fntest_option_map(){
assert_eq!(Some(5).map(|x|x+1),Some(6));
assert_eq!(None.map(|x:i32|x+1),None);
}

编译通过,非常优雅。

类型类

在Haskell和Scala中,其实是不需要这种泛型关联类型的。事实上,Haskell中Functors不使用任何关联类型。Haskell中Functor的类型类远远早于其他语言中关联类型的出现。为了进行比较,先看看它是什么样子。

classFunctorfwhere
map::(a->b)->fa->fb

instanceFunctorOptionwhere
mapfoption=
caseoptionof
Somex->Some(fx)
None->None
traitFunctor[F[_]]{
defmap[A,B](fa:F[A])(f:A=>B):F[B]
}

把它转换成Rust就是如下:

traitHktFunctor{
fnmapB>(self:Self,f:F)->Self;
}

implHktFunctorforOption{
fnmapB>(self:Option,f:F)->Option{
matchself{
Some(a)=>Some(f(a)),
None=>None,
}
}
}

但是上面的代码是编译不通过的。因为我试图给Self提供类型参数。但是在Rust中, 单独一个Option不是一个类型. Option要成为一个类型,必须有一个类型参数.比如: Option 是一个类型. Option却不是.

相反,在Haskell中,Maybe Int是kind type的一种类型。Maybe是类型构造函数,类型为type -> type。但是出于创建类型类和实例的目的,可以将Maybe处理为具有自己的类型。Haskell中的Functor作用于type->type。

这就是我所说的"高阶类型": 就是说我可以写出拥有类型的类型(Kind)。

Pointer 的实现

Haskell中有一个有争议的类型类叫做Pointed。之所以有争议的,是因为它引入了一个类型类,但没有与它相关的任何laws。我想在Rust中实现下Pointed。

什么是Pointed

Pointed的思想很简单:将一个值包装成一个类似Functor的东西。比如:在Option的情况下,它就用Some包装它。在Result中,它使用Ok。对于Vec,它是一个单值向量。与Functor不同,这里是一个静态方法,因为我们没有现有的Point值要改。让我们看看它在Rust中的实现。

traitPointed:Functor{
fnwrap(t:T)->Self::Wrapped;
}

implPointedforOption{
fnwrap(t:T)->Self::Wrapped{
Some(t)
}
}

这里需要注意的是: 我根本没有在Option实现中使用A类型参数。

还有一点值得注意。调用wrap的结果返回的是Self::Wrapped。关于Self::Wrapped,到底是什么?从之前Functor的定义中,确切地知道一件事:Wrapped必须是一个Functor的。

本篇文章关于GATs就先聊到这里。感兴趣的同学可以关注下公众号。下篇再见。

审核编辑:汤梓红

  • 函数
    +关注

    关注

    3

    文章

    4304

    浏览量

    62412
  • GAT
    GAT
    +关注

    关注

    0

    文章

    7

    浏览量

    6328
  • Rust
    +关注

    关注

    1

    文章

    228

    浏览量

    6567
收藏 人收藏

    评论

    相关推荐

    Rust的From和Into trait的基础使用方法和进阶用法

    、可靠和安全的系统级应用的首选语言。 Rust的From和Into是两个重要的trait,它们可以帮助我们进行类型转换。From trait允许我们从一个类型转换到另一个
    的头像 发表于 09-20 10:55 1723次阅读

    如何在Rust读写文件

    见的内存安全问题和数据竞争问题。 在Rust,读写文件是一项非常常见的任务。本教程将介绍如何在Rust读写文件,包括基础用法和进阶用法。 基础用法 读取文件内容 使用 std::f
    的头像 发表于 09-20 10:57 1965次阅读

    Rust的多线程编程概念和使用方法

    Rust是一种强类型、高性能的系统编程语言,其官方文档强调了Rust的标准库具有良好的并发编程支持。Thread是Rust
    的头像 发表于 09-20 11:15 921次阅读

    Windows -编程-数据类型

    Windows -编程-数据类型Rust 的每个值都有特定的数据类型,它告诉 Rust 指定了什么样的数据,以便它知道如何处理这些数据。我
    发表于 08-24 14:30

    RUST在嵌入式开发的应用是什么

    Rust是一种编程语言,它使用户能够构建可靠、高效的软件,尤其是用于嵌入式开发的软件。它的特点是:高性能:Rust具有惊人的速度和高内存利用率。可靠性:在编译过程可以消除内存错误。生产效率:优秀
    发表于 12-24 08:34

    Rust代码中加载静态库时,出现错误 ` rust-lld: error: undefined symbol: malloc `怎么解决?

    时,出现错误 ` [i]rust-lld: error: undefined symbol: malloc `。如何将这些定义包含在我的静态库
    发表于 06-09 08:44

    GAT 联栅晶体管

    GAT 联栅晶体
    发表于 11-06 17:07 722次阅读

    联栅晶体管(GAT)是什么意思?

    联栅晶体管(GAT)是什么意思?  联栅晶体管是一种新型功率开关半导体器件,简称GATGAT是介于双极型晶体管(BJT)和场效应晶体管(FET)之间的特种器
    发表于 03-05 14:35 2801次阅读

    Linux内核整合对 Rust 的支持

    Linux Plumbers Conference 2022 大会上举行了一个 Rust 相关的小型会议,该会议讨论的大方向大致为:正在进行的使 Rust 成为一种合适的系统编程语言的工作,以及在主线 Linux 内核整合对
    的头像 发表于 09-19 11:06 1154次阅读

    Rust原子类型和内存排序

    原子类型在构建无锁数据结构,跨线程共享数据,线程间同步等多线程并发编程场景起到至关重要的作用。本文将从Rust提供的原子类型和原子类型的内
    的头像 发表于 10-31 09:21 904次阅读

    什么是高阶类型啊?如何去定义高阶类型

    这不是Rust故意不添加,也不是Rust应该填补的一些差距。 其实这是Rust的一个有意的设计。
    的头像 发表于 11-07 10:20 1131次阅读

    Rust的错误处理方法

    Rust 没有提供类似于 Java、C++ 的 Exception 机制,而是使用 Result 枚举的方式来实现。
    的头像 发表于 02-20 09:37 909次阅读

    rust语言基础学习: Default trait

    Default trait 顾名思义是默认值,即Rust为特定类型实现 Default 特性时,可以为该类型赋予了可选的默认值。
    的头像 发表于 05-22 16:16 1243次阅读

    rust语言基础学习: rust的错误处理

    错误是软件不可避免的,所以 Rust 有一些处理出错情况的特性。在许多情况下,Rust 要求你承认错误的可能性,并在你的代码编译前采取一些行动。
    的头像 发表于 05-22 16:28 2051次阅读

    RustPin/Unpin详解

    对我来说,其中之一就是在Rust Pin/Unpin 。
    的头像 发表于 07-20 11:00 828次阅读