简体   繁体   中英

Type-level filtering using shapeless

Does anybody know how to make this test work using Shapeless.

package net.jtownson.swakka.jsonschema

import org.scalatest.FlatSpec
import org.scalatest.Matchers._

class OptionalFieldSpec extends FlatSpec {

  case class A(i: Int, j: Option[Int])

  "an extractor of some kind" should "get the (non)optional fields from a case class" in {

    extractNonOptionalFieldNames[A] shouldBe List("i")

    extractOptionalFieldNames[A] shouldBe List("j")

  }

  def extractNonOptionalFieldNames[T <: Product](/* implicit typeclass instances? */): List[String] = ???

  def extractOptionalFieldNames[T <: Product]: List[String] = ???

}

I have no runtime instance of A or its Generic equivalent as I am working at creating a JsonSchema for case class A, which is independent of any given instance. The schema has a required field, which is a list of the non-optional fields. eg

{
  "type" -> "object",
  "required" -> ["i"],
  "properties" -> {
    "i" -> {
      "type" -> "integer",
      "format" -> "int32"
     }
   }
}

Something like this:

trait FieldNameExtractor[T] extends Serializable {
  import shapeless.ops.hlist.{RightFolder, ToTraversable}
  import shapeless.ops.record.Keys
  import shapeless.{HList, HNil, LabelledGeneric, Poly2}

  /**
    * Extracts filtered field names for type [[T]],
    * given a polymorphic function that acts as the type filter
    */
  def extract[L <: HList, R <: HList, O <: HList](op: Poly2)(
      implicit lgen: LabelledGeneric.Aux[T, L],
      folder: RightFolder.Aux[L, HNil.type, op.type, R],
      keys: Keys.Aux[R, O],
      traversable: ToTraversable.Aux[O, List, Symbol]
  ): List[String] = {
    val result = keys().to[List]
    result.map(_.name)
  }
}

object FieldNameExtractor {
  def apply[T] = new FieldNameExtractor[T] {}
}

Usage:

import org.scalatest.FlatSpec
import org.scalatest.Matchers._

class Test extends FlatSpec {
  /* type filters */
  import shapeless.{HList, Poly2}
  import shapeless.labelled.KeyTag, shapeless.tag.Tagged

  type FilterO[A, T] = Option[A] with KeyTag[Symbol with Tagged[T], Option[A]]

  trait Ignore extends Poly2 {
    implicit def default[A, L <: HList] = at[A, L]((_, l) => l)
  }
  trait Accept extends Poly2 {
    implicit def default[A, L <: HList] = at[A, L](_ :: _)
  }

  object allOptions extends Ignore {
    implicit def option[A, T, L <: HList] = at[FilterO[A, T], L](_ :: _)
  }
  object noOptions extends Accept {
    implicit def option[A, T, L <: HList] = at[FilterO[A, T], L]((_, l) => l)
  }

  "an extractor of some kind" should "get the (non)optional fields from a case class" in {
    case class A(i: Int, j: Option[Int], k: String)

    val fne = FieldNameExtractor[A]
    fne.extract(noOptions) shouldBe List("i", "k") // extractNonOptionalFieldNames
    fne.extract(allOptions) shouldBe List("j")     // extractOptionalFieldNames
  }
}

Here is one way using type classes:

import shapeless._
import shapeless.labelled.FieldType


trait OptionExtractor[A] {
  type B <: HList
}

trait LowPriorityOptionExtractor {

  implicit def hconsExtractor[K, V, T <: HList](implicit
                                                       extractor: OptionExtractor[T]):
  OptionExtractor.Aux[FieldType[K, V] :: T, extractor.B] = new OptionExtractor[FieldType[K, V] :: T] {
    type B = extractor.B
  }

}

object OptionExtractor extends LowPriorityOptionExtractor {

  type Aux[A, B0 <: HList] = OptionExtractor[A] {type B = B0}

  def apply[A](implicit extractor: OptionExtractor[A]): OptionExtractor.Aux[A, extractor.B] = extractor

  implicit val hnilOptionExtractor: OptionExtractor.Aux[HNil, HNil] = new OptionExtractor[HNil] {
    type B = HNil
  }

  implicit def hconsOptionExtractor[K, V, T <: HList](implicit extractor: OptionExtractor[T]):
  OptionExtractor.Aux[FieldType[K, Option[V]] :: T, K :: extractor.B] = new OptionExtractor[FieldType[K, Option[V]] :: T] {
    type B = K :: extractor.B
  }

}

There are a few things that might need explanation:

  • Since you mentioned that you have no runtime instance of A . What is the type level representation that you'd like to get back? In this solution I just returned an HList of witnesses for the optional ones. I don't think a List[String] representation is sufficient, as filtering out non optional values would have the same type as doing nothing.
  • There is a priority for the type classes, so filtering options would be the same with reversed priority.

It can be used like this:

case class A(i: Int, j: Option[Int], k: Option[Long])
val x = LabelledGeneric[A]
type filteredType = OptionExtractor[x.Repr] 
//type B = Symbol with shapeless.tag.Tagged[String("j")] :: Symbol with shapeless.tag.Tagged[String("k")] :: shapeless.HNil

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM