Как использовать IO с итерациями Scalaz7 без переполнения стека?
рассмотрим этот код (взято из здесь и изменен для использования байтов, а не строк символов).
import java.io.{ File, InputStream, BufferedInputStream, FileInputStream }
import scalaz._, Scalaz._, effect._, iteratee.{ Iteratee => I, _ }
import std.list._
object IterateeIOExample {
type ErrorOr[+A] = EitherT[IO, Throwable, A]
def openStream(f: File) = IO(new BufferedInputStream(new FileInputStream(f)))
def readByte(s: InputStream) = IO(Some(s.read()).filter(_ != -1))
def closeStream(s: InputStream) = IO(s.close())
def tryIO[A, B](action: IO[B]) = I.iterateeT[A, ErrorOr, B] {
EitherT(action.catchLeft).map(r => I.sdone(r, I.emptyInput))
}
def enumBuffered(r: => BufferedInputStream) = new EnumeratorT[Int, ErrorOr] {
lazy val reader = r
def apply[A] = (s: StepT[Int, ErrorOr, A]) => s.mapCont(k =>
tryIO(readByte(reader)) flatMap {
case None => s.pointI
case Some(byte) => k(I.elInput(byte)) >>== apply[A]
})
}
def enumFile(f: File) = new EnumeratorT[Int, ErrorOr] {
def apply[A] = (s: StepT[Int, ErrorOr, A]) =>
tryIO(openStream(f)).flatMap(stream => I.iterateeT[Int, ErrorOr, A](
EitherT(
enumBuffered(stream).apply(s).value.run.ensuring(closeStream(stream)))))
}
def main(args: Array[String]) {
val action = (
I.consume[Int, ErrorOr, List] &=
enumFile(new File(args(0)))).run.run
println(action.unsafePerformIO())
}
}
запуск этого кода в файле приличного размера (8kb) создает исключение StackOverflowException. Некоторые поиски показали, что исключения можно избежать, используя монаду батута вместо IO, но это не похоже на отличное решение - пожертвовать функциональной чистотой, чтобы получить программу для завершения вообще. Очевидный способ исправить это-использовать IO или Батут как трансформатор монады, чтобы обернуть другой, но я не могу найти реализацию трансформаторной версии любого из них, и я недостаточно гуру функционального программирования, чтобы знать, как написать свой собственный (узнать больше о FP-одна из целей этого проекта, но я подозреваю, что создание новых трансформаторов монады немного выше моего уровня на данный момент). Я полагаю, что я мог бы просто обернуть большое действие IO вокруг создания, запуска и возврата результата моих итераций, но это больше похоже на обходного пути, чем решения.
предположительно, некоторые монады не могут быть преобразованы в трансформаторы монад, поэтому я хотел бы знать, можно ли работать с большими файлами без сброса IO или переполнения стека, и если да, то как?
бонусный вопрос: я не могу придумать никакого способа для iteratee сигнализировать, что он столкнулся с ошибкой при обработке, кроме как вернуть его, что делает их менее легкими для создания. Код выше показывает, как использовать EitherT для обработки ошибки в перечислителе, но как это работает для итераций?
1 ответов
после создания исключений и печати их длины стека в разных местах вашего кода я почувствовал, что код код не переполнены. Все, кажется, работает в постоянном размере стека. Поэтому я стал искать другие места. В конце концов я скопировал реализацию consume
и добавил некоторую печать глубины стека и подтвердил, что она переполнена там.
так это переполняет:
(I.consume[Int, Id, List] &= EnumeratorT.enumStream(Stream.fill(10000)(1))).run
но, я тогда узнал, что это не:
(I.putStrTo[Int](System.out) &= EnumeratorT.enumStream(Stream.fill(10000)(1)))
.run.unsafePerformIO()
putStrTo
использует foldM
и как-то не вызывая переполнения. Поэтому мне интересно,consume
может быть реализован в условиях foldM
. Я просто скопировал несколько вещей из consume и настроил, пока он не скомпилировал:
def consume1[E, F[_]:Monad, A[_]:PlusEmpty:Applicative]: IterateeT[E, F, A[E]] = {
I.foldM[E, F, A[E]](PlusEmpty[A].empty){ (acc: A[E], e: E) =>
(Applicative[A].point(e) <+> acc).point[F]
}
}
и это сработало! Печать длинного списка ints.