2014-01-05 68 views
3

假設我正在處理,看上去像一個簡單的冒號分隔的文本協議:慣用的Scala方式

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 
.... 

我想反序列化每行的一個實例案例類,但以類型安全的方式。我第一次嘗試使用類型類來爲我可能遇到的每種可能類型定義'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) 

的問題是,雖然我需要定義每個元組和功能的元數create方法,所以這意味着以創建22個版本。此外,這不涉及驗證或接收損壞的數據。

回答

2

是有無形標籤,一個可能的解決方案使用它,但我不是專家,我想我們可以做的更好:

首先,對缺乏驗證的,你應該簡單地有如果您不關心錯誤消息,請閱讀返回Try或scalaz.Validation或只是選項。

然後關於樣板,您可以嘗試使用HList。這樣你就不需要去追求所有的魔力。

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

} 

鑑於這種情況,你有一個富一個相當短的讀者:

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)) 
+0

我真的很喜歡你的解決方案,但如果我正確地理解它,它被鎖定到一個特定的案例班級的大小,不是嗎?你有沒有想辦法讓它更通用? – pommedeterresautee

+1

不確定你的意思*特定的案例類大小*。使用'case class Bar(a:Int,b:Int,c:String)''你可以'Reader [Int :: Int :: String :: HNil] .map(Generic [Bar] .from _)'。但是,它比一個完整的解決方案更像草圖。它根本不會處理變體類型。正如我所說,我對Shapeless不太熟悉,這可能不是最好的。另外,Shapeless 2.0的新版本剛剛推出,可能會讓它變得更容易。 –

+0

我正在嘗試編寫一個csv解析器。解析後,我得到一個字符串列表,但我試圖將其轉換爲提供的案例類,儘可能爲用戶提供儘可能少的樣板代碼。用上面的消息(這是我發現的最好的)顯示的當前解決方案,lib的用戶需要編寫一些代碼來解釋如何將字符串轉換爲它的類類,如您在上一個命令中所述。我會對如何做到這一點的建議感興趣(如果可能的話)。不支持變體類型在我的情況下不會成爲問題。 – pommedeterresautee

0

沒有scalaz和無形的,我覺得ideomatic Sc​​ala的方法來分析一些輸入是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 [email protected] 
integer --> [1.13] parsed: 5003 
trying date at [email protected] 
trying integer at [email protected] 
integer --> [1.30] parsed: 2013 
trying integer at [email protected] 
integer --> [1.33] parsed: 12 
trying integer at [email protected] 
integer --> [1.36] parsed: 6 
trying integer at [email protected] 
integer --> [1.39] parsed: 12 
trying integer at [email protected] 
integer --> [1.42] parsed: 37 
trying integer at [email protected] 
integer --> [1.45] parsed: 55 
date --> [1.45] parsed: 2013-12-06T12:37:55.000+01:00 

我覺得這是解析輸入以及輸入斯卡拉對象的簡單和維護的方法。它全部在覈心Scala API中,因此我稱之爲「慣用」。在Idea Scala工作表中輸入示例代碼時,完成和類型信息工作得很好。所以這種方式似乎很好地支持IDE。