How to impose a subtype not to include the super type?
How to impose a subtype not to include the super type?
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:
ConcertReservation[A]
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,
assert
ConcertReservation
assert((userTicket.nonEmpty && spouseTicket.nonEmpty) || (userTicket.isEmpty && spouseTicket.isEmpty))
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.
– Carlos Teixeira
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
ConcertReservation
ConcertReservation
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 Answers
3
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]
:
=!=
A
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!
You can set up Ticket
as a trait
and then do some implicit type checking.
Ticket
trait
sealed trait Ticketval 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.
But
Reservation[Ticket, Ticket](sit1,stand1)
would still compile, wouldn't it?– Tzach Zohar
Sep 10 '18 at 18:28
Reservation[Ticket, Ticket](sit1,stand1)
@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
T1 =:!= Ticket
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:
seats: Option[(String, String)]
val valid = (userTicket.seat, spouseTicket.seat) match
case (Some(_), Some(_))
Thanks for contributing an answer to Stack Overflow!
But avoid …
To learn more, see our tips on writing great answers.
Required, but never shown
Required, but never shown
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
Maybe you should use
assert
? Write in the body ofConcertReservation
validation logicassert((userTicket.nonEmpty && spouseTicket.nonEmpty) || (userTicket.isEmpty && spouseTicket.isEmpty))
– Aleksey Isachenkov
Sep 10 '18 at 17:16