In Scala, how would I combine event driven programming with a functional approach?
Asked Answered
M

5

10

To clarify what I mean by event driven I'm referring to a situation where I have

def onTrade(...)

Which is called every time a particular stock trades. Suppose I want to track the daily highest trade price. To me the obvious solution is:

var dailyHigh = 0

def onTrade(...) {
    if (price > dailyHigh) dailyHigh = price
}

Is there a way to achieve this functionality using val instead of var? Assume also that I may want to add dailyLow, volumeHigh, volumeLow etc in the future.

Merchantman answered 25/7, 2011 at 9:44 Comment(0)
G
8

Not much of a problem, actually. A full solution would probably use Reader, IO and State monads plus Iteratee and lenses, but here's a simpler version:

case class State(dailyHigh: Int = 0)

object Main {
  type Event = (State => State)

  def mainLoop(currState: State, events: Stream[Event]): State =
    if (events.nonEmpty) {
      val newState = events.head(currState)
      mainLoop(newState, events.tail)
    } else currState

  def onTrade(price: Int): Event = (s: State) =>
    if (price > s.dailyHigh) s.copy(dailyHigh = price) else s

  def main(args: Array[String]) {
    val events = onTrade(5) #:: onTrade(2) #:: onTrade(10) #:: onTrade(5) #:: Stream.empty
    val finalState = mainLoop(State(), events)
    println(finalState)
  }
}

Look, ma, no vars!

State can become quite complex, of course, but that's where lenses come in. With lenses, it is pretty easy to consult and change (copy with new value) arbitrarily complex data structures.

Using iteratees is natural for events -- in a very simple sense, "onTrade" becomes an iteratee that gets invoked by an enumerator (the thing that "generates" the events) with each event if composed from partial function, you can fold all of them into a single partial function.

Alternatively, State monads can be combined with IO monads on for-comprehensions.

Finally, there's the option of continuations. If some processing requires a chain of events to be received, then the result of each event can be a continuation, and the continuation themselves become part of the state.

Guinna answered 25/7, 2011 at 21:38 Comment(2)
I'm just getting my feet wet with functional programming and trying to learn the ropes (my history is procedural then object oriented). I struggle with why (or if) this approach is "better". I do understand the basic reasons for immutability in functional programming but this just seems so much more complex! Is it just because I'm new to it?Szabadka
@Szabadka Newness certainly plays a role -- possibly a major one. But it's not just that... these things are abstractions and, as such, they unify many things that are not the same in the mind of a programmer not used to it, and hide the details. Dealing with abstract concepts is harder than dealing with concrete things. As for better, it makes programs much more composable and easier to test. But it does seem out of place when you look at small examples.Guinna
L
9

The paper Deprecating the observer pattern might be of interest, but I believe the library it describes is not available yet.

Lunation answered 25/7, 2011 at 11:14 Comment(2)
@Didier I get a 404 using that link.Wonderland
google suggest infoscience.epfl.ch/record/176887/files/…Lunation
G
8

Not much of a problem, actually. A full solution would probably use Reader, IO and State monads plus Iteratee and lenses, but here's a simpler version:

case class State(dailyHigh: Int = 0)

object Main {
  type Event = (State => State)

  def mainLoop(currState: State, events: Stream[Event]): State =
    if (events.nonEmpty) {
      val newState = events.head(currState)
      mainLoop(newState, events.tail)
    } else currState

  def onTrade(price: Int): Event = (s: State) =>
    if (price > s.dailyHigh) s.copy(dailyHigh = price) else s

  def main(args: Array[String]) {
    val events = onTrade(5) #:: onTrade(2) #:: onTrade(10) #:: onTrade(5) #:: Stream.empty
    val finalState = mainLoop(State(), events)
    println(finalState)
  }
}

Look, ma, no vars!

State can become quite complex, of course, but that's where lenses come in. With lenses, it is pretty easy to consult and change (copy with new value) arbitrarily complex data structures.

Using iteratees is natural for events -- in a very simple sense, "onTrade" becomes an iteratee that gets invoked by an enumerator (the thing that "generates" the events) with each event if composed from partial function, you can fold all of them into a single partial function.

Alternatively, State monads can be combined with IO monads on for-comprehensions.

Finally, there's the option of continuations. If some processing requires a chain of events to be received, then the result of each event can be a continuation, and the continuation themselves become part of the state.

Guinna answered 25/7, 2011 at 21:38 Comment(2)
I'm just getting my feet wet with functional programming and trying to learn the ropes (my history is procedural then object oriented). I struggle with why (or if) this approach is "better". I do understand the basic reasons for immutability in functional programming but this just seems so much more complex! Is it just because I'm new to it?Szabadka
@Szabadka Newness certainly plays a role -- possibly a major one. But it's not just that... these things are abstractions and, as such, they unify many things that are not the same in the mind of a programmer not used to it, and hide the details. Dealing with abstract concepts is harder than dealing with concrete things. As for better, it makes programs much more composable and easier to test. But it does seem out of place when you look at small examples.Guinna
G
2

Sometimes mutable status is needed naturally, Following is an example from book 'scala by example'.
It also have some mutable status(maxBid,maxBidder)。So a var is not always bad idea. Some times it works fine.

   class Auction(seller: Actor, minBid: Int, closing: Date) extends Actor {
   val timeToShutdown = 36000000 // msec
   val bidIncrement = 10
   def act() {
      var maxBid = minBid - bidIncrement
      var maxBidder: Actor = null
      var running = true
      while (running) {
         receiveWithin((closing.getTime() - new Date().getTime())) {
            case Offer(bid, client) =>
               if (bid >= maxBid + bidIncrement) {
                  if (maxBid >= minBid) maxBidder ! BeatenOffer(bid)
                  maxBid = bid; maxBidder = client; client ! BestOffer
               } else {
                  client ! BeatenOffer(maxBid)
               }
            case Inquire(client) =>
               client ! Status(maxBid, closing)
            case TIMEOUT =>
               if (maxBid >= minBid) {
                  val reply = AuctionConcluded(seller, maxBidder)
                  maxBidder ! reply; seller ! reply
               } else {
                  seller ! AuctionFailed
               }
               receiveWithin(timeToShutdown) {
                  case Offer(_, client) => client ! AuctionOver
                  case TIMEOUT          => running = false
               }
         }
      }
   }
}
Garratt answered 25/7, 2011 at 11:2 Comment(0)
D
0

Never actually done it, but instead of modifying values you could create new instances in a Stream.

Other process could then iterate that Stream, which would make them wait when they reach the last instantiated element of a stream.

Dicentra answered 25/7, 2011 at 9:59 Comment(1)
Also check out this link, which is basically the same solution, that Jens Schauder suggests.Underhand
M
0

I highly recommend functional reactive programming for that task. Here is a Talk about such a library in scala: http://skillsmatter.com/podcast/scala/reactors

Misapprehend answered 25/7, 2011 at 11:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.