I'm a student at the university and am learning Scala. I've got an exercise which is to make cartesian product using pattern matching and not using any operations on list.
def find[A,B](aList: List[A], bList: List[B]): Set[(A,B)] = {
def findHelper[A,B](aList: List[A], bList: List[B], acc: Set[(A,B)]): Set[(A,B)] = {
(aList, bList) match {
case (head1 :: tail1, head2::tail2) => {
findHelper[A, B](tail1, tail2, acc ++ Set((head1, head2)))
}
case (List(), head2::tail2) => acc
case (List(), List()) => acc
}
}
findHelper(aList, bList, Set())
}
println(find(List(1,2,3,4,5), List(7,8,9,10,11)))
As a result I get only like (1,7), (2,8)
etc.
I obviously know why, but I don't know how to combine each pair with itself. I don't know what to do, when my first list gets empty after ::
operation.
As mentioned in the comments, the problem is that you are iterating both lists simultaneously whereas you need to iterate the second one once for each element of the first one.
def cartesianProduct[A, B](as: List[A], bs: List[B]): Set[(A, B)] = {
@annotation.tailrec
def loop(remainingAs: List[A], remainingBs: List[B], acc: Set[(A, B)]): Set[(A, B)] =
(remainingAs, remainingBs) match {
case (remainingAs @ (a :: _), b :: tailB) =>
loop(remainingAs, remainingBs = tailB, acc + (a -> b))
case (_ :: tailA, Nil) =>
loop(remainingAs = tailA, remainingBs = bs, acc)
case (Nil, _) =>
acc
}
loop(remainingAs = as, remainingBs = bs, acc = Set.empty)
}
What does that line mean? " case (remainingAs @ (a :: ), b :: tailB) " I mean, what does "@" and (a :: _) do?
The syntax case foo @ bar
means that if what your pattern matching matches the pattern bar
then assign it to the fresh variable foo
.
So, in this case, I am saying if the list of as is not empty (i.e. is a cons ::
) then take its head as a new variable a
and the whole list as a new variable remainingAs
. Note, that in this case, it was not needed at all since I could have just used the previous remainingAs
on which we are pattern matching which also contains the whole list; I just personally like to define all the variables I am going to use in the case
part, but you can just use case ((a :: _), b :: tailB)
and the code would compile and work as expected.
And you probably did which I needed: are remainingAs and as different values, and you just keep the full List in as/bs value and when it gets empty, you just again use the full one? For example here: "case ( :: tailA, Nil) => loop(remainingAs = tailA, remainingBs = bs, acc)"
I am not totally sure if I understand what you are saying but you are correct that I keep track of the original second lists so that when I exhaust it I can start over from the beginning.
So, as you can see the code has three cases and can be read more or less like:
Note: I personally believe it is easier to understand the version with two recursive functions. Since that looks more like two loops with the second one nested in the first one which is what you would do in an imperative language.
Other solutions include:
Two recursive functions:
def cartesianProduct[A, B](as: List[A], bs: List[B]): Set[(A, B)] = {
@annotation.tailrec
def outerLoop(remaining: List[A], acc: Set[(A, B)]): Set[(A, B)] =
remaining match {
case a :: tail =>
@annotation.tailrec
def innerLoop(remaining: List[B], acc: Set[(A, B)]): Set[(A, B)] =
remaining match {
case b :: tail =>
innerLoop(remaining = tail, acc + (a -> b))
case Nil =>
acc
}
val newAcc = innerLoop(remaining = bs, acc)
outerLoop(remaining = tail, newAcc)
case Nil =>
acc
}
outerLoop(remaining = as, acc = Set.empty)
}
Or higher-order functions:
(you can also write this using the for
syntax)
def cartesianProduct[A, B](as: List[A], bs: List[B]): Set[(A, B)] =
as.iterator.flatMap { a =>
bs.iterator.map { b =>
a -> b
}
}.toSet
You can see the code running in Scastie.