Rust 中函数的类型同义词

huangapple go评论71阅读模式
英文:

Type synonym for functions in Rust

问题

我想为函数创建类型别名,以减少混乱。例如,我想要像这样的东西:

type MyType<T, V> = FnMut(T) -> (T, V);

fn compose<T, U, V>(fst: MyType<T, U>, snd: MyType<U, V>) -> MyType<T, V> {
    |mut& x| {
        let (t, u) = fst(x);
        let (_, v) = snd(u);
        (t, v)
    }
}

但它无法编译。我可以添加 dyn 关键字,但类型别名不能用作特质。

Haskell 中可以使用类似这样的代码:

type MyType a b = a -> (a, b)

compose :: MyType a b -> MyType b c -> MyType a c
compose f g = \x ->
    let (a, b) = f x
        (_, c) = g b
    in  (a, c)

一个不那么简单的用例:该同义词需要使用 FnMut,因为我试图基于 nomParser 创建同义词。

英文:

I would like to have type synonyms for functions, so that they are less cluttersome. For instance, I would like something like this:

type MyType&lt;T, V&gt; = FnMut(T) -&gt; (T, V);

fn compose&lt;T, U, V&gt;(fst: MyType&lt;T, U&gt;, snd: MyType&lt;U, V&gt;) -&gt; MyType&lt;T, V&gt; {
    |mut&amp; x| {
        let (t, u) = fst(x);
        let (_, v) = snd(u);
        (t, v)
    }
}

But it fails to compile. I can add dyn keywords but type aliases cannot be used as traits.

Something like this works in Haskell:

type MyType a b = a -&gt; (a, b)

compose :: MyType a b -&gt; MyType b c -&gt; MyType a c
compose f g = \x -&gt;
    let (a, b) = f x
        (_, c) = g b
    in  (a, c)

A less-toy-example use-case: The synonym needs to use FnMut, as I am trying to make synonyms based off nom's Parser.

答案1

得分: 3

Peter Hall的答案很好地解释了Rust和Haskell之间类型系统的差异。他对此更加了解,所以我将指向他的答案,以获取有关该方面的解释。相反,我想为你提供在Rust中实现你想要的东西的实际方法。

Rust与其他常见语言非常不同的一点是,在Rust中,trait可以实现在几乎任何类型上,包括你的crate没有定义的类型。这允许你声明一个trait,然后在任何其他类型上实现它,与一些只能在你定义的类型上实现接口的语言形成对比。这为你提供了极大的自由度,需要一些时间来充分理解这种自由度的潜力。

因此,虽然你不能为trait创建别名,但你可以创建一个自定义trait,该trait会自动实现在实现另一个trait的任何类型上。从语义上来说,这是不同的,但在大多数情况下,它足够接近于相同的东西,以便你可以在大多数情况下像使用别名一样使用它。

trait MyType<T, V>: FnMut(T) -> (T, V) {}

这声明了名为MyType的trait,具有相同的泛型参数,但要求实现此trait的任何类型也必须实现闭包trait。这意味着当编译器看到实现MyType<T, V>的东西时,它知道它也实现了闭包supertrait。这很重要,以便你可以实际调用这个函数。

这只是解决方案的一部分,现在我们需要使实现闭包trait的任何类型也实现MyType。这很容易做到:

impl<F, T, V> MyType<T, V> for F
where F: FnMut(T) -> (T, V) {}

因此,现在我们有了一个trait,它:

  • 只能在也实现所需闭包trait的东西上实现,这意味着闭包trait也被实现。
  • 自动实现在实现所需闭包trait的一切上。

这是使MyType<T, V>FnMut(T) -> (T, V) "实际上" 等效的方程的两个方面,即使它们实际上不是相同的trait在类型系统中。它们不是相同的trait,但你几乎可以在大多数情况下交换使用它们。

现在,我们可以围绕我们的新trait调整compose的定义:

fn compose<T, U, V>(
    mut fst: impl MyType<T, U>,
    mut snd: impl MyType<U, V>,
) -> impl MyType<T, V> {
    move |x| {
        let (t, u) = fst(x);
        let (_, v) = snd(u);
        (t, v)
    }
}

这里有一些重要的变化:

  • 我们使用impl MyType<_, _>,以便函数可以接收任何实现你的trait的东西,其中包括你试图目标的闭包类型。请注意,没有dyn,这也意味着没有动态分发。这消除了一层间接性。
  • 我们还"返回" impl MyType<_, _>,这意味着我们可以返回一个闭包而不需要对它进行封箱,这既防止了不必要的堆分配,又减少了不必要的间接性。
  • 由于前两个点,编译器可以潜在地完全内联对compose的两次调用以及它返回的闭包的调用,这可以使这种抽象在运行时性能上 "免费"。
  • 由于前两个点,我们不得不将fstsnd更改为mut,以便调用这些函数,因为底层的闭包类型是FnMut
  • 我们不得不在闭包上添加move,以便闭包获取fstsnd的所有权,否则它会尝试借用返回值中的函数局部变量,这是行不通的。

