Sasha
Sasha

Reputation: 105

Pattern matching for case classes derived from same trait

everyone!

I have a little problem with Scala pattern matching. I am using Korolev framework for my web application, but I bet problem is deeper in Scala as is.

I have a base trait for all states:

trait BaseState {
  val notifications: List[Notification]
}

And two derived case classes:

case class GuestState(
                       notifications: List[Notification] = List(),
                       isRegistration: Boolean = false
                     ) extends BaseState

and

case class AuthenticatedState(
                          notifications: List[Notification] = List(),
                          user: UserWithPermissions
                        ) extends BaseState

In one of event handlers I need to get similar state without certain notification. For now it works like that:

event('click) { access =>
              access.transition {
                case s: GuestState => s.copy(notifications = notifications.filter(x => x != n))
                case s: AuthenticatedState => s.copy(notifications = notifications.filter(x => x != n))
              }
          }

For both types I have to do absolutely same thing with duplicating of code because BaseState does not have copy() method and compiler fails with error.

How can I do it properly in scala-way? Thank you.

Upvotes: 2

Views: 334

Answers (3)

Nazarii Bardiuk
Nazarii Bardiuk

Reputation: 4342

It is not solvable without changing base trait. Compiler cannot allow you to perform operation copy on trait when it clearly does not have it.

One approach is to specify mapping/filtering function on BaseState as other answers suggest.

Another approach is to replace inheritance with composition:

case class Notified(state: BaseState, notifications: List[Notification] = List())

sealed trait BaseState
case class GuestState(isRegistration: Boolean = false) extends BaseState 
case class AuthenticatedState(user: UserWithPermissions) extends BaseState

val notified = Notified(GuestState())
val result = notified.copy(notifications = notifications.filter(_ => true))

Upvotes: 2

Jens Halm
Jens Halm

Reputation: 499

I had similar scenarios in the past and never found a really elegant solution to this, as copy methods with identical or similar signatures cannot be easily expressed in a polymorphic way.

I usually added some plumbing to the trait and case classes so that the repetition is at least in one place:

trait BaseState {
  type ThisType <: BaseState
  val notifications: List[Notification]
  def withNotifications(notifications: List[Notification]): ThisType
}

case class GuestState(notifications: List[Notification] = List(),
                    isRegistration: Boolean = false) extends BaseState {
  type ThisType = GuestState
  def withNotifications(notifications: List[Notification]): ThisType = 
    copy(notifications = notifications)
}

case class AuthenticatedState(notifications: List[Notification] = List(),
                            user: UserWithPermissions) extends BaseState {
  type ThisType = AuthenticatedState
  def withNotifications(notifications: List[Notification]): ThisType = 
    copy(notifications = notifications)
}

This is quite far from ideal though, and maybe there are better patterns. But if you use logic like in your example a lot, it would at least reduce the boilerplate significantly. If the filter logic is also repeatedly used, you could add a more concrete filterNotififications to the base trait.

The advantage of this solution compared to the other answer is that you do not lose type precision when filtering.

And remember to seal the trait wherever possible.

Upvotes: 2

Tim
Tim

Reputation: 27421

The two cases do not actually do the same thing, because the two copy methods have different signatures. So while it may be textually the same, the code is actually doing something different in each case.

One option here is to add an abstract filterNotifications method to the trait and call that:

trait BaseState {
  val notifications: List[Notification]
  def filterNotifications(f: Notification => Boolean): BaseState
}

case class GuestState(
  notifications: List[Notification] = List(),
  isRegistration: Boolean = false
) extends BaseState {
  def filterNotifications(f: Notification => Boolean): BaseState =
    this.copy(notifications=notifications.filter(f))
}

case class AuthenticatedState(
  notifications: List[Notification] = List(),
  user: UserWithPermissions
) extends BaseState {
  def filterNotifications(f: Notification => Boolean): BaseState =
    this.copy(notifications=notifications.filter(f))
}

Upvotes: 2

Related Questions