如何迭代或映射元组?

22

我的初始问题是将一个由不同类型组成的元组转换为字符串。在Python中,这可能是这样的:

>> a = ( 1.3, 1, 'c' )
>> b = map(  lambda x:  str(x), a )
['1.3', '1', 'c']

>> " ".join(b)
'1.3 1 c"

然而,Rust并不支持在元组上进行map操作,只能在类似于向量的结构上进行。显然,这是由于元组可以将不同类型打包在一起以及缺乏函数重载所致。此外,我无法找到一种在运行时获取元组长度的方法。因此,我想可能需要使用宏来进行转换。

作为一个开始,我尝试匹配元组的头部,例如:

// doesn't work
match some_tuple {
    (a, ..) => println!("{}", a),
          _ => ()
}

我的问题是:

  1. 是否可以使用库函数将元组转换为字符串,指定任意分隔符?
  2. 如何编写一个宏来将函数映射到任意大小的元组?

1
请注意,在Rust中,元组的元数在编译时已知(不像Python),而且Rust目前还没有可变参数;元组由编译器特殊处理,并为许多元数手动实现了特性。 - Matthieu M.
1
Python有将类型组合在一起的倾向,而Rust则具有相反的倾向;在Python中,所有元组都是同一类型,所有函数也是同一类型;在Rust中,元组中字段类型的每种组合都是不同的类型,并且每个函数都是其自己独特的类型。这是一种方法上的差异:在Python中,所有内容都在运行时解决;在Rust中,在编译时解决。在Rust中,元组只是未命名的元组结构体,彼此之间没有关系。 - Chris Morgan
@MatthieuM:能否将元组的arity作为常量获取? - oleid
@ChrisMorgan:不过,如果元组中有不同类型,只要所有类型都实现了某个 trait,就应该能够调用某个 trait 的特定函数,例如“.to_string()”。我意识到,由于目前没有实现重载,因此无法调用任意函数。 - oleid
2
@oleid:不一定,只有在明确实现时才需要。毕竟,从人类友好的 fmt::Display(这就是 .to_string() 使用的内容)中,你会期望什么呢?a b ca, b, c(a, b, c)?它们都不是“正确”的,也没有一个是“正确”的。 - Chris Morgan
2个回答

23

这里有一个过于聪明的宏解决方案:

trait JoinTuple {
    fn join_tuple(&self, sep: &str) -> String;
}

macro_rules! tuple_impls {
    () => {};

    ( ($idx:tt => $typ:ident), $( ($nidx:tt => $ntyp:ident), )* ) => {
        impl<$typ, $( $ntyp ),*> JoinTuple for ($typ, $( $ntyp ),*)
        where
            $typ: ::std::fmt::Display,
            $( $ntyp: ::std::fmt::Display ),*
        {
            fn join_tuple(&self, sep: &str) -> String {
                let parts: &[&::std::fmt::Display] = &[&self.$idx, $( &self.$nidx ),*];
                parts.iter().rev().map(|x| x.to_string()).collect::<Vec<_>>().join(sep)
            }
        }

        tuple_impls!($( ($nidx => $ntyp), )*);
    };
}

tuple_impls!(
    (9 => J),
    (8 => I),
    (7 => H),
    (6 => G),
    (5 => F),
    (4 => E),
    (3 => D),
    (2 => C),
    (1 => B),
    (0 => A),
);

fn main() {
    let a = (1.3, 1, 'c');

    let s = a.join_tuple(", ");
    println!("{}", s);
    assert_eq!("1.3, 1, c", s);
}

基本思路是我们可以将元组解包成&[&fmt::Display]。一旦拥有了这个,就可以将每个项目映射为一个字符串,然后用分隔符将它们组合起来,这很简单直接。以下是单独使用的示例:

