Pre-RFC: Zip.pairmap


#1

In the impl specialization discussion, I brought forth the idea of having a .pairmap(_) method on Zips that map a function of two arguments instead of one argument tuple with two members.

Rationale: One often has predefined functions that take two arguments, and using those functions in a .zip(_).map(_) currently needs a closure to unpack the argument tuple. This obviously makes the code a little less readable.

If there’s interest, I could write up an RFC, and probably an implementation.


#2

It’s probably stupid (potentially frustrating and bug-inducing implicit behavior), but what about impl <F: FnMut(A, B) -> C> FnMut((A, B)) -> C for F that unpacks the tuple? Or a wrapper type that does the same thing?


#3

I think it would be frustrating to be incomplete – what about pair_flat_map, pair_filter, pair_find, etc.? Does pair_zip on a third iterator now result in a triplet?

At least @logician’s impl would be more general. That could naturally extend to Fn((A, B, C)) -> D and on, but I don’t know if we’d want to go so far as supporting nested tuples, like .zip.zip.map needing Fn(((A, B), C)) -> D.


#4

The reason we have first-class tuples and destructuring is precisely so that we don’t need to cater to special cases like pairs or whatnot.

.map(|(a, b)| ...) works, what’s even the point of adding less obvious magic for golfing?


#5

I fully sympathize with trying to solve this problem.

Closures using (a, b) arguments come up a lot, and anything to reduce the amount of punctuation you need to type to work with it would be great.


#6

In Haskell there’s a very handy function named zipWith, that I’ve often missed in the D standard library:

http://zvon.org/other/haskell/Outputprelude/zipWith_f.html

Usage examples:

let a = [1, 2, 3];
let b = [10, 20, 30];

struct V(u32, u32);
a.iter().zip(b).zipWith(V)
==> 
V(1,10), V(2,20), V(3,30)

a.iter().zip(b).zipWith(add)
==>
11, 22, 33

#7

I played around a bit with this general train of thought, to get an idea if I like it.

The first result is a unpack_map() for Iterators over n-ary tuples, that will call an n-ary function with the contents of the tuple:

#![feature(unboxed_closures, fn_traits)]

struct UnpackMap<I, F> {
    iter: I,
    f: F
}

impl<I: Iterator, F: FnMut<I::Item>> Iterator for UnpackMap<I, F> {
    type Item = F::Output;

    fn next(&mut self) -> Option<F::Output> {
        self.iter.next().map(|x| (&mut self.f).call_mut(x))
    }
}

trait IteratorExt : Iterator {
    fn unpack_map<F>(self, f: F) -> UnpackMap<Self, F>
        where Self: Sized, F: FnMut<Self::Item>
    {
        UnpackMap { iter: self, f: f }
    }
}

impl<T> IteratorExt for T where T: Iterator {}

The second is an adaptor as suggested by @logician:

struct PackedFn<F> {
    f: F
}

impl<Args, F: FnOnce<Args>> FnOnce<(Args,)> for PackedFn<F> {
    type Output = F::Output;

    extern "rust-call" fn call_once(self, a: (Args,)) -> F::Output {
        self.f.call_once(a.0)
    }
}

impl<Args, F: FnMut<Args>> FnMut<(Args,)> for PackedFn<F> {
    extern "rust-call" fn call_mut(&mut self, a: (Args,)) -> F::Output {
        (&mut self.f).call_mut(a.0)
    }
}

impl<Args, F: Fn<Args>> Fn<(Args,)> for PackedFn<F> {
    extern "rust-call" fn call(&self, a: (Args,)) -> F::Output {
        (&self.f).call(a.0)
    }
}

fn pack<Args, F>(f: F) -> PackedFn<F> where F: FnOnce<Args> {
    PackedFn { f: f }
}

Usage examples are as follows:

#[derive(Debug)]
struct V(u32, u32, u32);

fn main() {
    let a = vec![(1, 2, 3), (4, 5, 6), (7, 8, 9)];
    
    for x in a.clone().into_iter().unpack_map(V) {
        println!("{:?}", x);
    }
    
    for x in a.into_iter().map(pack(V)) {
        println!("{:?}", x);
    }
}

My general impression is that both are workable (with the current implementation of the Fn traits), but not worthwhile to add. In particular because what most people really want for higher arity, is something that will accept ((T, U), V).


#8

Another approach would be to have an operator that takes a function of N arguments and produces a function on N-tuples. That could be mapped easily. The idea generalizes naturally to higher arities. As a prefix / operator:

    let i1 = (0..20).step_by(2);
    let i2 = 5..15;
    let m : Vec<i32> = i1.zip(i2).map(/std::cmp::min).collect();

    assert_eq!(m, vec![0,2,4,6,8,10,11,12,13,14]);

#9

I’ll note that, while unstable, the Fn* traits natively operate on tuples:

#![feature(fn_traits)]
#![feature(unboxed_closures)]

use std::ops::FnOnce;

fn foo<T, F: FnOnce<T>>( x: T, f: F ) -> F::Output {
    f.call_once( x )
}

fn main() {
    println!("{:?}", foo( (1, 2), move |x, y| (x + 1, y + 3) ) );
}

The above code prints “2, 5” on Nightly as-is: https://play.rust-lang.org/?gist=88d3d5fbb8cca49256c7&version=nightly

Once that’s stabilized, which would be useful in its own right, special-casing anything of the sort would be rendered obsolete (save perhaps impl<T, F: Fn<T>> Fn<(T)> for F { ... } across the Fn* traits, or the nearest coherency allows, perhaps with Into - should have essentially zero runtime overhead).


#10

Note that it’s likely not going to be ever stabilized without generalized tuple manipulation and/or “variadic generics”.


#11

This crate provides a zip_with implementation: https://crates.io/crates/zipWith

Personally I think an iterator adapter like that should belong in std because this pattern is common with two iterators.


#12

It’s literally a.zip(b).map(|(a, b)| ...).
This is pretty much left-pad levels of usefulness.

However, it looks like that crate has a very specific usecase, and that is a workaround for the need of -> impl Iterator, not just sugar.

Moving forward with impl Trait is a much better approach than stabilizing another adapter in libstd.


#13

I fully agree – if we can have impl Trait within a reasonable time frame, I won’t slow you folks down by asking for band-aids like this. :smile: