1

I am using a slight abuse of the builder pattern to make a fluent imperative execution chain. What I am after is a way to make it a compile error to forget the execute method at the end. My goal is something like the following

WithServiceA {
 doStuff()
} WithServiceB {
  doStuff()
} withClient client

WithServiceA and WithServiceB can both return values, so if the return value is used it is obvious if the return type is wrong, but if they are used imperatively, the whole object just falls on the floor silently. I want to ensure that forgetting the withClient call is a compile error no matter what context it is used in.

I want to be able to skip blocks if they are unneeded and put them in an arbitrary order so I am looking to replace the nested inner class pattern that I was using previously ala

def onServiceA[A](body: ServiceA => A) = new {   
  def onServiceB[B >: A](body: ServiceB => B) = {b => {
    doStuff()
  }
}
tryx
  • 975
  • 7
  • 17

1 Answers1

5

It looks like type-safe builder pattern. See this answer.

In your case:

trait TTrue
trait TFalse

class Myclass[TA, TB, TC] private(){
  def withServiceA(x: => Unit)(implicit e: TA =:= TFalse) = {x; new Myclass[TTrue, TB, TC]}
  def withServiceB(x: => Unit)(implicit e: TB =:= TFalse) = {x; new Myclass[TA, TTrue, TC]}
  def withServiceC(x: => Unit)(implicit e: TC =:= TFalse) = {x; new Myclass[TA, TB, TTrue]}
  def withClient(x: => Unit)(implicit e1: TA =:= TTrue, e2: TB =:= TTrue) = x
}

object Myclass{
  def apply() = new Myclass[TFalse, TFalse, TFalse]
}

Usage:

Myclass()
  .withClient(println("withClient"))
//<console>:22: error: Cannot prove that TFalse =:= TTrue.
//                .withClient(println("withClient"))
//                           ^


Myclass()
  .withServiceB(println("with B"))
  .withServiceA(println("with A"))
  .withClient(println("withClient"))
//with B
//with A
//withClient

Myclass()
  .withServiceA(println("with A"))
  .withServiceC(println("with C"))
  .withServiceB(println("with B"))
  .withClient(println("withClient"))
//with A
//with C
//with B
//withClient

Myclass()
  .withServiceC(println("with C"))
  .withServiceB(println("with B"))
  .withServiceA(println("with A"))
  .withServiceC(println("with C2"))
  .withClient(println("withClient"))
//<console>:25: error: Cannot prove that TTrue =:= TFalse.
//                .withServiceC(println("with C2"))
//                             ^

You could provide custom error messages with custom replacements for =:= class.

If you want to be sure that after every Myclass.apply withClient will be called, you could call it manually like this:

sealed class Context private()
object Context {
   def withContext(f: Context => Myclass[TTrue, TTrue, _])(withClient: => Unit) =
     f(new Context).withClient(withClient)
}

object Myclass{
  def apply(c: Context) = new Myclass[TFalse, TFalse, TFalse]
}

Usage:

Context
  .withContext(
    Myclass(_)
      .withServiceA(println("with A"))
      .withServiceC(println("with C"))
      .withServiceB(println("with B"))
  )(println("withClient"))

On ideone.

One can't create Myclass outside of withContext method and withClient will be called at least once.

Community
  • 1
  • 1
senia
  • 37,745
  • 4
  • 88
  • 129
  • Thanks for clarifying the typesafe builder pattern with the example! I saw some of the papers and was a little lost in the type theory. Your examples are great. I still have two open questions though. 1. What happens if you forget the `.withClient` call? 2. If I want to make any combination of 1 or more `withService` calls, do I just need 1 counter type to keep track? – tryx Jul 14 '15 at 08:00
  • @tryx: you can get a warning in some cases without `withClient` since `withClient` is the only method that returns `Unit`: `a pure expression does nothing in statement position`. But only in cases when it's not the last expression in the code block. If you want multiple `withClient` calls you could change return type of `withClient` like this: `def withClient(...)(...) = {x, this}`. – senia Jul 14 '15 at 08:08
  • @tryx alternatively you could just save subresult in variable and reuse it `val subres = MyClass.with<...>; subres.withClinet(...); subres.withClinet(...)` – senia Jul 14 '15 at 08:10
  • I do get a warning, but I was hoping that there was some way to make it a type error to omit `withClient`. `withClient` should only ever be the terminal operation. – tryx Jul 14 '15 at 08:12
  • @tryx: you could return from `withClinet` some kind of useful object that have to be stored. For instance `def withClient(...)(...): MyResult`, `object Myclass{ def apply(x: MyContext)`, `def withContext(f: MyContext => MyResult)`. So `Myclass(context)` can be called only inside `withContext` and there should be `MyResult` from `withClient`. – senia Jul 14 '15 at 08:18
  • This doesn't quite answer the question that I had, but that's my fault for framing it badly. It still put me on the right track and thank you for your heroic effort and code samples! – tryx Jul 16 '15 at 01:18