1

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.

jaco0646
  • 15,303
  • 7
  • 59
  • 83
squall
  • 139
  • 5
  • 1
    What is the return you expect for your input? – Luis Miguel Mejía Suárez Mar 19 '21 at 22:15
  • Yes, it should remove duplicate pairs. My expected output is every pair (a,b) where a is from aList and b from bList – squall Mar 19 '21 at 22:22
  • @squall your problem is that you are iterating both list at once, whereas you should iterate the second list once for every element in the first list. – Luis Miguel Mejía Suárez Mar 19 '21 at 22:27
  • You are probably right, but I dont know how to iterate in other way. I also tried to do head1::tail1, bList, but I couldnt make anything with it anyway. – squall Mar 19 '21 at 22:29
  • @squall you need two recursive functions, one for the outer list and one for the inner one. – Luis Miguel Mejía Suárez Mar 19 '21 at 22:31
  • What do you mean by inner and outer list? – squall Mar 19 '21 at 22:33
  • I repeat, you need to iterate the second _(or inner)_ list once for every element in the first _(or outer)_ list. - BTW, the helper methods do not need the generic parameters. – Luis Miguel Mejía Suárez Mar 19 '21 at 22:34
  • "you need two recursive functions" - why that?... – Andrey Tyukin Mar 19 '21 at 22:35
  • I know, but it’s harder to code it :D i will try tomorrow and let you know what I found. Thanks for now :) – squall Mar 19 '21 at 22:36
  • @AndreyTyukin well not really, it can be a single one. I just personally believe it would be easier to make two recursive functions because that will look like two loops. _(it is also closer to how one would write it using the `for` syntax or plain `flatMap` + `map`, one function inside another)_ – Luis Miguel Mejía Suárez Mar 19 '21 at 22:40
  • @LuisMiguelMejíaSuárez thank you very much. I decided to take the recursiveSingleFunction and I have questions: What does that line mean? " case (remainingAs @ (a :: _), b :: tailB) " I mean, what does "@" and (a :: _) do? 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)" – squall Mar 20 '21 at 09:03

2 Answers2

2

As mentioned in the comments, you need to traverse one List only once, but the other List is traversed once for every item in the first List.

Here's one way to go about it.

def cartPrd[A,B](aList: List[A], bList: List[B]): Set[(A,B)] = {
  def getAs(as: List[A]): List[(A,B)] = as match {
    case Nil => Nil
    case hd::tl => getBs(hd, bList) ++ getAs(tl)
  }
  def getBs(a: A, bs: List[B]): List[(A,B)] = bs match {
    case Nil => Nil
    case hd::tl => (a,hd) :: getBs(a,tl)
  }
  getAs(aList).toSet
}

cartPrd(List(1,2,1), List('A','B','B'))
//res0: Set[(Int, Char)] = Set((1,A), (1,B), (2,A), (2,B))

This is all so much easier with a simple for comprehension.

jwvh
  • 50,871
  • 7
  • 38
  • 64
1

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.

  • @squall yeah you can use named parameters in **Scala**, it is helpful to make clear what you are passing but you can also just omit that it is just my personal preference. Yes, your understanding is correct, our function keeps a reference to the original lists, that way we can start iterating the second list again every time we exhausted it. Actually your original function could also do that, the problem was that since you used the same name for the inner function variables you shadowed them. – Luis Miguel Mejía Suárez Mar 21 '21 at 13:55