1

Practical problem:
Let's imagine the client of a spectacle-house makes a reservation for a concert.
Some tickets for the concert have a seat.
The client brings a spouse. Restriction:
1. Either both the ticket of the client and the corresponding spouse's ticket are seated OR both are not seated.
How do I impose this restriction at the typelevel?

What I initially thought:

case class Ticket[S <: Option[String]](id: String, seat: S)

case class ConcertReservation[A <: Option[String]](userTicket: Ticket[A],
                                                     spouseTicket: Ticket[A])

val concertReservation =
  ConcertReservation(
      userTicket = Ticket(id = "id1", seat = Some("<seatId>")),
      spouseTicket = Ticket(id = "id2", seat = None)
    )

With this I wanted to impose, via the type parameter A on ConcertReservation[A], that userTicket and spouseTicket must be of the same type.
Doing this allows the compiler to catch the above violation of the restriction:

Error:(12, 26) type mismatch;
 found   : .....Temp.Ticket[Some[String]]
 required: .....Ticket[Option[String]]
Note: Some[String] <: Option[String], but class Ticket is invariant in type S.
You may wish to define S as +S instead. (SLS 4.5)
      userTicket = Ticket(id = "id1", seat = Some("assad")),

But it is possible to overcome this. For example with the code below (which compiles):

  val concertReservation2: ConcertReservation[Option[String]] =
    ConcertReservation(
      userTicket = Ticket(id = "id1", seat = Some("assad")),
      spouseTicket = Ticket(id = "id2", seat = None)
    )

Is there a idiomatic way to achieve what I want? Some kind of "pattern" perhaps?
Thanks,

cmhteixeira
  • 877
  • 11
  • 22
  • Maybe you should use `assert`? Write in the body of `ConcertReservation` validation logic `assert((userTicket.nonEmpty && spouseTicket.nonEmpty) || (userTicket.isEmpty && spouseTicket.isEmpty))` – Aleksey Isachenkov Sep 10 '18 at 17:16
  • That would work in practice. Thanks for the trouble of answering. I am however looking for the type-level solution. The one that can be verified at compile time. – cmhteixeira Sep 10 '18 at 17:21
  • Also, you can create two subtypes of `ConcertReservation` with Some+Some and None+None reservations and make `ConcertReservation` abstract and sealed (like Option with its subclasses Some and None). – Aleksey Isachenkov Sep 10 '18 at 17:25
  • if you want a type restriction change ConcertReservation type parameter to [A <: Some[String]], the problem threre is that you cant assign a set with Option("id"), onyl Some("id") – Sebastian Celestino Sep 10 '18 at 17:46

3 Answers3

3

You can set up Ticket as a trait and then do some implicit type checking.

sealed trait Ticket{val id: String}
case class SeatedTicket(override val id: String, seat: String) extends Ticket
case class StandingTicket(override val id: String) extends Ticket

Next, you can take the type of the two parameters separately, and include an implicit check that they are equal as a parameter. You can also add a type inequality check to make sure the type isn't Ticket, but that will require you to include a library like shapeless, or do some more mucking around with the type system.

case class Reservation[T1 <: Ticket, T2 <: Ticket](user: T1, spouse: T2)(implicit ev: T1 =:= T2, ev2: T1 =:!= Ticket)

When T1 and T2 match, it works fine, but when they are different, the type system can pick up the error.

val sit1 = SeatedTicket("1","1A")
val sit2 = SeatedTicket("2","1B")
val stand1 = StandingTicket("3")
val stand2 = StandingTicket("4")
Reservation(sit1, sit2) //Runs fine
Reservation(stand1, stand2) //Runs fine
Reservation(sit1,stand1) //error: Cannot prove that SeatedTicket =:= StandingTicket.
Ethan
  • 821
  • 4
  • 12
  • But `Reservation[Ticket, Ticket](sit1,stand1)` would still compile, wouldn't it? – Tzach Zohar Sep 10 '18 at 18:28
  • @TzachZohar Oops, forgot to test that, you're right. I think it's fixable with a `T1 =:!= Ticket` check with shapeless, but don't currently have the time to verify. When I get a chance I'll verify then fix/delete this. – Ethan Sep 10 '18 at 19:12
2

If you copy the definition of =!= ("non-equal types") from this answer, you can then use it to make sure that A isn't Option[String]:

case class ConcertReservation[A <: Option[String]](userTicket: Ticket[A], spouseTicket: Ticket[A])
                                                  (implicit ev: A =!= Option[String])

This results in the expected behavior:

val seated1 = Ticket(id = "id1", seat = Some("1"))
val seated2 = Ticket(id = "id2", seat = Some("2"))
val unseated1 = Ticket(id = "id3", seat = None)
val unseated2 = Ticket(id = "id4", seat = None)

ConcertReservation(seated1, seated2)     // compiles
ConcertReservation(unseated1, unseated2) // compiles
ConcertReservation(seated1, unseated1)   // does not compile
ConcertReservation[Option[String]](seated1, unseated1) // does not compile either!
Tzach Zohar
  • 37,442
  • 3
  • 79
  • 85
0

As per my understanding of the problem, whether the seats are allocated or not is known only at the runtime, you cannot have a compile-time check for this.

But if you really want to achieve restriction for both or none, you should use: seats: Option[(String, String)] or if you want to check on runtime then you can do some pattern matching over both the seats:

val valid = (userTicket.seat, spouseTicket.seat) match {
  case (Some(_), Some(_)) | (None | None) => true
  case _ => false
}
lprakashv
  • 1,121
  • 10
  • 19