Search code examples
scalatypeshigher-kinded-typespartial-applicationkind-projector

Partially applied type lambda in Scala with kind projector


Consider the following type definition:

trait LiftF[F[_], G[_]] {
  def liftF[A](fa: F[A]): G[A]
}

When providing a requirement for an implicit of this type in context bounds (using kind projector plugin) we have to write it like this:

def func[A, G[_], F[_]: LiftF[?[_], G]](a: F[A]): G[A]

I would like to get rid of the ?[_] part, so my initial guess was to write a type To[G[_]] that returns LiftF[?[_], G] in order to transform the above function definition into

def func[A, G[_], F[_]: LiftF.To[G]](a: F[A]): G[A]

However, when writing type To definition as

type To[G[_]] = LiftF[?[_], G]

I get the following compilation error:

Error:(17, 20) type Λ$ takes type parameters
type To[G[_]] = LiftF[?[_], G]

Trying to rewrite it with existential types produces the following type definition:

type To[G[_]] = LiftF[F, G] forSome { type F[X] }

This compiles fine but, unsuprisingly, can't be applied to other type parameters, so desired function definition can't be achieved.

I managed to implement the "partial application" part with code inspired by the aux pattern:

trait To[G[_]] {
  type From[F[_]] = LiftF[F, G]
}

Sadly enough, this leaves me with syntax that is arguably worse than the original one:

def func[A, G[_], F[_]: LiftF.To[G]#From](a: F[A]): G[A]

My question is - can I achieve the originally proposed syntax in Scala with the help of kind projector or I should just stick with ?[_]?


Solution

  • As far as I understand it, the kind-projector can't really help you here:

    type To[G[_]] = LiftF[?[_], G]
    

    would simply be mechanically rewritten into something like

    type To[G[_]] = ({ type T[F[_]] = LiftF[F, G] })#T
    

    but it's invalid in 2.12.x, because it expects a plain type of kind * on the right hand side of the definition.

    If you move the parameter F to the left hand side, you end up with

    type To[G[_], F[_]] = LiftF[F, G]
    

    which you then have to use as To[G, ?[_]], which obviously doesn't buy you anything either, it simply swaps the order of arguments. Therefore I'd suggest to just use LiftF[?[_], G] and take solace from the fact that you don't have to write out ({ type L[F[_]] = LiftF[F, G] })#L explicitly.


    By the way, in Dotty, this works just fine:

    trait LiftF[F[_], G[_]] {
      def liftF[A](fa: F[A]): G[A]
    }
    
    type To[G[_]] = [F[_]] => LiftF[F, G]
    def f[A, G[_], F[_]: To[G]](a: F[A]): G[A] = implicitly[LiftF[F, G]].liftF(a)