2013-04-22 9 views
10

Considerare questo codice (preso da here e modificato per utilizzare byte anziché righe di caratteri).Come utilizzare IO con Scalaz7 Iteratees senza sovraccaricare lo stack?

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

L'esecuzione di questo codice su un file di dimensioni decenti (8kb) produce una StackOverflowException. Alcune ricerche hanno rivelato che l'eccezione poteva essere evitata usando la monade Trampoline invece di IO, ma questa non sembra una grande soluzione - sacrificare la purezza funzionale per completare il programma. Il modo ovvio per risolvere questo problema è usare IO o Trampoline come un trasformatore di Monade per avvolgere l'altro, ma non riesco a trovare un'implementazione della versione del trasformatore di nessuno di essi e non sono abbastanza un guru della programmazione funzionale per so come scrivere il mio (imparare di più su FP è uno degli scopi di questo progetto, ma sospetto che la creazione di nuovi trasformatori monad sia un po 'al di sopra del mio livello al momento). Suppongo che potrei semplicemente avvolgere una grande azione IO attorno alla creazione, all'esecuzione e alla restituzione del risultato dei miei iterate, ma mi sembra più una soluzione alternativa che una soluzione.

Presumibilmente alcune monade non possono essere convertite in trasformatori monad, quindi mi piacerebbe sapere se è possibile lavorare con file di grandi dimensioni senza perdere IO o sovraccaricare lo stack, e se sì, come?

Domanda bonus: Non riesco a pensare a un modo per iteratee per segnalare che è verificato un errore durante l'elaborazione fatta eccezione per averlo tornare entrambi i casi, che lo rende meno facile da comporre loro. Il codice sopra mostra come usare EitherT per gestire gli errori nell'enumeratore, ma come funziona per gli iterate?

+0

Questo potrebbe essere utile a voi: http://termsandtruthconditions.herokuapp.com/blog/2013/03/16/free-monad/ – Impredicative

+0

È una buona spiegazione del motivo per cui ho bisogno di usare il trampolino per evitare di traboccare lo stack, ma non copre come utilizzare sia l'IO che il trampolino. – Redattack34

+0

IO è già trampolino. – Apocalisp

risposta

3

Dopo aver creato le eccezioni e aver stampato la lunghezza dello stack in vari punti del codice, ho ritenuto che il codice del codice non fosse eccessivo. Tutto sembra funzionare in dimensioni di stack costanti. Quindi ho cercato altri posti. Alla fine ho copiato l'implementazione di consume e aggiunto un po 'di profondità di stampa dello stack e ho confermato che vi era traboccato.

Quindi questo trabocca:

(I.consume[Int, Id, List] &= EnumeratorT.enumStream(Stream.fill(10000)(1))).run 

Ma, ho poi scoperto che questo non lo fa:

(I.putStrTo[Int](System.out) &= EnumeratorT.enumStream(Stream.fill(10000)(1))) 
    .run.unsafePerformIO() 

putStrTo utilizza foldM e in qualche modo non sta causando un overflow. Quindi mi chiedo se lo consume possa essere implementato in termini di foldM. Ho appena copiato un po 'di cose da consumare e ottimizzato fino a compilare:

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

E ha funzionato! Stampa una lunga lista di int.

+0

Sembra che 'consume1' trabocchi con Scalaz 7.0.3, almeno per me. Otterrete lo stesso risultato se aumentate la dimensione del flusso? Sto cercando di rintracciare [un bug potenzialmente correlato] (https: // github.com/scalaz/scalaz/issues/554) - Ho notato che ottengo un overflow dello stack se corro in un contesto 'Id', mentre ottengo un errore nello spazio heap se eseguo un' trampolino'. Con il tuo caso, tuttavia, l'errore scompare in un contesto trampolino, che mi porta a sospettare che i problemi potrebbero non essere correlati dopo tutto ... –

+0

@AaronNovstrup, funziona ancora con 100000 e scalaz 7.0.3, quindi potrebbe essere il tuo problema è davvero diverso. – huynhjl

+0

Strano. Sto vedendo un overflow dello stack con 'consume1' nella console di Scala anche per un numero relativamente piccolo di elementi (100), utilizzando Scala 2.10.2, Scalaz 7.0.3, OpenJDK 64-bit server VM 1.7.0_25 e un dimensione dello stack di 256k. –

Problemi correlati