scalaz,读取并映射文件的行

时间:2016-09-13 17:16:18

标签: scala scalaz

以下用于读取和映射文件行的代码可以正常工作:

def readLines(fileName: String) = scala.io.Source.fromFile(fileName).getLines
def toInt(line: String) = line.toInt

val numbers: Iterator[Int] = readLines("/tmp/file.txt").map(toInt).map(_ * 2)
println(numbers.toList)

如果执行顺利,我得到Int s的迭代器。但是如果找不到文件,或者某些行包含字母,程序会抛出异常。

如何将程序转换为使用scalaz monad并获得Disjunction[Exception, List[Int]]

我在scalaz 7.2.6上尝试了这个,但它没有编译:

  import scalaz.Scalaz._
  import scalaz._

  def readLines(fileName: String): Disjunction[Any, List[String]] =
    try { scala.io.Source.fromFile(fileName).getLines.toList.right }
    catch { case e: java.io.IOException => e.left}

  def toInt(line: String): Disjunction[Any, Int] =
    try { line.toInt.right }
    catch { case e: NumberFormatException => e.left}

  val numbers: Disjunction[Any, Int] = for {
    lines: List[String] <- readLines("/tmp/file.txt")
    line: String <- lines
    n: Int <- toInt(line)
  } yield (n * 2)

无法使用这些错误进行编译:

Error:(89, 37) could not find implicit value for parameter M: scalaz.Monoid[Any]
    lines: List[String] <- readLines("/tmp/file.txt")
Error:(89, 37) not enough arguments for method filter: (implicit M: scalaz.Monoid[Any])scalaz.\/[Any,List[String]].
Unspecified value parameter M.
    lines: List[String] <- readLines("/tmp/file.txt")
Error:(91, 20) could not find implicit value for parameter M: scalaz.Monoid[Any]
    n: Int <- toInt(line)
Error:(91, 20) not enough arguments for method filter: (implicit M: scalaz.Monoid[Any])scalaz.\/[Any,Int].
Unspecified value parameter M.
    n: Int <- toInt(line)

我不明白错误。有什么问题?

以及如何改进此代码,以便它不会将所有文件读入内存,但它一次读取并映射每一行?

更新:来自Filippo的回答

  import scalaz._

  def readLines(fileName: String) = \/.fromTryCatchThrowable[List[String], Exception] {
    scala.io.Source.fromFile(fileName).getLines.toList
  }

  def toInt(line: String) = \/.fromTryCatchThrowable[Int, NumberFormatException](line.toInt)

  type λ[+A] = Exception \/ A

  val numbers = for {
    line: String <- ListT[λ, String](readLines("/tmp/file.txt"))
    n: Int       <- ListT[λ, Int](toInt(line).map(List(_)))
  } yield n * 2

  println(numbers)

2 个答案:

答案 0 :(得分:2)

要回答问题的第二部分,我只需使用Iterator方法中的fromFile

val lines: Iterator[String] = scala.io.Source.fromFile(fileName).getLines

如果您想使用toIntString转换为Int

import scala.util.Try

def toInt(line: String): Iterator[Int] =
  Try(line.toInt).map(Iterator(_)).getOrElse(Iterator.empty)

然后numbers看起来像:

val numbers = readLines("/tmp/file.txt").flatMap(toInt).map(_ * 2)

修改

由于存在所有这些trycatch,如果您想继续使用monadic-for,我建议您查看scalaz帮助,例如.fromTryCatchThrowable } Disjunction

import scalaz._, Scalaz._

def readLines(fileName: String): Disjunction[Exception, List[String]] =
  Disjunction.fromTryCatchThrowable(scala.io.Source.fromFile(fileName).getLines.toList)

def toInt(line: String): Disjunction[Exception, Int] =
  Disjunction.fromTryCatchThrowable(line.toInt)

现在我们还有Exception而不是Any作为左侧类型。

val numbers = for {
  lines: List[String] <- readLines("/tmp/file.txt")
  line: String        <- lines                      // The problem is here
  n: Int              <- toInt(line)
} yield n * 2

