Rust中的Monads和GATs介绍

645 阅读20分钟

这篇博文的灵感完全来自于阅读《GATs on Nightly!Reddit的帖子,作者是/u/C5H5N5O。我只是决定把事情做得有点过火,并认为就此发表一篇博文会很有趣。我想从一开始就说清楚。我在这篇文章中介绍了一些依赖于Rust中不稳定特性的高级概念。我完全不提倡使用这些功能。我只是在探索使用GATs可能和不可能实现的东西。

Rust与Haskell在类型系统层面上有许多相似之处。两者都有类型、通用类型、关联类型和特质/类型类(基本等同)。然而,Haskell有一个重要的附加功能,而Rust却没有。高等类型(HKTs)。这不是Rust中偶然的限制,也不是一些应该被填补的空白。这是一个有意的设计决定,至少就我所知。但结果是,有些东西直到现在还不能真正在Rust中实现。

以Haskell中的一个Functor 为例。尽管它的名字听起来很吓人,但今天几乎所有的开发者都熟悉FunctorFunctor 提供了一个通用的接口,用于 "在这个结构上映射一个函数"。Rust中的许多不同的结构都可以提供这样的映射功能,包括OptionResultIteratorFuture

然而,还不可能编写一个通用的Functor 特质,可以由多种类型实现。相反,个别类型可以作为该类型的方法来实现它们。例如,我们可以编写自己的自定义MyOptionMyResult 枚举,并提供map 方法。

#[derive(Debug, PartialEq)]
enum MyOption<A> {
    Some(A),
    None,
}

impl<A> MyOption<A> {
    fn map<F: FnOnce(A) -> B, B>(self, f: F) -> MyOption<B> {
        match self {
            MyOption::Some(a) => MyOption::Some(f(a)),
            MyOption::None => MyOption::None,
        }
    }
}

#[test]
fn test_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)]
enum MyResult<A, E> {
    Ok(A),
    Err(E),
}

impl<A, E> MyResult<A, E> {
    fn map<F: FnOnce(A) -> B, B>(self, f: F) -> MyResult<B, E> {
        match self {
            MyResult::Ok(a) => MyResult::Ok(f(a)),
            MyResult::Err(e) => MyResult::Err(e),
        }
    }
}

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

然而,如果没有GATs,还不可能将map 定义为一个trait方法。让我们来看看原因。下面是一个 "单态漏斗 "trait的天真方法,以及对Option 的实现。

/// Monomorphic functor trait
trait MonoFunctor {
    type Unwrapped; // value "contained inside"
    fn map<F>(self, f: F) -> Self
    where
        F: FnMut(Self::Unwrapped) -> Self::Unwrapped;
}

impl<A> MonoFunctor for Option<A> {
    type Unwrapped = A;
    fn map<F: FnMut(A) -> A>(self, mut f: F) -> Option<A> {
        match self {
            Some(a) => Some(f(a)),
            None => None,
        }
    }
}

在我们的特征定义中,我们定义了一个相关的类型Unwrapped ,用于 "住在 "MonoFunctor 的值。在Option<A> 的情况下,这将是A 。问题就出在这里。我们将Unwrapped 硬编码为一个类型,即A 。通常在使用map 函数时,我们希望将该类型改为B 。但在当前稳定的Rust中,我们没有办法说 "我想要一个与这个MonoFunctor 相关的类型,但在它的内部也有一点不同。"

这就是通用关联类型出现的原因。

多态向量

为了得到一个多态的向量,我们需要能够说 "如果我把一个不同的类型包在里面,我的类型会是这样的。"例如,对于Option ,我们想说 "嘿,我有Option<A> ,它包含一个A 类型,但如果它包含一个B 类型,它将是Option<B> 。" 为了做到这一点,我们将使用通用关联类型Wrapped<B>

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

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

