¿Cómo doblar un iterador Scala y obtener una secuencia perezosamente evaluada como resultado?

8

Tengo un iterador de cadenas, donde cada cadena puede ser "H"(encabezado) o "D"(detalle). Quiero dividir este iterador en bloques, donde cada bloque comienza con un encabezado y puede tener de 0 a muchos detalles.

Sé cómo resolver este problema cargando todo en la memoria. Por ejemplo, el siguiente código:

Seq("H","D","D","D","H","D","H","H","D","D","H","D").toIterator
  .foldLeft(List[List[String]]())((acc, x) => x match {
    case "H" => List(x) :: acc
    case "D" => (x :: acc.head) :: acc.tail })
  .map(_.reverse)
  .reverse

devuelve 5 bloques, List(List(H, D, D, D), List(H, D), List(H), List(H, D, D), List(H, D))que es lo que quiero.

Sin embargo, en lugar de List[List[String]]en el resultado, quiero una Iterator[List[String]]u otra estructura que me permita evaluar el resultado de forma perezosa y no cargar toda la entrada en la memoria si se consume todo el iterador , quiero cargar en la memoria solo el bloque que se consume a la vez (por ejemplo: cuando llamo iterator.next).

¿Cómo puedo modificar el código anterior para lograr el resultado que quiero?

EDITAR: Necesito esto en Scala 2.11 específicamente, ya que el entorno que uso se adhiere a él. Sin embargo, me alegro de aceptar también respuestas para otras versiones.

mvallebr
fuente
Tengo problemas para comprender esta parte: y no cargue toda la lista en la memoria si se consume todo el iterador . ¿No significa esto que el programa ya ha examinado todos los elementos? Si el resultado del algoritmo no se almacena de alguna manera (en la memoria o en el disco), parece que no hay forma de recuperarlo, excepto iterar sobre la lista nuevamente.
jrook
Lo que quise decir con esto es que espero tener un iterador como retorno o algo que se comporte así. Una secuencia, por ejemplo, de acuerdo con lo que me dijeron (podría estar equivocado) mantendrá en la memoria todos los elementos ya consumidos, ¿no? No quiero consumir dos veces, pero quiero consumir bloques.
mvallebr
2
Edité la pregunta para aclarar más, espero que esté claro ahora, de lo contrario, házmelo saber.
mvallebr
1
¿Mi respuesta funciona para ti?
Scalway
1
He añadido proposición sin deslizamiento. Es un poco más largo y tiene una limitación de tipo adicional, pero podría ser más eficiente, aún no estoy seguro. Que tengas un buen día :)
Scalway

Respuestas:

5

Aquí está la implementación más simple que pude encontrar (es genérica y perezosa):

/** takes 'it' and groups consecutive elements 
 *  until next item that satisfy 'startGroup' predicate occures. 
 *  It returns Iterator[List[T]] and is lazy 
 *  (keeps in memory only last group, not whole 'it'). 
*/
def groupUsing[T](it:Iterator[T])(startGroup:T => Boolean):Iterator[List[T]] = {
  val sc = it.scanLeft(List.empty[T]) {
    (a,b) => if (startGroup(b)) b::Nil else b::a
  }

  (sc ++ Iterator(Nil)).sliding(2,1).collect { 
    case Seq(a,b) if a.length >= b.length => a.reverse
  }
}

úsalo así:

val exampleIt = Seq("H1","D1","D2","D3","H2","D4","H3","H4","D5","D6","H5","D7").toIterator
groupUsing(exampleIt)(_.startsWith("H"))
// H1 D1 D2 D3 / H2 D4 / H3 / H4 D5 D6 / H5 D7

Aquí está la especificación:

X | GIVEN            | EXPECTED     |
O |                  |              | empty iterator
O | H                | H            | single header
O | D                | D            | single item (not header)
O | HD               | HD           |
O | HH               | H,H          | only headers
O | HHD              | H,HD         |
O | HDDDHD           | HDDD,HD      |
O | DDH              | DD,H         | heading D's have no Header as you can see.
O | HDDDHDHDD        | HDDD,HD,HDD  |

scalafiddle con pruebas y comentarios adicionales: https://scalafiddle.io/sf/q8xbQ9N/11

(si la respuesta es útil, vote por favor, por favor. Pasé demasiado tiempo :))

SEGUNDA APLICACIÓN:

Tiene una versión propuesta que no utiliza sliding . Aquí está, pero tiene sus propios problemas enumerados a continuación.

def groupUsing2[T >: Null](it:Iterator[T])(startGroup:T => Boolean):Iterator[List[T]] = {
  type TT = (List[T], List[T])
  val empty:TT = (Nil, Nil)
  //We need this ugly `++ Iterator(null)` to close last group.
  val sc = (it ++ Iterator(null)).scanLeft(empty) {
    (a,b) => if (b == null || startGroup(b)) (b::Nil, a._1) else (b::a._1, Nil)
  }

  sc.collect { 
    case (_, a) if a.nonEmpty => a.reverse
  }
}

