Is there a way to extend type declarations?

165 views Asked by At

I'm trying to use shapeless' Coproduct for error type aggregation. The following is an attempt to isolate the problem I currently have:

import shapeless._

case object F1
case object F2
type F12 = F1.type :+: F2.type :+: CNil

case object F3
case object F4
type F34 = F3.type :+: F4.type :+: CNil

type F1234 = F1.type :+: F2.type :+: F3.type :+: F4.type :+: CNil

def custom(f: Either[F12, F34]): F1234 = // how can I declare the resulting type?
  f.fold(_.extendRightBy[F34], _.extendLeftBy[F12])

object F1234Handler extends Poly1 {
  implicit def caseF1 = at[F1.type](_ => "got F1")
  implicit def caseF2 = at[F2.type](_ => "got F2")
  implicit def caseF3 = at[F3.type](_ => "got F3")
  implicit def caseF4 = at[F4.type](_ => "got F4")
}

custom(Left(Coproduct[F12](F2))).fold(F1234Handler) // got F2

How can I declare the result type in the custom fold without having to repeat myself? Ideally I don't want to declare F1234 in the way I did, I want to declare it by just making a union of the two existing type declarations, F12 and F34. This way I don't need to update the F1234 declaration whenever I add another failure type to either of these declarations. I can declare type F1234 = F1.type :+: F2.type :+: F34 but I can't declare type F1234 = F12 :+: F34 due to the CNil tail of the F12, which gets dropped by the extendBy operations.

2

There are 2 answers

1
Travis Brown On BEST ANSWER

The situation isn't quite as bad as lmm's answer suggests, in part because Shapeless provides a ExtendBy type class that packages up ExtendLeftBy and ExtendRightBy. So if you really wanted a return type for custom that you didn't compute yourself and write out by hand, you could use ExtendBy:

import shapeless._, ops.coproduct.ExtendBy

case object F1
case object F2
type F12 = F1.type :+: F2.type :+: CNil

case object F3
case object F4
type F34 = F3.type :+: F4.type :+: CNil

def custom(f: Either[F12, F34])(implicit ext: ExtendBy[F12, F34]): ext.Out =
  f.fold(ext.right(_), ext.left(_))

Even if you did need to use ExtendLeftBy and ExtendRightBy directly, you could convince the compiler that they have the same output type a little more cleanly with Aux types and a single shared type parameter. So instead of this (a complete working version of lmm's code):

import ops.coproduct.{ ExtendLeftBy, ExtendRightBy }

def custom[ERO, ELO](f: Either[F12, F34])(implicit
  el: ExtendRightBy[F12, F34] { type Out = ELO },
  er: ExtendLeftBy[F12, F34] { type Out = ERO },
  w: ELO =:= ERO
): ERO = f.fold(l => w(el(l)), er(_))

You would just write this:

def custom[Out <: Coproduct](f: Either[F12, F34])(implicit
  extL: ExtendRightBy.Aux[F12, F34, Out],
  extR: ExtendLeftBy.Aux[F12, F34, Out]
): Out = f.fold(extL(_), extR(_))

In most cases if you know the input types statically, though, you'd just write out the return type yourself and skip the implicit parameter business altogether. The implicit evidence is only necessary when you're working with generic types, like this:

def custom[A <: Coproduct, B <: Coproduct](f: Either[A, B])(implicit
  ext: ExtendBy[A, B]
): ext.Out = f.fold(ext.right(_), ext.left(_))

This works for any two coproducts, not just F12 and F34.

3
lmm On

Scala dependent types are always a bit weird and cumbersome. Type-level functions are encoded as implicits with the results as type members, but as far as the scala compiler is concerned it's just a type member. So you have to do something like this:

def custom(f: Either[F12, F34])(implicit er: ExtendRight[F12, F34]): er.Out
  = f.fold(_.extendRightBy[F34], _.extendLeftBy[F12])

Unfortunately that won't work because the compiler can't tell that the output of the extendLeftBy[F12] is the same type. We know the two types are always going to be the same, but the compiler doesn't, so we have to require a witness (that will in fact always be present). Something like:

def custom(f: Either[F12, F34])(implicit er: ExtendRight[F12, F34],
  el: ExtendLeft[F34, F12])(implicit w: er.Out =:= el.Out): er.Out
  = f.fold(_.extendRightBy[F34], w(_.extendLeftBy[F12]))

Unfortunately even that doesn't work because our type parameters aren't allowed to depend on type parameters from the same list and we can only have one implicit list. So we have to "lift" these types to be type parameters instead:

def custom[ERO, ELO](f: Either[F12, F34])(
  implicit er: ExtendRight[F12, F34]{type Out = ERO},
  el: ExtendLeft[F34, F12]{type Out = ELO}, w: ELO =:= ERO): ELO
  = f.fold(_.extendRightBy[F34], w(_.extendLeftBy[F12]))

As I said, cumbersome, but it should work. (ExtendRight and ExtendLeft are the types used by the extendRightBy and extendLeftBy method - any similarly dependently typed function will probably have similar "helper" types).