fn main() {
    let tup = (1.3, 1, 'c');

    let slice: &[&::std::fmt::Display] = &[&tup.0, &tup.1, &tup.2];
    let parts: Vec<_> = slice.iter().map(|x| x.to_string()).collect();
    let joined = parts.join(", ");

    println!("{}", joined);
}
下一步是创建一个trait并为特定情况实现它:
trait TupleJoin {
    fn tuple_join(&self, sep: &str) -> String;
}

impl<A, B, C> TupleJoin for (A, B, C)
where
    A: ::std::fmt::Display,
    B: ::std::fmt::Display,
    C: ::std::fmt::Display,
{
    fn tuple_join(&self, sep: &str) -> String {
        let slice: &[&::std::fmt::Display] = &[&self.0, &self.1, &self.2];
        let parts: Vec<_> = slice.iter().map(|x| x.to_string()).collect();
        parts.join(sep)
    }
}

fn main() {
    let tup = (1.3, 1, 'c');

    println!("{}", tup.tuple_join(", "));
}

这只为特定大小的元组实现了我们的特质,对于某些情况来说可能是可以接受的,但肯定还不够酷。 标准库 使用一些宏来减少重复复制和粘贴的繁琐工作,以获取更多的大小。我决定变得更懒,并减少那个解决方案的复制和粘贴!

我没有清晰而明确地列出每个元组大小及相应的索引/泛型名称,而是让我的宏递归。这样,我只需要列出一次,所有较小的大小都只是递归调用的一部分。不幸的是,我无法想出如何使其向前进行,所以我将一切都颠倒过来并向后进行。这意味着存在一个小小的低效性,因为我们必须使用反向迭代器,但总体上应该是支付的小代价。


2
另一个答案对我帮助很大,因为它清楚地展示了Rust简单宏系统的强大之处,一旦你利用递归和模式匹配。

在此基础上,我设法进行了一些粗略的改进(可能能够使模式更简单,但这相当棘手),以便宏在编译时将元组访问器->类型列表反转后,在扩展到特质实现之前,从而不再需要在运行时进行.rev()调用,从而使其更加高效:

trait JoinTuple {
    fn join_tuple(&self, sep: &str) -> String;
}

macro_rules! tuple_impls {
    () => {}; // no more

    (($idx:tt => $typ:ident), $( ($nidx:tt => $ntyp:ident), )*) => {
        /*
         * Invoke recursive reversal of list that ends in the macro expansion implementation
         * of the reversed list
        */
        tuple_impls!([($idx, $typ);] $( ($nidx => $ntyp), )*);
        tuple_impls!($( ($nidx => $ntyp), )*); // invoke macro on tail
    };

    /*
     * ([accumulatedList], listToReverse); recursively calls tuple_impls until the list to reverse
     + is empty (see next pattern)
    */
    ([$(($accIdx: tt, $accTyp: ident);)+]  ($idx:tt => $typ:ident), $( ($nidx:tt => $ntyp:ident), )*) => {
      tuple_impls!([($idx, $typ); $(($accIdx, $accTyp); )*] $( ($nidx => $ntyp), ) *);
    };

    // Finally expand into the implementation
    ([($idx:tt, $typ:ident); $( ($nidx:tt, $ntyp:ident); )*]) => {
        impl<$typ, $( $ntyp ),*> JoinTuple for ($typ, $( $ntyp ),*)
            where $typ: ::std::fmt::Display,
                  $( $ntyp: ::std::fmt::Display ),*
        {
            fn join_tuple(&self, sep: &str) -> String {
                let parts = vec![self.$idx.to_string(), $( self.$nidx.to_string() ),*];
                parts.join(sep)
            }
        }
    }
}

tuple_impls!(
    (9 => J),
    (8 => I),
    (7 => H),
    (6 => G),
    (5 => F),
    (4 => E),
    (3 => D),
    (2 => C),
    (1 => B),
    (0 => A),
);

#[test]
fn test_join_tuple() {
    let a = ( 1.3, 1, 'c' );

    let s = a.join_tuple(", ");
    println!("{}", s);
    assert_eq!("1.3, 1, c", s);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接