Chain functions in different way
Asked Answered
A

3

6

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)

Alcoholicity answered 1/11, 2016 at 10:43 Comment(2)
I don't see any chaining in your example. What's wrong with adding help.cleanUp at the last line?Alvaalvan
could you also add the return types of your methods? def prepareModel(model: TestModel) : _____? def commitModel(): _____?Crake
C
6

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.

Crake answered 1/11, 2016 at 21:20 Comment(0)
C
3

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
}
Crake answered 1/11, 2016 at 12:20 Comment(0)
M
1

Use autoClean this will automatically call cleanUp at the end.

create a HelperStuff trait which contains all the necessary functions.

Inside the Helper object create a private implementation of the HelperStuff and then have a method method called autoClean which does the work keeping the Helper instance private and safe way from the rouge users.

Helper.autoClean { helperStuff =>

  //write all your code here. clean up will happen automatically
  helper.foo()
  helper.commitModel()

}

Here is the autoClean function for you

trait HelperStuff {
 def foo(): Unit
 def commitModel: Unit
 def cleanUp(): Unit
}

object Helper {

  private class Helper extends HelperStuff {
   def foo(): Unit = println("foo")
   def cleanUp(): Unit = println("cleaning done")
  }

  private val helper = new Helper()

  def autoClean[T](code: HelperStuff => T): T = {
    val result = code(helper)
    helper.cleanUp()
    result
  }

}
Mouthwash answered 1/11, 2016 at 10:57 Comment(4)
With this approach, the user could still do new Helper().foo without any restriction.Crake
Make the Helper constructor private. Move autoClean to the Helper companion object.Commutual
@Crake made helper private in the implementation .. have a lookMouthwash
@Commutual thanks for the suggestion .. edited the answer. now helper is private and safeMouthwash

© 2022 - 2024 — McMap. All rights reserved.