Search code examples
scalamethodscompositionimplicitmethod-chaining

Chain functions in different way


Scala functions has following methods for chaining:

 fn1.andThen(fn2)
 fn1.compose(fn2)

But how can be written this case:

I have function cleanUp() which has to be called always as last step. And I have a bunch of other functions, like that:

class Helper {
  private[this] val umsHelper = new UmsHelper()
  private[this] val user = umsHelper.createUser()
  def cleanUp = ... // delete user/ and other entities

  def prepareModel(model: TestModel) = {
    // create model on behalf of the user
  }

  def commitModel() = {
    // commit model on behalf of the user
  }
}

And some external code can use code something like this:

val help = new Helper()
help.prepareModel()
help.commitModel()
// last step should be called implicitly cleanUp

How this can be written in a functional way, that chaining will always call cleanUp function implicitly as last step?

Note: I see it as analogue of destructor in C++. Some chaining (doesn't matter how this chain is done) fn1 andLater fn2 andLater fn3 have to call as last step cleanUp (fn1 andLater fn2 andLater fn3 andLater cleanUp). Wrong with directly writing cleanUp method is there is a big chance someone will miss this step and user will be leaked (will be stayed in database)


Solution

  • 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.