21

My initial problem was to convert a tuple of different types to a string. In Python, this would be something like:

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

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

Yet, Rust doesn't support map on tuples -- only on vector-like structures. Obviously, this is due to being able to pack different types into a tuple and the lack of function overloading. Also, I couldn't find a way to get the tuple length at runtime. So, I guess, a macro would be needed to do the conversion.

As a start, I tried to match the head of an tuple, something like:

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

So, my question:

  1. Is it possible, using library functions, to convert a tuple to a string, specifying an arbitrary separator?
  2. How to write a macro to be able to map functions to arbitrary sized tuples?
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
oleid
  • 323
  • 1
  • 2
  • 5
  • 1
    Note that in Rust the arity of the tuple is known at compile-time (unlike Python), and no Rust does not have *variadic parameters* yet; tuples are special cased by the compiler and traits are implemented for a number of arities "manually". – Matthieu M. Mar 19 '15 at 15:58
  • 1
    Python has a tendency to glom types together where Rust has the opposite tendency; in Python, all tuples are of the one type and all functions of the one type; in Rust, each combination of field types in a tuple is a different type, and each function is its own unique type. It's a difference in approach: in Python all is resolved at runtime; in Rust, at compile time. Tuples are in Rust simply unnamed tuple structs, with no relation to one another. – Chris Morgan Mar 20 '15 at 07:07
  • @MatthieuM.: Would it be possible to get the arity of a tuple as constant? – oleid Mar 20 '15 at 16:53
  • @ChrisMorgan: Still, if there are different types in a tuple, it should be possible to call a certain function of some trait, if all types implement that trait, e.g. ".to_string()". I realize, that calling arbitrary functions is not possible, as overloading isn't implemented atm. – oleid Mar 20 '15 at 16:59
  • 2
    @oleid: not necessarily, only if it's implemented explicitly. After all, what would you expect from the human-friendly `fmt::Display` (which is what `.to_string()` uses)? `a b c`, `a, b, c`, `(a, b, c)`? None of them is “right”, none of them is “correct”. – Chris Morgan Mar 21 '15 at 00:24

2 Answers2

23

Here's an overly-clever macro solution:

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

The basic idea is that we can take a tuple and unpack it into a &[&fmt::Display]. Once we have that, it's straight-forward to map each item into a string and then combine them all with a separator. Here's what that would look like on its own:

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

The next step would be to create a trait and implement it for the specific case:

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(", "));
}

This only implements our trait for a specific size of tuple, which may be fine for certain cases, but certainly isn't cool yet. The standard library uses some macros to reduce the drudgery of the copy-and-paste that you would need to do to get more sizes. I decided to be even lazier and reduce the copy-and-paste of that solution!

Instead of clearly and explicitly listing out each size of tuple and the corresponding index/generic name, I made my macro recursive. That way, I only have to list it out once, and all the smaller sizes are just part of the recursive call. Unfortunately, I couldn't figure out how to make it go in a forwards direction, so I just flipped everything around and went backwards. This means there's a small inefficiency in that we have to use a reverse iterator, but that should overall be a small price to pay.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
2

The other answer helped me a lot because it clearly illustrated the power of Rust's simple macro system once you make use of recursion and pattern matching.

I've managed to make a few crude improvements (might be able to make the patterns a bit simpler, but it's rather tricky) on top of it so that the tuple accessor->type list is reversed by the macro at compile time before expansion into the trait implementation so that we no longer need to have a .rev() call at runtime, thus making it more efficient:

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);
}
Community
  • 1
  • 1
lloydmeta
  • 1,289
  • 1
  • 15
  • 25