3

I've implemented a simple event service that allows for the publishing of read-only events (Event), and cancellable events (PreEvent). Cancellable events are reduced by all handlers and the result is returned to the caller.

The Event interaction works as expected, but the PreEvent type bound of T <: PreEvent is giving me some issues:

  • (1) When matching a PreEvent its copy has to be explicitly cast to T for the compiler to realise it is the same type as the method parameter.
  • (2) When attempting to pipe a PreEvent to a method reference the compiler suddenly forgets it is dealing with a PreEvent and attempts to call the Event variant of publish.

Aside from renaming the EventService::publish(PreEvent) method to avoid the disambiguation, are there any changes I could make to the type bound of Handler::reduce[T <: PreEvent](event: T): T to help Scala realise (event: T) will always be a PreEvent when the method is passed as a method reference? (and thus has no type information available, eventhough I would have expected the compiler to figure this out from the context)

Are there any changes I could make to the type bound of Handler::reduce[T <: PreEvent](event: T): T or handler match statement to help Scala realise that when I'm matching on the event parameter and copying that explicit event it will by default be of the same type as the parameter and thus the type bound?

import java.util.UUID

import scala.util.chaining._

trait Event

trait PreEvent

trait Handler {
  def handle(event: Event): Unit = {}
  def reduce[T <: PreEvent](event: T): T = event
}

class EventService {
  private var handlers: List[Handler] = Nil

  def publish(event: Event): Unit =
    handlers.foreach { _.handle(event) }

  def publish[T <: PreEvent](event: T): T =
    handlers.foldLeft(event) { (event, handler) => handler.reduce(event) }
}

// this works fine
case class ConnectEvent(id: UUID) extends Event

class ConnectHandler extends Handler {
  override def handle(event: Event): Unit = event match {
    case ConnectEvent(id) =>
    case _                =>
  }
}

// problem 1
case class PreConnectEvent(id: UUID, cancelled: Boolean = false) extends PreEvent

class PreConnectHandler extends Handler {
  override def reduce[T <: PreEvent](event: T): T = event match {
    // (1) the copy result needs to be explicitly cast to an instance of T
    case it: PreConnectEvent => it.copy(cancelled = true).asInstanceOf[T]
    case _                   => event
  }
}

// problem 2
class Service(eventService: EventService) {
  def publishPreEvent(): Unit = {
    // (2) the method reference of 'eventService.publish' needs to be explicitly turned
    // into an anonymous function with '(_)' or it tries to call EventService::publish(Event)
    val reduced = PreConnectEvent(UUID.randomUUID()).pipe { eventService.publish(_) }
    // do something with reduced event
  }

  // this works fine
  def publishEvent(): Unit =
    ConnectEvent(UUID.randomUUID()).tap { eventService.publish }
}
fishb6nes
  • 139
  • 13

2 Answers2

4

I think this is a variation of return-current-type problem. Consider typeclass solution

trait Handler {
  def handle(event: Event): Unit = {}
  def reduce[T](event: T)(implicit ev: EventReducer[T]): T = ev.reduce(event)
}

trait EventReducer[T] {
  def reduce(event: T): T
}

object EventReducer {
  implicit val preConneectEventReducer: EventReducer[PreConnectEvent] = 
    (it: PreConnectEvent) => it.copy(cancelled = true)

  implicit def otherEventReducer[T]: EventReducer[T] = 
    (event: T) => event
}

(new PreConnectHandler).reduce(PreConnectEvent(UUID.randomUUID()))
// res0: PreConnectEvent = PreConnectEvent(99bcd870-4b7d-4b28-a12a-eafe4da16c78,true)

(new PreConnectHandler).reduce(ConnectEvent(UUID.randomUUID()))
// res1: ConnectEvent = ConnectEvent(47af28b7-ea93-4da1-9ee6-e89d41540141)
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
4

Regarding your first question, see details here

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

The thing is that def reduce[T <: PreEvent](event: T): T is just incorrect signature for

event match {
  case it: PreConnectEvent => it.copy(cancelled = true)
  case _                   => event
}

Correct one would be def reduce[T is a subclass of PreEvent](event: T): T if such syntax were possible in Scala (<: means "is a subtype of").

Please consider type class approach (see @MarioGalic's answer) or type tags approach (see above link).

Regarding your second question, you can write

val reduced = PreConnectEvent(UUID.randomUUID()).pipe(eventService.publish[PreConnectEvent])

specifying that you're using the overloaded version of method that is generic. Well, hardly it's shorter than eventService.publish(_).

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Ah that makes perfect sense, coming from Java the subtle differences between Traits and Interfaces still trip me up. Thank you. – fishb6nes May 14 '20 at 07:42