The function you're looking for is usually called zipWith
. It's unfortunately not provided in the standard libraries, but it's pretty easy to write:
def zipWith[A,B,C](f: (A,B) => C, a: Iterable[A], b: Iterable[B]) =
new Iterable[C] {
def elements = (a.elements zip b.elements) map f.tupled
}
This will traverse only once, since the implementations for zip
and map
on iterators are fully lazy.
But why stop at Iterable
? This has an even more general form. We could declare an interface for all data structures that can be zipped this way.
trait Zip[F[_]] {
def zipWith[A,B,C](f: (A,B) => C, a: F[A], b: F[B]): F[C]
}
For example, we can zip functions:
trait Reader[A] {
type Read[B] = (A => B)
}
def readerZip[T] = new Zip[Reader[T]#Read] {
def zipWith[A,B,C](f: (A,B) => C, a: T => A, b: T => B): T => C =
(t: T) => f(a(t),b(t))
}
There turns out to be an even more general expression of this type. In general, type constructors that allow an implementation of this interface are applicative functors
trait Applicative[F[_]] {
def pure[A](a: A): F[A]
def map[A,B](f: A => B, a: F[A]): F[B]
def ap[A,B](f: F[A => B], a: F[A]): F[B]
}
An implementation of zipWith is then just this:
def zipWith[F[_],A,B,C](f: A => B => C, a: F[A], b: F[B])
(implicit m: Applicative[F]) =
m.ap(m.map(f,a), b)
This generalises to functions of any arity:
m.ap(m.ap(m.ap(m.map(f,a), b), c), d)
The Scalaz library provides Applicative instances for a lot of data structures in the standard library. It also does library pimping so that you can just do this:
import scalaz._
import Scalaz._
List(1,2,3).zipWith(List(2,3,4))(x: Int, y: Int) => x * y)
Also, convenient syntax is provided for ap
. In Scalaz, this function is called <*>
:
def zipWith[F[_]:Applicative,A,B,C](f: A => B => C, a: F[A], b: F[B]) =
(a map f) <*> b