Использование Scalaz Stream для анализа (замена итераторов Scalaz)
Вступление
Я использую итераторы Scalaz 7 в ряде проектов, в основном для обработки файлов большого размера. Я хотел бы начать переключаться на потоки Scalaz, которые предназначены для замены пакета iteratee (который, честно говоря, пропускает много кусочков и является довольно неудобной для использования).
Потоки основаны на машинах (еще один вариант идеи итерируемого), которые также были реализованы в Haskell. Я немного использовал библиотеку компьютеров Haskell, но связь между машинами и потоками не совсем очевидна (по крайней мере, для меня), и документация для библиотеки потоков все еще немного скудна.
Этот вопрос о простой задаче разбора, которую я хотел бы видеть реализованной с использованием потоков вместо итераторов. Я сам отвечу на вопрос, если никто не побьет меня, но я уверен, что я не единственный, кто делает (или, по крайней мере, рассматривает) этот переход, и, так как мне все равно нужно пройти через это упражнение, я подумал, что я мог бы сделать это публично.
задача
Предположим, у меня есть файл, содержащий предложения, которые были размечены и помечены частями речи:
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 вместо итераторов.
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
функция является общим комбинатором Скалаз. В этом случае он используется для прохождения, в Process
монада, предложение Vector
создано splitOn
, Это эквивалентно map
с последующим sequence
,