Идиоматический Scala способ десериализации строк с разделителями в классы case

Предположим, я имел дело с простым текстовым протоколом, разделенным двоеточием, который выглядел примерно так:

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, но безопасным для типов способом. Моя первая попытка использует классы типов для определения методов «чтения» для каждого возможного типа, с которым я могу столкнуться, в дополнение к методу «tupled» в классе case, чтобы вернуть функцию, которая может быть применена к кортежу аргументов, что-то вроде последующий:

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)

Проблема, однако, в том, что мне нужно определить метод создания для каждого кортежа и арности функции, так что это означает до 22 версий создания. Кроме того, это не заботится о проверке или получении поврежденных данных.


person Exponent    schedule 05.01.2014    source источник


Ответы (2)


Поскольку есть тег Shapeless, возможное решение с его использованием, но я не эксперт, и я думаю, что можно сделать лучше:

Во-первых, про отсутствие валидации, надо было просто прочитать return 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
    }
  }

}

Учитывая это, у вас есть достаточно короткий ридер для 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))
person Didier Dupont    schedule 06.01.2014
comment
Мне очень нравится ваше решение, но если я правильно его понимаю, оно привязано к определенному размеру класса case, не так ли? Вы видите способ сделать его более общим? - person pommedeterresautee; 06.05.2014
comment
Не уверен, что вы имеете в виду конкретный размер класса дел. С case class Bar(a: Int, b: Int, c: String) вы делаете Reader[Int :: Int :: String :: HNil].map(Generic[Bar].from _). Но тогда это скорее набросок, чем полноценное решение. Например, он вообще не будет обрабатывать вариантный тип. Как я уже сказал, я не слишком знаком с Shapeless, и это, вероятно, не лучшее, что можно сделать. Кроме того, только что вышла новая версия Shapeless 2.0, которая может упростить задачу. - person Didier Dupont; 06.05.2014
comment
Я пытаюсь написать парсер csv. После синтаксического анализа я получаю список строк, но я пытаюсь привести его к предоставленному классу case с минимальным количеством шаблонного кода для пользователя. С текущим решением, которое вы представили в сообщении выше (это лучшее, что я нашел), пользователю библиотеки потребуется написать некоторый код, чтобы объяснить, как преобразовать строки в его класс приведения, как вы объяснили в своей последней команде. Я был бы заинтересован в совете о том, как это сделать (если это возможно). В моем случае не было бы проблем с поддержкой типов вариантов. - person pommedeterresautee; 06.05.2014
comment
Не уверен, что понимаю, что вы ищете. Может, вам стоит задать новый вопрос — тем более, что вы могли бы тогда получить гораздо лучшие ответы, чем мои :) - person Didier Dupont; 06.05.2014
comment
Я последовал вашему совету :-) stackoverflow.com/questions/23540531/ - person pommedeterresautee; 08.05.2014
comment
предельно ясное и краткое объяснение. Я должен был реализовать несколько импортов Excel, это работает хорошо. Итак, я реализовал f: Seq[String] => Validated[A] - person Alexey Rykhalskiy; 20.12.2020

Без scalaz и shapeless, я думаю, что идеоматический способ 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.

person stefan.schwetschke    schedule 15.01.2014