Uso de Scalaz Stream para la tarea de análisis (en sustitución de Scalaz Iteratees)

Introducción

yo sueloScalaz 7Se itera en varios proyectos, principalmente para procesar archivos de gran tamaño. Me gustaría empezar a cambiar a Scalazarroyos, que están diseñados para reemplazar el paquete iteratee (que, francamente, le faltan muchas piezas y es un poco difícil de usar).

Las transmisiones se basan enmaquinas (Otra variación de la idea iteratee), que tienentambién se ha implementado en Haskell. He utilizado un poco la biblioteca de máquinas Haskell, pero la relación entre las máquinas y las secuencias no es del todo obvia (al menos para mí), y la documentación de la biblioteca de secuencias estodavía un poco escaso.

Esta pregunta es acerca de una tarea de análisis simple que me gustaría ver implementada utilizando flujos en lugar de iteraciones. Responderé yo mismo a la pregunta si nadie más me gana, pero estoy seguro de que no soy el único que está haciendo (o al menos considerando) esta transición, y como tengo que trabajar en este ejercicio de todos modos, Pensé que también podría hacerlo en público.

Tarea

Supongo que tengo un archivo que contiene oraciones que se han etiquetado y etiquetado con partes del discurso:

no UH
, ,
it PRP
was VBD
n't RB
monday NNP
. .

the DT
equity NN
market NN
was VBD
illiquid JJ
. .

Hay un token por línea, las palabras y las partes del discurso están separadas por un solo espacio, y las líneas en blanco representan los límites de las oraciones. Quiero analizar este archivo y devolver una lista de oraciones, que también podríamos representar como listas de tuplas de cadenas:

List((no,UH), (,,,), (it,PRP), (was,VBD), (n't,RB), (monday,NNP), (.,.))
List((the,DT), (equity,NN), (market,NN), (was,VBD), (illiquid,JJ), (.,.)

Como de costumbre, queremos fallar con gracia si encontramos excepciones de lectura de archivo o entrada no válidas, no debemos tener que preocuparnos por cerrar los recursos manualmente, etc.

Una solución iterativa.

Primero para algunas cosas de lectura de archivos generales (que realmente deberían ser parte del paquete iteratee, que actualmente no proporciona nada de forma remota a este alto nivel):

import java.io.{ BufferedReader, File, FileReader }
import scalaz._, Scalaz._, effect.IO
import iteratee.{ Iteratee => I, _ }

type ErrorOr[A] = EitherT[IO, Throwable, A]

def tryIO[A, B](action: IO[B]) = I.iterateeT[A, ErrorOr, B](
  EitherT(action.catchLeft).map(I.sdone(_, I.emptyInput))
)

def enumBuffered(r: => BufferedReader) = new EnumeratorT[String, ErrorOr] {
  lazy val reader = r
  def apply[A] = (s: StepT[String, ErrorOr, A]) => s.mapCont(k =>
    tryIO(IO(Option(reader.readLine))).flatMap {
      case None       => s.pointI
      case Some(line) => k(I.elInput(line)) >>== apply[A]
    }
  )
}

def enumFile(f: File) = new EnumeratorT[String, ErrorOr] {
  def apply[A] = (s: StepT[String, ErrorOr, A]) => tryIO(
    IO(new BufferedReader(new FileReader(f)))
  ).flatMap(reader => I.iterateeT[String, ErrorOr, A](
    EitherT(
      enumBuffered(reader).apply(s).value.run.ensuring(IO(reader.close()))
    )
  ))
}

Y luego nuestro lector de oraciones:

def sentence: IterateeT[String, ErrorOr, List[(String, String)]] = {
  import I._

  def loop(acc: List[(String, String)])(s: Input[String]):
    IterateeT[String, ErrorOr, List[(String, String)]] = s(
    el = _.trim.split(" ") match {
      case Array(form, pos) => cont(loop(acc :+ (form, pos)))
      case Array("")        => cont(done(acc, _))
      case pieces           =>
        val throwable: Throwable = new Exception(
          "Invalid line: %s!".format(pieces.mkString(" "))
        )

        val error: ErrorOr[List[(String, String)]] = EitherT.left(
          throwable.point[IO]
        )

        IterateeT.IterateeTMonadTrans[String].liftM(error)
    },
    empty = cont(loop(acc)),
    eof = done(acc, eofInput)
  )
  cont(loop(Nil))
}

Y finalmente nuestra acción de análisis:

val action =
  I.consume[List[(String, String)], ErrorOr, List] %=
  sentence.sequenceI &=
  enumFile(new File("example.txt"))

Podemos demostrar que funciona:

scala> action.run.run.unsafePerformIO().foreach(_.foreach(println))
List((no,UH), (,,,), (it,PRP), (was,VBD), (n't,RB), (monday,NNP), (.,.))
List((the,DT), (equity,NN), (market,NN), (was,VBD), (illiquid,JJ), (.,.))

Y hemos terminado.

Lo que quiero

Más o menos el mismo programa implementado utilizando flujos de Scalaz en lugar de iteraciones.

Respuestas a la pregunta(1)

Su respuesta a la pregunta