2

This is partly duplicate of this question: Getting subclasses of a sealed trait, but answer suggests runtime-reflection which is inappropriate for me and I would like to know if it is possible on compilation-time, probably using Shapeless.

So, having this ADT:

sealed trait ColumnAttribute
case class Default(value: String) extends ColumnAttribute
case class Identity(seed: Int, step: Int) extends ColumnAttribute
case class Encode(encoding: CompressionEncoding) extends ColumnAttribute
case object DistKey extends ColumnAttribute

How can I get something like Option[Default] :: Option[Identity] :: Option[Encode] :: Option[DistKey] :: HNil?

More specific question (probably I'm looking for wrong solution). Having above AST plus following class, how can I be sure in compile-time that Column will not get constructed with more than one Encode or DistKey or other ColumnAttribute.

case class Column(columnName: String, dataType: DataType, columnAttributes: Set[ColumnAttribute], columnConstraints: Set[ColumnConstraint])

UPD: columnAttributes should contain only one value of particular subtype, but can contain several values of distinct subtypes.

So, this pseudo-code should be correct:

columnConstraint = Default("foo") :: DistKey :: Identity(1,2) :: HNil

But this should fail:

columnConstraint = Default("foo") :: Default("bar") :: HNil

Community
  • 1
  • 1
chuwy
  • 6,310
  • 4
  • 20
  • 29
  • May be missing the point here but add a custom apply method for Column? Validating on construction? – Rhys Bradbury Jun 21 '16 at 18:06
  • Well yeah, I already thought about it. Because there's too many ways to construct invalid value (like negative `step`) even not taking in account these sets, so overall it probably doesn't worth it. But question is still valid just in sake of curiosity. – chuwy Jun 21 '16 at 18:16
  • 3
    I am not sure what you are trying, but since you mention shapeless and even provide an example: that HList of Options is not really a fair representation of the ADT. HLists are products, you need a coproduct: https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0#coproducts-and-discriminated-unions – pedrofurla Jun 21 '16 at 18:35
  • Hey @pedrofurla! For me it still looks more like a product. Because coproduct assumes we have a value which is `DistKey` *or* `Encode` *or* another `ColumnAttribute`, but what I need is (probably empty) product of optional `DistKey` *and* optional `Encode` etc – chuwy Jun 22 '16 at 05:32

1 Answers1

4

If we represent columnAttributes in Column as an HList, we first need to constrain the HList elements to be subtypes of ColumnAttribute and be distinct.

We can use the hlist constraints LUBConstraint and IsDistinctConstraint to achieve this.

import shapeless.{Default => _, _}
import LUBConstraint._
import IsDistinctConstraint._

def acceptOnlyDistinctAttributes
  [L <: HList : <<:[ColumnAttribute]#λ : IsDistinctConstraint](l: L): L = l

Now we need to constraint the HList so it can not contain both Encode and DistKey, unfortunately we need to write this type class ourselves.

We can use =:= to check the first element and the NotContainsConstraint to check if the tail doesn't contain the other type.

trait OnlyOneOfConstraint[L <: HList, A, B] extends Serializable

object OnlyOneOfConstraint {

  def apply[L <: HList, A, B]
    (implicit ooo: OnlyOneOfConstraint[L, A, B]): OnlyOneOfConstraint[L, A, B] = ooo

  type OnlyOneOf[A, B] = {
    type λ[L <: HList] = OnlyOneOfConstraint[L, A, B]
  }

  implicit def hnilOnlyOneOf[A, B] = new OnlyOneOfConstraint[HNil, A, B] {}

  // head is A, so tail cannot contain B
  implicit def hlistOnlyOneOfA[H, T <: HList, A, B](implicit
    ncB: T NotContainsConstraint B, 
    eq: A =:= H,
    oooT: OnlyOneOfConstraint[T, A, B]
  ) = new OnlyOneOfConstraint[H :: T, A, B] {}

  // head is B, so tail cannot contain A
  implicit def hlistOnlyOneOfB[H, T <: HList, A, B](implicit
    ncA: T NotContainsConstraint A, 
    eq: B =:= H,
    oooT: OnlyOneOfConstraint[T, A, B]
  ) = new OnlyOneOfConstraint[H :: T, A, B] {}

  // head is not A or B
  implicit def hlistOnlyOneOf[H, T <: HList, A, B](implicit
    neqA: A =:!= H,
    neqB: B =:!= H,
    oooT: OnlyOneOfConstraint[T, A, B]
  ) = new OnlyOneOfConstraint[H :: T, A, B] {}
}

Now we can write (a simplified) Column using these constraints :

type CompressionEncoding = String

sealed trait ColumnAttribute
case class Default(value: String) extends ColumnAttribute
case class Identity(seed: Int, step: Int) extends ColumnAttribute
case class Encode(encoding: CompressionEncoding) extends ColumnAttribute
case object DistKey extends ColumnAttribute

import OnlyOneOfConstraint._

case class Column[
  Attrs <: HList 
    : <<:[ColumnAttribute]#λ 
    : IsDistinctConstraint 
    : OnlyOneOf[Encode, DistKey.type]#λ
](columnAttributes: Attrs)

We now have a compile time guarantee that the attributes are distinct ColumnAttributes and will not contain not both a Encode and a DistKey :

Column(DistKey :: Default("s") :: HNil)
// Column[shapeless.::[DistKey.type,shapeless.::[Default,shapeless.HNil]]] = Column(DistKey :: Default(s) :: HNil)

Column(Default("s") :: Encode("a") :: HNil)
// Column[shapeless.::[Default,shapeless.::[Encode,shapeless.HNil]]] = Column(Default(s) :: Encode(a) :: HNil)

Column(DistKey :: Default("s") :: Encode("a") :: HNil)
// <console>:93: error: could not find implicit value for evidence parameter of type OnlyOneOfConstraint[shapeless.::[DistKey.type,shapeless.::[Default,shapeless.::[Encode,shapeless.HNil]]],Encode,DistKey.type]
//        Column(DistKey :: Default("s") :: Encode("a") :: HNil)
Peter Neyens
  • 9,770
  • 27
  • 33
  • Thank you @PeterNeyens. This partly answers my question. Partly because seems I formulated it not clearly enough (updated topic). But it satisfies my curiosity and looks like awesome scrap of Shapeless. – chuwy Jun 22 '16 at 06:27
  • 1
    With your updated question, it seems you don't need the `OnlyOneOf` constraint and just need the `LUB` and `IsDistinct`. – Peter Neyens Jun 22 '16 at 07:45