Rasgos:

  • (-) Funciona solo para T>:Nulltipos. Solo necesitamos agregar un elemento que cierre la última colección al final (nulo es perfecto pero limita nuestro tipo).
  • (~) debería crear la misma cantidad de trsh que la versión anterior. Simplemente creamos tuplas en el primer paso en lugar del segundo.
  • (+) no verifica la longitud de la Lista (y para ser honesto, esta es una gran ganancia).
  • (+) En esencia, es la respuesta de Ivan Kurchenko, pero sin boxeo adicional.

Aquí está scalafiddle: https://scalafiddle.io/sf/q8xbQ9N/11

Escalpelo
fuente
Amigo ... Eso es hermoso ... Me sorprendió cómo algo tan fácil de hacer en la programación imperativa podría ser tan difícil para mí en el paradigma funcional. Pero mirando la respuesta ahora, parece obvio y muy fácil de entender. La parte deslizante fue complicada: usted verifica si la longitud cambió, lo cual es algo específico para este caso de uso ... Pero tal vez podría haber verificado "startGroup" nuevamente allí, ¿verdad? Si b.head es el comienzo de un grupo, puedes recolectar ...
mvallebr
Pensando ahora, ¿realmente necesitas el deslizamiento de arriba? Creo que la mejor respuesta sería una combinación de la suya y la de Ivan arriba ... Puede cobrar directamente scanLefty llamar a startGroup solo una vez, sin verificar la longitud. Es impresionante cómo no pude resolverlo antes y gracias a su respuesta ahora incluso puedo ver posibles optimizaciones. ¡Gracias!
mvallebr
6

Si está utilizando Scala 2.13.x, puede crear uno nuevo Iteratordesplegando sobre el original Iterator.

import scala.collection.mutable.ListBuffer

val data = Seq("H","D","D","D","H","D","H","H","D","D","H","D").iterator

val rslt = Iterator.unfold(data.buffered){itr =>
  Option.when(itr.hasNext) {
    val lb = ListBuffer(itr.next())
    while (itr.hasNext && itr.head == "D")
      lb += itr.next()
    (lb.toList, itr)
  }
}

pruebas:

rslt.next()   //res0: List[String] = List(H, D, D, D)
rslt.next()   //res1: List[String] = List(H, D)
rslt.next()   //res2: List[String] = List(H)
rslt.next()   //res3: List[String] = List(H, D, D)
rslt.next()   //res4: List[String] = List(H, D)
rslt.hasNext  //res5: Boolean = false
jwvh
fuente
ufff, olvidé mencionar que tengo que apegarme a scala 2.11 debido a restricciones de EMR ...
Editaré
Además, nit: usaste itr.head, por lo que es un iterador protegido, ¿no?
mvallebr
2

Creo que la scanLeftoperación podría ayudar en este caso, si desea utilizar la versión Scala 2.11.

Me gustaría proponer la próxima solución, pero me temo que parece más complicada que la original:

def main(args: Array[String]): Unit = {
    sealed trait SequenceItem
    case class SequenceSymbol(value: String) extends SequenceItem
    case object Termination extends SequenceItem

    /**
      * _1 - HD sequence in progress
      * _2 - HD sequences which is ready
      */
    type ScanResult = (List[String], List[String])
    val init: ScanResult = Nil -> Nil

    val originalIterator: Iterator[SequenceItem] = Seq("H","D","D","D", "H","D", "H", "H","D","D", "H","D")
      .toIterator.map(SequenceSymbol)

    val iteratorWithTermination: Iterator[SequenceItem] = originalIterator ++ Seq(Termination).toIterator
    val result: Iterator[List[String]] = iteratorWithTermination
      .scanLeft(init) {
        case ((progress, _), SequenceSymbol("H")) =>  List("H") -> progress
        case ((progress, _), SequenceSymbol("D")) => ("D" :: progress) -> Nil
        case ((progress, _), Termination) => Nil -> progress
      }
      .collect {
        case (_, ready) if ready.nonEmpty => ready
      }
      .map(_.reverse)

    println(result.mkString(", "))
  }

Tipos añadidos, por ejemplo, legibilidad. ¡Espero que esto ayude!

Ivan Kurchenko
fuente
1
Esta respuesta probablemente fue más didáctica y me complacería aceptarla también. Sin embargo, como la respuesta de Scalway obtuvo más votos, la aceptaré como la mejor, pero también estoy muy agradecida por esta respuesta, ¡fue muy útil y la voté!
mvallebr
1
@mvallebr Claro, puedes elegir libremente lo que quieras y estoy de acuerdo en que la solución se ve mejor. Agradezco su atención y voto!
Ivan Kurchenko