0

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.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Chris J Harris
  • 1,597
  • 2
  • 14
  • 26

1 Answers1

4

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.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 1
    thanks - I actually found a statement from your other answer you linked to said it well: "If you repeat the same set of implicit parameters many times then idiomatic solution is to introduce your type class (or just single implicit) instead of that set of implicits and use this type class." – Chris J Harris Nov 16 '20 at 23:39
  • On my answer from a couple of days ago somebody said that the Aux pattern also helps the compiler infer types and so avoids the over-refined implicit problem. I had previously thought they were useful only to reduce boilerplate. Are they also helpful for type inference? – Chris J Harris Nov 16 '20 at 23:41
  • @Chrisper Well, this helped in that specific case. Generally, `Aux`-types are intended to be more or less equivalent to type refinements. – Dmytro Mitin Nov 16 '20 at 23:44
  • @Chrisper Similarly, path-dependent return types and return types with additional type parameter should be more or less interchangeable although in older versions of Scala they were not and there can be some specific situations now as well (I'll try to find the link). – Dmytro Mitin Nov 16 '20 at 23:47
  • 1
    @Chrisper I [found](https://stackoverflow.com/questions/61044126/how-to-create-an-instances-for-typeclass-with-dependent-type-using-shapeless). See "And thirdly..." in my answer there and the discussion in comments under **MateuszKubuszok**'s answer. – Dmytro Mitin Nov 17 '20 at 00:00
  • @Chrisper For future readers I'll put here the link to your question you've mentioned where as you said "the Aux pattern also helps the compiler infer types and so avoids the over-refined implicit problem" https://stackoverflow.com/questions/64814539/why-is-this-implicit-resolution-failing – Dmytro Mitin Nov 17 '20 at 00:17
  • thank you. If the Aux pattern reduces boilerplate and may sometimes help with type inference then it seems to be worth making a habit of. – Chris J Harris Nov 17 '20 at 00:18