Chris J Harris
Chris J Harris

Reputation: 1841

Avoiding boilerplate when using typeclass-based polymorphism

I'm finding that my code frequently looks a little like this:

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

This is actually more complex than a stand-alone getCombinedReversed method that uses implicits only and does not call the getThing1, getThing2 or combineThings methods:

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

I have no particular problem with this, but it does bloat out my code a bit, so I thought I'd check that there's no obvious solution. Obviously calling the getThing and combineThings methods without asserting that the correct implicit is in scope isn't possible.

Thanks for any assistance.

Upvotes: 0

Views: 81

Answers (1)

Dmytro Mitin
Dmytro Mitin

Reputation: 51648

In implicit parameters of a method you can prefer Aux-types rather than type refinements (you can automize generating Aux types with a macro annotation from AUXify). Also in return type of a method you can prefer path-dependent type rather than additional type parameter (to be inferred).

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

Besides that, regarding necessity to repeat implicit parameters please read

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

Pass implicit parameter through multiple objects

Generally speaking, writing your code like you described seems conventional. Implicit parameters help to understand the logic what method does (this surely demands some skill). If you start to hide implicits then your code can start to look less conventional :) If you repeat the same set of implicit parameters many times this is a signal to introduce a new type 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), ...)

In Scala 3 you can write

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 = ???

so type refinements or Aux-types become necessary rarer although sometimes they are still necessary. I'll copy my comments from here:

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.

Upvotes: 4

Related Questions