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

С целью изучения и дальнейшего изучения этого вопроса мне по-прежнему любопытны идиоматические альтернативы явной рекурсии для алгоритма, который проверяет, упорядочен ли список (или коллекция). (Я упрощаю это, используя оператор для сравнения и Int как тип; я хотел бы взглянуть на алгоритм, прежде чем углубляться в его обобщения)

Основной рекурсивной версией будет (@Luigi Plinge):

def isOrdered(l:List[Int]): Boolean = l match {
  case Nil => true
  case x :: Nil => true
  case x :: xs => x <= xs.head && isOrdered(xs)
}

Неэффективный идиоматический способ будет:

def isOrdered(l: List[Int]) = l == l.sorted

Альтернативный алгоритм с использованием Fold:

def isOrdered(l: List[Int]) =
  l.foldLeft((true, None:Option[Int]))((x,y) =>
    (x._1 && x._2.map(_ <= y).getOrElse(true), Some(y)))._1

У него есть недостаток, заключающийся в том, что он будет сравнивать все n элементов списка, даже если он мог бы остановиться раньше после нахождения первого вышедшего из строя элемента. Есть ли способ "остановить" фолд и, следовательно, сделать это лучшим решением?

Любые другие (элегантные) альтернативы?

9 ответов

Решение

Под "идиоматическим" я предполагаю, что вы говорите о "Идиомах" Макбрайда и Патерсона в их статье " Прикладное программирование с эффектами".: О)

Вот как вы можете использовать их идиомы, чтобы проверить, заказана ли коллекция:

import scalaz._
import Scalaz._

case class Lte[A](v: A, b: Boolean)

implicit def lteSemigroup[A:Order] = new Semigroup[Lte[A]] {
  def append(a1: Lte[A], a2: => Lte[A]) = {
    lazy val b = a1.v lte a2.v
    Lte(if (!a1.b || b) a1.v else a2.v, a1.b && b && a2.b)
  }
}

def isOrdered[T[_]:Traverse, A:Order](ta: T[A]) =
  ta.foldMapDefault(x => some(Lte(x, true))).fold(_.b, true)

Вот как это работает:

Любая структура данных T[A] где существует реализация Traverse[T], может быть пройден с Applicative функтор, или "идиома", или "сильный слабый моноидальный функтор". Так уж получилось, что каждый Monoid индуцирует такую ​​идиому бесплатно (см. раздел 4 статьи).

Моноид - это просто ассоциативная бинарная операция над каким-либо типом и элемент идентификации для этой операции. Я определяю Semigroup[Lte[A]] (полугруппа - это то же самое, что и моноид, но без элемента идентичности), ассоциативная операция которого отслеживает меньшее из двух значений и определяет, является ли левое значение меньшим, чем правое значение. И конечно Option[Lte[A]] это просто моноид, свободно генерируемый нашей полугруппой.

В заключение, foldMapDefault пересекает тип коллекции T в идиоме, вызванной моноидом. Результат b будет содержать значение true, если каждое значение было меньше, чем все последующие (это означает, что коллекция была упорядочена), или None если T не было элементов. С пустым T отсортировано по договоренности, проходим true как второй аргумент в финал fold из Option,

В качестве бонуса, это работает для всех перемещаемых коллекций. Демо:

scala> val b = isOrdered(List(1,3,5,7,123))
b: Boolean = true

scala> val b = isOrdered(Seq(5,7,2,3,6))
b: Boolean = false

scala> val b = isOrdered(Map((2 -> 22, 33 -> 3)))
b: Boolean = true

scala> val b = isOrdered(some("hello"))
b: Boolean = true

Тест:

import org.scalacheck._

scala> val p = forAll((xs: List[Int]) => (xs /== xs.sorted) ==> !isOrdered(xs))
p:org.scalacheck.Prop = Prop

scala> val q = forAll((xs: List[Int]) => isOrdered(xs.sorted))
q: org.scalacheck.Prop = Prop

scala> p && q check
+ OK, passed 100 tests.

И вот как вы делаете идиоматический обход, чтобы определить, заказана ли коллекция.

Это выйдет после первого элемента, который вышел из строя. Таким образом, он должен работать хорошо, но я этого не проверял. Это также намного более изящно по моему мнению.:)

def sorted(l:List[Int]) = l.view.zip(l.tail).forall(x => x._1 <= x._2)

Я иду с этим, который довольно похож на Ким Стебель, на самом деле.

