Como dobrar um iterador Scala e obter uma sequência avaliada preguiçosamente como resultado?

8

Eu tenho um iterador de strings, onde cada string pode ser "H"(cabeçalho) ou "D"(detalhe). Quero dividir esse iterador em blocos, onde cada bloco começa com um cabeçalho e pode ter de 0 a muitos detalhes.

Eu sei como resolver esse problema carregando tudo na memória. Por exemplo, o código abaixo:

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

retorna 5 blocos - List(List(H, D, D, D), List(H, D), List(H), List(H, D, D), List(H, D))- é o que eu quero.

No entanto, em vez de List[List[String]]no resultado, quero uma Iterator[List[String]]ou outra estrutura que permita avaliar o resultado preguiçosamente e não carregar toda a entrada na memória se o iterador inteiro for consumido , quero carregar na memória apenas o bloco que está sendo consumido de cada vez (por exemplo: quando ligo iterator.next).

Como posso modificar o código acima para alcançar o resultado desejado?

Edição: Eu preciso disso no Scala 2.11 especificamente, como o ambiente que eu uso gruda nele. Fico feliz em também aceitar respostas para outras versões.

mvallebr
fonte
Tenho problemas para entender esta parte: e não carregue a lista inteira na memória se o iterador inteiro for consumido . Isso não significa que o programa já examinou todos os elementos? Se o resultado do algoritmo não for armazenado de alguma forma (na memória ou no disco), parece que não há como recuperá-lo, exceto repetindo a lista novamente.
jrook 11/02
O que eu quis dizer com isso é que espero ter um iterador como retorno ou algo que se comporte como ele. Um fluxo, por exemplo, de acordo com o que me disseram (posso estar errado) manterá na memória todos os elementos já consumidos, não é? Não quero consumir duas vezes, mas quero consumir blocos.
mvallebr 11/02
2
Eu editei a pergunta para esclarecer mais, espero que esteja claro agora, caso contrário, deixe-me saber.
mvallebr 11/02
1
Minha resposta funciona para você?
Scalway
1
Eu adicionei proposição sem deslizar. É um pouco mais longo e possui limitação de tipo adicional, mas poderia ser mais eficiente, ainda não tenho certeza. Tenha um bom dia :)
Scalway

Respostas:

5

Aqui está a implementação mais simples que pude encontrar (é genérica e preguiçosa):

/** 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
  }
}

use-o assim:

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

aqui está specyfication:

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 com testes e comentários adicionais: https://scalafiddle.io/sf/q8xbQ9N/11

(se a resposta for útil, faça um voto positivo. Passei um pouco de tempo com isso :))

SEGUNDA IMPLEMENTAÇÃO:

Você propôs uma versão que não usa sliding . Aqui está, mas ele tem seus próprios problemas listados abaixo.

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

Traços:

  • (-) Funciona apenas para T>:Null tipos. Nós só precisamos adicionar um elemento que feche a última coleção no final (nulo é perfeito, mas limita nosso tipo).
  • (~) deve criar a mesma quantidade de trsh da versão anterior. Apenas criamos tuplas na primeira etapa, em vez da segunda.
  • (+) não verifica o comprimento da lista (e esse é um grande ganho para ser honesto).
  • (+) No núcleo, é a resposta de Ivan Kurchenko, mas sem boxe extra.

Aqui está o scalafiddle: https://scalafiddle.io/sf/q8xbQ9N/11

Scalway
fonte
Cara ... Isso é lindo ... Fiquei surpresa como algo tão fácil de fazer em programação imperativa poderia ser tão difícil para mim no paradigma funcional. Mas, olhando a resposta agora, parece óbvio e fácil de entender. A parte deslizante foi complicada - você verifica se o comprimento mudou, o que é algo específico para este caso de uso ... Mas talvez você possa ter verificado o "startGroup" novamente lá, certo? Se b.head é o início de um grupo, você pode coletar ...
mvallebr 12/02
Pensando agora, você realmente precisa do deslizamento acima? Acho que a melhor resposta seria uma combinação da sua e da de Ivan acima ... Você pode coletar diretamente scanLefte ligar para o startGroup apenas uma vez, sem verificar o tamanho. É impressionante como eu não conseguia resolvê-lo antes e, graças à sua resposta, agora posso ver possíveis otimizações. Obrigado!
mvallebr 12/02
6

Se você estiver usando o Scala 2.13.x, poderá criar um novo Iteratordesdobrando-se sobre o 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)
  }
}

teste:

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
fonte
ufff, esqueci de mencionar que tenho que me ater ao scala 2.11 devido a restrições de EMR ... vou editar a pergunta, mas voto a resposta, obrigado ...
mvallebr
Além disso, nit: você usou o itr.head - então é um iterador com buffer, não é?
mvallebr 12/02
2

Acho que a scanLeftoperação pode ajudar nesse caso, se você quiser usar a versão Scala 2.11.

Gostaria de chegar à próxima solução, mas receio que pareça mais complicado que o 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 adicionados, por exemplo, legibilidade. Espero que esta ajuda!

Ivan Kurchenko
fonte
1
Essa resposta foi provavelmente mais didática e eu ficaria feliz em aceitá-la também. No entanto, como a resposta da Scalway obteve mais votos, eu a aceitarei da melhor forma, mas também sou muito grata por essa resposta, foi muito útil e eu votei nela!
mvallebr 12/02
1
@mvallebr Claro, você é livre para escolher o que quiser, e eu concordo que a solução parece melhor. Agradeço sua atenção e voto positivo!
Ivan Kurchenko