我正在尝试编写一个枚举器,用于使用Scalaz 7的iteratee库从java.io.BufferedReader
逐行读取文件,该库目前仅为java.io.Reader
提供(极慢)枚举器
我遇到的问题与我使用的所有其他iteratee库(例如Haskell的Play 2.0's和John Millikin's enumerator
)都有错误状态这一事实有关。他们的Step
类型的构造函数,而Scalaz 7没有。
这是我现在拥有的。首先是一些导入和IO
包装器:
import java.io.{ BufferedReader, File, FileReader }
import scalaz._, Scalaz._, effect.IO, iteratee.{ Iteratee => I, _ }
def openFile(f: File) = IO(new BufferedReader(new FileReader(f)))
def readLine(r: BufferedReader) = IO(Option(r.readLine))
def closeReader(r: BufferedReader) = IO(r.close())
还有一个类型别名可以清理一下:
type ErrorOr[A] = Either[Throwable, A]
现在是一个tryIO
帮助器,在enumerator
中的def tryIO[A, B](action: IO[B]) = I.iterateeT[A, IO, ErrorOr[B]](
action.catchLeft.map(
r => I.sdone(r, r.fold(_ => I.eofInput, _ => I.emptyInput))
)
)
模拟(松散地,可能是错误的):
BufferedReader
def enumBuffered(r: => BufferedReader) = new EnumeratorT[ErrorOr[String], IO] {
lazy val reader = r
def apply[A] = (s: StepT[ErrorOr[String], IO, A]) => s.mapCont(k =>
tryIO(readLine(reader)) flatMap {
case Right(None) => s.pointI
case Right(Some(line)) => k(I.elInput(Right(line))) >>== apply[A]
case Left(e) => k(I.elInput(Left(e)))
}
)
}
本身的枚举器:
def enumFile(f: File) = new EnumeratorT[ErrorOr[String], IO] {
def apply[A] = (s: StepT[ErrorOr[String], IO, A]) => s.mapCont(k =>
tryIO(openFile(f)) flatMap {
case Right(reader) => I.iterateeT(
enumBuffered(reader).apply(s).value.ensuring(closeReader(reader))
)
case Left(e) => k(I.elInput(Left(e)))
}
)
}
最后是一位负责打开和关闭读者的调查员:
'0'
现在假设我想将一个包含至少二十五个val action: IO[ErrorOr[List[String]]] = (
I.consume[ErrorOr[String], IO, List] %=
I.filter(_.fold(_ => true, _.count(_ == '0') >= 25)) &=
enumFile(new File("big.txt"))
).run.map(_.sequence)
个字符的文件中的所有行收集到一个列表中。我可以写:
unsafePerformIO
在许多方面,这似乎工作得非常漂亮:我可以用Left
启动操作,它会在几分钟内在数千万行和数十亿字节的数据中,在恒定的内存中并且不会吹堆栈,然后在完成后关闭阅读器。如果我给它一个不存在的文件的名称,它会尽职尽责地给我回复包含在enumBuffered
中的异常,并且tryIO
至少看起来如果遇到异常则表现得恰当读数。
我对我的实施有一些担忧 - 特别是val it = for {
_ <- tryIO[Unit, Unit](IO(println("a")))
_ <- tryIO[Unit, Unit](IO(throw new Exception("!")))
r <- tryIO[Unit, Unit](IO(println("b")))
} yield r
。例如,假设我尝试编写一些迭代:
scala> it.run.unsafePerformIO()
a
b
res11: ErrorOr[Unit] = Right(())
如果我执行此操作,我会收到以下信息:
enumerator
如果我在GHCi中使用...> run $ tryIO (putStrLn "a") >> tryIO (error "!") >> tryIO (putStrLn "b")
a
Left !
尝试相同的操作,结果更像我期望的结果:
tryIO
我只是没有看到在iteratee库本身没有错误状态的情况下获得此行为的方法。
我并不认为他是迭代者的任何专家,但我在一些项目中使用了各种Haskell实现,感觉我或多或少了解基本概念,并曾与Oleg喝咖啡一次。不过,我在这里不知所措。在没有错误状态的情况下,这是处理异常的合理方法吗?有没有办法实现enumerator
更像{{1}}版本的行为?在我的实现方式不同的情况下,是否有某种定时炸弹等着我?
答案 0 :(得分:6)
这里编辑是真正的解决方案。我离开了原帖,因为我认为看到这个模式是值得的。什么适用于Klesli为IterateeT工作
import java.io.{ BufferedReader, File, FileReader }
import scalaz._, Scalaz._, effect._, iteratee.{ Iteratee => I, _ }
object IterateeIOExample {
type ErrorOr[+A] = EitherT[IO, Throwable, A]
def openFile(f: File) = IO(new BufferedReader(new FileReader(f)))
def readLine(r: BufferedReader) = IO(Option(r.readLine))
def closeReader(r: BufferedReader) = IO(r.close())
def tryIO[A, B](action: IO[B]) = I.iterateeT[A, ErrorOr, B] {
EitherT.fromEither(action.catchLeft).map(r => I.sdone(r, 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(readLine(reader)) 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(openFile(f)).flatMap(reader => I.iterateeT[String, ErrorOr, A](
EitherT(
enumBuffered(reader).apply(s).value.run.ensuring(closeReader(reader)))))
}
def main(args: Array[String]) {
val action = (
I.consume[String, ErrorOr, List] %=
I.filter(a => a.count(_ == '0') >= 25) &=
enumFile(new File(args(0)))).run.run
println(action.unsafePerformIO().map(_.size))
}
}
=====原帖:===
我觉得你需要一个混合的EitherT。如果没有EitherT,你最终只能得到3左派或者权利。使用EitherT,它将适合左侧。
我认为你真正想要的是
type ErrorOr[+A] = EitherT[IO, Throwable, A]
I.iterateeT[A, ErrorOr, B]
以下代码模仿您当前正在撰写的内容。因为IterateeT没有左右概念,所以当你编写它时,你最终会得到一堆IO / Id。
scala> Kleisli((a:Int) => 4.right[String].point[Id])
res11: scalaz.Kleisli[scalaz.Scalaz.Id,Int,scalaz.\/[String,Int]] = scalaz.KleisliFunctions$$anon$18@73e771ca
scala> Kleisli((a:Int) => "aa".left[Int].point[Id])
res12: scalaz.Kleisli[scalaz.Scalaz.Id,Int,scalaz.\/[String,Int]] = scalaz.KleisliFunctions$$anon$18@be41b41
scala> for { a <- res11; b <- res12 } yield (a,b)
res15: scalaz.Kleisli[scalaz.Scalaz.Id,Int,(scalaz.\/[String,Int], scalaz.\/[String,Int])] = scalaz.KleisliFunctions$$anon$18@42fd1445
scala> res15.run(1)
res16: (scalaz.\/[String,Int], scalaz.\/[String,Int]) = (\/-(4),-\/(aa))
在以下代码中,我们使用EitherT而不是使用Id。由于EitherT与Either具有相同的绑定行为,因此我们得到了我们想要的结果。
scala> type ErrorOr[+A] = EitherT[Id, String, A]
defined type alias ErrorOr
scala> Kleisli[ErrorOr, Int, Int]((a:Int) => EitherT(4.right[String].point[Id]))
res22: scalaz.Kleisli[ErrorOr,Int,Int] = scalaz.KleisliFunctions$$anon$18@58b547a0
scala> Kleisli[ErrorOr, Int, Int]((a:Int) => EitherT("aa".left[Int].point[Id]))
res24: scalaz.Kleisli[ErrorOr,Int,Int] = scalaz.KleisliFunctions$$anon$18@342f2ceb
scala> for { a <- res22; b <- res24 } yield 2
res25: scalaz.Kleisli[ErrorOr,Int,Int] = scalaz.KleisliFunctions$$anon$18@204eab31
scala> res25.run(2).run
res26: scalaz.Scalaz.Id[scalaz.\/[String,Int]] = -\/(aa)
你可以用IterateeT替换Keisli,用IO替换Id,以获得你需要的东西。
答案 1 :(得分:4)
pipes
的方式是使用Channel
类型类进行类型组合:
class Channel p where
{-| 'idT' acts like a \'T\'ransparent proxy, passing all requests further
upstream, and passing all responses further downstream. -}
idT :: (Monad m) => a' -> p a' a a' a m r
{-| Compose two proxies, satisfying all requests from downstream with
responses from upstream. -}
(>->) :: (Monad m)
=> (b' -> p a' a b' b m r)
-> (c' -> p b' b c' c m r)
-> (c' -> p a' a c' c m r)
p1 >-> p2 = p2 <-< p1
......并从基础组合物中获得了EitherT
以上的提升组合物。这是pipes-2.4
中引入的代理变换器原理的一个特例,它允许通过任意扩展来提升组合。
此提升需要在EitherT
中定义专门针对Proxy
类型形状的Control.Proxy.Trans.Either
:
newtype EitherP e p a' a b' b (m :: * -> *) r
= EitherP { runEitherP :: p a' a b' b m (Either e r) }
这种Proxy
形状的特化是必要的,以便能够定义Channel
类的良好类型的实例。在这方面,Scala可能比Haskell更灵活。
然后我只重新定义Monad
实例(以及其他实例)以及此专用类型的所有普通EitherT
操作:
throw :: (Monad (p a' a b' b m)) => e -> EitherP e p a' a b' b m r
throw = EitherP . return . Left
catch
:: (Monad (p a' a b' b m))
=> EitherP e p a' a b' b m r -- ^ Original computation
-> (e -> EitherP f p a' a b' b m r) -- ^ Handler
-> EitherP f p a' a b' b m r -- ^ Handled computation
catch m f = EitherP $ do
e <- runEitherP m
runEitherP $ case e of
Left l -> f l
Right r -> right r
有了这个,我可以定义以下提升的合成实例:
-- Given that 'p' is composable, so is 'EitherP e p'
instance (Channel p) => Channel (EitherP e p) where
idT = EitherP . idT
p1 >-> p2 = (EitherP .) $ runEitherP . p1 >-> runEitherP . p2
要了解正在发生的事情,请按以下类型进行操作:
p1 :: b' -> EitherP e p a' a b' b m r
p2 :: c' -> EitherP e p b' b c' c m r
runEitherP . p1 :: b' -> p a' a b' b m (Either e r)
runEitherP . p2 :: c' -> p b' b c' c m (Either e r)
-- Use the base composition for 'p'
runEitherP . p1 >-> runEitherP . p2
:: c' -> p a' a c' c m (Either e r)
-- Rewrap in EitherP
(EitherP . ) $ runEitherP . p1 >-> runEitherP . p2
:: c' -> EitherP e p a' a c' c m r
这使您可以在特定阶段内抛出并捕获错误,而不会中断其他阶段。以下是我在pipes-2.4
公告帖子中复制并粘贴的示例:
import Control.Monad (forever)
import Control.Monad.Trans (lift)
import Control.Proxy
import Control.Proxy.Trans.Either as E
import Safe (readMay)
promptInts :: () -> EitherP String Proxy C () () Int IO r
promptInts () = recover $ forever $ do
str <- lift getLine
case readMay str of
Nothing -> E.throw "Could not parse an integer"
Just n -> liftP $ respond n
recover p =
p `E.catch` (\str -> lift (putStrLn str) >> recover p)
main = runProxy $ runEitherK $ mapP printD <-< promptInts
结果如下:
>>> main
1<Enter>
1
Test<Enter>
Could not parse an integer
Apple<Enter>
Could not parse an integer
5<Enter>
5
iteratee方法的答案是类似的。您必须采用现有的方法来编写迭代,并将其提升到EitherT
。您是否使用类型类或仅定义新的合成运算符取决于您。
其他一些有用的链接: