Шаг за шагом к решению задач 8–11 из 99 упражнений в обобщенном виде
В этом руководстве представлены решения для решения задач 8–11 знаменитых 99 упражнений на Прологе (автор Вернер Хетт). Имея список целых чисел, мы рассмотрим различные методы обработки последовательных дубликатов. Сначала мы начнем с фрагмента кода для задачи № 8, а затем постепенно опишем более общий алгоритм, использующий функции более высокого порядка для решения этих четырех проблем на Scala.
Я использую следующий целочисленный список, содержащий несколько последовательных дубликатов, в качестве основы для тестирования.
val listdup = List (1,1,1,1,2,2,4,4,5,3,4,4,4,3,3)
Давайте начнем с первой проблемы, где мы представляем два примера кода: жестко закодированный подход и гибкий функциональный метод, обеспечивающий возможность повторного использования.
Проблема № 8: Удалите последовательные дубликаты в списке
*Жестко запрограммированный подход
Учитывая приведенный выше список, удалите повторяющиеся копии каждого последовательного целого числа, сохраняя при этом последовательность нетронутой.
def removeDup(lst : List[Int]) : List[Int] = {
lst match {
case head::tail => {
val (duplst,remainlst) = lst.span(_ == head)
duplst.head :: removeDup(remainlst) }
case Nil => List()
}
}
scala> removeDup(listdup)
res: List[Int] = List(1, 2, 4, 5, 3, 4, 3)
В приведенной выше функции список разбивается на два подсписка с помощью оператора span. Выбирается заголовок первого списка, который содержит дубликаты, а второй список (который содержит оставшиеся элементы) передается рекурсивно с использованием той же функции. Рекурсия завершается, когда в оставшемся списке больше нет элементов, которые нужно удалить.
* Использование функционального подхода
Приведенные выше фрагменты кода прямолинейны, а устранение дубликатов жестко закодировано в рекурсивной функции. Чтобы сделать код более пригодным для повторного использования, мы можем использовать внешнюю функцию, которая берет список повторяющихся целых чисел и очищает их до одного. Затем мы передаем функцию очистки в качестве параметра функции более высокого порядка reduceDup, которая вызывает рекурсивный алгоритм iter. Вот код для трех функций:
def reduceDup(ls : List[Int], reduce: List[Int] => List[Int]) : List[Int] = {
def iter (lst : List[Int]): List[Int] ={
lst match {
case head::tail => {
val (duplst,remainlst) = lst.span(_ == head)
reduce(duplst) ::: iter(remainlst) }
case Nil => List()
}
}
iter(ls)
}
def purge(ls : List[Int]) : List[Int] = {
ls.toSet.toList
}
scala> reduceDup(listdup,purge)
res: List[Int] = List(1, 2, 4, 5, 3, 4, 3)
Без внутренней функции iter вы не можете рекурсивно перемещаться по списку. Это обычная техника, используемая в Scala для таких случаев, и язык позволяет программистам определять вложенные функции, в которых параметры внешней функции входят в область действия внутренней функции. Кроме того, в reduceDup мы объединяем два списка с помощью оператора :::, а в removeDup мы добавляем только один элемент в начало списка с помощью оператора ::.
Проблема № 10: Кодирование длин серий
Проблема: подсчет данных длин серий
Повторно используя код задачи № 8, мы можем очень легко реализовать простой подсчет данных, широко известный как кодирование длин серий. Последовательные дубликаты элементов объединяются, и их количество регистрируется в списке. Таким образом, все, что нам нужно, — это определить новую внешнюю функцию, которая выполняет операцию, а затем передает ее в качестве параметра рекурсивной функции reduceDup, установленной выше.
def length(ls : List[Int]) : List[Int] ={
List(ls.size)
}
scala> reduceDup(listdup,length)
res: List[Int] = List(4, 2, 2, 1, 1, 3, 2)
Это довольно просто, и мы можем видеть здесь красоту функционального программирования и повторного использования кода, когда функции могут передаваться в качестве параметров функциям более высокого порядка.
Проблема № 10: Кодирование длин серий — простое сжатие данных
Аналогично результату предыдущей задачи, мы можем реализовать Задачу №10 — так называемый метод сжатия данных с кодированием длин серий. В этой задаче последовательные дубликаты данных кодируются как кортеж (N,D), где N — количество дубликатов элемента данных D.
Например, учитывая наш список данных, нам нужно создать следующий список кортежей:
val listdup = List (1,1,1,1,2,2,4,4,5,3,4,4,4,3,3)
res: List( (4,1), (2,2), (2,4), (1,5), (1,3), (3,4), (2,3) )
Если мы попытаемся использовать функцию reduceDup, определенную выше, с новой функцией, которую мы назовем runLength, мы зайдем в тупик. Поскольку параметр сокращения (который принимает функцию в reduceDup) является отображением из List[Int] в List[Int], в то время как здесь мы берем список целых чисел, но генерируем кортеж в форме List[(Int,Int)]. Чтобы преодолеть эту проблему, мы переопределяем наш набор рекурсивных функций, используя универсальный тип T. Код для этого выглядит следующим образом.
// generic version
def processDup[T](ls : List[Int], reduce: List[Int] => List[T]) : List[T] = {
def iter (lst : List[Int]): List[T] ={
lst match {
case head::tail => {
val (duplst,remainlst) = lst.span(_ == head)
reduce(duplst) ::: iter(remainlst) }
case Nil => List[T]()
}
}
iter(ls)
}
Тип T был абстрагирован и назначен в возвращаемых типах как внешней, так и внутренней функций; в результате мы получаем большую гибкость при передаче различных видов внешних функций в качестве параметров для выполнения различных операций. Например, приведенную ниже функцию runLength можно использовать для решения задачи № 10 и возврата списка кортежей вместо списка целых чисел.
def runLength(ls : List[Int]) : List[(Int, Int)] ={
List((ls.size,ls.head))
}
scala> processDup(listdup,runLength)
res: List[(Int, Int)] = List((4,1), (2,2), (2,4), (1,5), (1,3), (3,4), (2,3))
Проблема № 8: Повторное посещение
Мы также можем использовать параметризованную универсальную версию для решения Задачи № 8, не написав ни одной строки кода. Простой вызов processDup вместо reduceDup и использование функции очистки сделают эту работу.
def purge(ls : List[Int]) : List[Int] = {
ls.toSet.toList
}
scala> processDup(listdup,purge)
res: List[Int] = List(1, 2, 4, 5, 3, 4, 3)
Проблема № 9: упаковать последовательные дубликаты списка в подсписки
В Задаче №9 нам нужно сгруппировать повторяющиеся элементы в отдельные подсписки. Учитывая нашу универсальную функцию processDup выше, решение удивительно простое:
val listdup = List (1,1,1,1,2,2,4,4,5,3,4,4,4,3,3)
def group(ls : List[Int]) : List[List[Int]] = {
List(ls)
}
scala> processDup(listdup,group)
res: List[List[Int]] = List(List(1, 1, 1, 1), List(2, 2), List(4, 4), List(5), List(3), List(4, 4, 4), List(3, 3))
Проблема № 11: Модифицированное кодирование длин серий — простое сжатие данных
В этой задаче мы модифицируем результат задачи № 10 таким образом, что если элемент не имеет дубликатов, он просто копируется в список результатов. Только элементы с дубликатами передаются как кортеж (N,D). Например, учитывая наш список данных, нам нужно создать следующий список данных/кортежей:
val listdup = List (1,1,1,1,2,2,4,4,5,3,4,4,4,3,3)
res: List( (4,1), (2,2), (2,4), 5, 3, (3,4), (2,3) )
Используя результат Задачи № 10, мы сопоставляем каждый элемент в списке кортежей в соответствии с тем, равно ли N 1 или отличается от него. Фрагмент кода для выполнения преобразования:
scala> processDup(listdup, runLength).map{ case (len,e) => { if (len==1) e else (len,e) } }
Заключительные замечания
В этих фрагментах кода мы продемонстрировали, что решение нескольких задач унифицированным способом не очень сложно. При правильном подходе и использовании абстракций, параметризованных типов и функций более высокого порядка вы можете писать на Scala очень лаконичный и пригодный для повторного использования код. Пожалуйста, не торопитесь, чтобы поэкспериментировать с этими примерами в качестве первого шага к изучению идиоматического подхода к функциональному программированию.
Наконец, в качестве бонусного упражнения вы можете попробовать изменить приведенную выше функцию processDup, чтобы она работала с другим параметризованным типом данных, например со строками. Изменив обработанный список как List[A] вместо List[Int], вы можете создать более общую рекурсивную функцию, которую можно использовать без изменения какой-либо из 4 представленных внешних функций.
Вот ссылка на историю на github:
https://github.com/abbas-taher/scala-processing-duplicates-in-a-list-the-functional-way