所以我们要说的是:

  • 每个漏斗都有一个关联类型Unwrapped ,这是它所包含的东西
  • 当我们知道一个漏斗时,我们也可以算出另一个相关的类型Wrapped<B> ,它是 "像Self ,但下面有一个不同的被包裹的值"
  • 像以前一样,map 是一个方法,需要两个参数:self 和一个函数
  • 函数参数将从当前的底层Unwrapped 值映射到一些新的类型B
  • map 的输出将是一个Wrapped<B>

这是一个有点抽象的说法。让我们看看Option 类型是什么样子的。

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

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

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

如果你玩了所有的类型体操,你会发现这最终与我们在上面为MyOption 特设的map 方法完全相同(除了FnOnceFnMut 之间的区别)。酷!"。

题外话:HKTs

在Haskell中,不需要这种通用的关联类型业务。事实上,Haskell的Functor,不使用任何关联类型。在Haskell中,Functor 的类型类别远远早于语言中关联类型的存在。为了进行比较,让我们看看它是什么样子的,重新命名一下以与Rust相匹配。

class Functor f where
    map :: (a -> b) -> f a -> f b
instance Functor Option where
    map f option =
        case option of
            Some x -> Some (f x)
            None -> None

或者,把它翻译成类似Rust的语法。

trait HktFunctor {
    fn map<A, B, F: FnMut(A) -> B>(self: Self<A>, f: F) -> Self<B>;

impl HktFunctor for Option {
    fn map<A, B, F: FnMut(A) -> B>(self: Option<A>, f: F) -> Option<B> {
        match self {
            Some(a) => Some(f(a)),
            None => None,
        }
    }
}

但这并不是有效的Rust!这是因为我们试图为Self 提供类型参数。但在Rust中,Option 并不是一个类型。Option 必须在成为一个类型参数之前应用于一个类型。Option<i32> 是一个类型。Option 本身不是。

相比之下,在Haskell中,Maybe Int 是一个类型,属于TypeMaybe 是一个类型 构造函数,属于Type -> Type 。但是你可以把Maybe 视为它自己的一个类型,用于创建类型类和实例。在Haskell中,Functor 在类型Type -> Type 上工作。这就是我们所说的 "更高的类型":我们可以有种类高于Type 的类型。

Rust中的GATs是对下面的例子中缺少HKTs的一种变通方法。但正如我们最终会看到的,它们更加脆弱,更加啰嗦。这并不是说GATs是个坏东西,远非如此。而是说尝试用Rust写Haskell可能不是一个好主意。

好了,现在我们已经彻底确定了我们要做的事情不是一个好主意......让我们开始吧!"。

指向性

在Haskell中有一个有争议的类型类,叫做Pointed 。它之所以有争议,是因为它引入了一个没有任何法则关联的类型类,这通常是不太受欢迎的。但是,既然我已经告诉你这都是一个坏主意,那么就让我们来实现Pointed

Pointed 的想法很简单:把一个值包装成一个类似Functor 的东西。因此,在Option 的情况下,它就像用Some 来包装它。在Result 的情况下,它就用Ok 。而对于Vec ,它将是一个单元素的向量。不像Functor ,这将是一个静态的方法,因为我们没有一个现有的Pointed 的值来改变。让我们看看它的操作。

trait Pointed: Functor {
    fn wrap<T>(t: T) -> Self::Wrapped<T>;
}

impl<A> Pointed for Option<A> {
    fn wrap<T>(t: T) -> Option<T> {
        Some(t)
    }
}

特别有趣的是,在Option 的实现中,我们根本就没有使用A 的类型参数。

还有一件事值得注意。调用wrap 的结果是一个Self::Wrapped<T> 的值。关于Self::Wrapped<T> ,我们到底知道什么?好吧,从Functor 特质的定义中,我们确切地知道一件事:Wrapped<T> 必须是一个Functor 。有趣的是,我们在这里失去了 Self::Wrapped<T> 也是一个Pointed知识。这将是接下来几个特质中反复出现的主题。

但让我以不同的方式重申这一点。当我们在处理一个一般的Functor 特质的实现时,除了它实现了Functor 本身之外,我们对Wrapped 相关的类型一点都不了解。从逻辑上讲,我们知道对于一个Option<A> 的实现,我们希望Wrapped 是一个Option<B> 的类型。但是GAT的实现并没有强制执行它。(相比之下,Haskell中的HKT方法确实执行了这一点。)没有什么能阻止我们写出一个可怕的、不可理喻的实现,比如说:

impl<A> Functor for MyOption<A> {
    type Unwrapped = A;
    type Wrapped<B> = Result<B, String>; // wut?

