1

I have the following traits and classes:

sealed trait Signal

sealed trait Description[T]

final case class S1(name: String) extends Signal

final case class D1(name: String) extends Description[S1]

What I try to achieve is that anyone who wants to add Signal will have (at compile time) to create a description.

I don't want to change the signature of Description but for sure not of Signal

I set my compiler to fail on warning, so I can leverage the fact that my ADT is sealed.

My idea was to have such a "compilation guard":

def compilationGuard[S <: Signal](s: S): Description[S] = s match { case S1(name) => D1(name) }

but I get the following error:

<console>:17: error: type mismatch;
 found   : D1
 required: Description[S]
       def compilationGuard[S <: Signal](s: S): Description[S] = s match { case S1(name) => D1(name) }
                                                                                              ^
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Noam Shaish
  • 1,613
  • 2
  • 16
  • 37

3 Answers3

3
def compilationGuard[S <: Signal](s: S): Description[S] = s match { case S1(name) => D1(name) }

can't compile for the same reason as

def returnItself[S <: Signal](s: S): S = s match { case S1(name) => S1(name) }

Reasons are explained here in details:

Why can't I return a concrete subtype of A if a generic subtype of A is declared as return parameter?

Type mismatch on abstract type used in pattern matching

If you don't want to mix Description logic to ADT or define instances of a type class like SignalMapper manually you can use Shapeless

import shapeless.ops.coproduct.Mapper
import shapeless.{:+:, CNil, Coproduct, Generic, Poly1}

def compilationGuard[C <: Coproduct]()(implicit
  gen: Generic.Aux[Signal, C],
  mapper: Mapper[uniqueDescriptionPoly.type, C]
) = null

object uniqueDescriptionPoly extends Poly1 {
  implicit def cse[S <: Signal, C1 <: Coproduct](implicit
    gen1: Generic.Aux[Description[S], C1],
    ev: C1 <:< (_ :+: CNil)
  ): Case.Aux[S, Null] = null
}

compilationGuard()

Testing:

final case class S1(name: String) extends Signal
final case class S2(name: String) extends Signal
final case class D1(name: String) extends Description[S1] 
// doesn't compile

final case class S1(name: String) extends Signal
final case class S2(name: String) extends Signal
final case class D1(name: String) extends Description[S1]
final case class D2(name: String) extends Description[S1]
// doesn't compile

final case class S1(name: String) extends Signal
final case class S2(name: String) extends Signal
final case class D1(name: String) extends Description[S1]
final case class D2(name: String) extends Description[S2]
// compiles
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • this seems not to compile: ``` could not find implicit value for parameter gen: shapeless.Generic.Aux[Signal,C] compilationGuard() ``` – Noam Shaish May 11 '20 at 11:30
  • @NoamShaish The following code compiles https://scastie.scala-lang.org/YDGlShJdTq6j76zIJO43bQ – Dmytro Mitin May 11 '20 at 12:19
  • Can you please help me understand why this does not compile: https://scastie.scala-lang.org/RwBjDWGnQJij3gcZLP0AuQ – Noam Shaish May 11 '20 at 13:02
  • 1
    @NoamShaish Trait `Description` must be sealed (as well as `Signal`). – Dmytro Mitin May 11 '20 at 13:18
  • One small side question how could I improve the implicit error message. I tried using ```@implicitNotFound("Cannot find Description of ${T}")``` on compilationGuard but this doesnt seems to work. Thanks again – Noam Shaish May 11 '20 at 13:42
  • 1
    @NoamShaish `@implicitNotFound` [can annotate](https://github.com/scala/scala/blob/2.13.x/src/library/scala/annotation/implicitNotFound.scala#L15-L55) a class or implicit parameter, not method. So either do `def compilationGuard[C <: Coproduct]()( implicit gen: Generic.Aux[Signal, C], @implicitNotFound("No unique Description for all children of Signal") mapper: Mapper[uniqueDescriptionPoly.type, C] ) = null` or create a type class and annotate it. – Dmytro Mitin May 11 '20 at 14:12
  • Once again thanks, it didnt work on the parameter but I solved it by declaring a type alias ```@implicitNotFound("No unique Description for all children of Signal") type MapperC[C <: Coproduct] = Mapper[uniqueDescriptionPoly.type, C]``` and updating compilationGuard accordingly – Noam Shaish May 11 '20 at 19:16
  • @NoamShaish Yeah, annotating type alias is also an option. Annotating implicit parameters works starting from 2.13.0 https://github.com/scala/scala/releases/tag/v2.13.0 "Support implicitNotFound on parameters (#6340)" – Dmytro Mitin May 11 '20 at 20:51
2

What I try to achieve is that anyone who wants to add Signal will have (at compile time to create a description.

The easy way to do so is to make it part of Signal:

sealed trait Signal[S <: Signal[S, D], D <: Description[S]] {
  // optionally
  def description: D
}

final case class S1(name: String) extends Signal[S1, D1] {
  def description = D1(name)
}

or

sealed trait Signal[S <: Signal[S]] {
  type Descr <: Description[S]
  // optionally
  def description: Descr
}

final case class S1(name: String) extends Signal[S1] {
  type Descr = D1
  def description = D1(name)
}

Of course it isn't far from the simpler

sealed trait Signal[S <: Signal] {
  def description: Description[S]
}

depending on your requirements.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • Thats mixing logic into my ADT which I would like to avoid so Signal is pure ADT that can be loaded from files with tools like pureconfig – Noam Shaish May 11 '20 at 08:00
2

Your program is failing, because it can't prove, that type S is S1.

Instead of doing pattern matching, you could introduce typeclass which would do a mapping on compile time:

trait SignalMapper[S] { //typeclass handling of mapping S to D
  type D <: Description[S]

  def map(signal: S): D
}

//instance of typeclass SignalMapper for S1
//if you'd put it in a companion object of S1, it would be always in scope
object S1 { 

  implicit val mapperS1: SignalMapper[S1] = new SignalMapper[S1] {
    type D = D1

    def map(signal: S1) = D1(signal.name)
  }

}

Then you can rewrite compilationGuard as:

def compilationGuard[S <: Signal](s: S)(implicit mapper: SignalMapper[S]): Description[S] = mapper.map(s)

Scastie

Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76