Mahesh Govind
Mahesh Govind

Reputation: 467

Handling contexts and state management

I am a newbie to scala and functional programing . I have the use case of implementing a communication protocol . Could some one point the right way to handle contexts ( Store ,create, do state changes ) and data in a functional way .

For example consider the sample telephony protocol . The switch will get 1000s of calls. Hence need to keep the context of each of the call to handle them properly .How do i handle the context handling in an immutable way ?

Caller------------------------------Callee [telephone switch]

placecall------------------>--------Callreceived

Alert--------------------------------Alert

Answer--------------------------------Answer

Inconversation

Disconnect--------------------------------Disconnect

If i was doing C programing, i will allocate a context storage ( may be a hash ) to store all the contexts and when I get a call , I will do a fetch context from the store .If it is a new call , I might get back null and i need to create a new context and if it is an existing call , i will get the right context .

It will be great if you could provide some insights into the right functional design pattern . Will we use IO monad and state monad ? How do we model the context store. How we ensure concurrency in functional world when we do a context fetch .

Thanks in advance ..

regards Mahesh

Upvotes: 2

Views: 702

Answers (1)

yǝsʞǝla
yǝsʞǝla

Reputation: 16412

Any useful program uses side effects. In FP we try to push these side effecting operations to the edges of the program while trying to keep as much of the code pure as we can.

Eventually there will be a place where side effects will happen whether you use state monad or some other technique.

Usually you have a pure function that takes some state and transforms it to a new state which is returned back to the caller along with the result. This can be done with either regular argument passing or with a state wrapped in a state monad with transformations running in its context.

A simple argument passing example:

object StateExample extends App {

  //--- start of pure part of your program ---//
  type PhoneNumber = String
  type CallState = Map[PhoneNumber, User]

  case class User(username: String)

  def startCall(calls: CallState, caller: User, phone: PhoneNumber): CallState =
    calls + (phone -> caller)

  def finishCall(calls: CallState, phone: PhoneNumber): CallState =
    calls - phone

  def startCallingPeople(calls: CallState) = {
    val intermediateState1 = startCall(calls, User("one"), "123")
    val intermediateState2 = startCall(intermediateState1, User("two"), "456")
    intermediateState2
  }

  def hangupCalls(calls: CallState) = {
    val intermediateState1 = finishCall(calls, "123")
    val intermediateState2 = finishCall(intermediateState1, "456")
    intermediateState2
  }
  // --- end of pure part of the program ---//


  // --- start of impure part of your program ---//
  var callState: CallState = Map()

  def runSimulation(): Unit = {
    println(s"BEFORE ANY CALLS: $callState")
    callState = startCallingPeople(callState)
    println(s"AFTER CALLING 2 PEOPLE: $callState")
    callState = hangupCalls(callState)
    println(s"AFTER HANGING UP: $callState")
  }

  runSimulation()
  // --- end of impure part of your program ---//
}

prints:

BEFORE ANY CALLS: Map()
AFTER CALLING 2 PEOPLE: Map(123 -> User(one), 456 -> User(two))
AFTER HANGING UP: Map()

See how state is being passed from function to function and transformed state being generated and passed further. There are no side effects in that 'pure' part of the code, it only describes how things should be transformed. The 'impure' part of the code does the dirty job of using the rest of the program and performs side effects.

Alternatively you could use a state monad in the 'pure' part of the program and run it in the 'impure' part and save produced state similarly to how the state saved in a var above. Here is an example:

import scalaz.State

object StateMonadExample extends App {

  def startCall(caller: User, phone: PhoneNumber): State[CallState, Unit] =
    State { s => s + (phone -> caller) ->() }

  def finishCall(phone: PhoneNumber): State[CallState, Unit] =
    State { s => (s - phone) ->() }

  def startCallingPeople: State[CallState, Unit] =
    startCall(User("one"), "123").flatMap(_ => startCall(User("two"), "456"))

  def hangupCalls: State[CallState, Unit] =
    finishCall("123").flatMap(_ => finishCall("456"))

  // mutable part of your program
  var callState: CallState = Map()

  // side effecting part of your program
  def runSimulation(): Unit = {
    test1() // intermediate state being saved
    test2() // intermediate state being passed through
    test3() // same as test 2 but also outputs intermediate state without updating it as test 1 does
  }

  def test1(): Unit = {
    // reset initial state just in case
    callState = Map()
    println("TEST 1:")
    println(s"BEFORE ANY CALLS: $callState")
    callState = startCallingPeople.run(callState)._1
    println(s"AFTER CALLING 2 PEOPLE: $callState")
    callState = hangupCalls.run(callState)._1
    println(s"AFTER HANGING UP: $callState")
    println("END OF TEST 1.\n")
  }

  def test2(): Unit = {
    // reset initial state just in case
    callState = Map()
    println("TEST 2:")
    println(s"BEFORE ANY CALLS: $callState")
    val computation = for {
      _ <- startCallingPeople
      _ <- hangupCalls
    } yield ()
    callState = computation.run(callState)._1
    println(s"AFTER CALL AND HANGUP: $callState")
    println("END OF TEST 2.\n")
  }

  def test3(): Unit = {
    // reset initial state just in case
    callState = Map()
    println("TEST 3:")
    println(s"BEFORE ANY CALLS: $callState")
    val computation = for {
      s1 <- startCallingPeople.flatMap(_ => State { s: CallState => println(s"AFTER CALLING 2 PEOPLE: $s"); s -> () })
      _ <- hangupCalls
    } yield ()
    callState = computation.run(callState)._1
    println(s"AFTER HANGING UP: $callState")
    println("END OF TEST 3.\n")
  }

  runSimulation()
}

prints:

TEST 1:
BEFORE ANY CALLS: Map()
AFTER CALLING 2 PEOPLE: Map(123 -> User(one), 456 -> User(two))
AFTER HANGING UP: Map()
END OF TEST 1.

TEST 2:
BEFORE ANY CALLS: Map()
AFTER CALL AND HANGUP: Map()
END OF TEST 2.

TEST 3:
BEFORE ANY CALLS: Map()
AFTER CALLING 2 PEOPLE: Map(123 -> User(one), 456 -> User(two))
AFTER HANGING UP: Map()
END OF TEST 3.

Note that State monad takes care of passing the state through. Essentially we are just building up a bunch of computations and then execute them by calling run and passing an initial state.

When concurrency comes into consideration you can apply the same principles on the smaller scale but certain things start to break. For instance, if you let 2 threads update the same state you'll need to synchronize on it and be sure that none of those threads reads an outdated version of the state. Synchronization leads to blocking and slows down the program.

A practical approach is either to keep state externally in some database (manages synchronization for you), or to avoid synchronization somehow. If I would have to keep state in memory I would probably use Akka and represent each active call as an actor. Actors can safely encapsulate mutable state because they process each message sequentially. When the call is finished I would kill the actor to free up resources. You can partition your application in a different way - maybe instead of having one actor per call you could have one actor per switch. It really depends on the requirements. Note that actors embrace mutability so it's not pure FP solution.

The conclusion is that eventually you will have side effects, but you need to know how to minimize and isolate them from the rest of the program.

See complete project here: https://github.com/izmailoff/scala-state-example

Upvotes: 3

Related Questions