【问题标题】:How to iterate or map over tuples?如何迭代或映射元组?
【发布时间】:2015-05-22 19:13:02
【问题描述】:

我最初的问题是将不同类型的元组转换为字符串。在 Python 中,这将类似于:

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

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

然而,Rust 不支持元组上的映射——只支持类向量结构。显然,这是由于能够将不同的类型打包到一个元组中,并且没有函数重载。此外,我找不到在运行时获取元组长度的方法。所以,我想,需要一个宏来进行转换。

首先,我尝试匹配元组的头部,例如:

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

所以,我的问题:

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

【问题讨论】:

  • 请注意,在 Rust 中,元组的数量在编译时是已知的(与 Python 不同),并且没有 Rust 没有 可变参数;元组是编译器的特殊情况,并且特征是“手动”为许多 arities 实现的。
  • Python 倾向于将类型聚集在一起,而 Rust 则相反;在 Python 中,所有元组都是一种类型,所有函数都是一种类型;在 Rust 中,元组中的每个字段类型组合都是不同的类型,每个函数都是自己唯一的类型。这是方法上的不同:在 Python 中,一切都在运行时解决;在 Rust 中,在编译时。元组在 Rust 中只是未命名的元组结构,彼此没有关系。
  • @MatthieuM.: 是否有可能将元组的 arity 设为常量?
  • @ChrisMorgan:不过,如果元组中有不同的类型,如果所有类型都实现该特征,则应该可以调用某个特征的某个函数,例如“.to_string()”。我意识到,调用任意函数是不可能的,因为 atm 没有实现重载。
  • @oleid: 不一定,只有当它被明确实现时。毕竟,你对人性化的fmt::Display 有什么期望(.to_string() 使用的是什么)? a b c, a, b, c, (a, b, c)?没有一个是“正确的”,没有一个是“正确的”。

标签: loops tuples rust


【解决方案1】:

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

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);
}

基本思想是我们可以把一个元组解压成一个&amp;[&amp;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 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(", "));
}

这仅针对特定大小的元组实现我们的特征,这在某些情况下可能没问题,但肯定还不是standard library 使用一些宏来减少复制和粘贴的繁琐工作,而您需要这样做才能获得更多尺寸。我决定更加懒惰并减少该解决方案的复制和粘贴!

我没有明确地列出元组的每个大小和相应的索引/通用名称,而是让我的宏递归。这样,我只需要列出一次,所有较小的尺寸只是递归调用的一部分。不幸的是,我不知道如何让它向前走,所以我只是把所有东西都翻转过来,然后向后走。这意味着我们必须使用反向迭代器有一点效率低下,但总的来说这应该是一个很小的代价。

【讨论】:

    【解决方案2】:

    other answer 对我帮助很大,因为它清楚地说明了使用递归和模式匹配后 Rust 的简单宏系统的强大功能。

    我已经设法在它之上进行了一些粗略的改进(可能能够使模式更简单,但它相当棘手),以便元组访问器->类型列表在编译时被宏反转在扩展到 trait 实现之前的时间,这样我们就不再需要在运行时调用 .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);
    }
    

    【讨论】:

      猜你喜欢
      • 2013-01-13
      • 2022-01-19
      • 1970-01-01
      • 2011-05-29
      • 2018-11-15
      • 1970-01-01
      • 2016-04-10
      • 1970-01-01
      • 2011-05-11
      相关资源
      最近更新 更多