scalashapelessimplicits

Test two scala shapeless HList types for equivalence via implicit


I'm interested in testing whether two HList heterogeneous records are "equivalent"; that is, they have the same key/val pairs, but not necessarily in the same order. Is there a predefined type predicate that does what EquivHLists does in the code fragment below?

// shapeless heterogeneous records with "equivalent" types.
// these should compile if given as the arguments to 'f' below.
val hrec1 = ("a" ->> 1) :: ("b" ->> 2) :: HNil
val hrec2 = ("b" ->> 2) :: ("a" ->> 1) :: HNil

// only compiles if two HList records contain same information
def f(hr1: H1 <: HList, hr2 : H2 <: HList)(implicit equiv: EquivHLists[H1, H2]) = {
  // biz logic
}

Solution

  • I believe the Align[M,L] typeclass supports what you want, it lets you rearrange the elements of one hlist to match the order of another with the same types.

    Here's a function that I think does what you want. It will tell you if two equivalent hlists have the same values for each type. If the two lists don't have the same types, it won't compile.

    import shapeless._
    import ops.hlist._
    
    def equiv[H <: HList, L <: HList]
      (h : H, l : L)(implicit align: Align[H, L]): Boolean = align(h) == l
    
    scala> equiv(3 :: "hello" :: HNil, "hello" :: 3 :: HNil)
    res11: Boolean = true
    
    scala> equiv(4 :: "hello" :: HNil, "hello" :: 3 :: HNil)
    res12: Boolean = false
    
    scala> equiv(4 :: "hello" :: HNil, "hello" :: 3.0 :: HNil)
    <console>:19: error: could not find implicit value for parameter align: shapeless.ops.hlist.Align[Int :: String :: shapeless.HNil,String :: Double :: shapeless.HNil]
    

    edit : after some further experimentation, this will give false negatives if the hlists have more than one value of the same type:

    scala> equiv(3 :: "hello" :: 4 :: HNil, 4 :: "hello" :: 3 :: HNil)
    res14: Boolean = false
    

    This is because of the way Align works: it basically just iterates over one hlist and pulls out the first element of the other with the same type. But if you're using singleton-typed literals then this shouldn't be an issue.

    So this does work with the above records at least in regards to the keys:

    scala> equiv(hrec1, hrec2)
    res16: Boolean = true
    
    //change one of the keys
    scala> val hrec3 = ("c" ->> 2) :: ("a" ->> 1) :: HNil
    hrec3: Int with shapeless.labelled.KeyTag[String("c"),Int] :: Int with shapeless.labelled.KeyTag[String("a"),Int] :: shapeless.HNil = 2 :: 1 :: HNil
    
    scala> equiv(hrec1, hrec3)
    <console>:27: error: could not find implicit value for parameter align ...
    
    //change one of the values, it compiles but returns false
    scala> val hrec4 = ("b" ->> 2) :: ("a" ->> 3) :: HNil
    hrec4: Int with shapeless.labelled.KeyTag[String("b"),Int] :: Int with shapeless.labelled.KeyTag[String("a"),Int] :: shapeless.HNil = 2 :: 3 :: HNil
    
    scala> equiv(hrec1, hrec4)
    res18: Boolean = false