【问题标题】:Avoiding boilerplate when using typeclass-based polymorphism使用基于类型类的多态性时避免样板
【发布时间】:2021-02-28 04:20:26
【问题描述】:

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

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)))
}

这实际上比仅使用隐式而不调用getThing1、getThing2 或combineThings 方法的独立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)))

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

感谢您的帮助。

【问题讨论】:

    标签: scala typeclass implicit shapeless boilerplate


    【解决方案1】:

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

    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)))
    

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

    How to wrap a method having implicits with another method in Scala?

    Pass implicit parameter through multiple objects

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

    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-types 变得越来越少,尽管有时它们仍然是必要的。我会从here复制我的cmets:

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

    不编译

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

    和

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

    做。

    【讨论】:

    • 谢谢-我实际上从您链接到的其他答案中找到了一个声明说得很好:“如果您多次重复同一组隐式参数,那么惯用的解决方案是引入您的类型类(或者只是单个隐式)而不是那组隐式并使用此类型类。”
    • 在我几天前的回答中,有人说 Aux 模式还可以帮助编译器推断类型,从而避免过度细化的隐含问题。我以前认为它们仅对减少样板文件有用。它们对类型推断也有帮助吗?
    • @Chrisper 好吧,这在特定情况下有所帮助。通常,Aux-types 旨在或多或少等同于类型细化。
    • @Chrisper 同样,路径相关的返回类型和带有附加类型参数的返回类型应该或多或少可以互换,尽管在旧版本的 Scala 中它们不是,现在也可能存在一些特定情况(我会尝试找到链接)。
    • @Chrisper I found。请参阅我的答案中的“第三个......”以及 MateuszKubuszok 的答案下 cmets 中的讨论。
    猜你喜欢
    • 1970-01-01
    • 2021-04-02
    • 1970-01-01
    • 1970-01-01
    • 2014-11-26
    • 1970-01-01
    • 1970-01-01
    • 2019-05-09
    • 1970-01-01
    相关资源
    最近更新 更多