Подтвердить что ты не робот

Использование Scalaz Stream для задачи синтаксического анализа (замена Scalaz Iteratees)

Введение

Я использую Scalaz 7 итерации в ряде проектов, в основном для обработки больших файлов. Я хотел бы начать переходить на Scalaz потоки, которые предназначены для замены пакета iteratee (который, откровенно говоря, не хватает большого количества кусков и является своего рода болью для использования).

Потоки основаны на machines (еще одна вариация на итерационную идею), которые также реализованы в Хаскелле. Я немного использовал библиотеку машин Haskell, но связь между машинами и потоками не совсем очевидна (по крайней мере, для меня), а документация для библиотеки потоков немного редкий.

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

Task

Предположим, у меня есть файл, содержащий предложения, которые были маркированы и помечены частями речи:

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

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

Там один токен на строку, слова и части речи разделены одним пробелом, а пустые строки представляют границы предложений. Я хочу проанализировать этот файл и вернуть список предложений, которые мы могли бы также представлять в виде списков кортежей строк:

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

Как обычно, мы хотим изящно потерпеть неудачу, если ударим недопустимые исключения для ввода или чтения файлов, мы не хотим беспокоиться о закрытии ресурсов вручную и т.д.

Итерационное решение

Сначала для некоторых общих файлов чтения файлов (которые действительно должны быть частью пакета iteratee, который в настоящее время не предоставляет ничего удаленно этого высокого уровня):

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

И затем наш читатель предложений:

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

И, наконец, наше синтаксическое действие:

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

Мы можем продемонстрировать, что он работает:

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

И все готово.

Что я хочу

Более или менее ту же программу, реализованную с использованием потоков Scalaz, а не итерации.

4b9b3361

Ответ 1

Решение для скалярного потока:

import scalaz.std.vector._
import scalaz.syntax.traverse._
import scalaz.std.string._

val action = linesR("example.txt").map(_.trim).
  splitOn("").flatMap(_.traverseU { s => s.split(" ") match {
    case Array(form, pos) => emit(form -> pos)
    case _ => fail(new Exception(s"Invalid input $s"))
  }})

Мы можем продемонстрировать, что он работает:

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

И все готово.

Функция traverseU является общим комбинатором Scalaz. В этом случае он используется для перемещения в монаде Process предложения Vector, сгенерированного splitOn. Это эквивалентно map, за которым следует sequence.