将定界字符串反序列化为case类的惯用Scala方法

时间:2014-01-05 21:56:54

标签: scala scalaz shapeless

假设我正在处理一个简单的冒号分隔的文本协议,它看起来像:

Event:005003:information:2013 12 06 12 37 55:n3.swmml20861:1:Full client swmml20861 registered [entry=280 PID=20864 queue=0x4ca9001b]
RSET:m3node:AUTRS:1-1-24:A:0:LOADSHARE:INHIBITED:0
M3UA_IP_LINK:m3node:AUT001LKSET1:AUT001LK1:r
OPC:m3node:1-10-2(P):A7:NAT0
....

我想将每一行反序列化为一个case类的实例,但是以类型安全的方式。我的第一次尝试使用类型类为我可以遇到的每种可能类型定义'read'方法,除了case类上的'tupled'方法以获取可以应用于参数元组的函数,类似于以下内容:

case class Foo(a: String, b: Integer)

trait Reader[T] {
  def read(s: String): T
}

object Reader {
  implicit object StringParser extends Reader[String] { def read(s: String): String = s }
  implicit object IntParser extends Reader[Integer] { def read(s: String): Integer = s.toInt }
}

def create[A1, A2, Ret](fs: Seq[String], f: ((A1, A2)) => Ret)(implicit A1Reader: Reader[A1], A2Reader: Reader[A2]): Ret = {
  f((A1Reader.read(fs(0)), A2Reader.read(fs(1))))
}

create(Seq("foo", "42"), Foo.tupled) // gives me a Foo("foo", 42)

问题是我需要为每个元组和函数arity定义create方法,这意味着最多可以创建22个版本的create。此外,这不会处理验证或接收损坏的数据。

2 个答案:

答案 0 :(得分:2)

因为有一个无形标签,使用它的可能解决方案,但我不是专家,我想可以做得更好:

首先,关于缺乏验证,您应该只读取返回Try,或scalaz.Validation或只是选项,如果您不关心错误消息。

然后关于样板文件,您可以尝试使用HList。这样你就不需要去所有的arities了。

import scala.util._
import shapeless._

trait Reader[+A] { self =>
  def read(s: String) : Try[A]
  def map[B](f: A => B): Reader[B] = new Reader[B] {
    def read(s: String) = self.read(s).map(f)
  }
}    

object Reader {
  // convenience
  def apply[A: Reader] : Reader[A] = implicitly[Reader[A]]
  def read[A: Reader](s: String): Try[A] = implicitly[Reader[A]].read(s)

  // base types
  implicit object StringReader extends Reader[String] {
    def read(s: String) = Success(s)
  }
  implicit object IntReader extends Reader[Int] {
    def read(s: String) = Try {s.toInt}
  }

  // HLists, parts separated by ":"
  implicit object HNilReader extends Reader[HNil] {
    def read(s: String) = 
      if (s.isEmpty()) Success(HNil) 
      else Failure(new Exception("Expect empty"))
  }
  implicit def HListReader[A : Reader, H <: HList : Reader] : Reader[A :: H] 
  = new Reader[A :: H] {
    def read(s: String) = {
      val (before, colonAndBeyond) = s.span(_ != ':')
      val after = if (colonAndBeyond.isEmpty()) "" else colonAndBeyond.tail
      for {
        a <- Reader.read[A](before)
        b <- Reader.read[H](after)
      } yield a :: b
    }
  }

}

鉴于此,你有一个相当短的Foo读者:

case class Foo(a: Int, s: String) 

object Foo {
  implicit val FooReader : Reader[Foo] = 
    Reader[Int :: String :: HNil].map(Generic[Foo].from _)
}

有效:

println(Reader.read[Foo]("12:text"))
Success(Foo(12,text))

答案 1 :(得分:0)

没有scalaz和无形,我认为解析一些输入的思想Scala方法是Scala解析器组合器。在你的例子中,我会尝试这样的事情:

import org.joda.time.DateTime
import scala.util.parsing.combinator.JavaTokenParsers

val input =
  """Event:005003:information:2013 12 06 12 37 55:n3.swmml20861:1:Full client swmml20861 registered [entry=280 PID=20864 queue=0x4ca9001b]
    |RSET:m3node:AUTRS:1-1-24:A:0:LOADSHARE:INHIBITED:0
    |M3UA_IP_LINK:m3node:AUT001LKSET1:AUT001LK1:r
    |OPC:m3node:1-10-2(P):A7:NAT0""".stripMargin

trait LineContent
case class Event(number : Int, typ : String, when : DateTime, stuff : List[String]) extends LineContent
case class Reset(node : String, stuff : List[String]) extends LineContent
case class Other(typ : String, stuff : List[String]) extends LineContent

object LineContentParser extends JavaTokenParsers {
  override val whiteSpace=""":""".r

  val space="""\s+""".r
  val lineEnd = """"\n""".r  //"""\s*(\r?\n\r?)+""".r
  val field = """[^:]*""".r

  def stuff : Parser[List[String]] = rep(field)
  def integer : Parser[Int] = log(wholeNumber ^^ {_.toInt})("integer")

  def date : Parser[DateTime] = log((repsep(integer, space)  filter (_.length == 6))  ^^ (l =>
      new DateTime(l(0), l(1), l(2), l(3), l(4), l(5), 0)
    ))("date")

  def event : Parser[Event] = "Event" ~> integer ~ field ~ date ~ stuff ^^ {
    case number~typ~when~stuff => Event(number, typ, when, stuff)}

  def reset : Parser[Reset] = "RSET" ~> field ~ stuff ^^ { case node~stuff =>
    Reset(node, stuff)
  }

  def other : Parser[Other] = ("M3UA_IP_LINK" | "OPC") ~ stuff ^^ { case typ~stuff =>
    Other(typ, stuff)
  }

  def line : Parser[LineContent] = event | reset | other
  def lines = repsep(line, lineEnd)

  def parseLines(s : String) = parseAll(lines, s)
}

LineContentParser.parseLines(input)

解析器组合器中的模式是不言自明的。我总是尽可能早地将每个成功解析的块转换为部分结果。然后将部分结果合并到最终结果中。

调试提示:您始终可以添加log解析器。它将在应用规则之前和之后打印。与给定名称(例如“日期”)一起,它还将打印输入源的当前位置,其中应用规则,并在适用时打印已解析的部分结果。

示例输出如下所示:

trying integer at scala.util.parsing.input.CharSequenceReader@108589b
integer --> [1.13] parsed: 5003
trying date at scala.util.parsing.input.CharSequenceReader@cec2e3
trying integer at scala.util.parsing.input.CharSequenceReader@cec2e3
integer --> [1.30] parsed: 2013
trying integer at scala.util.parsing.input.CharSequenceReader@14da3
integer --> [1.33] parsed: 12
trying integer at scala.util.parsing.input.CharSequenceReader@1902929
integer --> [1.36] parsed: 6
trying integer at scala.util.parsing.input.CharSequenceReader@17e4dce
integer --> [1.39] parsed: 12
trying integer at scala.util.parsing.input.CharSequenceReader@1747fd8
integer --> [1.42] parsed: 37
trying integer at scala.util.parsing.input.CharSequenceReader@1757f47
integer --> [1.45] parsed: 55
date --> [1.45] parsed: 2013-12-06T12:37:55.000+01:00

我认为这是一种简单且可维护的方法,可以将输入解析为类型良好的Scala对象。它全部在核心Scala API中,因此我将其称为“惯用语”。在Idea Scala工作表中键入示例代码时,完成和类型信息非常有效。所以这种方式似乎得到了IDE的良好支持。