Question

The Background

I'm working on an event library in Scala. In my library you can define events like this:

val e1 = new ImperativeEvent[Int]

You can trigger them like this:

e1(42)

You can create reactions like this:

val r1 = (i: Int) => println(i)

And attach them to the event like this:

e1 += r1

There is also some other stuff (like event transformations, compositions etc). I use the Esper CEP engine as the backend of my library. Esper uses an SQL-like language called EPL for most operations.

The Problem

I'm trying to implement some more advanced concepts like event joins. So now you can define events with multiple properties like this (using tuple types):

val e2 = new ImperativeEvent[(Int, String)]

And then join them like this:

val e3 = e1 join e2 windowLength (30) on "E1.P1 = E2.P1"

which performs a join of e1 and e2 on the last 30 occurrances of both on the condition that their respective first properties are equal.

This is alright but I'd like to get rid of the strings in my implementation to make the event expressions type checkable. I'd like to change the join expression to something like this:

val e3 = e1 join e2 windowLength (30) on e1._1 === e2._1

similar to the way it is done in eg. Squeryl. The problem with this is, I can't access the types of the elements of the tuple type...

The Question

How can I access the tuple types statically? Right now I've only managed to access them at run-time through reflection which does not help me. I'm pretty sure that what I want to achieve is not possible with tuples but I'm wondering if using HLists from the shapeless library or something similar might help in achieving my goal.

Was it helpful?

Solution

Without more details on your DSL, I'm afraid it's not clear what you mean by "access the tuple types statically". Here's a simplified version of the API that has no trouble with tuple types:

class Event[T] {
  def joinOn[T2, R](ev2: Event[T2])(f: (T, T2) => R) = new Event[R]
}

You can use this as follows:

val e1 = new Event[(Int, String)]
val e2 = new Event[(Int, String)]
val e3 = e1.joinOn(e2)(_._1 == _._2)

It should be easy to see how this could be extended to supporting your join/windowLength/on syntax.

Update: I can see that your use case is complicated by the fact that you need to translate the Scala-encoded query expression to another query language. In this case, you want the on method's signature to look like:

def on[T2, R](f: (Expr[T], Expr[T2]) => Expr[R]): Event[R]

Internally, each event object would create its own Expr representation and would pass this representation into the function supplied to the on method.

The Expr type could be defined like:

trait Expr[T] {
  protected val repr: String

  def _1[A](implicit ev: T <:< Tuple2[A,_]): Expr[A] = 
    ??? // create an Expr[A] whose string representation is (repr + ".P1")

  // abstracting over tuple arities (using Shapeless)
  import shapeless._, nat._
  @scala.annotation.implicitNotFound("A tuple with at least 3 elements is required")
  type At2 = ops.tuple.At[T, _2]

  def _3(implicit at: At2): Expr[at.Out] = 
    ??? // create an Expr[at.Out] whose string representation is (repr + ".P3")

  def ===(other: Expr[T]): Expr[Boolean] =
    ??? // create an Expr[T] whose string representation is (repr + " = " + other.repr)
}

This is obviously dramatically simplified but should help to get you started.

OTHER TIPS

I. There is a SynapseGrid functional reactive programming library. In the sources you can find some helpful tips.

All processing in the library is type safe. You have complete access to tuples.

For instance if I had to implement join in SynapseGrid I would define a method join of the following signature:

implicit class RichContact[T] (c:Contact[T]){ // contact == event in SynapseGrid's terminology
  ...
  def join[T2](c2:Contact[T2]):Contact[(T, T2)] = {
    // construct a contact/event that do nothing more than joining two events.
  }
}

implicit class RichTupledContact[T, T2](c:Contact[(T, T2)])
  def windowLength(len:Int):Contact[(T, T2)] = { 
    // construct the next step of processing events — window 
  }
}

and so on. Gradually building event processing greatly simplifies system's construction.

II. If, however, you need to construct everything at once then you may return some intermediate object that has construction method:

implicit class RichContact[T] (c:Contact[T]){ // contact == event in SynapseGrid's terminology
  ...
  def join[T2](c2:Contact[T2]):Contact[(T, T2)] = {
    new {
      def windowLength(len:Int) = ...
    }
  }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top