monadic-for的问题在于第一行和第三行使用Disjunction上下文,但第二行使用List monad。在这里使用像ListTDisjunctionT这样的monad变换器是可能的,但可能有点过分。

编辑 - 回复评论

如上所述,如果我们想要一个monadic-for理解,我们需要一个monad变换器,在这种情况下ListTDisjunction有两个类型参数,而Monad M[_]显然只有一个。我们需要处理这个“额外类型参数”,例如使用type lambda

def readLines(fileName: String) = \/.fromTryCatchThrowable[List[String], Exception] {
  fromFile(fileName).getLines.toList
}

val listTLines = ListT[({type λ[+a] = Exception \/ a})#λ, String](readLines("/tmp/file.txt"))

listTLines的类型是什么? ListT变换器:ListT[\/[Exception, +?], String]

原始for-comprehension的最后一步是toInt

def toInt(line: String) = \/.fromTryCatchThrowable[Int, NumberFormatException](line.toInt)

val listTNumber = ListT[\/[Exception, +?], Int](toInt("line"))

listTNumber的类型是什么?它甚至没有编译,因为toInt返回Int而不是List[Int]。我们需要ListT加入for-comprehension,其中一个技巧可能是listTNumber更改为:

val listTNumber = ListT[\/[Exception, +?], Int](toInt("line").map(List(_)))

现在我们有两个步骤:

val numbers = for {
  line: String <- ListT[\/[Exception, +?], String](readLines("/tmp/file.txt"))
  n: Int       <- ListT[\/[Exception, +?], Int](toInt(line).map(List(_)))
} yield n * 2

scala> numbers.run.getOrElse(List.empty) foreach println
2
20
200

如果您想知道为什么要解开这个问题:

scala> val unwrap1 = numbers.run
unwrap1: scalaz.\/[Exception,List[Int]] = \/-(List(2, 20, 200))

scala> val unwrap2 = unwrap1.getOrElse(List())
unwrap2: List[Int] = List(2, 20, 200)

scala> unwrap2 foreach println
2
20
200

(假设示例文件包含以下行:1,10,100)

编辑 - 关于编译问题的评论

上面的代码归功于Kind Projector插件:

addCompilerPlugin("org.spire-math" % "kind-projector_2.11" % "0.5.2")

使用Kind Projector我们可以使用匿名类型:

Either[Int, +?]          // equivalent to: type R[+A] = Either[Int, A]

而不是:

type IntOrA[A] = Either[Int, A]

// or
({type L[A] = Either[Int, A]})#L

答案 1 :(得分:0)

首先,编译器会警告您正在使用混合类型的理解。您的代码由编译器转换为:

readLines("/tmp/file.txt") flatMap { lines => lines } map { line => toInt(line) }

flatMap的定义是:

def flatMap[A,B](ma: F[A])(f: A => F[B]): F[B]

在你的情况下,F是\ /,而这个 flatMap {lines =&gt; line} 是错误的。编译器会发出类似“ List [Nothing] required”的消息:scalaz。\ / [Any,Int] “因为将列表视为一个没有参数的函数而List [Nothing]作为结果类型。像这样更改你的代码:

import scalaz.Scalaz._
import scalaz._

def readLines(fileName: String): Disjunction[Any, List[String]] =
 try { scala.io.Source.fromFile(fileName).getLines.toList.right }
  catch { case e: java.io.IOException => e.left}

def toInt(line: List[String]): Disjunction[Any, List[Int]] =
  try { (line map { _ toInt }).right }
  catch { case e: NumberFormatException => e.left}                                                

val numbers = for {
  lines <- readLines("/tmp/file.txt")
  n <- toInt(lines)
 } yield (n map (_ * 2))                                 

有效。

对于逐行读取,FileInputStream可能更容易:

 fis = new FileInputStream("/tmp/file.txt");
 reader = new BufferedReader(new InputStreamReader(fis));
 String line = reader.readLine();

 while(line != null){
   System.out.println(line);
   line = reader.readLine();
 }

或者您可以从Source类测试readline函数。