【问题标题】:Scala shapeless typing Map[Symbol, String] with case classes带有案例类的 Scala 无形类型 Map[Symbol, String]
【发布时间】:2019-10-31 11:06:02
【问题描述】:

我正在读取查询参数并将它们转换为Map[Symbol, String]。我想通过一组案例类为这些查询参数添加一些类型安全性。

这些case类会根据传入的http请求而有所不同,所以这需要支持不同的case类。

如果传入的查询参数与定义的case class 不匹配,则Parser 应返回None

我尝试使用 shapeless 来实现通用解析器。如果所有参数的类型都是String,它就可以工作。但我需要支持任何类型的查询参数。

我已尝试合并本文中看到的隐式转换逻辑,但无法使其正常工作。 https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation/(无形新手)

现有的Parser(没有字符串到类型的转换):

class Parser[A] {
  def from[R <: HList]
  (m: Map[Symbol, String])
  (implicit
   gen: LabelledGeneric.Aux[A, R],
   fromMap: FromMap[R]
  ): Option[A] = fromMap(m).map(gen.from)
}

object Parser {
  def to[A]: Parser[A] = new Parser[A]
}

描述问题的测试:

class ParserSpec extends FlatSpec with Matchers {
  private val sampleName: String = "Bob"
  private val sampleVersion: Int = 1

  //Partial Solution
  case class QueryParams(name: String, version: String)

  //Full Solution (not working)
  case class QueryParams2(name: String, version: Int)

  "A Parser" should "parse query parameters from a map with only string values" in {
    val mapOfQueryParams = Map('name -> sampleName, 'version -> sampleVersion.toString)
    val result = Parser.to[QueryParams].from(mapOfQueryParams)

    result shouldBe 'defined
    result.get.name shouldEqual sampleName
    result.get.version shouldEqual sampleVersion.toString
  }
  it should "parse query parameters from a map with any type of value" in {
    val mapOfQueryParams = Map('name -> sampleName, 'version -> sampleVersion.toString)
    val result = Parser.to[QueryParams2].from(mapOfQueryParams)

    //result is not defined as it's not able to convert a string to integer
    result shouldBe 'defined
    result.get.name shouldEqual sampleName
    result.get.version shouldEqual sampleVersion
  }
}

【问题讨论】:

    标签: scala shapeless


    【解决方案1】:

    FromMap 使用shapeless.Typeable 将值转换为预期的类型。因此,使您的代码工作的最简单方法是定义一个 Typeable 的实例以从 String 转换为 Int(以及出现在您的案例类中的任何值类型的附加 Typeable 实例):

    implicit val stringToInt: Typeable[Int] = new Typeable[Int] {
      override def cast(t: Any): Option[Int] = t match {
        case t: String => Try(t.toInt).toOption
        case _ => Typeable.intTypeable.cast(t)
      }
    
      override def describe: String = "Int from String"
    }
    

    但这不是Typeable 的预期用途,它旨在确认Any 类型的变量已经是预期类型的​​实例,无需任何转换。换句话说,它旨在成为asInstanceOf 的类型安全实现,也可以解决类型擦除问题。


    为了正确起见,您可以定义自己的ReadFromMap 类型类,它使用您自己的Read 类型类从Strings 转换为预期类型。下面是 Read 类型类的简单实现(假设 Scala 2.12):

    import scala.util.Try
    
    trait Read[T] {
      def apply(string: String): Option[T]
    }
    
    object Read {
      implicit val readString: Read[String] = Some(_)
      implicit val readInt: Read[Int] = s => Try(s.toInt).toOption
      // Add more implicits for other types in your case classes
    }
    

    您可以复制和调整FromMap 的实现以使用此Read 类型类:

    import shapeless._
    import shapeless.labelled._
    
    trait ReadFromMap[R <: HList] extends Serializable {
      def apply(map: Map[Symbol, String]): Option[R]
    }
    
    object ReadFromMap {
      implicit def hnil: ReadFromMap[HNil] = _ => Some(HNil)
    
      implicit def hlist[K <: Symbol, V, T <: HList](implicit
        keyWitness: Witness.Aux[K],
        readValue: Read[V],
        readRest: ReadFromMap[T]
      ): ReadFromMap[FieldType[K, V] :: T] = map => for {
        value <- map.get(keyWitness.value)
        converted <- readValue(value)
        rest <- readRest(map)
      } yield field[K](converted) :: rest
    }
    

    然后只需在您的Parser 中使用这个新的类型类:

    class Parser[A] {
      def from[R <: HList]
      (m: Map[Symbol, String])
      (implicit
        gen: LabelledGeneric.Aux[A, R],
        fromMap: ReadFromMap[R]
      ): Option[A] = fromMap(m).map(gen.from)
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-03-15
      • 2020-06-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多