Пошаговое объяснение синтаксиса Scala, используемого в примере quicksort от Википедии
Я пытаюсь понять Scala пример quicksort из Википедии. Каким образом образец может быть разобран шаг за шагом и что означает весь синтаксический сахар?
def qsort: List[Int] => List[Int] = {
case Nil => Nil
case pivot :: tail =>
val (smaller, rest) = tail.partition(_ < pivot)
qsort(smaller) ::: pivot :: qsort(rest)
}
Насколько я могу собрать на этом этапе qsort - это функция, которая не принимает никаких параметров и возвращает новый Function1 [List [Int], List [Int]], который реализует quicksort посредством использования сопоставления шаблонов, манипулирования списком и рекурсивных вызовов. Но я не могу понять, откуда возникает точка опоры, и как именно работает синтаксис соответствия шаблону в этом случае.
UPDATE:
Спасибо всем за великие объяснения!
Я просто хотел поделиться другим примером реализации quicksort, который я обнаружил в Scala по примеру Мартин Одерский. Хотя он основан на массивах, а не на списках, и меньше на шоу в терминах varios Scala, я лично считаю его менее запутанным, чем его коллега из Википедии, и гораздо более понятным и точным выражением базового алгоритма:
def sort(xs: Array[Int]): Array[Int] = {
if (xs.length <= 1) xs
else {
val pivot = xs(xs.length / 2)
Array.concat(
sort(xs filter (pivot >)),
xs filter (pivot ==),
sort(xs filter (pivot <)))
}
}
Ответы
Ответ 1
def qsort: List[Int] => List[Int] = {
case Nil => Nil
case pivot :: tail =>
val (smaller, rest) = tail.partition(_ < pivot)
qsort(smaller) ::: pivot :: qsort(rest)
}
Перепишите это. Во-первых, замените литерал функции экземпляром Function1
:
def qsort: List[Int] => List[Int] = new Function1[List[Int], List[Int]] {
def apply(input: List[Int]): List[Int] = input match {
case Nil => Nil
case pivot :: tail =>
val (smaller, rest) = tail.partition(_ < pivot)
qsort(smaller) ::: pivot :: qsort(rest)
}
}
Далее, я собираюсь заменить совпадение шаблонов эквивалентными выражениями if
/else
. Обратите внимание, что они эквивалентны, а не то же самое. Байт-код для совпадений шаблонов более оптимизирован. Например, второй if
и исключение исключений ниже не существуют, поскольку компилятор знает, что второе совпадение всегда произойдет, если первый сбой.
def qsort: List[Int] => List[Int] = new Function1[List[Int], List[Int]] {
def apply(input: List[Int]): List[Int] = if (input == Nil) {
Nil
} else if (input.isInstanceOf[::[_]] &&
scala.collection.immutable.::.unapply(input.asInstanceOf[::[Int]]) != None) {
val unapplyResult = scala.collection.immutable.::.unapply(input.asInstanceOf[::[Int]]).get
val pivot = unapplyResult._1
val tail = unapplyResult._2
val (smaller, rest) = tail.partition(_ < pivot)
qsort(smaller) ::: pivot :: qsort(rest)
} else {
throw new scala.MatchError(input)
}
}
Собственно, val (smaller, rest)
также соответствует шаблону, поэтому также разложите его:
def qsort: List[Int] => List[Int] = new Function1[List[Int], List[Int]] {
def apply(input: List[Int]): List[Int] = if (input == Nil) {
Nil
} else if (input.isInstanceOf[::[_]] &&
scala.collection.immutable.::.unapply(input.asInstanceOf[::[Int]]) != None) {
val unapplyResult0 = scala.collection.immutable.::.unapply(input.asInstanceOf[::[Int]]).get
val pivot = unapplyResult0._1
val tail = unapplyResult0._2
val tmp0 = tail.partition(_ < pivot)
if (Tuple2.unapply(tmp0) == None)
throw new scala.MatchError(tmp0)
val unapplyResult1 = Tuple2.unapply(tmp0).get
val smaller = unapplyResult1._1
val rest = unapplyResult1._2
qsort(smaller) ::: pivot :: qsort(rest)
} else {
throw new scala.MatchError(input)
}
}
Очевидно, это очень неоптимизировано. Хуже того, некоторые вызовы функций выполняются более одного раза, чего не бывает в оригинале. К сожалению, для устранения этого потребуются некоторые структурные изменения кода.
Здесь все еще есть синтаксический сахар. Существует анонимная функция, передаваемая в раздел, и для вызова функций есть синтаксический сахар. Переписывая их, получаем следующее:
def qsort: List[Int] => List[Int] = new Function1[List[Int], List[Int]] {
def apply(input: List[Int]): List[Int] = if (input == Nil) {
Nil
} else if (input.isInstanceOf[::[_]] &&
scala.collection.immutable.::.unapply(input.asInstanceOf[::[Int]]) != None) {
val unapplyResult0 = scala.collection.immutable.::.unapply(input.asInstanceOf[::[Int]]).get
val pivot = unapplyResult0._1
val tail = unapplyResult0._2
val func0 = new Function1[Int, Boolean] {
def apply(input: Int): Boolean = input < pivot
}
val tmp0 = tail.partition(func0)
if (Tuple2.unapply(tmp0) == None)
throw new scala.MatchError(tmp0)
val unapplyResult1 = Tuple2.unapply(tmp0).get
val smaller = unapplyResult1._1
val rest = unapplyResult1._2
qsort.apply(smaller) ::: pivot :: qsort.apply(rest)
} else {
throw new scala.MatchError(input)
}
}
На этот раз обширные объяснения по каждому синтаксическому сахару и тому, как он работает, выполняются другими.:-) Надеюсь, это дополняет их ответы. Как последнее замечание, следующие две строки эквивалентны:
qsort(smaller) ::: pivot :: qsort(rest)
qsort(rest).::(pivot).:::(qsort(smaller))
Ответ 2
def qsort: List[Int] => List[Int] = {
case Nil => Nil
case pivot :: tail =>
val (smaller, rest) = tail.partition(_ < pivot)
qsort(smaller) ::: pivot :: qsort(rest)
}
пусть выделит несколько бит.
Нейминг
Операторы (например, *
или +
) являются допустимыми кандидатами для имен методов и классов в Scala (следовательно, вы можете иметь класс с именем ::
(или метод с именем ::
, если на то пошло - и действительно оба существуют). Scala, похоже, имеет перегрузку оператора, но на самом деле это не так: просто вы можете объявить метод с тем же именем.
Соответствие шаблону
target match {
case p1 =>
case p2 =>
}
Где p1
и p2
являются шаблонами. Существует много допустимых шаблонов (вы можете сопоставлять строки, типы, конкретные экземпляры и т.д.). Вы также можете сопоставлять с тем, что называется экстрактором. Экстрактор в основном извлекает аргументы для вас в случае совпадения, поэтому:
target match {
case MyExtractor(arg1, arg2, arg3) => //I can now use arg1, arg2 etc
}
В scala, если экстрактор (из которого класс case является примером) существует с именем X
, то шаблон X(a, b)
эквивалентен a X b
. Класс case ::
имеет конструктор, принимающий 2 аргумента и складывая это вместе, получаем:
case x :: xs =>
case ::(x, xs) =>
Являются эквивалентными. В этом совпадении говорится: "Если мой список является экземпляром ::
, извлеките значение head
в X
и tail
в xs
". сопоставление шаблонов также используется в описании переменных. Например, если p
является шаблоном, это действительно:
val p = expression
Вот почему мы можем объявлять такие переменные, как:
val x :: xs = List(1, 2, 3)
val (a, b) = xs.partition(_ % 2 == 0 ) //returns a Tuple2 which is a pattern (t1, t2)
Анонимные функции
Во-вторых, мы имеем функцию "literal". tail
- это экземпляр List
, который имеет метод под названием partition
, который берет предикат и возвращает два списка; одна из тех записей, которые удовлетворяют предикату, и одна из тех записей, которые этого не сделали.
val pred = (el: Int) => e < 2
Объявляет предикат функции, который принимает Int
и возвращает true, если значение int меньше 2. Существует сокращенное обозначение для написания функций inline
tail.partition(_ < pivot) // _ is a placeholder for the parameter
tail.partition( (e: Int) => e < pivot )
Эти два выражения означают одно и то же.
Списки
A List
- это замкнутый абстрактный класс с двумя реализациями, Nil
(пустой список) и ::
(также называемый cons), который является непустым списком, состоящим из головы и хвоста ( который также является списком). Теперь вы можете видеть, что совпадение с шаблоном является совпадением, является ли список пустым или нет. a List
можно создать, перейдя в другие списки:
val l = 1 :: 2 :: Nil
val m = List(1, 2, 3) ::: List(4, 5, 6)
Вышеуказанные строки - это просто вызовы методов (::
- допустимое имя метода в scala). Единственная разница между этими и обычными вызовами метода заключается в том, что если метод заканчивается в двоеточии :
и вызывается с пробелами, порядок цели и параметра меняется на противоположный:
a :: b === b.::(a)
Типы функций
val f: A => B
предыдущая строка вводит ссылку f
как функцию, которая принимает A
и возвращает a B
, поэтому я мог бы сделать:
val a = new A
val b: B = f(a)
Следовательно, вы можете видеть, что def qsort: List[Int] => List[Int]
объявляет метод с именем qsort
, который возвращает функцию с List[Int]
и возвращает List[Int]
. Поэтому я, очевидно, мог:
val l = List(2, 4, 1)
val m = qsort.apply(l) //apply is to Function what run is to Runnable
val n = qsort(l) //syntactic sugar - you don't have to define apply explicitly!
Рекурсия
Когда вызов метода хвост рекурсивный, Scala оптимизирует его в шаблоне итератора. В моем исходном ответе был msitake, потому что qsort
выше не является хвостовым рекурсивным (хвостовой вызов является оператором cons)
Ответ 3
Сводка в этом примере сопоставления шаблонов - это первый элемент списка:
scala> List(1,2,3) match {
| case x :: xs => println(x)
| case _ => println("empty")
| }
1
Согласование шаблонов основано на экстракторах, а минусы не являются частью языка. Он использует синтаксис infix. Вы также можете написать
scala> List(1,2,3) match {
| case ::(x,xs) => println(x)
| case _ => println("empty")
| }
1
. Таким образом, существует type::, который выглядит как оператор cons. Этот тип определяет, как он извлекается:
final case class ::[B](private var hd: B, private[scala] var tl: List[B]){ ... }
Это класс case, поэтому экстрактор будет сгенерирован компилятором Scala. Как в этом примере класс A.
case class A(x : Int, y : Int)
A(1,2) match { case x A y => printf("%s %s", x, y)}
-> 1 2
Основываясь на этом сопоставлении шаблонов, поддерживается для списков, Regexp и XML.