Użycie Scalaz Stream do parsowania zadania (zastąpienie iteratów Scalaz)

Wprowadzenie

używamScalaz 7iteracje w wielu projektach, głównie do przetwarzania dużych plików. Chciałbym zacząć przechodzić na Scalazstrumienie, które mają zastąpić pakiet iterowany (który szczerze brakuje wielu kawałków i jest dość trudny w użyciu).

Strumienie są oparte namaszyny (inna odmiana idei iteracyjnej), która marównież zostały wdrożone w Haskell. Użyłem nieco biblioteki maszyn Haskell, ale związek między maszynami i strumieniami nie jest całkowicie oczywisty (przynajmniej dla mnie), a dokumentacja biblioteki strumieniowej towciąż trochę rzadki.

To pytanie dotyczy prostego zadania analizowania, które chciałbym zobaczyć zaimplementowane za pomocą strumieni zamiast iteratów. Odpowiem sobie na pytanie, czy nikt mnie do tego nie uderzy, ale jestem pewien, że nie jestem jedynym, który dokonuje (a przynajmniej rozważa) to przejście, a ponieważ i tak muszę pracować nad tym ćwiczeniem, ja doszedłem do wniosku, że równie dobrze mogę to zrobić publicznie.

Zadanie

Przypuszczam, że mam plik zawierający zdania, które zostały tokenowane i oznaczone częściami mowy:

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

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

Na linię przypada jeden token, słowa i części mowy są oddzielone pojedynczym odstępem, a puste linie oznaczają granice zdania. Chcę przeanalizować ten plik i zwrócić listę zdań, które równie dobrze możemy przedstawić jako listy krotek łańcuchów:

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

Jak zwykle chcemy zawieść z wdziękiem, jeśli trafimy na nieprawidłowe dane wejściowe lub wyjątki odczytu plików, nie chcemy martwić się o ręczne zamykanie zasobów itp.

Rozwiązanie iteracyjne

Najpierw niektóre ogólne pliki do odczytu plików (które naprawdę powinny być częścią pakietu iteratee, który obecnie nie zapewnia niczego zdalnie na tym wysokim poziomie):

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

A potem nasz czytnik zdań:

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

I wreszcie nasza akcja parsowania:

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

Możemy wykazać, że działa:

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), (.,.))

I skończyliśmy.

Czego chcę

Mniej więcej ten sam program zaimplementowany przy użyciu strumieni Scalaz zamiast iteratów.

questionAnswers(1)

yourAnswerToTheQuestion