def isOrdered(list: List[Int]): Boolean = (
  list 
  sliding 2 
  map { 
    case List(a, b) => () => a < b 
  } 
  forall (_())
)

В случае, если вы пропустили элегантное решение отсутствующего фактора в комментариях выше:

(l, l.tail).zipped.forall(_ <= _)

Это решение очень читабельно и будет выходить из первого вышедшего из строя элемента.

Рекурсивная версия хороша, но ограничена List (с ограниченными изменениями, это будет хорошо работать на LinearSeq).

Если бы это было реализовано в стандартной библиотеке (имело бы смысл), это, вероятно, было бы сделано в IterableLike и иметь полностью обязательную реализацию (см., например, метод find)

Вы можете прервать foldLeft с return (в этом случае вам нужен только предыдущий элемент, а не логическое все время)

import Ordering.Implicits._
def isOrdered[A: Ordering](seq: Seq[A]): Boolean = {
  if (!seq.isEmpty)
    seq.tail.foldLeft(seq.head){(previous, current) => 
      if (previous > current) return false; current
    }
  true
}

но я не вижу, как это лучше или даже идиоматично, чем императивная реализация. Я не уверен, что не назвал бы это императивом на самом деле.

Другое решение может быть

def isOrdered[A: Ordering](seq: Seq[A]): Boolean = 
  ! seq.sliding(2).exists{s => s.length == 2 && s(0) > s(1)}

Скорее, кратко, и, может быть, это можно назвать идиоматическим, я не уверен. Но я думаю, что это не слишком ясно. Более того, все эти методы, вероятно, будут работать намного хуже, чем императивная или хвостовая рекурсивная версия, и я не думаю, что у них есть какая-то дополнительная ясность, которая бы это купила.

Также вы должны взглянуть на этот вопрос.

Чтобы остановить итерацию, вы можете использовать Iteratee:

import scalaz._
import Scalaz._
import IterV._
import math.Ordering
import Ordering.Implicits._

implicit val ListEnumerator = new Enumerator[List] {
  def apply[E, A](e: List[E], i: IterV[E, A]): IterV[E, A] = e match {
    case List() => i
    case x :: xs => i.fold(done = (_, _) => i,
                           cont = k => apply(xs, k(El(x))))
  }
}

def sorted[E: Ordering] : IterV[E, Boolean] = {
  def step(is: Boolean, e: E)(s: Input[E]): IterV[E, Boolean] = 
    s(el = e2 => if (is && e < e2)
                   Cont(step(is, e2))
                 else
                   Done(false, EOF[E]),
      empty = Cont(step(is, e)),
      eof = Done(is, EOF[E]))

  def first(s: Input[E]): IterV[E, Boolean] = 
    s(el = e1 => Cont(step(true, e1)),
      empty = Cont(first),
      eof = Done(true, EOF[E]))

  Cont(first)
}


scala> val s = sorted[Int]
s: scalaz.IterV[Int,Boolean] = scalaz.IterV$Cont$$anon$2@5e9132b3

scala> s(List(1,2,3)).run
res11: Boolean = true

scala> s(List(1,2,3,0)).run
res12: Boolean = false

Если вы разделите Список на две части и убедитесь, что последняя из первой части меньше первой из второй части. Если это так, вы можете проверить параллельно для обеих частей. Вот схематическая идея, сначала без параллели:

def isOrdered (l: List [Int]): Boolean = l.size/2 match {
  case 0 => true 
  case m => {
    val  low = l.take (m)
    val high = l.drop (m)
    low.last <= high.head && isOrdered (low) && isOrdered (high) 
  }
}

А теперь параллельно и с помощью splitAt вместо take/drop:

def isOrdered (l: List[Int]): Boolean = l.size/2 match {
  case 0 => true 
  case m => {
    val (low, high) = l.splitAt (m)
    low.last <= high.head && ! List (low, high).par.exists (x => isOrdered (x) == false) 
  }
}
def isSorted[A <: Ordered[A]](sequence: List[A]): Boolean = {
  sequence match {
    case Nil        => true
    case x::Nil     => true
    case x::y::rest => (x < y) && isSorted(y::rest)
  }
}

Explain how it works. 

Мое решение сочетается с решением недостающего фактора и упорядочением

def isSorted[T](l: Seq[T])(implicit ord: Ordering[T]) = (l, l.tail).zipped.forall(ord.lt(_, _))

и вы можете использовать свой собственный метод сравнения. Например

isSorted(dataList)(Ordering.by[Post, Date](_.lastUpdateTime))
Другие вопросы по тегам