Implicit parameter precedence
Asked Answered
E

1

1

I was trying to convert generic type to HList:

trait ToHList[T] {
  type Out <: HList
  def apply(value: T): Out
}

trait LowPriorityToHList {
  implicit def default[T]: ToHList.Aux[T, T :: HNil] =
    new ToHList[T] {
      override type Out = T :: HNil

      override def apply(value: T): T :: HNil = value :: HNil
    }
}

object ToHList extends LowPriorityToHList {
  type Aux[T, Out0] = ToHList[T] { type Out = Out0 }

  def apply[T](implicit toHList: ToHList[T]): Aux[T, toHList.Out] = toHList

  implicit def toHList[T, Repr <: HList, N <: Nat](implicit
      gen: Generic.Aux[T, Repr],
      len: Length.Aux[Repr, N],
      lt: LT[Nat._0, N]): ToHList.Aux[T, Repr] =
    new ToHList[T] {
      override type Out = Repr

      override def apply(value: T): Repr = gen.to(value)
    }
}

object Main extends App {
  println(ToHList.apply[Int].apply(1)) // expected 1 :: HNil
  println(ToHList.apply[(Int, Int)].apply((1, 2))) // expected 1 :: 2 :: HNil
}

I intended to give priority to ToHList.toHList over ToHList.default but this code cause following compilation error:

[error] ToHList.scala:39:24: ambiguous implicit values:
[error]  both method toHList in object ToHList of type [T, Repr <: shapeless.HList, N <: shapeless.Nat](implicit gen: shapeless.Generic.Aux[T,Repr], implicit len: shapeless.ops.hlist.Length.Aux[Repr,N], implicit lt: shapeless.ops.nat.LT[shapeless.Nat._0,N])ToHList.Aux[T,Repr]
[error]  and method default in trait LowPriorityToHList of type [T]=> ToHList.Aux[T,T :: shapeless.HNil]
[error]  match expected type ToHList[(Int, Int)]
[error]   println(ToHList.apply[(Int, Int)].apply((1, 2))) // expected 1 :: 2 :: HNil

I want to give priority to ToHList.toHList over ToHList.default. How can I fix this error?

Eisegesis answered 19/6, 2021 at 12:51 Comment(0)
T
3

If both toHList and default are applicable then they have the same priority, so they make ambiguity. Indeed, although default is defined in a low-priority super-trait but it's more specific than toHList. See details in Why is this implicit ambiguity behaviour happening?

So there is no reason to put default into a low-priority super-trait, this will not make desired impact. But if you put toHList and default into the same object, default will win as more specific. And from expected 1 :: 2 :: HNil it seems you want vice versa toHList to win. You can use shapeless.LowPriority

object ToHList {
  type Aux[T, Out0] = ToHList[T] { type Out = Out0 }

  def apply[T](implicit toHList: ToHList[T]): Aux[T, toHList.Out] = toHList

  implicit def toHList[T, Repr <: HList, N <: Nat](implicit
    gen: Generic.Aux[T, Repr],
    len: Length.Aux[Repr, N],
    lt: LT[Nat._0, N]
  ): ToHList.Aux[T, Repr] =
    new ToHList[T] {
      override type Out = Repr
      override def apply(value: T): Repr = gen.to(value)
    }

  implicit def default[T](implicit 
    lowPriority: LowPriority
  ): ToHList.Aux[T, T :: HNil] =
    new ToHList[T] {
      override type Out = T :: HNil
      override def apply(value: T): T :: HNil = value :: HNil
    }
}

Alternatively in this specific case you can use shapeless.Refute, shapeless.OrElse

implicit def default[T](implicit
  orElse: OrElse[Refute[Generic[T]], Generic.Aux[T, HNil]]
): ToHList.Aux[T, T :: HNil] =
  new ToHList[T] {
    override type Out = T :: HNil
    override def apply(value: T): T :: HNil = value :: HNil
  }
Theodor answered 19/6, 2021 at 18:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.