使用基于类型类的多态性时避免样板

Avoiding boilerplate when using typeclass-based polymorphism

我发现我的代码经常看起来有点像这样:

trait Example { 
  def getThing1[A, O <: HList](a: A)(implicit g1: GetThing1[A] { type Out = O }): O = g1(a)
  def getThing2[A, O <: HList](a: A)(implicit g2: GetThing2[A] { type Out = O }): O = g2(a)
  def combineThings[T1 <: HList, T2 <: HList, O <: HList](t1: T1, t2: T2)(implicit 
    c: CombineThings[T1, T2] {type Out = O},
  ): O = c(t1, t2)
  def getCombinedReversed[A, T1 <: HList, T2 <: HList, C <: HList, O <: HList](a: A)(implicit 
    g1: GetThing1[A] {type Out = T1},
    g2: GetThing2[A] {type Out = T2},
    c: CombineThings[T1, T2] {type Out = C},
    r: Reverse[C] {type Out = O},
  ): O = r(combineThings(getThing1(a), getThing2(a)))
}

这实际上比仅使用隐式且不调用 getThing1getThing2combineThings 方法的独立 getCombinedReversed 方法更复杂:

 def getCombinedReversedStandAlone[A, T1 <: HList, T2 <: HList, C <: HList, O <: HList](a: A)(implicit 
    g1: GetThing1[A] {type Out = T1},
    g2: GetThing2[A] {type Out = T2},
    c: CombineThings[T1, T2] {type Out = C},
    r: Reverse[C] {type Out = O},
  ): O = r(c(g1(a), g2(a)))

我对此没有特别的问题,但它确实使我的代码膨胀了一点,所以我想我会检查一下是否没有明显的解决方案。显然调用 getThingcombineThings 方法而不断言正确的隐含在范围内是不可能的。

感谢您的帮助。

在方法的隐式参数中,您可以更喜欢 Aux 类型而不是类型细化(您可以 automize 使用 AUXify 的宏注释生成 Aux 类型)。同样在 return 类型的方法中,您可以更喜欢依赖于路径的类型而不是附加类型参数(待推断)。

def getThing1[A](a: A)(implicit g1: GetThing1[A]): g1.Out = g1(a)
def getThing2[A](a: A)(implicit g2: GetThing2[A]): g2.Out = g2(a)
def combineThings[T1 <: HList, T2 <: HList](t1: T1, t2: T2)(implicit
  c: CombineThings[T1, T2]
): c.Out = c(t1, t2)

def getCombinedReversed[A, T1 <: HList, T2 <: HList, C <: HList](a: A)(implicit
  g1: GetThing1.Aux[A, T1],
  g2: GetThing2.Aux[A, T2],
  c: CombineThings.Aux[T1, T2, C],
  r: Reverse[C]
): r.Out = r(combineThings(getThing1(a), getThing2(a)))

def getCombinedReversedStandAlone[A, T1 <: HList, T2 <: HList, C <: HList](a: A)(implicit
  g1: GetThing1.Aux[A, T1],
  g2: GetThing2.Aux[A, T2],
  c: CombineThings.Aux[T1, T2, C],
  r: Reverse[C]
): r.Out = r(c(g1(a), g2(a)))

此外,关于重复隐式参数的必要性,请阅读

一般来说,按照您描述的方式编写代码似乎很传统。隐式参数有助于理解方法的逻辑(这当然需要一些技巧)。如果您开始隐藏隐式参数,那么您的代码可能会开始看起来不那么传统:) 如果您多次重复同一组隐式参数,这是引入新类型的信号 class.

import com.github.dmytromitin.auxify.macros.{aux, instance}
import shapeless.DepFn1

@aux @instance
trait GetCombinedReversed[A] extends DepFn1[A] {
  type Out
  def apply(a: A): Out
}
object GetCombinedReversed {
  implicit def mkGetCombinedReversed[A, T1 <: HList, T2 <: HList, C <: HList](implicit
    g1: GetThing1.Aux[A, T1],
    g2: GetThing2.Aux[A, T2],
    c: CombineThings.Aux[T1, T2, C],
    r: Reverse[C]
  ): Aux[A, r.Out] = instance(a => r(c(g1(a), g2(a))))
}
  
def foo1[..., A, A1, ...](implicit ..., gcr: GetCombinedReversed.Aux[A, A1], ...) = 
  f(..., gcr(a), ...)
def foo2[..., A, A1, ...](implicit ..., gcr: GetCombinedReversed.Aux[A, A1], ...) = 
  g(..., gcr(a), ...)

在 Scala 3 中你可以这样写

def getCombinedReversed[A, T1 <: HList, T2 <: HList, C <: HList](a: A)(using
  g1: GetThing1[A],
  g2: GetThing2[A],
  c: CombineThings[g1.Out, g2.Out],
  r: Reverse[c.Out]
): r.Out = ???

因此类型改进或 Aux-类型变得越来越必要,尽管有时它们仍然是必要的。我将从 :

复制我的评论
def foo(using tc1: TC1[tc2.Out], tc2: TC2[tc1.Out]) = ???

doesn't compile while

def bar[A, B](using tc1: TC1.Aux[A, B], tc2: TC2.Aux[B, A]) = ???  

and

def baz[A](using tc1: TC1[A], tc2: TC2.Aux[tc1.Out, A]) = ??? 

do.