    fn map<F: FnMut(A) -> B, B>(self, mut f: F) -> Result<B, String> {
        match self {
            MyOption::Some(a) => Ok(f(a)),
            MyOption::None => Err("Well this is weird, isn't it?".to_owned()),
        }
    }
}

你可能在想,"那又怎样,没有人会写这样的东西。如果他们这样做,那是他们自己的错。" 这不是重点。重点是,编译器不可能知道SelfWrapped<B> 之间有联系。既然它不能知道,那么有些东西我们就不能进行类型检查。我将在最后向你展示其中的一个。

应用性

当我给Haskell培训时,当我讲到Functor/Applicative/Monad 部分时,大多数人对Monads感到紧张。根据我的经验,真正令人困惑的部分其实是Applicative 。一旦你理解了这一点,Monad 就相对容易了。

Haskell中的Applicative 类型类有两个方法。pure 相当于我放在Pointed 中的wrap ,所以我们可以忽略它。另一个方法是<*> ,被称为 "apply",或 "splat",或 "the tie fighter"。我最初用一个叫做apply 的方法来实现Applicative ,与该操作符相匹配,但发现还是走另一条路更好。

相反,有一种另类的方法来定义一个Applicative 类型,基于一个不同的函数,叫做liftA2 (或者,在Rust中,lift_a2 )。这个想法是这样的。假设我有两个函数。

fn birth_year() -> Option<i32> { ... }
fn current_year() -> Option<i32> { ... }

我可能不知道当前年份或出生年份,在这种情况下,我将返回None 。但是如果我在这两个函数的调用中都得到一个Some ,那么我就可以计算出年龄。在正常的Rust代码中,这可能看起来像。

fn age() -> Option<i32> {
    let birth_year = birth_year()?;
    let current_year = current_year()?;
    Some(current_year - birth_year)
}

但这是利用了? 和提前返回。Applicative 的一个主要目的是解决同样的问题。因此,让我们重写这个,不使用任何提前返回,而是使用一些模式匹配。

fn age() -> Option<i32> {
    match (birth_year(), current_year()) {
        (Some(birth_year), Some(current_year)) => Some(current_year - birth_year),
        _ => None,
    }
}

这当然有效,但它很冗长。它也不能推广到其他情况,比如说Result 。还有一个非常复杂的情况,比如说 "我有一个Future ,它将返回出生年份,一个Future ,它将返回当前年份,我想产生一个Future ,找出其中的差别。" 用async/await语法,这很容易做到。但我们也可以用Applicative ,用我们的lift_a2 方法来做。

lift_a2 的意义在于。我有两个值,也许都在一个Option 。我想用一个函数把它们结合起来。让我们看看这在Rust中是什么样子的。

trait Applicative: Pointed {
    fn lift_a2<F, B, C>(self, b: Self::Wrapped<B>, f: F) -> Self::Wrapped<C>
    where
        F: FnMut(Self::Unwrapped, B) -> C;
}

impl<A> Applicative for Option<A> {
    fn lift_a2<F, B, C>(self, b: Self::Wrapped<B>, mut f: F) -> Self::Wrapped<C>
    where
        F: FnMut(Self::Unwrapped, B) -> C
    {
        let a = self?;
        let b = b?;
        Some(f(a, b))
    }
}

有了这个定义,我们现在可以把age 改写成:

fn age() -> Option<i32> {
    current_year().lift_a2(birth_year(), |cy, by| cy - by)
}

这是否是一种改进,可能在很大程度上取决于你在生活中写过多少Haskell。再说一遍,我并不主张在这里改变Rust,但这确实很有趣。

我们也可以用Result 做同样的事情。

fn birth_year() -> Result<i32, String> {
    Err("No birth year".to_string())
}

fn current_year() -> Result<i32, String> {
    Err("No current year".to_string())
}

fn age() -> Result<i32, String> {
    current_year().lift_a2(birth_year(), |cy, by| cy - by)
}

这可能会引出一个问题:在这两个Err 值中,我们该取哪一个?嗯,这取决于我们对Applicative 的实现,但通常我们会选择第一个。

impl<A, E> Applicative for Result<A, E> {
    fn lift_a2<F, B, C>(self, b: Self::Wrapped<B>, mut f: F) -> Self::Wrapped<C>
    where
        F: FnMut(Self::Unwrapped, B) -> C
    {
        match (self, b) {
            (Ok(a), Ok(b)) => Ok(f(a, b)),
            (Err(e), _) => Err(e),
            (_, Err(e)) => Err(e),
        }
    }
}

但如果我们两个都想要呢?这里有一个案例,Applicative 给我们提供了? 没有的功能。

验证

来自Haskell的Validation 类型代表了 "我要尝试很多东西,其中一些可能会失败,我想把所有的错误结果收集起来 "的想法。一个典型的例子是网络表单解析。如果一个用户输入了无效的电子邮件地址、无效的电话号码,并且忘记点击 "我同意 "框,你就会想生成所有这三种错误信息。你不希望只生成一条。

为了开始我们的Validation ,我们需要再引入一个Haskell-y的类型,这次是为了表示 "将多个值组合在一起 "的概念。我们可以在这里硬编码Vec ,但这有什么意思?相反,让我们引入名字很奇怪的Semigroup 特质。这甚至不需要任何特殊的GAT代码。

trait Semigroup {
    fn append(self, rhs: Self) -> Self;
}

impl Semigroup for String {
    fn append(mut self, rhs: Self) -> Self {
        self += &rhs;
        self
    }
}

impl<T> Semigroup for Vec<T> {
    fn append(mut self, mut rhs: Self) -> Self {
        Vec::append(&mut self, &mut rhs);
        self
    }
}

impl Semigroup for () {
    fn append(self, (): ()) -> () {}
}

有了这个,我们现在可以定义一个新的enum ,叫做Validation

#[derive(PartialEq, Debug)]
enum Validation<A, E> {
    Ok(A),
    Err(E),
}

FunctorPointed 的实现很无聊,让我们直接跳到Applicative 的实现上。

impl<A, E: Semigroup> Applicative for Validation<A, E> {
    fn lift_a2<F, B, C>(self, b: Self::Wrapped<B>, mut f: F) -> Self::Wrapped<C>
    where
        F: FnMut(Self::Unwrapped, B) -> C
    {
        match (self, b) {
            (Validation::Ok(a), Validation::Ok(b)) => Validation::Ok(f(a, b)),
            (Validation::Err(e), Validation::Ok(_)) => Validation::Err(e),
            (Validation::Ok(_), Validation::Err(e)) => Validation::Err(e),
            (Validation::Err(e1), Validation::Err(e2)) => Validation::Err(e1.append(e2)),
        }
    }
}

这里,我们说错误类型参数必须实现Semigroup 。如果两个值都是Ok ,我们对它们应用f 函数,并将结果包在Ok 。如果只有一个值是Err ,我们就返回那个错误。但如果两个都是错误,我们就利用Semigroupappend 方法将它们结合起来。这是你在? 风格的错误处理中无法得到的东西。

单体

最后,可怕的单体又出现了!它是一种新概念。但实际上,至少对Rustaceans来说,monad并不令人惊讶。你已经习惯了它:它是and_then 方法。在Rust中,几乎所有以? 结尾的语句链都可以被重新想象成单体绑定。在我看来,monad具有不可知的诱惑力的主要原因是一系列特别糟糕的教程在人们的头脑中巩固了这种想法。

Option无论如何,由于我们只是试图在and_then 上匹配现有的方法签名,我不打算花太多时间来激励 "为什么是monad"。相反,让我们只看一下特质的定义。

trait Monad : Applicative {
    fn bind<B, F>(self, f: F) -> Self::Wrapped<B>
    where
        F: FnMut(Self::Unwrapped) -> Self::Wrapped<B>;
}

impl<A> Monad for Option<A> {
    fn bind<B, F>(self, f: F) -> Option<B>
    where
        F: FnMut(A) -> Option<B>,
    {
        self.and_then(f)
    }
}

就这样,我们已经有了单体Rust。是时候在夕阳下驰骋了。

但是等等,还有更多

单体转化器

总的来说,我不是一个单体转化器的超级粉丝。我认为它们在Haskell中被过度使用,并导致了大量的复杂化。我主张采用ReaderT设计模式。但是,这篇文章绝对不是关于最佳实践。

通常,每个单体实例都提供了某种额外的功能。Option 表示 "它可能不会产生一个值"。Result 表示 "它可能会出错而失败"。如果我们提供了它,Future 表示 "它不会立即产生一个值,但它最终会产生"。作为最后一个例子,Reader 单体表示 "我对一些环境数据有只读权限"。

但是如果我们想拥有两块功能呢?没有明显的方法来结合一个Reader 和一个Result 。在Rust中,我们确实通过async 函数和? ,把ResultFuture 结合在一起,但这必须有精心设计的语言支持。相反,Haskell解决这个问题的方法是:只需提供do 符号(单体的语法糖),然后将你的单体转化器分层,将所有的功能加在一起。

我已经考虑就这种哲学上的差异写一篇博文了。(但是现在,让我们简单地探讨一下在Rust中提供单体转换器是什么样子的。我们将为所有单体转化器中最无聊的一个实现,IdentityT 。这是一个完全不做任何事情的转化器。(如果你想知道 "为什么要有它",请考虑一下为什么Rust有1-tuples。有时,你需要一些适合某种形状的东西来使一些通用代码很好地工作)。

由于IdentityT 不做任何事情,所以看到它的类型完美地反映了这一点是令人欣慰的。

struct IdentityT<M>(M);

我把类型参数称为M ,因为它本身就是Monad 的一个实现。这就是这里的想法:每个单体转化器都位于一个 "基单体 "之上。

接下来,让我们看看一个Functor 的实现。我们的想法是解开IdentityT 层,利用底层的map 方法,然后重新包装IdentityT

impl<M: Functor> Functor for IdentityT<M> {
    type Unwrapped = M::Unwrapped;
    type Wrapped<A> = IdentityT<M::Wrapped<A>>;

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

对于我们的关联类型,我们利用M 的关联类型。在map 中,我们使用self.0 来获取底层的M ,并用IdentityT 来包装map 方法调用的结果。酷!

PointedApplicativeMonad 的实现遵循类似的模式,所以我也会把这些都放进去。

impl<M: Pointed> Pointed for IdentityT<M> {
    fn wrap<T>(t: T) -> IdentityT<M::Wrapped<T>> {
        IdentityT(M::wrap(t))
    }
}

impl<M: Applicative> Applicative for IdentityT<M> {
    fn lift_a2<F, B, C>(self, b: Self::Wrapped<B>, f: F) -> Self::Wrapped<C>
    where
        F: FnMut(Self::Unwrapped, B) -> C
    {
        IdentityT(self.0.lift_a2(b.0, f))
    }
}

impl<M: Monad> Monad for IdentityT<M> {
    fn bind<B, F>(self, mut f: F) -> Self::Wrapped<B>
    where
        F: FnMut(Self::Unwrapped) -> Self::Wrapped<B>
    {
        IdentityT(self.0.bind(|x| f(x).0))
    }
}

最后,我们将定义一个新的特质:MonadTransMonadTrans 捕捉到了将基础单体 "分层 "到转换后的单体的想法。在Haskell中,你会经常看到像lift (readFile "foo.txt") 这样的代码,其中readFile 在基础单体中工作,而我们则坐在其上的一个层中。

trait MonadTrans {
    type Base: Monad;

    fn lift(base: Self::Base) -> Self;
}

impl<M: Monad> MonadTrans for IdentityT<M> {
    type Base = M;

    fn lift(base: M) -> Self {
        IdentityT(base)
    }
}

那么这有用吗?就其本身而言并不十分有用。可以说,我们可以创建一个由ReaderTWriterTContTConduitT 等组成的生态系统,并开始建立起复杂的系统。但我强烈认为,我们不需要Rust中的那些东西,至少现在不需要。我很乐意在我的实现中走到这一步,去探索GAT的奥妙,但我们不要因为我们可以做一些有用的东西就疯狂地去尝试。

加入

好了,现在有趣的事情开始了。我们已经在实践中看到了GATs。而且看起来Rust与Haskell的步伐保持得很好。这就要结束了。

在Haskell中还有一种与Monads相配合的方法,叫做join 。它的威力相当于我们已经看到的bind 方法,但工作方式不同。join 在Haskell中 "扁平化 "了两层单体。顺便提一下:在Rust中已经有一个叫做flatten 的方法,对OptionResult 做到这一点。

join 的问题是:单体必须是相同的。换句话说,join (Just (Just 5)) == Just 5 ,但join (Just (Right 6)) 是一个类型错误,因为Just 是一个Maybe 数据构造器,而Right 是一个Either 数据构造器。

现在我们有点进退两难了。在Haskell中,我们有更高的类型,很容易说 "Maybe 必须与Maybe 相同"。

join :: Monad m => m (m a) -> m a
join m = bind m (\x -> x)

但是我想不出一种方法来用Rust中的GATs来表达同样的想法,并且让编译器接受这种语法。这是我最接近的方法。

fn join<MOuter, MInner, A>(outer: MOuter) -> MOuter::Wrapped<A>
where
    MOuter: Monad<Unwrapped = MInner>,
    MInner: Monad<Unwrapped = A, Wrapped = MOuter::Wrapped<A>>,
{
    outer.bind(|inner| inner)
}

#[test]
fn test_join() {
    assert_eq!(join(Some(Some(true))), Some(true));
}

不幸的是,这破坏了编译器。

error: internal compiler error: compiler\rustc_middle\src\ty\subst.rs:529:17: type parameter `B/#1` (B/1) out of range when substituting, substs=[MInner]

thread 'rustc' panicked at 'Box<Any>', /rustc/b7ebc6b0c1ba3c27ebb17c0b496ece778ef11e18\compiler\rustc_errors\src\lib.rs:904:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

note: the compiler unexpectedly panicked. this is a bug.

note: we would appreciate a bug report: https://github.com/rust-lang/rust/issues/new?labels=C-bug%2C+I-ICE%2C+T-compiler&template=ice.md

note: rustc 1.50.0-nightly (b7ebc6b0c 2020-11-30) running on x86_64-pc-windows-msvc

note: compiler flags: -C embed-bitcode=no -C debuginfo=2 -C incremental --crate-type bin

note: some of the compiler flags provided by cargo are hidden

我认为可以说我在这里把编译器推到了极限。无论如何,我为此在GitHub上开了一个问题

mapM/traverse

我们已经被join 困扰了。另一个流行的函数式成语:traverse 。正如我之前提到的traverse 在Scala中非常流行,在Haskell中也相当常见。它的功能非常类似于map ,只是每一步通过map 的结果都被包裹在一些Applicative ,而Applicative 的值则被组合成一个整体的数据结构。

听起来让人困惑?有道理。举个更简单的例子:如果你有一个Vec<A> 值,和一个从AOption<B> 的函数,traverse 可以把这些组合成一个Option<Vec<B>> 。或者使用我们上面的Validation 类型,你可以把Vec<A>Fn(A) -> Validation<B, Vec<MyErr>> 组合成一个Validation<Vec<B>, Vec<MyErr>> ,返回所有成功生成的B 值,或者所有一路发生的错误。

总之,我最后用这个作为我们函数的起始类型签名。

fn traverse<F, M, A, B, I>(iter: I, f: F) -> M::Wrapped<Vec<B>>

然后我们就有了下面的特征界线:

  • I: IntoIterator<Item = A>:I 是一个A 值的迭代器。为了简化,你可以把它看成是Vec<A>.j
  • M: Applicative<Unwrapped = B>:MApplicative 的一些实现,它解开了一个B 。在我们的例子中:这将是Validation<B, Vec<MyErr>>
  • F: FnMut(A) -> M:F 是一个函数,它从迭代器中获取A 值,并产生M 值。
  • M::Wrapped<Vec<B>>: Applicative<Unwrapped = Vec<B>>:在M's wrapping中包裹结果Vec<B> ,产生一个值,也是一个Applicative

这最后一条显示了我上面提到的一个痛点。因为我们从Wrapped 关联类型本身根本不知道什么,我们只是 "免费 "得到了Functor 的约束。我们需要明确地说,它也是Applicative ,并且再次解开它将得到一个Vec<B>

无论如何,我还没有聪明到能想出一个办法来使所有这些都能被编译。这是我想出的最终版本的代码。

fn traverse<F, M, A, B, I>(iter: I, f: F) -> M::Wrapped<Vec<B>>
where
    F: FnMut(A) -> M,
    M: Applicative<Unwrapped = B>,
    I: IntoIterator<Item = A>,
    M::Wrapped<Vec<B>>: Applicative<Unwrapped = Vec<B>>,
{
    let mut iter = iter.into_iter().map(f);

    let mut result: M::Wrapped<Vec<B>> = match iter.next() {
        Some(b) => b.map(|x| vec![x]),
        None => return M::wrap(Vec::new()),
    };

    for m in iter {
        result = result.lift_a2(m, |vec, b| {
            vec.push(b);
            vec
        });
    }

    result
}

但是,这与错误信息一起失败了:

error[E0308]: mismatched types
   --> src\main.rs:448:33
    |
433 | fn traverse<F, M, A, B, I>(iter: I, f: F) -> M::Wrapped<Vec<B>>
    |                - this type parameter
...
448 |         result = result.lift_a2(m, |vec, b| {
    |                                 ^ expected associated type, found type parameter `M`
    |
    = note: expected associated type `<<M as Functor>::Wrapped<Vec<B>> as Functor>::Wrapped<_>`
                found type parameter `M`
    = note: you might be missing a type parameter or trait bound

error[E0308]: mismatched types
   --> src\main.rs:448:18
    |
433 |   fn traverse<F, M, A, B, I>(iter: I, f: F) -> M::Wrapped<Vec<B>>
    |                  - this type parameter
...
448 |           result = result.lift_a2(m, |vec, b| {
    |  __________________^
449 | |             vec.push(b);
450 | |             vec
451 | |         });
    | |__________^ expected type parameter `M`, found associated type
    |
    = note: expected associated type `<M as Functor>::Wrapped<Vec<B>>`
               found associated type `<<M as Functor>::Wrapped<Vec<B>> as Functor>::Wrapped<Vec<B>>`
help: consider further restricting this bound
    |
436 |     M: Applicative<Unwrapped = B> + Functor<Wrapped = M>,
    |                                   ^^^^^^^^^^^^^^^^^^^^^^

也许这是GATs的一个限制。也许我只是不够聪明,没有想出办法。但我认为这是一个好的点,可以叫停了。如果有人知道有什么窍门可以让它发挥作用,请告诉我

我们应该在Rust中使用HKT吗?

这是一次有趣的冒险。GATs看起来是对Rust中特征系统的一个很好的扩展。我期待着这个功能稳定下来并落地。而且,玩这些东西当然也很有趣。

但Rust不是Haskell。在我看来,GATs的人机工程学永远无法在Haskell的主场与更高类型的类型竞争。而且我也不相信它应该这样做。Rust是一种很棒的语言。我很高兴能在Rust代码库中写出Rust风格,并把我的Haskell编码留给我的Haskell代码库。

我希望其他人能像我一样享受这次冒险。我的代码的一个非常丑陋的版本可以在Gist中找到。你需要使用最近的Rust nightly build,但除此之外,它没有任何依赖性。