typelevel / shapeless-3

Generic programming for Scala
189 stars 22 forks source link

Support Mirror.Sum for union types #21

Open bilal-fazlani opened 3 years ago

bilal-fazlani commented 3 years ago
type Color = "Brown" | "White" | "Yellow" | "Black"
given Encoder[Color] = Encoder.derived
inline def derived[T](using gen: K0.Generic[T]): Encoder[T] =
  gen.derive(product, coproduct)
[error] 19 |  given Encoder[Color] = Encoder.derived
[error]    |                                        ^
[error]    |     no implicit argument of type shapeless3.deriving.K0.Generic[
[error]    |       (("Brown" : String) | ("White" : String) | ("Yellow" : String) |
[error]    |         ("Black" : String)
[error]    |       )
[error]    |     ] was found for parameter gen of method derived in object Encoder
milessabin commented 3 years ago

Thanks for the report ... shapeless 3 doesn't currently support union types.

In principle support could be added. We would have to support the creation of CoproductGeneric instances for union types. There are no Mirrors for these so this would most likely need macro-support of some form.

bilal-fazlani commented 3 years ago

Thanks! Sorry if this was obvious. I have just started learning shapeless.

iRevive commented 3 years ago

I've been experimenting with Union types recently.

Perhaps some samples can be reused in shapeless-3:

Actually, it would be nice to have this logic as a part of the Scala compiler (perhaps as a part of the Mirror).

milessabin commented 3 years ago

@iRevive nice! Do you think that can be generalized for kinds other than *?

Bear in mind that Mirror is basically just a type class, albeit one with compiler support for generating a specific sort of instance. There's nothing to stop an external macro from generating instances for other types.

If you were interested in exploring that space I'd be super keen to see that in shapeless 3.

iRevive commented 3 years ago

@milessabin thank you for the feedback.

I experimented a bit with the Mirror. The example is available here https://scastie.scala-lang.org/k3VLtxUTTie18YodrQKtJw.

Mirror.SumOf

In the example below the compiler loses type information of the MirroredElemTypes type. It's resolved only as Tuple, instead of a proper type.

given unionMirror: Mirror.SumOf[Int | String | Long] = new Mirror.Sum {
  type MirroredType = Int | String | Long
  type MirroredMonoType = Int | String | Long
  type MirroredLabel = "Int | String | Long"
  type MirroredElemTypes = Int *: String *: Long *: EmptyTuple
  type MirroredElemLabels = Tuple3["Int", "String", "Long"]

  def ordinal(x: MirroredMonoType): Int = x match {
    case _: Int => 0
    case _: String => 1
    case _: Long => 2
  }
}

summon[Show[Int | String | Long]].show("string-value") // does not compile
// Error:
// cannot reduce inline match with
//  scrutinee:  scala.compiletime.erasedValue[Playground.unionMirror.MirroredElemTypes] : Playground.unionMirror.MirroredElemTypes
// patterns :  case _:EmptyTuple
//             case _:*:[t @ _, ts @ _]

This is how the compiler resolves the MirroredElemTypes (selType variable) at this breakpoint: image

Alias for Mirror.Sum with full type info

When Mirror.SumOf is replaced with a new type alias (with explicit underlying types), the derived[A](using m: Mirror.Of[A]) method picks up the given value and the code compiles.

type MirrorUnion[TPE, MET, MEL] = Mirror.Sum {
  type MirroredType = TPE
  type MirroredMonoType = TPE
  type MirroredElemTypes = MET
  type MirroredElemLabels = MEL
}

given unionMirror: MirrorUnion[Int | String | Long, Int *: String *: Long *: EmptyTuple, ("Int", "String", "Long")] = 
  new Mirror.Sum {
    //body the same as above
  }

summon[Show[Int | String | Long]].show("string-value") // compiles

Next steps

I assume it's expected behavior, since Mirror.SumOf clearly says that MirroredElemTypes is only a subtype of Tuple, and the real type is not known at this stage.

type SumOf[T] = Mirror.Sum { type MirroredType = T; type MirroredMonoType = T; type MirroredElemTypes <: Tuple }

Currently, I do not understand how the macro can be defined, since the result type should be known. But both MirroredElemTypes and MirroredElemLabels can be decided only during compilation.

I tried the following trick, but the compiler does not like it:

object UnionMirror {

  // does not compile with the following error:
  // access to parameter u from wrong staging level:
  // - the definition is at level 0,
  // - but the access is at level -1.
  inline given derived[A](using u: UnionInfo[A]): u.Mirror = ${ deriveImpl[A] }

  def deriveImpl[A: Type](using quotes: Quotes, u: UnionInfo[A]): Expr[u.Mirror] = {  
    ???
  }

  trait UnionInfo[A] {
    type MirroredElemTypes
    type MirroredElemLabels

    type Mirror = Mirror.Sum {
      type MirroredType = A
      type MirroredMonoType = A
      type MirroredElemTypes = self.MirroredElemTypes
      type MirroredElemLabels = self.MirroredElemLabels
    }
  }

  object UnionInfo {
    inline given derived[A]: UnionInfo[A] = ...
  }
}
joroKr21 commented 3 years ago

The problem with your manual given is that you fixed the type without the refinement. You could try given Mirror.Sum with instead. As for the macro - I think transparent inline should work because it preserves the narrow type.