Chain functions in different way

This is a more advanced alternative:

When you hear "context" and "steps", there's a functional pattern that directly comes to mind: Monads. Rolling up your own monad instance can simplify the user-side of putting valid steps together, while providing warranties that the context will be cleaned up after them.

Here, we are going to develop a "CleanableContext" construction that follows that pattern.

We base our construct on the most simple monad, one whose only function is to hold a value. We're going to call that Context

trait Context[A] { self => 
  def flatMap[B](f:A => Context[B]): Context[B] = f(value)
  def map[B](f:A => B): Context[B] = flatMap(f andThen ((b:B) => Context(b)))
  def value: A
}

object Context {
  def apply[T](x:T): Context[T] = new Context[T] { val value = x  }
}

Then we have a CleanableContext, which is capable of "cleaning up after itself" provided some 'cleanup' function:

trait CleanableContext[A] extends Context[A] {
  override def flatMap[B](f:A => Context[B]): Context[B] = {
    val res = super.flatMap(f)
    cleanup
    res
  }
  def cleanup: Unit
}

And now, we have an object that's able to produce a cleanable UserContext that will take care of managing the creation and destruction of users.

object UserContext {
  def apply(x:UserManager): CleanableContext[User] = new CleanableContext[User] {
    val value = x.createUser
    def cleanup = x.deleteUser(value)
  }
}

Let's say that we have also our model and business functions already defined:

trait Model
trait TestModel extends Model
trait ValidatedModel extends Model
trait OpResult
object Ops {
  def prepareModel(user: User, model: TestModel): Model = new Model {}

  def validateModel(model: Model): ValidatedModel = new ValidatedModel {}

  def commitModel(user: User, vmodel: ValidatedModel): OpResult = new OpResult {}
}

Usage

With that reusable machinery in place, our users can express our process in a succinct way:

import Ops._
val ctxResult = for {
  user <- UserContext(new UserManager{})
  validatedModel <- Context(Ops.prepareModel(user, testModel)).map(Ops.validateModel)
  commitResult <- Context(commitModel(user, validatedModel))
} yield commitResult

The result of the process is still encapsulated, and can be taken "out" from the Context with the value method:

val result = ctxResult.value

Notice that we need to encapsulate the business operations into a Context to be used in this monadic composition. Note as well that we don't need to manually create nor cleanup the user used for the operations. That's taken care of for us.

Furthermore, if we needed more than one kind of managed resource, this method could be used to take care of managing additional resources by composing different contexts together.

With this, I just want to provide another angle to the problem. The plumbing is more complex, but it creates a solid ground for users to create safe processes through composition.


I think that the core of the question is "how to keep a resource within a managed context". i.e. provide users with a way to use the resource and prevent it to 'leak' outside its context.

One possible approach is to provide a functional access to the managed resource, where the API requires functions to operate over the resource in question. Let me illustrate this with an example:

First, we define the domain of our model: (I've added some subtypes of Model to make the example more clear)

trait User
trait Model
trait TestModel extends Model
trait ValidatedModel extends Model
trait OpResult
// Some external resource provider
trait Ums {
  def createUser: User
  def deleteUser(user: User)
}

Then we create a class to hold our specific context.

class Context {
  private val ums = new Ums{ 
    def createUser = new User{} 
    def deleteUser(user: User) = ???
  } 

  def withUserDo[T](ops: User => T):T = {
    val user = ums.createUser
    val result = ops(user)
    ums.deleteUser(user)
    result
  }
}

The companion object provides (some) operations on the managed resource. Users can provide their own functions as well.

object Context {
  def prepareModel(model: TestModel): User => Model = ???

  val validateModel: Model => ValidatedModel = ???

  val commitModel: ValidatedModel => OpResult = ???
}

We can instantiate our context and declare operations on it, using a classic declaration, like:

val ctx  = new Context 
val testModel = new TestModel{}

val result = ctx.withUserDo{ user => 
  val preparedModel = prepareModel(testModel)(user)
  val validatedModel = validateModel(preparedModel)
  commitModel(validatedModel)
}

Or, given the desire in the question to use functional composition, we could rewrite this as:

val result = ctx.withUserDo{
  prepareModel(testModel) andThen validateModel andThen commitModel
}