Search code examples
scalapattern-matchingcartesian

Cartesian product by pattern matching


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.


Solution

  • 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:

    1. While the first list is not empty, take its head.
    2. Then iterate the second list by taking its head and adding the pair of both heads to the set and continue the process with the tail of the second list.
    3. When you reach the tail of the second list, then start again with the tail of the first list and restarting the second list to its original form.
    4. Continue the process until the first list is empty, at that point return the current accumulator.

    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.