Декартово произведение путем сопоставления с образцом

Я учусь в университете и изучаю Scala. У меня есть упражнение, которое состоит в том, чтобы сделать декартово произведение с использованием сопоставления с образцом и без использования каких-либо операций в списке.

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)))

В итоге получаю только лайк (1,7), (2,8) и т.д. Понятно почему, но не знаю как совместить каждую пару саму с собой. Я не знаю, что делать, когда мой первый список становится пустым после операции ::.


person squall    schedule 19.03.2021    source источник
comment
Какую отдачу вы ожидаете от своего вклада?   -  person Luis Miguel Mejía Suárez    schedule 20.03.2021
comment
Да, он должен удалить повторяющиеся пары. Мой ожидаемый результат - каждая пара (a, b), где a из aList и b из bList   -  person squall    schedule 20.03.2021
comment
@squall ваша проблема в том, что вы повторяете оба списка одновременно, тогда как вы должны повторять второй список один раз для каждого элемента в первом списке.   -  person Luis Miguel Mejía Suárez    schedule 20.03.2021
comment
Вы, наверное, правы, но я не знаю, как итерировать по-другому. Я также пытался сделать head1::tail1, bList, но у меня все равно ничего не получилось.   -  person squall    schedule 20.03.2021
comment
@squall вам нужны две рекурсивные функции: одна для внешнего списка и одна для внутреннего.   -  person Luis Miguel Mejía Suárez    schedule 20.03.2021
comment
Что вы подразумеваете под внутренним и внешним списком?   -  person squall    schedule 20.03.2021
comment
Повторяю, вам нужно повторить второй список (или внутренний) один раз для каждого элемента в первом списке (или внешнем). - Кстати, вспомогательным методам не нужны общие параметры.   -  person Luis Miguel Mejía Suárez    schedule 20.03.2021
comment
вам нужны две рекурсивные функции - зачем это?...   -  person Andrey Tyukin    schedule 20.03.2021
comment
Я знаю, но это сложнее кодировать :D Я попробую завтра и дам вам знать, что я нашел. Спасибо пока :)   -  person squall    schedule 20.03.2021
comment
@AndreyTyukin ну не совсем, может и один. Я просто лично считаю, что было бы проще сделать две рекурсивные функции, потому что это будет выглядеть как два цикла. (это также ближе к тому, как можно было бы написать это, используя синтаксис for или просто flatMap + map, одна функция внутри другой)   -  person Luis Miguel Mejía Suárez    schedule 20.03.2021
comment
@LuisMiguelMejíaSuárez большое спасибо. Я решил взять recursiveSingleFunction и у меня есть вопросы: Что означает эта строка? case (остальные As @(a::), b::tailB) Я имею в виду, что делает @ и (a::_)? И вы, вероятно, сделали то, что мне было нужно: оставшиеся As и as разные значения, и вы просто сохраняете полный список в значении as/bs, а когда он становится пустым, вы просто снова используете полный? Например здесь: case ( :: tailA, Nil) =› loop(оставшиеся As = tailA,оставшиесяBs = bs, acc)   -  person squall    schedule 20.03.2021


Ответы (2)


Как упоминалось в комментариях, проблема заключается в том, что вы выполняете итерацию обоих списков одновременно, тогда как вам нужно повторять второй список один раз для каждого элемента первого.

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)
}

Что означает эта строка? case (остальные As @(a::), b::tailB) Я имею в виду, что делает @ и (a::_)?

Синтаксис case foo @ bar означает, что если то, что соответствует вашему шаблону, соответствует шаблону bar, назначьте его новой переменной foo.

Итак, в этом случае я говорю, что если список as не пуст (т. е. является минусом ::), тогда возьмите его заголовок как новую переменную a и весь список как новую переменную remainingAs. Обратите внимание, что в данном случае это было вообще не нужно, так как я мог бы просто использовать предыдущий remainingAs, на котором мы сопоставляем шаблон, который также содержит весь список; Лично мне нравится определять все переменные, которые я собираюсь использовать в части case, но вы можете просто использовать case ((a :: _), b :: tailB), и код будет компилироваться и работать, как и ожидалось.

И вы, вероятно, сделали то, что мне было нужно: оставшиеся As и as разные значения, и вы просто сохраняете полный список в значении as/bs, а когда он становится пустым, вы просто снова используете полный? Например здесь: case(::tailA,Nil)=›loop(оставшиесяAs=tailA,оставшиесяBs=bs,acc)

Я не совсем уверен, понимаю ли я, что вы говорите, но вы правы в том, что я отслеживаю исходные вторые списки, чтобы, когда я их исчерпаю, я мог начать все сначала.

Итак, как вы можете видеть, код имеет три случая и может быть прочитан примерно так:

  1. Пока первый список не пуст, займитесь его головой.
  2. Затем повторите второй список, взяв его голову и добавив пару обеих головок в набор, и продолжите процесс с хвостом второго списка.
  3. Когда вы дойдете до хвоста второго списка, снова начните с хвоста первого списка и перезапустите второй список до его исходной формы.
  4. Продолжайте процесс, пока первый список не станет пустым, в этот момент верните текущий аккумулятор.

Примечание. Я лично считаю, что версию с двумя рекурсивными функциями легче понять. Поскольку это больше похоже на два цикла со вторым, вложенным в первый, что вы бы сделали в императивном языке.


Другие решения включают в себя:

Две рекурсивные функции:

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)
}

Или функции более высокого порядка:
(вы также можете написать это, используя синтаксис for)

def cartesianProduct[A, B](as: List[A], bs: List[B]): Set[(A, B)] =
  as.iterator.flatMap { a =>
    bs.iterator.map { b =>
      a -> b
    }
  }.toSet

Вы можете увидеть работающий код в Scastie.

person Luis Miguel Mejía Suárez    schedule 20.03.2021
comment
@squall да, вы можете использовать именованные параметры в Scala, полезно четко указать, что вы передаете, но вы также можете просто опустить, что это просто мое личное предпочтение. Да, вы правильно поняли, наша функция сохраняет ссылку на исходные списки, таким образом, мы можем снова начинать итерацию второго списка каждый раз, когда мы его исчерпали. На самом деле ваша исходная функция тоже могла это сделать, проблема заключалась в том, что, поскольку вы использовали одно и то же имя для переменных внутренней функции, вы скрывали их. - person Luis Miguel Mejía Suárez; 21.03.2021

Как упоминалось в комментариях, вам нужно пройти один List только один раз, а другой List пройти один раз для каждого элемента в первом List.

Вот один из способов сделать это.

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))

Все это намного проще с простым for пониманием.

person jwvh    schedule 19.03.2021