Playground

英文:

Peter Hall's answer does a good job of explaining the differences in the type systems between Rust and Haskell. He is much more knowledgeable about that, so I will point to his answer for any explanations about that. Instead, I want to give you a practical way that you can accomplish what you want within Rust.

One of the things that's very different about Rust compared to other common languages is that traits in Rust can be implemented on pretty much any type, including types your crate doesn't define. This allows you to declare a trait and then implement it on anything else, in contrast to languages where you can only implement interfaces on types that you are defining. This gives you an incredibly large amount of freedom, and it takes some time to fully grasp the potential this freedom grants you.

So, while you can't create aliases for traits, you can create your own trait that is automatically implemented on anything that implements the other trait. Semantically this is different, but it winds up being close enough to the same thing that you can use it like an alias in most cases.

trait MyType&lt;T, V&gt;: FnMut(T) -&gt; (T, V) {}

This declares the trait MyType with the same generic arguments, but requires that anything implementing this trait must also implement the closure trait. This means when the compiler sees something implementing MyType&lt;T, V&gt;, it knows it also implements the closure supertrait. This is important so that you can actually invoke the function.

That's half of the solution, but now we need MyType to actually be implemented on anything implementing the closure trait. This is pretty easy to do:

impl&lt;F, T, V&gt; MyType&lt;T, V&gt; for F
where F: FnMut(T) -&gt; (T, V) {}

So now we have a trait that:

  • Can only be implemented on things that also implement the required closure trait, implying that the closure trait is also implemented.
  • Is automatically implemented on everything implementing the required closure trait.

These are two sides of the equation that makes MyType&lt;T, V&gt; and FnMut(T) -&gt; (T, V) effectively equivalent, even if they aren't actually the same trait within the type system. They aren't the same trait, but you can use them almost interchangeably.

Now, we can adjust the definition of compose around our new trait:

fn compose&lt;T, U, V&gt;(
    mut fst: impl MyType&lt;T, U&gt;,
    mut snd: impl MyType&lt;U, V&gt;,
) -&gt; impl MyType&lt;T, V&gt; {
    move |x| {
        let (t, u) = fst(x);
        let (_, v) = snd(u);
        (t, v)
    }
}

A few important changes here:

  • We use impl MyType&lt;_, _&gt; so that the function can receive anything implementing your trait, which includes the closure types you're trying to target. Note there is no dyn which also means there is no dynamic dispatch. This removes a level of indirection.
  • We also return impl MyType&lt;_, _&gt; which means we can return a closure without boxing it, which both prevents an unnecessary heap allocation as well as an unnecessary level of indirection.
  • Because of the prior two points, the compiler can potentially fully inline both calls to compose as well as calls to the closure it returns, which can make this abstraction "free" in terms of runtime performance!
  • We had to change fst and snd to be mut in order to invoke the functions because the underlying closure type is FnMut.
  • We had to add move to the closure so that the closure takes ownership of fst and snd, otherwise it would try to borrow function local variables in the return value, which cannot work.

(Playground)

答案2

得分: 2

在Rust中,函数的表达方式与Haskell中不同。

在Haskell中,函数签名对应于类型。具有相同签名的两个函数具有相同的类型,即使它们的实现方式不同,甚至它们是具有不同环境的闭包。类型仅从函数的参数和返回值派生。

在Rust中,只有一种可命名的函数类型,即fn。这些是没有环境的函数指针。闭包始终是不同的类型,即使它们具有相同的参数和返回类型。这只是语言之间的一种权衡。在Rust中,FnFnOnceFnMut是特征(traits),而不是类型,无法为特征创建别名。

在Rust中存在一个用于特征别名的RFC,这可能部分满足您的需求。但这自2017年以来一直存在,并且似乎还没有稳定下来。

英文:

Functions in Rust are expressed differently from how they are expressed in Haskell.

In Haskell, function signatures correspond to types. Two functions with the same signature have the same type, even if they are implemented differently and even if they are closures with different environments. The type is purely derived from the arguments and return value of the function.

In Rust, there is only one "class" of nameable function types, that is fn. These are function pointers that have no environment. Closures are always different types, even if they have the same arguments and return type. This is just a trade off in the languages. In Rust Fn, FnOnce and FnMut are traits not types,
and you cannot create aliases for traits.

There is an RFC for trait aliases in Rust, which might partly do what you want. But this has been going since 2017 and does not appear close to being stabilised.

huangapple
  • 本文由 发表于 2023年3月7日 08:42:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/75657090.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定